Compare commits
2
Commits
45a77885f9
...
06cb7f0c4d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cb7f0c4d | ||
|
|
69d9b956e6 |
@@ -28,15 +28,15 @@ print, or rebuild an existing case. Generated definitions live under
|
|||||||
`cases/<name>/`; exports, previews, revision history, and `vendor/bundle` are
|
`cases/<name>/`; exports, previews, revision history, and `vendor/bundle` are
|
||||||
ignored by Git.
|
ignored by Git.
|
||||||
|
|
||||||
OpenSCAD and Blender must be available on `PATH`. On Ubuntu:
|
OpenSCAD, Blender, and Python 3 must be available on `PATH`. On Ubuntu:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install openscad blender
|
sudo apt-get install openscad blender python3
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `OPENSCAD=/path/to/openscad` or `BLENDER=/path/to/blender` when either
|
Set `OPENSCAD=/path/to/openscad`, `BLENDER=/path/to/blender`, or
|
||||||
program is installed elsewhere. The wizard checks both tools before replacing
|
`PYTHON=/path/to/python3` when a program is installed elsewhere. The wizard
|
||||||
an existing package.
|
checks all three tools before replacing an existing package.
|
||||||
|
|
||||||
The wizard can add clearance beyond either short end of the Feather PCB for
|
The wizard can add clearance beyond either short end of the Feather PCB for
|
||||||
right-angle headers, plug housings, and wire bends. Front clearance moves the
|
right-angle headers, plug housings, and wire bends. Front clearance moves the
|
||||||
@@ -132,6 +132,25 @@ remains based on the taller switches plus `oled_face_gap`, placing the display
|
|||||||
face about 1.7 mm below the lid's inner plane. The default 0.7 mm captive-button
|
face about 1.7 mm below the lid's inner plane. The default 0.7 mm captive-button
|
||||||
contact extension spans most of the 0.8 mm switch-to-lid gap.
|
contact extension spans most of the 0.8 mm switch-to-lid gap.
|
||||||
|
|
||||||
|
When the battery compartment and tripod insert are both enabled, the generator
|
||||||
|
adds board-side clearance to keep the insert centered across the case width and
|
||||||
|
moves it 5 mm toward the USB wall to clear the nearest Feather standoff. The
|
||||||
|
battery partition defaults to three 0.6 mm extrusion lines and can be adjusted
|
||||||
|
with `battery_partition_thickness`.
|
||||||
|
|
||||||
|
## Two-color lid artwork
|
||||||
|
|
||||||
|
Set `lid_label` to a device name and/or enable `lid_logo_enabled` to add flush
|
||||||
|
second-color artwork to non-OLED lids. Choose `lid_art_orientation` from
|
||||||
|
`usb_left`, `usb_front`, `usb_right`, or `usb_rear`; it describes where the USB
|
||||||
|
edge sits when the exterior artwork is upright. OLED profiles reject lid
|
||||||
|
artwork until a window/button-safe layout is defined.
|
||||||
|
|
||||||
|
Generated artwork packages include separate `lid_base` and `lid_accent` meshes,
|
||||||
|
a complete single-color `lid`, and `lid_multicolor.3mf`. The multicolor file is
|
||||||
|
one aligned multipart object with its base assigned to filament 1 and its logo
|
||||||
|
and title assigned to filament 2. Print the lid exterior-face-down.
|
||||||
|
|
||||||
## Custom wire egress
|
## Custom wire egress
|
||||||
|
|
||||||
`extra_cutouts` accepts any number of entries:
|
`extra_cutouts` accepts any number of entries:
|
||||||
@@ -164,9 +183,11 @@ points.
|
|||||||
1. Export and print `part="insert_coupon"` in the intended material.
|
1. Export and print `part="insert_coupon"` in the intended material.
|
||||||
2. Test both M2.5 and 1/4-20 inserts in the five stepped holes.
|
2. Test both M2.5 and 1/4-20 inserts in the five stepped holes.
|
||||||
3. Set `m25_insert_hole_diameter` and `tripod_insert_hole_diameter` to the best
|
3. Set `m25_insert_hole_diameter` and `tripod_insert_hole_diameter` to the best
|
||||||
coupon values.
|
coupon values. These parameters are the finished pilot diameters in the
|
||||||
4. Print `part="switch_coupon"` and choose the best hole for the actual DMWD
|
model; `hole_compensation` is not added to heat-set pockets. The coupon uses
|
||||||
switch batch.
|
the same tapered pocket geometry as the cases.
|
||||||
|
4. Print `part="switch_coupon"` and choose the best rounded-rectangle opening
|
||||||
|
for the actual KCD1-101 rocker-switch batch.
|
||||||
5. Print `part="fit_coupon"` and tune `fit_clearance` before committing to the
|
5. Print `part="fit_coupon"` and tune `fit_clearance` before committing to the
|
||||||
complete lid and body.
|
complete lid and body.
|
||||||
|
|
||||||
@@ -180,13 +201,16 @@ temperature control, and the same extrusion settings intended for the case.
|
|||||||
opening faces the exterior bottom.
|
opening faces the exterior bottom.
|
||||||
2. Heat-set four M2.5 inserts into the board standoffs.
|
2. Heat-set four M2.5 inserts into the board standoffs.
|
||||||
3. Fasten the ESP32 Feather V2, then install the stacking headers and Wings.
|
3. Fasten the ESP32 Feather V2, then install the stacking headers and Wings.
|
||||||
4. For battery builds, place the protected 69 x 18 mm pack in the cradle, route
|
4. For battery builds, place the protected 69 x 18 mm pack in the cradle and
|
||||||
its lead through the partition notch, and mount the latching switch in the
|
route its lead through the combined front partition/JST opening and beneath
|
||||||
battery-side wall.
|
the Feather PCB.
|
||||||
5. Heat-set the four lid inserts downward into the corner bosses.
|
5. Snap the KCD1-101 rocker switch into the rear lid opening, connect its bent
|
||||||
6. For OLED builds, insert each enabled printed plunger through the lid from
|
quick-connect terminals or low-profile wiring, and lower the wired lid onto
|
||||||
|
the case.
|
||||||
|
6. Heat-set the four lid inserts downward into the corner bosses.
|
||||||
|
7. For OLED builds, insert each enabled printed plunger through the lid from
|
||||||
the inside. The elongated flange stays in the internal recess.
|
the inside. The elongated flange stays in the internal recess.
|
||||||
7. Engage the lid rim and fasten four standard M2.5 socket-head screws into the
|
8. Engage the lid rim and fasten four standard M2.5 socket-head screws into the
|
||||||
2.5 mm-deep counterbores.
|
2.5 mm-deep counterbores.
|
||||||
|
|
||||||
Confirm the USB plug, switch, battery, insert, and lid fits without force before
|
Confirm the USB plug, switch, battery, insert, and lid fits without force before
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="210mm"
|
||||||
|
height="297mm"
|
||||||
|
viewBox="0 0 210 297"
|
||||||
|
version="1.1"
|
||||||
|
id="svg584"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="NME-fixed.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview586"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.914906"
|
||||||
|
inkscape:cx="1166.24"
|
||||||
|
inkscape:cy="477.6447"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1390"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1080"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs581" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g772">
|
||||||
|
<path
|
||||||
|
id="path2"
|
||||||
|
d="m 97.576583,26.288608 c -0.51788,0.06491 -2.361848,0.291394 -4.044598,0.485422 -15.62735,1.747308 -32.128529,8.73619 -44.81195,18.960393 -9.803341,7.926917 -17.342201,17.309748 -23.360236,29.151792 -2.879725,5.629978 -3.170767,6.632927 -1.941336,6.535912 0.388054,-0.03245 2.556227,-0.06491 4.788604,-0.03245 l 4.0767,0.03245 4.820708,5.662083 c 2.653242,3.106208 4.918075,5.662083 5.01509,5.662083 0.09737,0 0.291394,-1.035402 0.420862,-2.297287 0.09701,-1.261888 0.517525,-3.688291 0.87348,-5.403146 3.106208,-14.559843 9.415285,-26.692928 19.186524,-36.852576 6.276975,-6.568017 12.521494,-11.098035 21.030846,-15.304204 5.597525,-2.749903 9.933164,-4.367742 15.562791,-5.823656 l 3.559172,-0.938388 -2.10291,0.03245 c -1.164868,0.03246 -2.523768,0.09701 -3.073747,0.129117"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path3"
|
||||||
|
d="m 169.08146,36.027745 c -26.62837,2.232377 -57.46291,14.33336 -85.90315,33.68181 -5.85611,3.979685 -15.530335,11.130137 -15.530335,11.453635 0,0.16157 1.3589,0.258937 3.008842,0.258937 h 2.976737 l 3.7211,-2.78271 c 4.917723,-3.688644 13.977408,-9.738784 19.574581,-13.071475 5.759445,-3.46216 18.668995,-9.997721 24.751595,-12.55395 21.90467,-9.188802 42.51501,-12.747625 55.16562,-9.576858 5.04754,1.261888 9.35073,4.432654 11.09769,8.120946 0.80891,1.747308 0.87381,2.167818 0.87381,6.406443 0,4.270727 -0.0649,4.691592 -1.06748,7.603419 -2.65325,7.700433 -8.70374,17.439568 -16.8575,27.178351 -1.26188,1.520825 -2.29693,2.814815 -2.29693,2.91183 0,0.09737 4.7886,0.129468 10.67718,0.06491 l 10.67715,-0.09701 V 90.772856 85.91934 l 2.07081,-2.814815 C 196.58332,76.860006 200.59546,69.677098 202.56925,64.11203 208.1015,48.484325 201.79243,38.616074 184.77337,36.286682 181.6996,35.86582 172.7373,35.704247 169.08146,36.027745"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path4"
|
||||||
|
d="m 19.11528,111.8358 v 26.20751 h 11.000669 v -35.91419 l 1.552926,1.71486 c 0.841375,0.93839 4.59458,5.27402 8.347782,9.64212 3.753202,4.33564 9.350727,10.80664 12.456583,14.36582 3.106208,3.55882 6.341533,7.31202 7.215362,8.34742 l 1.552929,1.84397 h 5.629979 5.62963 V 111.8358 85.628298 H 66.8066 61.079958 l 0.161573,17.957092 c 0.06491,9.86825 0.06491,17.95709 -0.03246,17.95709 -0.09666,-0.0324 -0.711552,-0.67945 -1.326443,-1.45591 -0.61454,-0.77646 -4.27073,-5.01509 -8.088842,-9.41564 C 40.857677,98.052425 37.978307,94.720085 33.998622,90.060598 l -3.7211,-4.367742 -5.565071,-0.03246 -5.597171,-0.0321 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path5"
|
||||||
|
d="m 77.030804,111.8358 v 26.20751 h 11.001023 v -35.04035 l 6.7945,9.02687 c 3.720748,4.98299 8.023933,10.67753 9.544753,12.68342 l 2.81481,3.65584 9.5123,-12.68307 9.5123,-12.68306 0.0649,13.65391 0.097,13.65355 3.85022,-3.26743 c 2.13571,-1.81186 4.56212,-3.91512 5.43595,-4.69159 l 1.55293,-1.39135 V 103.294 85.628298 h -5.56507 l -5.59753,0.0321 -9.38283,12.456935 c -5.14456,6.826957 -9.4802,12.392027 -9.57721,12.359567 -0.19438,-0.097 -4.4002,-5.56507 -14.33336,-18.798467 L 88.1934,85.628298 H 82.628329 77.030804 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path6"
|
||||||
|
d="m 170.4079,106.88563 -12.58605,0.097 -6.66538,6.56802 c -21.32189,20.93383 -39.11741,34.16723 -59.727393,44.39108 -28.796192,14.30126 -53.936193,18.6369 -68.334112,11.77749 -2.976739,-1.42381 -6.115402,-4.10916 -7.441848,-6.34154 -2.78271,-4.7565 -2.750253,-12.03642 0.06491,-18.41006 1.002946,-2.29729 1.002946,-2.3943 -0.161925,-2.3943 h -0.93839 l -0.09701,-2.84727 -0.09701,-2.81517 -1.553282,2.58869 c -3.8177601,6.40609 -6.9236165,14.00986 -7.8295498,19.18652 -1.3267981,7.50641 0.1615731,13.07183 4.6912397,17.53659 2.6856951,2.65289 5.0150871,4.04459 8.9947751,5.37068 8.088841,2.6857 18.863026,3.0413 30.608055,0.93874 l 4.238273,-0.74436 3.882673,2.39431 c 14.689312,8.96232 30.252104,13.39497 46.947304,13.39497 14.49529,0 26.66083,-2.81482 39.53828,-9.12389 14.59194,-7.18326 27.33992,-18.70146 35.91418,-32.45238 1.97345,-3.17112 5.92103,-10.80664 6.50312,-12.52185 l 0.32385,-1.0675 -0.54999,0.80892 c -8.60672,12.42447 -18.15112,20.90137 -30.5756,27.08133 -18.15112,9.05934 -39.14987,10.90366 -57.980434,5.04719 -3.268133,-1.00294 -10.839098,-4.01178 -10.806642,-4.27072 0,-0.097 3.461809,-1.90888 7.667977,-3.97969 4.206169,-2.0708 9.706679,-4.95053 12.197999,-6.43855 9.93281,-5.824 22.4222,-14.5923 31.70801,-22.26027 l 3.55918,-2.94464 0.097,1.29434 0.097,1.29399 h 23.94302 23.91021 v -5.01509 -5.01473 h -18.44215 -18.44287 v -1.22943 c 0,-1.16488 0.25929,-1.48873 4.49756,-5.50052 l 4.49721,-4.27072 h 10.54806 l 10.54769,-0.0325 v -5.17701 c 0,-2.81482 -0.0324,-5.11175 -0.0646,-5.07965 -0.0649,0.0646 -5.75944,0.12947 -12.68341,0.16193"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path7"
|
||||||
|
d="m 34.484042,128.23997 v 14.17144 l -6.018035,0.16192 -6.01839,0.16157 1.326798,2.91218 c 3.559175,7.79745 8.703382,15.82173 13.912496,21.67785 l 1.423812,1.58538 4.691592,-0.38806 c 5.079645,-0.42086 9.609312,-1.19732 15.497879,-2.6857 3.429706,-0.84102 11.518548,-3.39725 11.906604,-3.75285 0.06491,-0.0649 -0.582438,-0.74471 -1.455915,-1.48872 -9.544756,-7.95937 -17.083264,-18.15112 -21.80731,-29.44318 -1.455914,-3.39725 -1.649941,-3.68829 -7.312379,-10.25631 -3.202869,-3.7211 -5.888564,-6.7624 -5.985579,-6.7624 -0.09701,-0.0324 -0.161573,6.34154 -0.161573,14.10688"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<g
|
||||||
|
id="g475"
|
||||||
|
transform="matrix(0.14274222,0,0,0.14274222,123.03997,38.158339)">
|
||||||
|
<path
|
||||||
|
d="m 134.23594,367.14576 v 34.66667 h 69.06667 c 37.86666,0 71.06666,-0.4 73.86666,-0.8 4.4,-0.8 7.2,-3.73333 31.33333,-32.8 14.53334,-17.46667 26.53334,-32.4 26.8,-32.93333 0.26667,-0.66667 -0.93333,-1.46667 -2.66666,-2 -1.73334,-0.4 -47.06667,-0.8 -100.66667,-0.8 h -97.73333 z"
|
||||||
|
id="path251"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
<path
|
||||||
|
d="m 134.90261,432.74576 c -0.4,0.93334 -0.53333,27.2 -0.4,58.4 l 0.4,56.53333 38.4,-37.06666 38.26666,-37.06667 -0.26666,-20.93333 -0.4,-20.8 -37.73334,-0.4 c -29.46666,-0.26667 -37.86666,0 -38.26666,1.33333 z"
|
||||||
|
id="path253"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -1,25 +0,0 @@
|
|||||||
# Attitude IMU
|
|
||||||
|
|
||||||
Generated by the Feather Case Wizard.
|
|
||||||
|
|
||||||
- Top profile: `generic`
|
|
||||||
- FeatherWings: 1
|
|
||||||
- Battery compartment: no
|
|
||||||
- 1/4-20 insert: yes
|
|
||||||
- Approximate exterior: 60.8 × 41.7 × 35.8 mm
|
|
||||||
- Formats: 3mf
|
|
||||||
- Parts: `body`, `lid`
|
|
||||||
- Custom cutouts: 0
|
|
||||||
|
|
||||||
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
|
||||||
OpenSCAD and Blender must be installed. Set `OPENSCAD` or `BLENDER` to
|
|
||||||
override their executable paths.
|
|
||||||
|
|
||||||
To open the saved Customizer values manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openscad -p parameters.json -P attitude-imu source/feather_case.scad
|
|
||||||
```
|
|
||||||
|
|
||||||
The 3MF files contain portable geometry only. Choose printer, filament,
|
|
||||||
plate, and slicing settings in Bambu Studio.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
"parameterSets": {
|
|
||||||
"attitude-imu": {
|
|
||||||
"top_profile": "\"generic\"",
|
|
||||||
"wing_count": "1",
|
|
||||||
"wing_pitch": "12.25",
|
|
||||||
"stack_height_override": "0.0",
|
|
||||||
"generic_component_height": "3.0",
|
|
||||||
"generic_top_gap": "2.0",
|
|
||||||
"gps_antenna_height": "6.0",
|
|
||||||
"gps_top_gap": "5.0",
|
|
||||||
"oled_component_height": "2.4",
|
|
||||||
"oled_display_height": "1.5",
|
|
||||||
"oled_face_gap": "0.8",
|
|
||||||
"button_a": "true",
|
|
||||||
"button_b": "true",
|
|
||||||
"button_c": "true",
|
|
||||||
"button_reset": "false",
|
|
||||||
"button_projection": "1.5",
|
|
||||||
"button_cap_size": "[5.0,3.2]",
|
|
||||||
"button_flange_size": "[6.6,3.6]",
|
|
||||||
"button_flange_thickness": "0.9",
|
|
||||||
"button_travel": "0.5",
|
|
||||||
"button_contact_extension": "0.7",
|
|
||||||
"battery_enabled": "false",
|
|
||||||
"battery_length": "69.0",
|
|
||||||
"battery_diameter": "18.0",
|
|
||||||
"battery_radial_clearance": "0.6",
|
|
||||||
"battery_end_clearance": "1.0",
|
|
||||||
"switch_hole_diameter": "12.0",
|
|
||||||
"switch_hole_compensation": "0.3",
|
|
||||||
"switch_cap_diameter": "8.0",
|
|
||||||
"switch_flange_diameter": "16.2",
|
|
||||||
"switch_body_length": "24.0",
|
|
||||||
"switch_body_diameter": "12.0",
|
|
||||||
"switch_wire_allowance": "3.0",
|
|
||||||
"tripod_insert_enabled": "true",
|
|
||||||
"tripod_insert_length": "6.4",
|
|
||||||
"tripod_insert_hole_diameter": "8.8",
|
|
||||||
"tripod_thread_clearance_diameter": "6.8",
|
|
||||||
"tripod_boss_diameter": "14.0",
|
|
||||||
"m25_insert_length": "4.0",
|
|
||||||
"m25_insert_hole_diameter": "3.7",
|
|
||||||
"m25_screw_clearance": "2.8",
|
|
||||||
"m25_boss_diameter": "7.0",
|
|
||||||
"insert_entry_chamfer": "0.6",
|
|
||||||
"insert_melt_relief": "0.8",
|
|
||||||
"wall_thickness": "2.4",
|
|
||||||
"floor_thickness": "2.4",
|
|
||||||
"lid_thickness": "3.2",
|
|
||||||
"corner_radius": "4.0",
|
|
||||||
"board_bottom_clearance": "5.4",
|
|
||||||
"board_side_clearance": "7.0",
|
|
||||||
"board_rear_clearance": "4.0",
|
|
||||||
"usb_board_wall_gap": "1.2",
|
|
||||||
"lid_lip_depth": "2.4",
|
|
||||||
"lid_lip_thickness": "1.2",
|
|
||||||
"fit_clearance": "0.35",
|
|
||||||
"hole_compensation": "0.2",
|
|
||||||
"lid_screw_head_diameter": "5.2",
|
|
||||||
"lid_screw_head_height": "2.5",
|
|
||||||
"usb_opening_width": "12.5",
|
|
||||||
"usb_opening_height": "7.0",
|
|
||||||
"usb_opening_radius": "1.5",
|
|
||||||
"usb_vertical_offset": "0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fileFormatVersion": "1"
|
|
||||||
}
|
|
||||||
@@ -1,872 +0,0 @@
|
|||||||
/*
|
|
||||||
* Parametric enclosure for the Adafruit ESP32 Feather V2 and FeatherWings.
|
|
||||||
*
|
|
||||||
* Coordinate system:
|
|
||||||
* X: USB-C wall (0) toward the rear of the case
|
|
||||||
* Y: non-battery side (0) toward the battery/JST side
|
|
||||||
* Z: outside bottom (0) upward
|
|
||||||
*
|
|
||||||
* Board coordinates are derived from Adafruit's Eagle PCB files:
|
|
||||||
* https://github.com/adafruit/Adafruit-ESP32-Feather-V2-PCB
|
|
||||||
* https://github.com/adafruit/Adafruit-OLED-FeatherWing-PCB
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* [Output] */
|
|
||||||
part = "assembly"; // [assembly,body,lid,buttons,insert_coupon,switch_coupon,fit_coupon]
|
|
||||||
exploded_view = 8; // [0:1:20]
|
|
||||||
show_references = true;
|
|
||||||
|
|
||||||
/* [Stack] */
|
|
||||||
top_profile = "generic"; // [generic,gps,oled128x64]
|
|
||||||
wing_count = 1; // [1:1:8]
|
|
||||||
wing_pitch = 12.25; // [7:0.1:16]
|
|
||||||
stack_height_override = 0; // [0:0.1:100]
|
|
||||||
generic_component_height = 3; // [0:0.1:30]
|
|
||||||
generic_top_gap = 2; // [0.5:0.1:10]
|
|
||||||
gps_antenna_height = 6;
|
|
||||||
gps_top_gap = 5;
|
|
||||||
oled_component_height = 2.4;
|
|
||||||
oled_display_height = 1.5;
|
|
||||||
oled_face_gap = 0.8;
|
|
||||||
|
|
||||||
/* [OLED controls] */
|
|
||||||
button_a = true;
|
|
||||||
button_b = true;
|
|
||||||
button_c = true;
|
|
||||||
button_reset = false;
|
|
||||||
button_projection = 1.5; // [0.5:0.1:3]
|
|
||||||
button_cap_size = [5, 3.2];
|
|
||||||
button_flange_size = [6.6, 3.6];
|
|
||||||
button_flange_thickness = 0.9;
|
|
||||||
button_travel = 0.5;
|
|
||||||
button_contact_extension = 0.7;
|
|
||||||
|
|
||||||
/* [Battery and switch] */
|
|
||||||
battery_enabled = false;
|
|
||||||
battery_length = 69;
|
|
||||||
battery_diameter = 18;
|
|
||||||
battery_radial_clearance = 0.6;
|
|
||||||
battery_end_clearance = 1;
|
|
||||||
switch_hole_diameter = 12;
|
|
||||||
switch_hole_compensation = 0.3;
|
|
||||||
switch_cap_diameter = 8;
|
|
||||||
switch_flange_diameter = 16.2;
|
|
||||||
switch_body_length = 24;
|
|
||||||
switch_body_diameter = 12;
|
|
||||||
switch_wire_allowance = 3;
|
|
||||||
|
|
||||||
/* [Tripod mount] */
|
|
||||||
tripod_insert_enabled = false;
|
|
||||||
tripod_insert_length = 6.4;
|
|
||||||
tripod_insert_hole_diameter = 8.8;
|
|
||||||
tripod_thread_clearance_diameter = 6.8;
|
|
||||||
tripod_boss_diameter = 14;
|
|
||||||
|
|
||||||
/* [M2.5 inserts] */
|
|
||||||
m25_insert_length = 4;
|
|
||||||
m25_insert_hole_diameter = 3.7;
|
|
||||||
m25_screw_clearance = 2.8;
|
|
||||||
m25_boss_diameter = 7;
|
|
||||||
insert_entry_chamfer = 0.6;
|
|
||||||
insert_melt_relief = 0.8;
|
|
||||||
|
|
||||||
/* [Shell and fit] */
|
|
||||||
wall_thickness = 2.4;
|
|
||||||
floor_thickness = 2.4;
|
|
||||||
lid_thickness = 3.2;
|
|
||||||
corner_radius = 4;
|
|
||||||
board_bottom_clearance = 5.4;
|
|
||||||
board_side_clearance = 7;
|
|
||||||
board_rear_clearance = 4;
|
|
||||||
usb_board_wall_gap = 1.2;
|
|
||||||
lid_lip_depth = 2.4;
|
|
||||||
lid_lip_thickness = 1.2;
|
|
||||||
fit_clearance = 0.35;
|
|
||||||
hole_compensation = 0.2;
|
|
||||||
lid_screw_head_diameter = 5.2;
|
|
||||||
lid_screw_head_height = 2.5;
|
|
||||||
|
|
||||||
/* [USB-C opening] */
|
|
||||||
usb_opening_width = 12.5;
|
|
||||||
usb_opening_height = 7;
|
|
||||||
usb_opening_radius = 1.5;
|
|
||||||
usb_vertical_offset = 0;
|
|
||||||
|
|
||||||
/* [Custom wall cutouts] */
|
|
||||||
// Entry format: [face, shape, u, z, width, height, radius]
|
|
||||||
// face: front, rear, left, right. u is Y on front/rear and X on left/right.
|
|
||||||
// shape: circle, rect, roundrect, slot. Circle uses width as its diameter.
|
|
||||||
extra_cutouts = [];
|
|
||||||
|
|
||||||
/* [Calibration] */
|
|
||||||
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
|
||||||
|
|
||||||
$fn = $preview ? 36 : 72;
|
|
||||||
eps = 0.02;
|
|
||||||
|
|
||||||
// Adafruit Feather mechanical reference, millimetres.
|
|
||||||
feather_length = 50.8;
|
|
||||||
feather_width = 22.86;
|
|
||||||
board_thickness = 1.6;
|
|
||||||
mount_x = [2.54, 48.26];
|
|
||||||
mount_y = [2.54, 20.32];
|
|
||||||
usb_center_y = 11.43;
|
|
||||||
|
|
||||||
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
|
||||||
oled_visible_min = [11.42, 3.78];
|
|
||||||
oled_visible_size = [30, 15.3];
|
|
||||||
oled_button_a_xy = [4.318, 15.875];
|
|
||||||
oled_button_b_xy = [4.318, 11.43];
|
|
||||||
oled_button_c_xy = [4.318, 6.985];
|
|
||||||
oled_button_reset_xy = [7.366, 20.828];
|
|
||||||
|
|
||||||
effective_floor = tripod_insert_enabled
|
|
||||||
? max(floor_thickness, tripod_insert_length + insert_melt_relief + 1.2)
|
|
||||||
: floor_thickness;
|
|
||||||
effective_board_clearance = max(
|
|
||||||
board_bottom_clearance,
|
|
||||||
m25_insert_length + insert_melt_relief + 0.6
|
|
||||||
);
|
|
||||||
|
|
||||||
board_origin_x = wall_thickness + usb_board_wall_gap;
|
|
||||||
board_origin_y = wall_thickness + board_side_clearance;
|
|
||||||
board_bottom_z = effective_floor + effective_board_clearance;
|
|
||||||
board_top_z = board_bottom_z + board_thickness;
|
|
||||||
|
|
||||||
computed_top_pcb_z = board_top_z + wing_count * wing_pitch;
|
|
||||||
top_pcb_z = stack_height_override > 0
|
|
||||||
? board_top_z + stack_height_override
|
|
||||||
: computed_top_pcb_z;
|
|
||||||
profile_component_height =
|
|
||||||
top_profile == "gps" ? gps_antenna_height :
|
|
||||||
top_profile == "oled128x64" ? oled_component_height :
|
|
||||||
generic_component_height;
|
|
||||||
profile_top_gap =
|
|
||||||
top_profile == "gps" ? gps_top_gap :
|
|
||||||
top_profile == "oled128x64" ? oled_face_gap :
|
|
||||||
generic_top_gap;
|
|
||||||
|
|
||||||
partition_y = board_origin_y + feather_width + 1;
|
|
||||||
battery_center_y = partition_y + wall_thickness + battery_radial_clearance
|
|
||||||
+ battery_diameter / 2;
|
|
||||||
battery_start_x = board_origin_x + battery_end_clearance;
|
|
||||||
battery_end_x = battery_start_x + battery_length;
|
|
||||||
switch_center_x = battery_end_x + max(
|
|
||||||
switch_body_diameter / 2 + 0.8,
|
|
||||||
switch_flange_diameter / 2 + 0.5
|
|
||||||
);
|
|
||||||
|
|
||||||
case_length_plain = board_origin_x + feather_length
|
|
||||||
+ board_rear_clearance + wall_thickness;
|
|
||||||
case_length_battery = switch_center_x + switch_flange_diameter / 2
|
|
||||||
+ 1.2 + wall_thickness;
|
|
||||||
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
|
||||||
: case_length_plain;
|
|
||||||
|
|
||||||
case_width_plain = board_origin_y + feather_width
|
|
||||||
+ board_side_clearance + wall_thickness;
|
|
||||||
case_width_battery = battery_center_y + battery_diameter / 2
|
|
||||||
+ battery_radial_clearance + wall_thickness;
|
|
||||||
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
|
||||||
: case_width_plain;
|
|
||||||
|
|
||||||
battery_center_z = effective_floor + battery_radial_clearance
|
|
||||||
+ battery_diameter / 2;
|
|
||||||
switch_center_z = max(
|
|
||||||
effective_floor + switch_flange_diameter / 2 + 1,
|
|
||||||
battery_center_z
|
|
||||||
);
|
|
||||||
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
|
||||||
battery_ceiling_z = battery_enabled
|
|
||||||
? max(
|
|
||||||
battery_center_z + battery_diameter / 2 + battery_radial_clearance,
|
|
||||||
switch_center_z + switch_flange_diameter / 2 + 1
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
body_height = max(electronics_ceiling_z, battery_ceiling_z);
|
|
||||||
|
|
||||||
lid_boss_r = m25_boss_diameter / 2;
|
|
||||||
lid_boss_wall_overlap = 0.6;
|
|
||||||
lid_boss_xy = [
|
|
||||||
[
|
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap
|
|
||||||
],
|
|
||||||
[
|
|
||||||
case_length - wall_thickness - lid_boss_r + lid_boss_wall_overlap,
|
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap
|
|
||||||
],
|
|
||||||
[
|
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
|
||||||
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
|
||||||
],
|
|
||||||
[
|
|
||||||
case_length - wall_thickness - lid_boss_r + lid_boss_wall_overlap,
|
|
||||||
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
assert(wing_count >= 1, "wing_count must be at least one");
|
|
||||||
assert(top_profile == "generic" || top_profile == "gps"
|
|
||||||
|| top_profile == "oled128x64", "Unsupported top_profile");
|
|
||||||
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
|
||||||
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
|
||||||
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
|
||||||
"M2.5 boss does not have enough radial material");
|
|
||||||
assert(!tripod_insert_enabled
|
|
||||||
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
|
||||||
"Tripod insert boss does not have enough radial material");
|
|
||||||
|
|
||||||
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
|
||||||
|
|
||||||
module rounded_rect_2d(size, radius) {
|
|
||||||
r = clamp(radius, 0.01, min(size[0], size[1]) / 2 - 0.01);
|
|
||||||
hull() {
|
|
||||||
for (x = [r, size[0] - r], y = [r, size[1] - r])
|
|
||||||
translate([x, y]) circle(r = r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module centered_rounded_rect_2d(size, radius) {
|
|
||||||
translate([-size[0] / 2, -size[1] / 2])
|
|
||||||
rounded_rect_2d(size, radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
module centered_rounded_prism(size, height, radius) {
|
|
||||||
linear_extrude(height = height)
|
|
||||||
centered_rounded_rect_2d(size, radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
module rounded_box(size, radius) {
|
|
||||||
linear_extrude(height = size[2])
|
|
||||||
rounded_rect_2d([size[0], size[1]], radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|
||||||
bore_d = diameter + hole_compensation;
|
|
||||||
if (downward) {
|
|
||||||
translate([0, 0, entry_z - depth])
|
|
||||||
cylinder(h = depth + eps, d = bore_d);
|
|
||||||
translate([0, 0, entry_z - insert_entry_chamfer])
|
|
||||||
cylinder(
|
|
||||||
h = insert_entry_chamfer + eps,
|
|
||||||
d1 = bore_d,
|
|
||||||
d2 = bore_d + 2 * insert_entry_chamfer
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
translate([0, 0, entry_z - eps])
|
|
||||||
cylinder(h = depth + eps, d = bore_d);
|
|
||||||
translate([0, 0, entry_z - eps])
|
|
||||||
cylinder(
|
|
||||||
h = insert_entry_chamfer + eps,
|
|
||||||
d1 = bore_d + 2 * insert_entry_chamfer,
|
|
||||||
d2 = bore_d
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module face_rounded_cutout(face, u, z, width, height, radius) {
|
|
||||||
r = clamp(radius, 0.01, min(width, height) / 2);
|
|
||||||
depth = wall_thickness + 2 * eps;
|
|
||||||
if (face == "front" || face == "rear") {
|
|
||||||
x0 = face == "front" ? -eps : case_length - wall_thickness - eps;
|
|
||||||
hull()
|
|
||||||
for (yy = [u - width / 2 + r, u + width / 2 - r],
|
|
||||||
zz = [z - height / 2 + r, z + height / 2 - r])
|
|
||||||
translate([x0, yy, zz])
|
|
||||||
rotate([0, 90, 0]) cylinder(h = depth, r = r);
|
|
||||||
} else if (face == "left" || face == "right") {
|
|
||||||
y0 = face == "left" ? -eps : case_width - wall_thickness - eps;
|
|
||||||
hull()
|
|
||||||
for (xx = [u - width / 2 + r, u + width / 2 - r],
|
|
||||||
zz = [z - height / 2 + r, z + height / 2 - r])
|
|
||||||
translate([xx, y0, zz])
|
|
||||||
rotate([-90, 0, 0]) cylinder(h = depth, r = r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module face_circle_cutout(face, u, z, diameter) {
|
|
||||||
depth = wall_thickness + 2 * eps;
|
|
||||||
if (face == "front")
|
|
||||||
translate([-eps, u, z])
|
|
||||||
rotate([0, 90, 0]) cylinder(h = depth, d = diameter);
|
|
||||||
else if (face == "rear")
|
|
||||||
translate([case_length - wall_thickness - eps, u, z])
|
|
||||||
rotate([0, 90, 0]) cylinder(h = depth, d = diameter);
|
|
||||||
else if (face == "left")
|
|
||||||
translate([u, -eps, z])
|
|
||||||
rotate([-90, 0, 0]) cylinder(h = depth, d = diameter);
|
|
||||||
else if (face == "right")
|
|
||||||
translate([u, case_width - wall_thickness - eps, z])
|
|
||||||
rotate([-90, 0, 0]) cylinder(h = depth, d = diameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
module custom_wall_cutout(entry) {
|
|
||||||
face = entry[0];
|
|
||||||
shape = entry[1];
|
|
||||||
u = entry[2];
|
|
||||||
z = entry[3];
|
|
||||||
width = entry[4];
|
|
||||||
height = len(entry) > 5 ? entry[5] : width;
|
|
||||||
radius = len(entry) > 6 ? entry[6] : min(width, height) / 2;
|
|
||||||
assert(face == "front" || face == "rear"
|
|
||||||
|| face == "left" || face == "right", "Invalid cutout face");
|
|
||||||
assert(shape == "circle" || shape == "rect"
|
|
||||||
|| shape == "roundrect" || shape == "slot", "Invalid cutout shape");
|
|
||||||
assert(z - height / 2 >= effective_floor,
|
|
||||||
"Cutout intersects the floor");
|
|
||||||
assert(z + height / 2 <= body_height - lid_lip_depth,
|
|
||||||
"Cutout intersects the lid locating lip");
|
|
||||||
assert(
|
|
||||||
(face == "front" || face == "rear")
|
|
||||||
? u - width / 2 >= 0 && u + width / 2 <= case_width
|
|
||||||
: u - width / 2 >= 0 && u + width / 2 <= case_length,
|
|
||||||
"Cutout extends beyond its wall"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shape == "circle")
|
|
||||||
face_circle_cutout(face, u, z, width);
|
|
||||||
else
|
|
||||||
face_rounded_cutout(
|
|
||||||
face, u, z, width, height,
|
|
||||||
shape == "rect" ? 0.02 :
|
|
||||||
shape == "slot" ? min(width, height) / 2 : radius
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module board_standoffs() {
|
|
||||||
for (mx = mount_x, my = mount_y)
|
|
||||||
translate([
|
|
||||||
board_origin_x + mx,
|
|
||||||
board_origin_y + my,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cylinder(
|
|
||||||
h = effective_board_clearance + eps,
|
|
||||||
d = m25_boss_diameter
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module lid_bosses() {
|
|
||||||
for (p = lid_boss_xy)
|
|
||||||
translate([p[0], p[1], effective_floor - eps])
|
|
||||||
cylinder(
|
|
||||||
h = body_height - effective_floor + eps,
|
|
||||||
d = m25_boss_diameter
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module battery_cradle() {
|
|
||||||
partition_height = min(
|
|
||||||
body_height - lid_lip_depth - 0.5,
|
|
||||||
battery_center_z + battery_diameter / 2 + 1.5
|
|
||||||
);
|
|
||||||
rib_width = 2.4;
|
|
||||||
rib_positions = [
|
|
||||||
battery_start_x + 4,
|
|
||||||
(battery_start_x + battery_end_x) / 2,
|
|
||||||
battery_end_x - 4
|
|
||||||
];
|
|
||||||
|
|
||||||
// Board/battery partition, with a lead-routing notch near the JST end.
|
|
||||||
difference() {
|
|
||||||
translate([wall_thickness, partition_y, effective_floor - eps])
|
|
||||||
cube([
|
|
||||||
battery_end_x - wall_thickness + battery_end_clearance,
|
|
||||||
wall_thickness,
|
|
||||||
partition_height - effective_floor + eps
|
|
||||||
]);
|
|
||||||
translate([
|
|
||||||
board_origin_x + 6.5,
|
|
||||||
partition_y - eps,
|
|
||||||
effective_floor + 1.5
|
|
||||||
])
|
|
||||||
cube([9, wall_thickness + 2 * eps, 6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Three low curved ribs cradle the protected cylindrical pack.
|
|
||||||
for (x = rib_positions)
|
|
||||||
difference() {
|
|
||||||
translate([
|
|
||||||
x - rib_width / 2,
|
|
||||||
battery_center_y - battery_diameter / 2
|
|
||||||
- battery_radial_clearance,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
rib_width,
|
|
||||||
battery_diameter + 2 * battery_radial_clearance,
|
|
||||||
battery_diameter / 2 + battery_radial_clearance + eps
|
|
||||||
]);
|
|
||||||
translate([x - rib_width, battery_center_y, battery_center_z])
|
|
||||||
rotate([0, 90, 0])
|
|
||||||
cylinder(
|
|
||||||
h = rib_width * 2,
|
|
||||||
d = battery_diameter + 2 * battery_radial_clearance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// End stops remain below the cell center so the lid can remove vertically.
|
|
||||||
for (x = [
|
|
||||||
battery_start_x - battery_end_clearance - wall_thickness,
|
|
||||||
battery_end_x + battery_end_clearance
|
|
||||||
])
|
|
||||||
translate([
|
|
||||||
x,
|
|
||||||
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
wall_thickness,
|
|
||||||
battery_diameter + 2 * battery_radial_clearance,
|
|
||||||
battery_diameter / 2 + 1 + eps
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Rear cross-strip separates the side-mounted switch body from the cell.
|
|
||||||
translate([
|
|
||||||
battery_end_x + battery_end_clearance,
|
|
||||||
partition_y,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
case_length - wall_thickness - battery_end_x - battery_end_clearance,
|
|
||||||
case_width - wall_thickness - partition_y,
|
|
||||||
min(body_height - effective_floor, switch_center_z
|
|
||||||
+ switch_body_diameter / 2 + switch_wire_allowance
|
|
||||||
- effective_floor) + eps
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
module body_positive() {
|
|
||||||
union() {
|
|
||||||
difference() {
|
|
||||||
rounded_box([case_length, case_width, body_height], corner_radius);
|
|
||||||
translate([wall_thickness, wall_thickness, effective_floor])
|
|
||||||
rounded_box([
|
|
||||||
case_length - 2 * wall_thickness,
|
|
||||||
case_width - 2 * wall_thickness,
|
|
||||||
body_height - effective_floor + eps
|
|
||||||
], max(0.6, corner_radius - wall_thickness));
|
|
||||||
}
|
|
||||||
board_standoffs();
|
|
||||||
lid_bosses();
|
|
||||||
if (battery_enabled)
|
|
||||||
battery_cradle();
|
|
||||||
if (tripod_insert_enabled)
|
|
||||||
translate([case_length / 2, case_width / 2, 0])
|
|
||||||
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module body_cutouts() {
|
|
||||||
// Board mounting insert pockets, loaded from above before the Feather.
|
|
||||||
for (mx = mount_x, my = mount_y)
|
|
||||||
translate([board_origin_x + mx, board_origin_y + my, 0])
|
|
||||||
insert_pocket(
|
|
||||||
m25_insert_hole_diameter,
|
|
||||||
m25_insert_length + insert_melt_relief,
|
|
||||||
board_bottom_z
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lid insert pockets, loaded from the top of the body.
|
|
||||||
for (p = lid_boss_xy)
|
|
||||||
translate([p[0], p[1], 0])
|
|
||||||
insert_pocket(
|
|
||||||
m25_insert_hole_diameter,
|
|
||||||
m25_insert_length + insert_melt_relief,
|
|
||||||
body_height
|
|
||||||
);
|
|
||||||
|
|
||||||
// USB-C cable opening. Connector center is from the official ESP32 V2 PCB.
|
|
||||||
usb_center_z = board_top_z + 1.6 + usb_vertical_offset;
|
|
||||||
face_rounded_cutout(
|
|
||||||
"front",
|
|
||||||
board_origin_y + usb_center_y,
|
|
||||||
usb_center_z,
|
|
||||||
usb_opening_width + hole_compensation,
|
|
||||||
usb_opening_height + hole_compensation,
|
|
||||||
usb_opening_radius
|
|
||||||
);
|
|
||||||
|
|
||||||
if (battery_enabled) {
|
|
||||||
face_circle_cutout(
|
|
||||||
"right",
|
|
||||||
switch_center_x,
|
|
||||||
switch_center_z,
|
|
||||||
switch_hole_diameter + switch_hole_compensation
|
|
||||||
);
|
|
||||||
// Clear the switch body and its prewired terminals through the rear bay.
|
|
||||||
translate([
|
|
||||||
switch_center_x,
|
|
||||||
case_width - wall_thickness + eps,
|
|
||||||
switch_center_z
|
|
||||||
])
|
|
||||||
rotate([90, 0, 0])
|
|
||||||
cylinder(
|
|
||||||
h = switch_body_length + switch_wire_allowance
|
|
||||||
+ wall_thickness + eps,
|
|
||||||
d = switch_body_diameter + 2 * fit_clearance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tripod_insert_enabled) {
|
|
||||||
translate([case_length / 2, case_width / 2, 0]) {
|
|
||||||
translate([0, 0, -eps])
|
|
||||||
cylinder(
|
|
||||||
h = effective_floor + 2 * eps,
|
|
||||||
d = tripod_thread_clearance_diameter + hole_compensation
|
|
||||||
);
|
|
||||||
insert_pocket(
|
|
||||||
tripod_insert_hole_diameter,
|
|
||||||
tripod_insert_length + insert_melt_relief,
|
|
||||||
effective_floor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (entry = extra_cutouts)
|
|
||||||
custom_wall_cutout(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
module body() {
|
|
||||||
difference() {
|
|
||||||
body_positive();
|
|
||||||
body_cutouts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module lid_plate_positive() {
|
|
||||||
union() {
|
|
||||||
rounded_box([case_length, case_width, lid_thickness], corner_radius);
|
|
||||||
// Inset rim: exported pointing upward so the outer lid face prints flat.
|
|
||||||
translate([
|
|
||||||
wall_thickness + fit_clearance,
|
|
||||||
wall_thickness + fit_clearance,
|
|
||||||
lid_thickness
|
|
||||||
])
|
|
||||||
linear_extrude(height = lid_lip_depth)
|
|
||||||
difference() {
|
|
||||||
rounded_rect_2d([
|
|
||||||
case_length - 2 * (wall_thickness + fit_clearance),
|
|
||||||
case_width - 2 * (wall_thickness + fit_clearance)
|
|
||||||
], max(0.6, corner_radius - wall_thickness));
|
|
||||||
translate([lid_lip_thickness, lid_lip_thickness])
|
|
||||||
rounded_rect_2d([
|
|
||||||
case_length - 2 * (
|
|
||||||
wall_thickness + fit_clearance
|
|
||||||
+ lid_lip_thickness
|
|
||||||
),
|
|
||||||
case_width - 2 * (
|
|
||||||
wall_thickness + fit_clearance
|
|
||||||
+ lid_lip_thickness
|
|
||||||
)
|
|
||||||
], max(0.5, corner_radius - wall_thickness
|
|
||||||
- lid_lip_thickness));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module beveled_oled_window() {
|
|
||||||
inner_size = oled_visible_size;
|
|
||||||
outer_size = [oled_visible_size[0] + 3, oled_visible_size[1] + 3];
|
|
||||||
center = [
|
|
||||||
board_origin_x + oled_visible_min[0] + oled_visible_size[0] / 2,
|
|
||||||
board_origin_y + oled_visible_min[1] + oled_visible_size[1] / 2
|
|
||||||
];
|
|
||||||
hull() {
|
|
||||||
translate([center[0], center[1], -eps])
|
|
||||||
linear_extrude(height = eps)
|
|
||||||
centered_rounded_rect_2d(outer_size, 1.5);
|
|
||||||
translate([center[0], center[1], lid_thickness - eps])
|
|
||||||
linear_extrude(height = 2 * eps)
|
|
||||||
centered_rounded_rect_2d(inner_size, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module oled_button_lid_cutout(xy) {
|
|
||||||
center = [board_origin_x + xy[0], board_origin_y + xy[1]];
|
|
||||||
shaft_size = [
|
|
||||||
button_cap_size[0] + fit_clearance * 2,
|
|
||||||
button_cap_size[1] + fit_clearance * 2
|
|
||||||
];
|
|
||||||
recess_size = [
|
|
||||||
button_flange_size[0] + fit_clearance * 2,
|
|
||||||
button_flange_size[1] + fit_clearance * 2
|
|
||||||
];
|
|
||||||
recess_depth = button_flange_thickness + button_travel;
|
|
||||||
translate([center[0], center[1], -eps])
|
|
||||||
centered_rounded_prism(
|
|
||||||
shaft_size,
|
|
||||||
lid_thickness + 2 * eps,
|
|
||||||
shaft_size[1] / 2
|
|
||||||
);
|
|
||||||
translate([center[0], center[1], lid_thickness - recess_depth])
|
|
||||||
centered_rounded_prism(
|
|
||||||
recess_size,
|
|
||||||
recess_depth + lid_lip_depth + eps,
|
|
||||||
recess_size[1] / 2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module lid_cutouts() {
|
|
||||||
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
|
||||||
for (p = lid_boss_xy) {
|
|
||||||
translate([p[0], p[1], -eps])
|
|
||||||
cylinder(
|
|
||||||
h = lid_thickness + lid_lip_depth + 2 * eps,
|
|
||||||
d = m25_screw_clearance + hole_compensation
|
|
||||||
);
|
|
||||||
translate([p[0], p[1], -eps])
|
|
||||||
cylinder(
|
|
||||||
h = lid_screw_head_height + eps,
|
|
||||||
d = lid_screw_head_diameter + hole_compensation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (top_profile == "oled128x64") {
|
|
||||||
beveled_oled_window();
|
|
||||||
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
|
||||||
if (button_b) oled_button_lid_cutout(oled_button_b_xy);
|
|
||||||
if (button_c) oled_button_lid_cutout(oled_button_c_xy);
|
|
||||||
if (button_reset) oled_button_lid_cutout(oled_button_reset_xy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module lid() {
|
|
||||||
difference() {
|
|
||||||
lid_plate_positive();
|
|
||||||
lid_cutouts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module one_oled_button() {
|
|
||||||
recess_depth = button_flange_thickness + button_travel;
|
|
||||||
stem_height = lid_thickness - recess_depth + button_projection;
|
|
||||||
// Print outer cap face down. The flange and contact nub build upward.
|
|
||||||
centered_rounded_prism(
|
|
||||||
button_cap_size,
|
|
||||||
stem_height,
|
|
||||||
button_cap_size[1] / 2
|
|
||||||
);
|
|
||||||
translate([0, 0, stem_height])
|
|
||||||
centered_rounded_prism(
|
|
||||||
button_flange_size,
|
|
||||||
button_flange_thickness,
|
|
||||||
button_flange_size[1] / 2
|
|
||||||
);
|
|
||||||
translate([0, 0, stem_height + button_flange_thickness])
|
|
||||||
cylinder(h = button_contact_extension, d = 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
module buttons() {
|
|
||||||
enabled = [
|
|
||||||
button_a,
|
|
||||||
button_b,
|
|
||||||
button_c,
|
|
||||||
button_reset
|
|
||||||
];
|
|
||||||
count_enabled = len([for (v = enabled) if (v) 1]);
|
|
||||||
if (count_enabled == 0)
|
|
||||||
echo("No OLED buttons are enabled.");
|
|
||||||
for (i = [0 : len(enabled) - 1])
|
|
||||||
if (enabled[i])
|
|
||||||
translate([i * (button_flange_size[0] + 3), 0, 0])
|
|
||||||
one_oled_button();
|
|
||||||
}
|
|
||||||
|
|
||||||
module reference_board() {
|
|
||||||
color([0.05, 0.25, 0.55, 0.75])
|
|
||||||
translate([board_origin_x, board_origin_y, board_bottom_z])
|
|
||||||
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
|
||||||
color([0.12, 0.45, 0.75, 0.45])
|
|
||||||
for (i = [1 : wing_count])
|
|
||||||
translate([
|
|
||||||
board_origin_x,
|
|
||||||
board_origin_y,
|
|
||||||
board_top_z + i * wing_pitch - board_thickness
|
|
||||||
])
|
|
||||||
rounded_box(
|
|
||||||
[feather_length, feather_width, board_thickness], 2.54
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module reference_top_feature() {
|
|
||||||
if (top_profile == "gps")
|
|
||||||
color([0.92, 0.78, 0.35, 0.8])
|
|
||||||
translate([
|
|
||||||
board_origin_x + 31,
|
|
||||||
board_origin_y + (feather_width - 15) / 2,
|
|
||||||
top_pcb_z
|
|
||||||
])
|
|
||||||
cube([15, 15, gps_antenna_height]);
|
|
||||||
else if (top_profile == "oled128x64") {
|
|
||||||
color([0.12, 0.12, 0.12, 0.9])
|
|
||||||
translate([
|
|
||||||
board_origin_x + oled_visible_min[0],
|
|
||||||
board_origin_y + oled_visible_min[1],
|
|
||||||
top_pcb_z
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
oled_visible_size[0],
|
|
||||||
oled_visible_size[1],
|
|
||||||
oled_display_height
|
|
||||||
]);
|
|
||||||
color([0.6, 0.6, 0.6, 0.9])
|
|
||||||
for (xy = [
|
|
||||||
oled_button_a_xy,
|
|
||||||
oled_button_b_xy,
|
|
||||||
oled_button_c_xy,
|
|
||||||
oled_button_reset_xy
|
|
||||||
])
|
|
||||||
translate([
|
|
||||||
board_origin_x + xy[0],
|
|
||||||
board_origin_y + xy[1],
|
|
||||||
top_pcb_z
|
|
||||||
])
|
|
||||||
cylinder(h = 1.6, d = 2.1);
|
|
||||||
} else
|
|
||||||
color([0.3, 0.65, 0.35, 0.6])
|
|
||||||
translate([
|
|
||||||
board_origin_x + 10,
|
|
||||||
board_origin_y + 4,
|
|
||||||
top_pcb_z
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
feather_length - 20,
|
|
||||||
feather_width - 8,
|
|
||||||
generic_component_height
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
module reference_battery_and_switch() {
|
|
||||||
if (battery_enabled) {
|
|
||||||
color([0.35, 0.18, 0.55, 0.75])
|
|
||||||
translate([battery_start_x, battery_center_y, battery_center_z])
|
|
||||||
rotate([0, 90, 0])
|
|
||||||
cylinder(h = battery_length, d = battery_diameter);
|
|
||||||
color([0.8, 0.15, 0.1, 0.65])
|
|
||||||
translate([
|
|
||||||
switch_center_x,
|
|
||||||
case_width - wall_thickness,
|
|
||||||
switch_center_z
|
|
||||||
])
|
|
||||||
rotate([90, 0, 0]) {
|
|
||||||
cylinder(h = switch_body_length, d = switch_body_diameter);
|
|
||||||
translate([0, 0, -1.5])
|
|
||||||
cylinder(h = 1.5, d = switch_flange_diameter);
|
|
||||||
translate([0, 0, -3])
|
|
||||||
cylinder(h = 3, d = switch_cap_diameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module assembly() {
|
|
||||||
color([0.82, 0.84, 0.88, 1]) body();
|
|
||||||
if ($preview && show_references) {
|
|
||||||
reference_board();
|
|
||||||
reference_top_feature();
|
|
||||||
reference_battery_and_switch();
|
|
||||||
}
|
|
||||||
// Printable lid is mirrored into its installed orientation and exploded up.
|
|
||||||
color([0.92, 0.92, 0.95, 0.88])
|
|
||||||
translate([0, case_width, body_height + lid_thickness + exploded_view])
|
|
||||||
rotate([180, 0, 0])
|
|
||||||
lid();
|
|
||||||
}
|
|
||||||
|
|
||||||
module coupon_hole(x, y, diameter, depth) {
|
|
||||||
translate([x, y, -eps]) cylinder(h = depth + 2 * eps, d = diameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
module insert_coupon() {
|
|
||||||
spacing = 15;
|
|
||||||
coupon_length = spacing * len(coupon_steps) + 8;
|
|
||||||
coupon_width = 32;
|
|
||||||
coupon_height = max(
|
|
||||||
m25_insert_length + insert_melt_relief + 1.2,
|
|
||||||
tripod_insert_length + insert_melt_relief + 1.2
|
|
||||||
);
|
|
||||||
difference() {
|
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
|
||||||
for (i = [0 : len(coupon_steps) - 1]) {
|
|
||||||
x = 7 + i * spacing;
|
|
||||||
coupon_hole(
|
|
||||||
x, 8,
|
|
||||||
m25_insert_hole_diameter + coupon_steps[i],
|
|
||||||
m25_insert_length + insert_melt_relief
|
|
||||||
);
|
|
||||||
coupon_hole(
|
|
||||||
x, 23,
|
|
||||||
tripod_insert_hole_diameter + coupon_steps[i],
|
|
||||||
tripod_insert_length + insert_melt_relief
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module switch_coupon() {
|
|
||||||
spacing = 18;
|
|
||||||
coupon_length = spacing * len(coupon_steps) + 6;
|
|
||||||
coupon_width = 24;
|
|
||||||
coupon_height = wall_thickness;
|
|
||||||
difference() {
|
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
|
||||||
for (i = [0 : len(coupon_steps) - 1])
|
|
||||||
translate([6 + i * spacing, coupon_width / 2, -eps])
|
|
||||||
cylinder(
|
|
||||||
h = coupon_height + 2 * eps,
|
|
||||||
d = switch_hole_diameter + coupon_steps[i]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module fit_coupon() {
|
|
||||||
segment_length = 34;
|
|
||||||
base_width = 16;
|
|
||||||
base_height = 6;
|
|
||||||
cavity_size = [segment_length - 8, base_width - 8];
|
|
||||||
male_size = [
|
|
||||||
cavity_size[0] - 2 * fit_clearance,
|
|
||||||
cavity_size[1] - 2 * fit_clearance
|
|
||||||
];
|
|
||||||
difference() {
|
|
||||||
rounded_box([segment_length, base_width, base_height], 2);
|
|
||||||
translate([4, 4, 2])
|
|
||||||
cube([segment_length - 8, base_width - 8, base_height]);
|
|
||||||
}
|
|
||||||
translate([segment_length + 5, 0, 0])
|
|
||||||
union() {
|
|
||||||
cube([segment_length, base_width, lid_thickness]);
|
|
||||||
translate([
|
|
||||||
4 + fit_clearance,
|
|
||||||
4 + fit_clearance,
|
|
||||||
lid_thickness - eps
|
|
||||||
])
|
|
||||||
linear_extrude(height = lid_lip_depth + eps)
|
|
||||||
difference() {
|
|
||||||
rounded_rect_2d(male_size, 1);
|
|
||||||
translate([lid_lip_thickness, lid_lip_thickness])
|
|
||||||
rounded_rect_2d([
|
|
||||||
male_size[0] - 2 * lid_lip_thickness,
|
|
||||||
male_size[1] - 2 * lid_lip_thickness
|
|
||||||
], 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part == "assembly")
|
|
||||||
assembly();
|
|
||||||
else if (part == "body")
|
|
||||||
body();
|
|
||||||
else if (part == "lid")
|
|
||||||
lid();
|
|
||||||
else if (part == "buttons")
|
|
||||||
buttons();
|
|
||||||
else if (part == "insert_coupon")
|
|
||||||
insert_coupon();
|
|
||||||
else if (part == "switch_coupon")
|
|
||||||
switch_coupon();
|
|
||||||
else if (part == "fit_coupon")
|
|
||||||
fit_coupon();
|
|
||||||
else
|
|
||||||
assert(false, str("Unknown part: ", part));
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Attitude no battery
|
||||||
|
|
||||||
|
Generated by the Feather Case Wizard.
|
||||||
|
|
||||||
|
- Top profile: `generic`
|
||||||
|
- FeatherWings: 1
|
||||||
|
- Battery compartment: no
|
||||||
|
- 1/4-20 insert: yes
|
||||||
|
- Approximate exterior: 61.6 × 41.7 × 34.4 mm
|
||||||
|
- Formats: 3mf
|
||||||
|
- Parts: `body`, `lid`, `lid_base`, `lid_accent`
|
||||||
|
- Custom cutouts: 0
|
||||||
|
|
||||||
|
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
||||||
|
OpenSCAD, Blender, and Python 3 must be installed. Set `OPENSCAD`,
|
||||||
|
`BLENDER`, or `PYTHON` to override their executable paths.
|
||||||
|
|
||||||
|
To open the saved Customizer values manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openscad -p parameters.json -P attitude-no-battery source/feather_case.scad
|
||||||
|
```
|
||||||
|
|
||||||
|
The 3MF files contain portable geometry only. Choose printer, filament,
|
||||||
|
plate, and slicing settings in Bambu Studio.
|
||||||
|
|
||||||
|
For a two-color lid, open `exports/3mf/lid_multicolor.3mf` as one
|
||||||
|
multipart object. Assign “Lid base — Filament 1” and “Accent inlays —
|
||||||
|
Filament 2” to the desired filaments. `lid.3mf` is the complete
|
||||||
|
single-color fallback; `lid_base.3mf` and `lid_accent.3mf` are also
|
||||||
|
provided separately.
|
||||||
@@ -44,13 +44,18 @@ config = YAML.safe_load_file(ROOT.join("case.yml"), permitted_classes: [],
|
|||||||
permitted_symbols: [], aliases: false)
|
permitted_symbols: [], aliases: false)
|
||||||
openscad = executable("OPENSCAD", "openscad")
|
openscad = executable("OPENSCAD", "openscad")
|
||||||
blender = executable("BLENDER", "blender")
|
blender = executable("BLENDER", "blender")
|
||||||
|
python = executable("PYTHON", "python3")
|
||||||
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
||||||
abort "Blender not found; install it or set BLENDER" unless blender
|
abort "Blender not found; install it or set BLENDER" unless blender
|
||||||
|
|
||||||
parameters = config.fetch("parameters").merge(
|
parameters = config.fetch("parameters").merge(
|
||||||
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
||||||
)
|
)
|
||||||
|
artwork = !parameters.fetch("lid_label", "").empty? ||
|
||||||
|
parameters.fetch("lid_logo_enabled", false)
|
||||||
|
abort "Python 3 not found; install it or set PYTHON" if artwork && !python
|
||||||
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
||||||
|
parts.concat(%w[lid_base lid_accent]) if artwork
|
||||||
buttons = %w[button_a button_b button_c button_reset]
|
buttons = %w[button_a button_b button_c button_reset]
|
||||||
if parameters["top_profile"] == "oled128x64" &&
|
if parameters["top_profile"] == "oled128x64" &&
|
||||||
buttons.any? { |name| parameters[name] }
|
buttons.any? { |name| parameters[name] }
|
||||||
@@ -80,6 +85,16 @@ begin
|
|||||||
puts "Exporting #{part}.#{format}…"
|
puts "Exporting #{part}.#{format}…"
|
||||||
export.call(directory.join("#{part}.#{format}"), part)
|
export.call(directory.join("#{part}.#{format}"), part)
|
||||||
end
|
end
|
||||||
|
if artwork && format == "3mf"
|
||||||
|
puts "Packaging lid_multicolor.3mf…"
|
||||||
|
run!(
|
||||||
|
python, ROOT.join("tools/package_multimaterial_3mf.py").to_s,
|
||||||
|
directory.join("lid_base.3mf").to_s,
|
||||||
|
directory.join("lid_accent.3mf").to_s,
|
||||||
|
directory.join("lid_multicolor.3mf").to_s,
|
||||||
|
"--title", "#{config.fetch("name")} Lid"
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assembly = scratch.join("assembly.stl")
|
assembly = scratch.join("assembly.stl")
|
||||||
@@ -97,6 +112,16 @@ begin
|
|||||||
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
||||||
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
||||||
end
|
end
|
||||||
|
if artwork
|
||||||
|
puts "Rendering multicolor lid preview…"
|
||||||
|
run!(
|
||||||
|
blender, "--background", "-noaudio", "--python",
|
||||||
|
ROOT.join("source/render_multicolor_lid.py").to_s, "--",
|
||||||
|
scratch.join("lid_base.stl").to_s,
|
||||||
|
scratch.join("lid_accent.stl").to_s,
|
||||||
|
previews.to_s, "lid_multicolor", parameters["lid_art_orientation"], env: render_env
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
backup = ROOT.join(".build-old-#{Process.pid}")
|
backup = ROOT.join(".build-old-#{Process.pid}")
|
||||||
FileUtils.mkdir_p(backup)
|
FileUtils.mkdir_p(backup)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
name: Attitude IMU
|
name: Attitude no battery
|
||||||
slug: attitude-imu
|
slug: attitude-no-battery
|
||||||
generator:
|
generator:
|
||||||
sha256: be611e3255e7d802719ebb0628e3d11481f8de05fc96c24d452f026d7b8e3353
|
sha256: 281917098487eeacd19a136ba27752698712e90cb4c03c6525a1ea3167c9ea13
|
||||||
created_with: feather-case-wizard
|
created_with: feather-case-wizard
|
||||||
parameters:
|
parameters:
|
||||||
top_profile: generic
|
top_profile: generic
|
||||||
@@ -36,20 +36,30 @@ parameters:
|
|||||||
battery_diameter: 18.0
|
battery_diameter: 18.0
|
||||||
battery_radial_clearance: 0.6
|
battery_radial_clearance: 0.6
|
||||||
battery_end_clearance: 1.0
|
battery_end_clearance: 1.0
|
||||||
switch_hole_diameter: 12.0
|
battery_partition_thickness: 1.8
|
||||||
switch_hole_compensation: 0.3
|
battery_board_clearance: 3.0
|
||||||
switch_cap_diameter: 8.0
|
rocker_cutout_size:
|
||||||
switch_flange_diameter: 16.2
|
- 12.9
|
||||||
switch_body_length: 24.0
|
- 19.4
|
||||||
switch_body_diameter: 12.0
|
rocker_corner_radius: 0.8
|
||||||
switch_wire_allowance: 3.0
|
rocker_body_size:
|
||||||
|
- 15.0
|
||||||
|
- 21.0
|
||||||
|
- 21.0
|
||||||
|
rocker_artwork_gap: 2.0
|
||||||
|
rocker_partition_offset: 2.0
|
||||||
|
rocker_tab_panel_thickness: 2.0
|
||||||
|
rocker_tab_land: 2.0
|
||||||
|
rocker_tab_taper_length: 0.6
|
||||||
tripod_insert_enabled: true
|
tripod_insert_enabled: true
|
||||||
tripod_insert_length: 6.4
|
tripod_insert_length: 5.0
|
||||||
tripod_insert_hole_diameter: 8.8
|
tripod_insert_hole_diameter: 7.2
|
||||||
|
tripod_insert_outer_diameter: 8.0
|
||||||
tripod_thread_clearance_diameter: 6.8
|
tripod_thread_clearance_diameter: 6.8
|
||||||
tripod_boss_diameter: 14.0
|
tripod_boss_diameter: 14.0
|
||||||
m25_insert_length: 4.0
|
m25_insert_length: 4.0
|
||||||
m25_insert_hole_diameter: 3.7
|
m25_insert_hole_diameter: 3.1
|
||||||
|
m25_insert_outer_diameter: 3.5
|
||||||
m25_screw_clearance: 2.8
|
m25_screw_clearance: 2.8
|
||||||
m25_boss_diameter: 7.0
|
m25_boss_diameter: 7.0
|
||||||
insert_entry_chamfer: 0.6
|
insert_entry_chamfer: 0.6
|
||||||
@@ -61,17 +71,31 @@ parameters:
|
|||||||
board_bottom_clearance: 5.4
|
board_bottom_clearance: 5.4
|
||||||
board_side_clearance: 7.0
|
board_side_clearance: 7.0
|
||||||
board_rear_clearance: 4.0
|
board_rear_clearance: 4.0
|
||||||
usb_board_wall_gap: 1.2
|
usb_board_wall_gap: 2.0
|
||||||
lid_lip_depth: 2.4
|
lid_lip_depth: 2.4
|
||||||
lid_lip_thickness: 1.2
|
lid_lip_thickness: 1.2
|
||||||
fit_clearance: 0.35
|
fit_clearance: 0.35
|
||||||
|
cutout_min_web: 1.2
|
||||||
hole_compensation: 0.2
|
hole_compensation: 0.2
|
||||||
lid_screw_head_diameter: 5.2
|
lid_screw_head_diameter: 5.2
|
||||||
lid_screw_head_height: 2.5
|
lid_screw_head_height: 2.5
|
||||||
usb_opening_width: 12.5
|
usb_opening_enabled: true
|
||||||
usb_opening_height: 7.0
|
usb_guide_enabled: true
|
||||||
|
usb_connector_overhang: 2.0
|
||||||
|
usb_guide_wall_thickness: 1.2
|
||||||
|
usb_guide_end_gap: 1.0
|
||||||
|
lid_label: IMU
|
||||||
|
lid_logo_enabled: true
|
||||||
|
lid_art_orientation: usb_front
|
||||||
|
lid_inlay_depth: 0.6
|
||||||
|
lid_inlay_fit_clearance: 0.08
|
||||||
|
lid_label_size: 5.0
|
||||||
|
lid_label_logo_gap: 2.0
|
||||||
|
lid_art_margin: 8.5
|
||||||
|
usb_opening_width: 13.5
|
||||||
|
usb_opening_height: 8.0
|
||||||
usb_opening_radius: 1.5
|
usb_opening_radius: 1.5
|
||||||
usb_vertical_offset: 0.0
|
usb_vertical_offset: 0.5
|
||||||
cutouts: []
|
cutouts: []
|
||||||
outputs:
|
outputs:
|
||||||
formats:
|
formats:
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"parameterSets": {
|
"parameterSets": {
|
||||||
"pan-tilt-head": {
|
"attitude-no-battery": {
|
||||||
"top_profile": "\"oled128x64\"",
|
"top_profile": "\"generic\"",
|
||||||
"wing_count": "3",
|
"wing_count": "1",
|
||||||
"wing_pitch": "12.25",
|
"wing_pitch": "12.25",
|
||||||
"stack_height_override": "38.0",
|
"stack_height_override": "0.0",
|
||||||
"generic_component_height": "3.0",
|
"generic_component_height": "3.0",
|
||||||
"generic_top_gap": "2.0",
|
"generic_top_gap": "2.0",
|
||||||
"gps_antenna_height": "6.0",
|
"gps_antenna_height": "6.0",
|
||||||
@@ -27,20 +27,25 @@
|
|||||||
"battery_diameter": "18.0",
|
"battery_diameter": "18.0",
|
||||||
"battery_radial_clearance": "0.6",
|
"battery_radial_clearance": "0.6",
|
||||||
"battery_end_clearance": "1.0",
|
"battery_end_clearance": "1.0",
|
||||||
"switch_hole_diameter": "12.0",
|
"battery_partition_thickness": "1.8",
|
||||||
"switch_hole_compensation": "0.3",
|
"battery_board_clearance": "3.0",
|
||||||
"switch_cap_diameter": "8.0",
|
"rocker_cutout_size": "[12.9,19.4]",
|
||||||
"switch_flange_diameter": "16.2",
|
"rocker_corner_radius": "0.8",
|
||||||
"switch_body_length": "24.0",
|
"rocker_body_size": "[15.0,21.0,21.0]",
|
||||||
"switch_body_diameter": "12.0",
|
"rocker_artwork_gap": "2.0",
|
||||||
"switch_wire_allowance": "3.0",
|
"rocker_partition_offset": "2.0",
|
||||||
|
"rocker_tab_panel_thickness": "2.0",
|
||||||
|
"rocker_tab_land": "2.0",
|
||||||
|
"rocker_tab_taper_length": "0.6",
|
||||||
"tripod_insert_enabled": "true",
|
"tripod_insert_enabled": "true",
|
||||||
"tripod_insert_length": "6.4",
|
"tripod_insert_length": "5.0",
|
||||||
"tripod_insert_hole_diameter": "8.8",
|
"tripod_insert_hole_diameter": "7.2",
|
||||||
|
"tripod_insert_outer_diameter": "8.0",
|
||||||
"tripod_thread_clearance_diameter": "6.8",
|
"tripod_thread_clearance_diameter": "6.8",
|
||||||
"tripod_boss_diameter": "14.0",
|
"tripod_boss_diameter": "14.0",
|
||||||
"m25_insert_length": "4.0",
|
"m25_insert_length": "4.0",
|
||||||
"m25_insert_hole_diameter": "3.7",
|
"m25_insert_hole_diameter": "3.1",
|
||||||
|
"m25_insert_outer_diameter": "3.5",
|
||||||
"m25_screw_clearance": "2.8",
|
"m25_screw_clearance": "2.8",
|
||||||
"m25_boss_diameter": "7.0",
|
"m25_boss_diameter": "7.0",
|
||||||
"insert_entry_chamfer": "0.6",
|
"insert_entry_chamfer": "0.6",
|
||||||
@@ -51,8 +56,8 @@
|
|||||||
"corner_radius": "4.0",
|
"corner_radius": "4.0",
|
||||||
"board_bottom_clearance": "5.4",
|
"board_bottom_clearance": "5.4",
|
||||||
"board_side_clearance": "7.0",
|
"board_side_clearance": "7.0",
|
||||||
"board_rear_clearance": "20.0",
|
"board_rear_clearance": "4.0",
|
||||||
"usb_board_wall_gap": "20.0",
|
"usb_board_wall_gap": "2.0",
|
||||||
"lid_lip_depth": "2.4",
|
"lid_lip_depth": "2.4",
|
||||||
"lid_lip_thickness": "1.2",
|
"lid_lip_thickness": "1.2",
|
||||||
"fit_clearance": "0.35",
|
"fit_clearance": "0.35",
|
||||||
@@ -65,10 +70,18 @@
|
|||||||
"usb_connector_overhang": "2.0",
|
"usb_connector_overhang": "2.0",
|
||||||
"usb_guide_wall_thickness": "1.2",
|
"usb_guide_wall_thickness": "1.2",
|
||||||
"usb_guide_end_gap": "1.0",
|
"usb_guide_end_gap": "1.0",
|
||||||
"usb_opening_width": "12.5",
|
"lid_label": "\"IMU\"",
|
||||||
"usb_opening_height": "7.0",
|
"lid_logo_enabled": "true",
|
||||||
|
"lid_art_orientation": "\"usb_front\"",
|
||||||
|
"lid_inlay_depth": "0.6",
|
||||||
|
"lid_inlay_fit_clearance": "0.08",
|
||||||
|
"lid_label_size": "5.0",
|
||||||
|
"lid_label_logo_gap": "2.0",
|
||||||
|
"lid_art_margin": "8.5",
|
||||||
|
"usb_opening_width": "13.5",
|
||||||
|
"usb_opening_height": "8.0",
|
||||||
"usb_opening_radius": "1.5",
|
"usb_opening_radius": "1.5",
|
||||||
"usb_vertical_offset": "0.0"
|
"usb_vertical_offset": "0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fileFormatVersion": "1"
|
"fileFormatVersion": "1"
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="210mm"
|
||||||
|
height="297mm"
|
||||||
|
viewBox="0 0 210 297"
|
||||||
|
version="1.1"
|
||||||
|
id="svg584"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="NME-fixed.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview586"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.914906"
|
||||||
|
inkscape:cx="1166.24"
|
||||||
|
inkscape:cy="477.6447"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1390"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1080"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs581" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g772">
|
||||||
|
<path
|
||||||
|
id="path2"
|
||||||
|
d="m 97.576583,26.288608 c -0.51788,0.06491 -2.361848,0.291394 -4.044598,0.485422 -15.62735,1.747308 -32.128529,8.73619 -44.81195,18.960393 -9.803341,7.926917 -17.342201,17.309748 -23.360236,29.151792 -2.879725,5.629978 -3.170767,6.632927 -1.941336,6.535912 0.388054,-0.03245 2.556227,-0.06491 4.788604,-0.03245 l 4.0767,0.03245 4.820708,5.662083 c 2.653242,3.106208 4.918075,5.662083 5.01509,5.662083 0.09737,0 0.291394,-1.035402 0.420862,-2.297287 0.09701,-1.261888 0.517525,-3.688291 0.87348,-5.403146 3.106208,-14.559843 9.415285,-26.692928 19.186524,-36.852576 6.276975,-6.568017 12.521494,-11.098035 21.030846,-15.304204 5.597525,-2.749903 9.933164,-4.367742 15.562791,-5.823656 l 3.559172,-0.938388 -2.10291,0.03245 c -1.164868,0.03246 -2.523768,0.09701 -3.073747,0.129117"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path3"
|
||||||
|
d="m 169.08146,36.027745 c -26.62837,2.232377 -57.46291,14.33336 -85.90315,33.68181 -5.85611,3.979685 -15.530335,11.130137 -15.530335,11.453635 0,0.16157 1.3589,0.258937 3.008842,0.258937 h 2.976737 l 3.7211,-2.78271 c 4.917723,-3.688644 13.977408,-9.738784 19.574581,-13.071475 5.759445,-3.46216 18.668995,-9.997721 24.751595,-12.55395 21.90467,-9.188802 42.51501,-12.747625 55.16562,-9.576858 5.04754,1.261888 9.35073,4.432654 11.09769,8.120946 0.80891,1.747308 0.87381,2.167818 0.87381,6.406443 0,4.270727 -0.0649,4.691592 -1.06748,7.603419 -2.65325,7.700433 -8.70374,17.439568 -16.8575,27.178351 -1.26188,1.520825 -2.29693,2.814815 -2.29693,2.91183 0,0.09737 4.7886,0.129468 10.67718,0.06491 l 10.67715,-0.09701 V 90.772856 85.91934 l 2.07081,-2.814815 C 196.58332,76.860006 200.59546,69.677098 202.56925,64.11203 208.1015,48.484325 201.79243,38.616074 184.77337,36.286682 181.6996,35.86582 172.7373,35.704247 169.08146,36.027745"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path4"
|
||||||
|
d="m 19.11528,111.8358 v 26.20751 h 11.000669 v -35.91419 l 1.552926,1.71486 c 0.841375,0.93839 4.59458,5.27402 8.347782,9.64212 3.753202,4.33564 9.350727,10.80664 12.456583,14.36582 3.106208,3.55882 6.341533,7.31202 7.215362,8.34742 l 1.552929,1.84397 h 5.629979 5.62963 V 111.8358 85.628298 H 66.8066 61.079958 l 0.161573,17.957092 c 0.06491,9.86825 0.06491,17.95709 -0.03246,17.95709 -0.09666,-0.0324 -0.711552,-0.67945 -1.326443,-1.45591 -0.61454,-0.77646 -4.27073,-5.01509 -8.088842,-9.41564 C 40.857677,98.052425 37.978307,94.720085 33.998622,90.060598 l -3.7211,-4.367742 -5.565071,-0.03246 -5.597171,-0.0321 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path5"
|
||||||
|
d="m 77.030804,111.8358 v 26.20751 h 11.001023 v -35.04035 l 6.7945,9.02687 c 3.720748,4.98299 8.023933,10.67753 9.544753,12.68342 l 2.81481,3.65584 9.5123,-12.68307 9.5123,-12.68306 0.0649,13.65391 0.097,13.65355 3.85022,-3.26743 c 2.13571,-1.81186 4.56212,-3.91512 5.43595,-4.69159 l 1.55293,-1.39135 V 103.294 85.628298 h -5.56507 l -5.59753,0.0321 -9.38283,12.456935 c -5.14456,6.826957 -9.4802,12.392027 -9.57721,12.359567 -0.19438,-0.097 -4.4002,-5.56507 -14.33336,-18.798467 L 88.1934,85.628298 H 82.628329 77.030804 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path6"
|
||||||
|
d="m 170.4079,106.88563 -12.58605,0.097 -6.66538,6.56802 c -21.32189,20.93383 -39.11741,34.16723 -59.727393,44.39108 -28.796192,14.30126 -53.936193,18.6369 -68.334112,11.77749 -2.976739,-1.42381 -6.115402,-4.10916 -7.441848,-6.34154 -2.78271,-4.7565 -2.750253,-12.03642 0.06491,-18.41006 1.002946,-2.29729 1.002946,-2.3943 -0.161925,-2.3943 h -0.93839 l -0.09701,-2.84727 -0.09701,-2.81517 -1.553282,2.58869 c -3.8177601,6.40609 -6.9236165,14.00986 -7.8295498,19.18652 -1.3267981,7.50641 0.1615731,13.07183 4.6912397,17.53659 2.6856951,2.65289 5.0150871,4.04459 8.9947751,5.37068 8.088841,2.6857 18.863026,3.0413 30.608055,0.93874 l 4.238273,-0.74436 3.882673,2.39431 c 14.689312,8.96232 30.252104,13.39497 46.947304,13.39497 14.49529,0 26.66083,-2.81482 39.53828,-9.12389 14.59194,-7.18326 27.33992,-18.70146 35.91418,-32.45238 1.97345,-3.17112 5.92103,-10.80664 6.50312,-12.52185 l 0.32385,-1.0675 -0.54999,0.80892 c -8.60672,12.42447 -18.15112,20.90137 -30.5756,27.08133 -18.15112,9.05934 -39.14987,10.90366 -57.980434,5.04719 -3.268133,-1.00294 -10.839098,-4.01178 -10.806642,-4.27072 0,-0.097 3.461809,-1.90888 7.667977,-3.97969 4.206169,-2.0708 9.706679,-4.95053 12.197999,-6.43855 9.93281,-5.824 22.4222,-14.5923 31.70801,-22.26027 l 3.55918,-2.94464 0.097,1.29434 0.097,1.29399 h 23.94302 23.91021 v -5.01509 -5.01473 h -18.44215 -18.44287 v -1.22943 c 0,-1.16488 0.25929,-1.48873 4.49756,-5.50052 l 4.49721,-4.27072 h 10.54806 l 10.54769,-0.0325 v -5.17701 c 0,-2.81482 -0.0324,-5.11175 -0.0646,-5.07965 -0.0649,0.0646 -5.75944,0.12947 -12.68341,0.16193"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path7"
|
||||||
|
d="m 34.484042,128.23997 v 14.17144 l -6.018035,0.16192 -6.01839,0.16157 1.326798,2.91218 c 3.559175,7.79745 8.703382,15.82173 13.912496,21.67785 l 1.423812,1.58538 4.691592,-0.38806 c 5.079645,-0.42086 9.609312,-1.19732 15.497879,-2.6857 3.429706,-0.84102 11.518548,-3.39725 11.906604,-3.75285 0.06491,-0.0649 -0.582438,-0.74471 -1.455915,-1.48872 -9.544756,-7.95937 -17.083264,-18.15112 -21.80731,-29.44318 -1.455914,-3.39725 -1.649941,-3.68829 -7.312379,-10.25631 -3.202869,-3.7211 -5.888564,-6.7624 -5.985579,-6.7624 -0.09701,-0.0324 -0.161573,6.34154 -0.161573,14.10688"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<g
|
||||||
|
id="g475"
|
||||||
|
transform="matrix(0.14274222,0,0,0.14274222,123.03997,38.158339)">
|
||||||
|
<path
|
||||||
|
d="m 134.23594,367.14576 v 34.66667 h 69.06667 c 37.86666,0 71.06666,-0.4 73.86666,-0.8 4.4,-0.8 7.2,-3.73333 31.33333,-32.8 14.53334,-17.46667 26.53334,-32.4 26.8,-32.93333 0.26667,-0.66667 -0.93333,-1.46667 -2.66666,-2 -1.73334,-0.4 -47.06667,-0.8 -100.66667,-0.8 h -97.73333 z"
|
||||||
|
id="path251"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
<path
|
||||||
|
d="m 134.90261,432.74576 c -0.4,0.93334 -0.53333,27.2 -0.4,58.4 l 0.4,56.53333 38.4,-37.06666 38.26666,-37.06667 -0.26666,-20.93333 -0.4,-20.8 -37.73334,-0.4 c -29.46666,-0.26667 -37.86666,0 -38.26666,1.33333 z"
|
||||||
|
id="path253"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
+386
-114
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* [Output] */
|
/* [Output] */
|
||||||
part = "assembly"; // [assembly,body,lid,buttons,insert_coupon,switch_coupon,fit_coupon]
|
part = "assembly"; // [assembly,body,lid,lid_base,lid_accent,buttons,insert_coupon,switch_coupon,fit_coupon]
|
||||||
exploded_view = 8; // [0:1:20]
|
exploded_view = 8; // [0:1:20]
|
||||||
show_references = true;
|
show_references = true;
|
||||||
|
|
||||||
@@ -47,24 +47,29 @@ battery_length = 69;
|
|||||||
battery_diameter = 18;
|
battery_diameter = 18;
|
||||||
battery_radial_clearance = 0.6;
|
battery_radial_clearance = 0.6;
|
||||||
battery_end_clearance = 1;
|
battery_end_clearance = 1;
|
||||||
switch_hole_diameter = 12;
|
battery_partition_thickness = 1.8;
|
||||||
switch_hole_compensation = 0.3;
|
battery_board_clearance = 3;
|
||||||
switch_cap_diameter = 8;
|
rocker_cutout_size = [12.9, 19.4];
|
||||||
switch_flange_diameter = 16.2;
|
rocker_corner_radius = 0.8;
|
||||||
switch_body_length = 24;
|
rocker_body_size = [15, 21, 21];
|
||||||
switch_body_diameter = 12;
|
rocker_artwork_gap = 2;
|
||||||
switch_wire_allowance = 3;
|
rocker_partition_offset = 2;
|
||||||
|
rocker_tab_panel_thickness = 2;
|
||||||
|
rocker_tab_land = 2;
|
||||||
|
rocker_tab_taper_length = 0.6;
|
||||||
|
|
||||||
/* [Tripod mount] */
|
/* [Tripod mount] */
|
||||||
tripod_insert_enabled = false;
|
tripod_insert_enabled = false;
|
||||||
tripod_insert_length = 6.4;
|
tripod_insert_length = 5;
|
||||||
tripod_insert_hole_diameter = 8.8;
|
tripod_insert_hole_diameter = 7.2;
|
||||||
|
tripod_insert_outer_diameter = 8;
|
||||||
tripod_thread_clearance_diameter = 6.8;
|
tripod_thread_clearance_diameter = 6.8;
|
||||||
tripod_boss_diameter = 14;
|
tripod_boss_diameter = 14;
|
||||||
|
|
||||||
/* [M2.5 inserts] */
|
/* [M2.5 inserts] */
|
||||||
m25_insert_length = 4;
|
m25_insert_length = 4;
|
||||||
m25_insert_hole_diameter = 3.7;
|
m25_insert_hole_diameter = 3.1;
|
||||||
|
m25_insert_outer_diameter = 3.5;
|
||||||
m25_screw_clearance = 2.8;
|
m25_screw_clearance = 2.8;
|
||||||
m25_boss_diameter = 7;
|
m25_boss_diameter = 7;
|
||||||
insert_entry_chamfer = 0.6;
|
insert_entry_chamfer = 0.6;
|
||||||
@@ -87,16 +92,27 @@ hole_compensation = 0.2;
|
|||||||
lid_screw_head_diameter = 5.2;
|
lid_screw_head_diameter = 5.2;
|
||||||
lid_screw_head_height = 2.5;
|
lid_screw_head_height = 2.5;
|
||||||
|
|
||||||
|
/* [Two-color lid artwork] */
|
||||||
|
lid_label = "";
|
||||||
|
lid_logo_enabled = false;
|
||||||
|
lid_art_orientation = "usb_left"; // [usb_left,usb_front,usb_right,usb_rear]
|
||||||
|
lid_inlay_depth = 0.6; // [0.4:0.2:1]
|
||||||
|
lid_inlay_fit_clearance = 0.08;
|
||||||
|
lid_label_size = 5;
|
||||||
|
lid_label_logo_gap = 2;
|
||||||
|
lid_art_margin = 8.5;
|
||||||
|
logo_file = "assets/NME-fixed.svg";
|
||||||
|
|
||||||
/* [USB-C opening] */
|
/* [USB-C opening] */
|
||||||
usb_opening_enabled = true;
|
usb_opening_enabled = true;
|
||||||
usb_guide_enabled = true;
|
usb_guide_enabled = true;
|
||||||
usb_connector_overhang = 2;
|
usb_connector_overhang = 2;
|
||||||
usb_guide_wall_thickness = 1.2;
|
usb_guide_wall_thickness = 1.2;
|
||||||
usb_guide_end_gap = 1;
|
usb_guide_end_gap = 1;
|
||||||
usb_opening_width = 12.5;
|
usb_opening_width = 13.5;
|
||||||
usb_opening_height = 7;
|
usb_opening_height = 8;
|
||||||
usb_opening_radius = 1.5;
|
usb_opening_radius = 1.5;
|
||||||
usb_vertical_offset = 0;
|
usb_vertical_offset = 0.5;
|
||||||
|
|
||||||
/* [Custom wall cutouts] */
|
/* [Custom wall cutouts] */
|
||||||
// Entry format: [face, shape, u, z, width, height, radius]
|
// Entry format: [face, shape, u, z, width, height, radius]
|
||||||
@@ -106,6 +122,8 @@ extra_cutouts = [];
|
|||||||
|
|
||||||
/* [Calibration] */
|
/* [Calibration] */
|
||||||
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
m25_coupon_steps = [-0.15, -0.075, 0, 0.075, 0.15];
|
||||||
|
tripod_coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
|
||||||
$fn = $preview ? 36 : 72;
|
$fn = $preview ? 36 : 72;
|
||||||
eps = 0.02;
|
eps = 0.02;
|
||||||
@@ -117,6 +135,9 @@ board_thickness = 1.6;
|
|||||||
mount_x = [2.54, 48.26];
|
mount_x = [2.54, 48.26];
|
||||||
mount_y = [2.54, 20.32];
|
mount_y = [2.54, 20.32];
|
||||||
usb_center_y = 11.43;
|
usb_center_y = 11.43;
|
||||||
|
jst_center_xy = [10.9855, 19.304];
|
||||||
|
jst_body_size = [8, 7.5, 4.5];
|
||||||
|
jst_relief_clearance = 0.5;
|
||||||
|
|
||||||
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
||||||
oled_visible_min = [11.42, 3.78];
|
oled_visible_min = [11.42, 3.78];
|
||||||
@@ -133,9 +154,18 @@ effective_board_clearance = max(
|
|||||||
board_bottom_clearance,
|
board_bottom_clearance,
|
||||||
m25_insert_length + insert_melt_relief + 0.6
|
m25_insert_length + insert_melt_relief + 0.6
|
||||||
);
|
);
|
||||||
|
lid_boss_r = m25_boss_diameter / 2;
|
||||||
|
lid_boss_wall_overlap = 0.6;
|
||||||
|
front_lid_boss_center = wall_thickness + lid_boss_r
|
||||||
|
- lid_boss_wall_overlap;
|
||||||
|
|
||||||
|
// Combined battery/tripod builds need room around the centered insert entry.
|
||||||
|
combined_tripod_board_gutter = battery_enabled && tripod_insert_enabled ? 2.4 : 0;
|
||||||
|
combined_tripod_x_shift = battery_enabled && tripod_insert_enabled ? 5 : 0;
|
||||||
|
|
||||||
board_origin_x = wall_thickness + usb_board_wall_gap;
|
board_origin_x = wall_thickness + usb_board_wall_gap;
|
||||||
board_origin_y = wall_thickness + board_side_clearance;
|
board_origin_y = wall_thickness + board_side_clearance
|
||||||
|
+ combined_tripod_board_gutter;
|
||||||
board_bottom_z = effective_floor + effective_board_clearance;
|
board_bottom_z = effective_floor + effective_board_clearance;
|
||||||
board_top_z = board_bottom_z + board_thickness;
|
board_top_z = board_bottom_z + board_thickness;
|
||||||
usb_center_u = board_origin_y + usb_center_y;
|
usb_center_u = board_origin_y + usb_center_y;
|
||||||
@@ -166,20 +196,17 @@ profile_top_gap =
|
|||||||
top_profile == "oled128x64" ? oled_face_gap :
|
top_profile == "oled128x64" ? oled_face_gap :
|
||||||
generic_top_gap;
|
generic_top_gap;
|
||||||
|
|
||||||
partition_y = board_origin_y + feather_width + 1;
|
partition_y = board_origin_y + feather_width + battery_board_clearance;
|
||||||
battery_center_y = partition_y + wall_thickness + battery_radial_clearance
|
battery_center_y = partition_y + battery_partition_thickness
|
||||||
+ battery_diameter / 2;
|
+ battery_radial_clearance + battery_diameter / 2;
|
||||||
battery_start_x = board_origin_x + battery_end_clearance;
|
battery_start_x = front_lid_boss_center + lid_boss_r
|
||||||
|
+ battery_end_clearance;
|
||||||
battery_end_x = battery_start_x + battery_length;
|
battery_end_x = battery_start_x + battery_length;
|
||||||
switch_center_x = battery_end_x + max(
|
|
||||||
switch_body_diameter / 2 + 0.8,
|
|
||||||
switch_flange_diameter / 2 + 0.5
|
|
||||||
);
|
|
||||||
|
|
||||||
case_length_plain = board_origin_x + feather_length
|
case_length_plain = board_origin_x + feather_length
|
||||||
+ board_rear_clearance + wall_thickness;
|
+ board_rear_clearance + wall_thickness;
|
||||||
case_length_battery = switch_center_x + switch_flange_diameter / 2
|
case_length_battery = battery_end_x + battery_end_clearance
|
||||||
+ 1.2 + wall_thickness;
|
+ wall_thickness + m25_boss_diameter - lid_boss_wall_overlap;
|
||||||
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
||||||
: case_length_plain;
|
: case_length_plain;
|
||||||
|
|
||||||
@@ -190,23 +217,74 @@ case_width_battery = battery_center_y + battery_diameter / 2
|
|||||||
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
||||||
: case_width_plain;
|
: case_width_plain;
|
||||||
|
|
||||||
|
rocker_installed_center_x = case_length - wall_thickness - fit_clearance
|
||||||
|
- lid_lip_thickness - fit_clearance - rocker_body_size[0] / 2;
|
||||||
|
rocker_installed_center_y = front_lid_boss_center + lid_boss_r + fit_clearance
|
||||||
|
+ rocker_body_size[1] / 2 + rocker_partition_offset;
|
||||||
|
// The printable lid is flipped across Y when installed, so its local cutout
|
||||||
|
// coordinate must be mirrored from the assembled switch location.
|
||||||
|
rocker_lid_center_x = rocker_installed_center_x;
|
||||||
|
rocker_lid_center_y = case_width - rocker_installed_center_y;
|
||||||
|
|
||||||
|
lid_artwork_enabled = lid_label != "" || lid_logo_enabled;
|
||||||
|
lid_art_rotation =
|
||||||
|
lid_art_orientation == "usb_left" ? 0 :
|
||||||
|
lid_art_orientation == "usb_front" ? 90 :
|
||||||
|
lid_art_orientation == "usb_right" ? 180 : 270;
|
||||||
|
lid_art_safe_max_x = battery_enabled
|
||||||
|
? rocker_lid_center_x - rocker_cutout_size[0] / 2 - rocker_artwork_gap
|
||||||
|
: case_length - lid_art_margin;
|
||||||
|
lid_art_physical_size = [
|
||||||
|
lid_art_safe_max_x - lid_art_margin,
|
||||||
|
case_width - 2 * lid_art_margin
|
||||||
|
];
|
||||||
|
lid_art_center = [
|
||||||
|
lid_art_margin + lid_art_physical_size[0] / 2,
|
||||||
|
case_width / 2
|
||||||
|
];
|
||||||
|
lid_art_canvas_size = lid_art_rotation == 0 || lid_art_rotation == 180
|
||||||
|
? lid_art_physical_size
|
||||||
|
: [lid_art_physical_size[1], lid_art_physical_size[0]];
|
||||||
|
lid_art_available_size = lid_art_canvas_size;
|
||||||
|
lid_label_height = lid_label == "" ? 0 : lid_label_size;
|
||||||
|
lid_art_gap = lid_label != "" && lid_logo_enabled ? lid_label_logo_gap : 0;
|
||||||
|
logo_native_size = [30, 25.75];
|
||||||
|
logo_max_height = lid_art_available_size[1] - lid_label_height - lid_art_gap;
|
||||||
|
logo_scale = lid_logo_enabled ? min(
|
||||||
|
lid_art_available_size[0] / logo_native_size[0],
|
||||||
|
logo_max_height / logo_native_size[1]
|
||||||
|
) : 0;
|
||||||
|
logo_size = [logo_native_size[0] * logo_scale,
|
||||||
|
logo_native_size[1] * logo_scale];
|
||||||
|
lid_art_total_height = logo_size[1] + lid_art_gap + lid_label_height;
|
||||||
|
lid_label_center_y = -lid_art_total_height / 2 + lid_label_height / 2;
|
||||||
|
lid_logo_center_y = -lid_art_total_height / 2 + lid_label_height
|
||||||
|
+ lid_art_gap + logo_size[1] / 2;
|
||||||
|
|
||||||
|
tripod_center_x = case_length / 2 - combined_tripod_x_shift;
|
||||||
|
tripod_center_y = case_width / 2;
|
||||||
|
tripod_entry_radius = tripod_insert_outer_diameter / 2;
|
||||||
|
tripod_standoff_clearance = min([
|
||||||
|
for (mx = mount_x, my = mount_y)
|
||||||
|
sqrt(
|
||||||
|
pow(board_origin_x + mx - tripod_center_x, 2)
|
||||||
|
+ pow(board_origin_y + my - tripod_center_y, 2)
|
||||||
|
) - tripod_entry_radius - m25_boss_diameter / 2
|
||||||
|
]);
|
||||||
|
tripod_partition_clearance = partition_y
|
||||||
|
- (tripod_center_y + tripod_entry_radius);
|
||||||
|
|
||||||
battery_center_z = effective_floor + battery_radial_clearance
|
battery_center_z = effective_floor + battery_radial_clearance
|
||||||
+ battery_diameter / 2;
|
+ battery_diameter / 2;
|
||||||
switch_center_z = max(
|
|
||||||
effective_floor + switch_flange_diameter / 2 + 1,
|
|
||||||
battery_center_z
|
|
||||||
);
|
|
||||||
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
||||||
battery_ceiling_z = battery_enabled
|
battery_ceiling_z = battery_enabled
|
||||||
? max(
|
? battery_center_z + battery_diameter / 2 + battery_radial_clearance
|
||||||
battery_center_z + battery_diameter / 2 + battery_radial_clearance,
|
|
||||||
switch_center_z + switch_flange_diameter / 2 + 1
|
|
||||||
)
|
|
||||||
: 0;
|
: 0;
|
||||||
body_height = max(electronics_ceiling_z, battery_ceiling_z);
|
rocker_ceiling_z = battery_enabled
|
||||||
|
? effective_floor + rocker_body_size[2] + fit_clearance
|
||||||
|
: 0;
|
||||||
|
body_height = max(electronics_ceiling_z, battery_ceiling_z, rocker_ceiling_z);
|
||||||
|
|
||||||
lid_boss_r = m25_boss_diameter / 2;
|
|
||||||
lid_boss_wall_overlap = 0.6;
|
|
||||||
lid_boss_xy = [
|
lid_boss_xy = [
|
||||||
[
|
[
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
||||||
@@ -225,19 +303,121 @@ lid_boss_xy = [
|
|||||||
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
rocker_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(
|
||||||
|
abs(p[0] - rocker_installed_center_x)
|
||||||
|
- rocker_body_size[0] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
+ pow(max(
|
||||||
|
abs(p[1] - rocker_installed_center_y)
|
||||||
|
- rocker_body_size[1] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
rocker_relief_half_size = [
|
||||||
|
rocker_cutout_size[0] / 2,
|
||||||
|
rocker_cutout_size[1] / 2 + rocker_tab_land + rocker_tab_taper_length
|
||||||
|
];
|
||||||
|
rocker_relief_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(abs(p[0] - rocker_lid_center_x)
|
||||||
|
- rocker_relief_half_size[0], 0), 2)
|
||||||
|
+ pow(max(abs(p[1] - rocker_lid_center_y)
|
||||||
|
- rocker_relief_half_size[1], 0), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
|
||||||
assert(wing_count >= 1, "wing_count must be at least one");
|
assert(wing_count >= 1, "wing_count must be at least one");
|
||||||
assert(top_profile == "generic" || top_profile == "gps"
|
assert(top_profile == "generic" || top_profile == "gps"
|
||||||
|| top_profile == "oled128x64", "Unsupported top_profile");
|
|| top_profile == "oled128x64", "Unsupported top_profile");
|
||||||
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_partition_thickness >= 1.2,
|
||||||
|
"battery_partition_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_board_clearance > 0,
|
||||||
|
"battery_board_clearance must be positive");
|
||||||
|
assert(rocker_tab_panel_thickness >= 1.2,
|
||||||
|
"rocker tab panel needs at least two 0.6 mm lines");
|
||||||
|
assert(rocker_tab_panel_thickness < lid_thickness,
|
||||||
|
"rocker tab panel must be thinner than the lid");
|
||||||
|
assert(rocker_tab_land > 0 && rocker_tab_taper_length > 0,
|
||||||
|
"rocker tab relief land and taper must be positive");
|
||||||
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
||||||
|
assert(lid_art_orientation == "usb_left"
|
||||||
|
|| lid_art_orientation == "usb_front"
|
||||||
|
|| lid_art_orientation == "usb_right"
|
||||||
|
|| lid_art_orientation == "usb_rear",
|
||||||
|
"Unsupported lid_art_orientation");
|
||||||
|
assert(!lid_artwork_enabled || top_profile != "oled128x64",
|
||||||
|
"Lid artwork is not supported with the OLED profile");
|
||||||
|
assert(!lid_artwork_enabled || lid_inlay_depth < lid_thickness,
|
||||||
|
"Lid inlay must be shallower than the lid");
|
||||||
|
assert(!lid_artwork_enabled
|
||||||
|
|| (lid_art_available_size[0] > 0 && lid_art_available_size[1] > 0),
|
||||||
|
"Lid artwork margin leaves no printable area");
|
||||||
|
assert(!lid_logo_enabled || logo_scale > 0,
|
||||||
|
"Lid logo has no printable area");
|
||||||
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
||||||
"M2.5 boss does not have enough radial material");
|
"M2.5 boss does not have enough radial material");
|
||||||
|
assert(m25_insert_outer_diameter >= m25_insert_hole_diameter,
|
||||||
|
"M2.5 insert entry must not be smaller than its pilot");
|
||||||
assert(!tripod_insert_enabled
|
assert(!tripod_insert_enabled
|
||||||
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
||||||
"Tripod insert boss does not have enough radial material");
|
"Tripod insert boss does not have enough radial material");
|
||||||
assert(usb_opening_enabled || !usb_guide_enabled,
|
assert(usb_opening_enabled || !usb_guide_enabled,
|
||||||
"USB guide requires the USB opening");
|
"USB guide requires the USB opening");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z - usb_reserved_height / 2 >= effective_floor,
|
||||||
|
"USB opening or guide intersects the case floor");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z + usb_reserved_height / 2
|
||||||
|
<= body_height - lid_lip_depth,
|
||||||
|
"USB opening or guide intersects the lid lip");
|
||||||
|
assert(!tripod_insert_enabled
|
||||||
|
|| tripod_insert_outer_diameter >= tripod_insert_hole_diameter,
|
||||||
|
"Tripod insert entry must not be smaller than its pilot");
|
||||||
|
assert(!tripod_insert_enabled || tripod_standoff_clearance >= 1.2,
|
||||||
|
"Tripod insert entry is too close to a Feather standoff");
|
||||||
|
assert(!battery_enabled || !tripod_insert_enabled
|
||||||
|
|| tripod_partition_clearance >= 1,
|
||||||
|
"Tripod insert entry is too close to the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= board_origin_x + feather_length + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather PCB");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y + rocker_body_size[1] / 2
|
||||||
|
<= partition_y - fit_clearance,
|
||||||
|
"Rocker body overlaps the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the front lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x + rocker_body_size[0] / 2
|
||||||
|
<= case_length - wall_thickness - lid_lip_thickness - fit_clearance,
|
||||||
|
"Rocker body overlaps the rear lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y - rocker_body_size[1] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather-side lid lip");
|
||||||
|
assert(!battery_enabled || rocker_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker body overlaps a lid boss");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_relief_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker tab relief overlaps a lid boss scallop");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| abs(
|
||||||
|
case_width - rocker_lid_center_y - rocker_installed_center_y
|
||||||
|
) < eps,
|
||||||
|
"Rocker lid cutout does not mirror to the installed switch location");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| body_height - rocker_body_size[2] >= effective_floor,
|
||||||
|
"Rocker body intersects the case floor");
|
||||||
|
|
||||||
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
||||||
function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4];
|
function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4];
|
||||||
@@ -297,8 +477,8 @@ module rounded_box(size, radius) {
|
|||||||
rounded_rect_2d([size[0], size[1]], radius);
|
rounded_rect_2d([size[0], size[1]], radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
module insert_pocket(diameter, depth, entry_z, downward = true) {
|
module insert_pocket(diameter, entry_diameter, depth, entry_z, downward = true) {
|
||||||
bore_d = diameter + hole_compensation;
|
bore_d = diameter;
|
||||||
if (downward) {
|
if (downward) {
|
||||||
translate([0, 0, entry_z - depth])
|
translate([0, 0, entry_z - depth])
|
||||||
cylinder(h = depth + eps, d = bore_d);
|
cylinder(h = depth + eps, d = bore_d);
|
||||||
@@ -306,7 +486,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d,
|
d1 = bore_d,
|
||||||
d2 = bore_d + 2 * insert_entry_chamfer
|
d2 = entry_diameter
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
@@ -314,7 +494,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d + 2 * insert_entry_chamfer,
|
d1 = entry_diameter,
|
||||||
d2 = bore_d
|
d2 = bore_d
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -457,20 +637,27 @@ module battery_cradle() {
|
|||||||
battery_end_x - 4
|
battery_end_x - 4
|
||||||
];
|
];
|
||||||
|
|
||||||
// Board/battery partition, with a lead-routing notch near the JST end.
|
// One open-topped front passage provides JST plug access and enough room
|
||||||
|
// to route the battery lead beneath the Feather PCB beside the lid boss.
|
||||||
difference() {
|
difference() {
|
||||||
translate([wall_thickness, partition_y, effective_floor - eps])
|
translate([wall_thickness, partition_y, effective_floor - eps])
|
||||||
cube([
|
cube([
|
||||||
battery_end_x - wall_thickness + battery_end_clearance,
|
battery_end_x - wall_thickness + battery_end_clearance,
|
||||||
wall_thickness,
|
battery_partition_thickness,
|
||||||
partition_height - effective_floor + eps
|
partition_height - effective_floor + eps
|
||||||
]);
|
]);
|
||||||
translate([
|
translate([
|
||||||
board_origin_x + 6.5,
|
wall_thickness - eps,
|
||||||
partition_y - eps,
|
partition_y - eps,
|
||||||
effective_floor + 1.5
|
effective_floor + 1.5
|
||||||
])
|
])
|
||||||
cube([9, wall_thickness + 2 * eps, 6]);
|
cube([
|
||||||
|
board_origin_x + jst_center_xy[0]
|
||||||
|
+ (jst_body_size[0] + 2 * jst_relief_clearance) / 2
|
||||||
|
- wall_thickness + 2 * eps,
|
||||||
|
battery_partition_thickness + 2 * eps,
|
||||||
|
partition_height - effective_floor - 1.5 + eps
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three low curved ribs cradle the protected cylindrical pack.
|
// Three low curved ribs cradle the protected cylindrical pack.
|
||||||
@@ -501,29 +688,16 @@ module battery_cradle() {
|
|||||||
battery_end_x + battery_end_clearance
|
battery_end_x + battery_end_clearance
|
||||||
])
|
])
|
||||||
translate([
|
translate([
|
||||||
x,
|
x - eps,
|
||||||
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
||||||
effective_floor - eps
|
effective_floor - eps
|
||||||
])
|
])
|
||||||
cube([
|
cube([
|
||||||
wall_thickness,
|
wall_thickness + 2 * eps,
|
||||||
battery_diameter + 2 * battery_radial_clearance,
|
battery_diameter + 2 * battery_radial_clearance,
|
||||||
battery_diameter / 2 + 1 + eps
|
battery_diameter / 2 + 1 + eps
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Rear cross-strip separates the side-mounted switch body from the cell.
|
|
||||||
translate([
|
|
||||||
battery_end_x + battery_end_clearance,
|
|
||||||
partition_y,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
case_length - wall_thickness - battery_end_x - battery_end_clearance,
|
|
||||||
case_width - wall_thickness - partition_y,
|
|
||||||
min(body_height - effective_floor, switch_center_z
|
|
||||||
+ switch_body_diameter / 2 + switch_wire_allowance
|
|
||||||
- effective_floor) + eps
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module body_positive() {
|
module body_positive() {
|
||||||
@@ -542,7 +716,7 @@ module body_positive() {
|
|||||||
if (battery_enabled)
|
if (battery_enabled)
|
||||||
battery_cradle();
|
battery_cradle();
|
||||||
if (tripod_insert_enabled)
|
if (tripod_insert_enabled)
|
||||||
translate([case_length / 2, case_width / 2, 0])
|
translate([tripod_center_x, tripod_center_y, 0])
|
||||||
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
||||||
if (usb_guide_active)
|
if (usb_guide_active)
|
||||||
usb_guide_shell();
|
usb_guide_shell();
|
||||||
@@ -555,6 +729,7 @@ module body_cutouts() {
|
|||||||
translate([board_origin_x + mx, board_origin_y + my, 0])
|
translate([board_origin_x + mx, board_origin_y + my, 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
board_bottom_z
|
board_bottom_z
|
||||||
);
|
);
|
||||||
@@ -564,6 +739,7 @@ module body_cutouts() {
|
|||||||
translate([p[0], p[1], 0])
|
translate([p[0], p[1], 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
body_height
|
body_height
|
||||||
);
|
);
|
||||||
@@ -579,29 +755,8 @@ module body_cutouts() {
|
|||||||
usb_opening_radius
|
usb_opening_radius
|
||||||
);
|
);
|
||||||
|
|
||||||
if (battery_enabled) {
|
|
||||||
face_circle_cutout(
|
|
||||||
"right",
|
|
||||||
switch_center_x,
|
|
||||||
switch_center_z,
|
|
||||||
switch_hole_diameter + switch_hole_compensation
|
|
||||||
);
|
|
||||||
// Clear the switch body and its prewired terminals through the rear bay.
|
|
||||||
translate([
|
|
||||||
switch_center_x,
|
|
||||||
case_width - wall_thickness + eps,
|
|
||||||
switch_center_z
|
|
||||||
])
|
|
||||||
rotate([90, 0, 0])
|
|
||||||
cylinder(
|
|
||||||
h = switch_body_length + switch_wire_allowance
|
|
||||||
+ wall_thickness + eps,
|
|
||||||
d = switch_body_diameter + 2 * fit_clearance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tripod_insert_enabled) {
|
if (tripod_insert_enabled) {
|
||||||
translate([case_length / 2, case_width / 2, 0]) {
|
translate([tripod_center_x, tripod_center_y, 0]) {
|
||||||
translate([0, 0, -eps])
|
translate([0, 0, -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = effective_floor + 2 * eps,
|
h = effective_floor + 2 * eps,
|
||||||
@@ -609,6 +764,7 @@ module body_cutouts() {
|
|||||||
);
|
);
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter,
|
tripod_insert_hole_diameter,
|
||||||
|
tripod_insert_outer_diameter,
|
||||||
tripod_insert_length + insert_melt_relief,
|
tripod_insert_length + insert_melt_relief,
|
||||||
effective_floor
|
effective_floor
|
||||||
);
|
);
|
||||||
@@ -702,6 +858,13 @@ module oled_button_lid_cutout(xy) {
|
|||||||
module lid_cutouts() {
|
module lid_cutouts() {
|
||||||
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
||||||
for (p = lid_boss_xy) {
|
for (p = lid_boss_xy) {
|
||||||
|
// Scallop only the locating lip so it can pass over the full-height
|
||||||
|
// insert boss while leaving the lid plate supported by the boss top.
|
||||||
|
translate([p[0], p[1], lid_thickness - eps])
|
||||||
|
cylinder(
|
||||||
|
h = lid_lip_depth + 2 * eps,
|
||||||
|
d = m25_boss_diameter + 2 * fit_clearance
|
||||||
|
);
|
||||||
translate([p[0], p[1], -eps])
|
translate([p[0], p[1], -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = lid_thickness + lid_lip_depth + 2 * eps,
|
h = lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
@@ -714,6 +877,19 @@ module lid_cutouts() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (battery_enabled) {
|
||||||
|
translate([rocker_lid_center_x, rocker_lid_center_y, -eps])
|
||||||
|
centered_rounded_prism(
|
||||||
|
rocker_cutout_size,
|
||||||
|
lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[rocker_lid_center_x, rocker_lid_center_y],
|
||||||
|
rocker_cutout_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (top_profile == "oled128x64") {
|
if (top_profile == "oled128x64") {
|
||||||
beveled_oled_window();
|
beveled_oled_window();
|
||||||
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
||||||
@@ -723,13 +899,90 @@ module lid_cutouts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module lid() {
|
// Thin the horizontal top/bottom edges of the upright switch opening where the
|
||||||
|
// spring tabs latch. Each full-width land overlaps the rounded opening by its
|
||||||
|
// corner radius, then extends outward before tapering to the surrounding lid.
|
||||||
|
module rocker_tab_reliefs(center, opening_size) {
|
||||||
|
relief_depth = lid_thickness - rocker_tab_panel_thickness;
|
||||||
|
opening_overlap = rocker_corner_radius;
|
||||||
|
strip = 0.02;
|
||||||
|
for (side = [-1, 1]) {
|
||||||
|
edge_y = center[1] + side * opening_size[1] / 2;
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y - (side < 0 ? rocker_tab_land : opening_overlap),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], rocker_tab_land + opening_overlap,
|
||||||
|
relief_depth + eps]);
|
||||||
|
|
||||||
|
hull() {
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * rocker_tab_land - (side < 0 ? strip : 0),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, relief_depth + eps]);
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * (rocker_tab_land + rocker_tab_taper_length)
|
||||||
|
- (side < 0 ? strip : 0),
|
||||||
|
lid_thickness - strip
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, strip + eps]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_nominal_2d() {
|
||||||
|
translate(lid_art_center)
|
||||||
|
rotate(lid_art_rotation)
|
||||||
|
// The lid is modeled exterior-face-down; mirror the artwork so it
|
||||||
|
// reads normally after the lid is turned into service position.
|
||||||
|
mirror([0, 1, 0]) {
|
||||||
|
if (lid_logo_enabled)
|
||||||
|
translate([0, lid_logo_center_y])
|
||||||
|
resize(logo_size)
|
||||||
|
import(logo_file, center = true);
|
||||||
|
if (lid_label != "")
|
||||||
|
translate([0, lid_label_center_y])
|
||||||
|
text(
|
||||||
|
lid_label,
|
||||||
|
size = lid_label_size,
|
||||||
|
halign = "center",
|
||||||
|
valign = "center",
|
||||||
|
font = "DejaVu Sans:style=Bold"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_2d(inset = 0) {
|
||||||
|
offset(delta = inset)
|
||||||
|
lid_artwork_nominal_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_base() {
|
||||||
difference() {
|
difference() {
|
||||||
lid_plate_positive();
|
lid_plate_positive();
|
||||||
lid_cutouts();
|
lid_cutouts();
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
translate([0, 0, -eps])
|
||||||
|
linear_extrude(height = lid_inlay_depth + eps)
|
||||||
|
lid_artwork_2d(lid_inlay_fit_clearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module lid_accent() {
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
linear_extrude(height = lid_inlay_depth)
|
||||||
|
lid_artwork_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
color([0.92, 0.92, 0.95]) lid_base();
|
||||||
|
color([0.95, 0.42, 0.08]) lid_accent();
|
||||||
|
}
|
||||||
|
|
||||||
module one_oled_button() {
|
module one_oled_button() {
|
||||||
recess_depth = button_flange_thickness + button_travel;
|
recess_depth = button_flange_thickness + button_travel;
|
||||||
stem_height = lid_thickness - recess_depth + button_projection;
|
stem_height = lid_thickness - recess_depth + button_projection;
|
||||||
@@ -769,6 +1022,13 @@ module reference_board() {
|
|||||||
color([0.05, 0.25, 0.55, 0.75])
|
color([0.05, 0.25, 0.55, 0.75])
|
||||||
translate([board_origin_x, board_origin_y, board_bottom_z])
|
translate([board_origin_x, board_origin_y, board_bottom_z])
|
||||||
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
||||||
|
color([0.92, 0.92, 0.86, 0.9])
|
||||||
|
translate([
|
||||||
|
board_origin_x + jst_center_xy[0] - jst_body_size[0] / 2,
|
||||||
|
board_origin_y + jst_center_xy[1] - 4.5,
|
||||||
|
board_top_z
|
||||||
|
])
|
||||||
|
cube(jst_body_size);
|
||||||
color([0.12, 0.45, 0.75, 0.45])
|
color([0.12, 0.45, 0.75, 0.45])
|
||||||
for (i = [1 : wing_count])
|
for (i = [1 : wing_count])
|
||||||
translate([
|
translate([
|
||||||
@@ -835,19 +1095,20 @@ module reference_battery_and_switch() {
|
|||||||
translate([battery_start_x, battery_center_y, battery_center_z])
|
translate([battery_start_x, battery_center_y, battery_center_z])
|
||||||
rotate([0, 90, 0])
|
rotate([0, 90, 0])
|
||||||
cylinder(h = battery_length, d = battery_diameter);
|
cylinder(h = battery_length, d = battery_diameter);
|
||||||
color([0.8, 0.15, 0.1, 0.65])
|
color([0.08, 0.08, 0.08, 0.85])
|
||||||
translate([
|
translate([
|
||||||
switch_center_x,
|
rocker_installed_center_x - rocker_body_size[0] / 2,
|
||||||
case_width - wall_thickness,
|
rocker_installed_center_y - rocker_body_size[1] / 2,
|
||||||
switch_center_z
|
body_height + exploded_view - rocker_body_size[2]
|
||||||
])
|
])
|
||||||
rotate([90, 0, 0]) {
|
cube(rocker_body_size);
|
||||||
cylinder(h = switch_body_length, d = switch_body_diameter);
|
color([0.12, 0.12, 0.12, 0.95])
|
||||||
translate([0, 0, -1.5])
|
translate([
|
||||||
cylinder(h = 1.5, d = switch_flange_diameter);
|
rocker_installed_center_x - rocker_cutout_size[0] / 2,
|
||||||
translate([0, 0, -3])
|
rocker_installed_center_y - rocker_cutout_size[1] / 2,
|
||||||
cylinder(h = 3, d = switch_cap_diameter);
|
body_height + exploded_view + lid_thickness
|
||||||
}
|
])
|
||||||
|
cube([rocker_cutout_size[0], rocker_cutout_size[1], 2.5]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,13 +1126,9 @@ module assembly() {
|
|||||||
lid();
|
lid();
|
||||||
}
|
}
|
||||||
|
|
||||||
module coupon_hole(x, y, diameter, depth) {
|
|
||||||
translate([x, y, -eps]) cylinder(h = depth + 2 * eps, d = diameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
module insert_coupon() {
|
module insert_coupon() {
|
||||||
spacing = 15;
|
spacing = 15;
|
||||||
coupon_length = spacing * len(coupon_steps) + 8;
|
coupon_length = spacing * len(tripod_coupon_steps) + 8;
|
||||||
coupon_width = 32;
|
coupon_width = 32;
|
||||||
coupon_height = max(
|
coupon_height = max(
|
||||||
m25_insert_length + insert_melt_relief + 1.2,
|
m25_insert_length + insert_melt_relief + 1.2,
|
||||||
@@ -879,17 +1136,21 @@ module insert_coupon() {
|
|||||||
);
|
);
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1]) {
|
for (i = [0 : len(tripod_coupon_steps) - 1]) {
|
||||||
x = 7 + i * spacing;
|
x = 7 + i * spacing;
|
||||||
coupon_hole(
|
translate([x, 8, 0])
|
||||||
x, 8,
|
insert_pocket(
|
||||||
m25_insert_hole_diameter + coupon_steps[i],
|
m25_insert_hole_diameter + m25_coupon_steps[i],
|
||||||
m25_insert_length + insert_melt_relief
|
m25_insert_outer_diameter,
|
||||||
|
m25_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
coupon_hole(
|
translate([x, 23, 0])
|
||||||
x, 23,
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter + coupon_steps[i],
|
tripod_insert_hole_diameter + tripod_coupon_steps[i],
|
||||||
tripod_insert_length + insert_melt_relief
|
tripod_insert_outer_diameter,
|
||||||
|
tripod_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -898,16 +1159,23 @@ module insert_coupon() {
|
|||||||
module switch_coupon() {
|
module switch_coupon() {
|
||||||
spacing = 18;
|
spacing = 18;
|
||||||
coupon_length = spacing * len(coupon_steps) + 6;
|
coupon_length = spacing * len(coupon_steps) + 6;
|
||||||
coupon_width = 24;
|
coupon_width = rocker_cutout_size[1] + 6;
|
||||||
coupon_height = wall_thickness;
|
coupon_height = lid_thickness;
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1])
|
for (i = [0 : len(coupon_steps) - 1]) {
|
||||||
translate([6 + i * spacing, coupon_width / 2, -eps])
|
translate([6 + i * spacing, coupon_width / 2, -eps])
|
||||||
cylinder(
|
centered_rounded_prism(
|
||||||
h = coupon_height + 2 * eps,
|
rocker_cutout_size
|
||||||
d = switch_hole_diameter + coupon_steps[i]
|
+ [coupon_steps[i], coupon_steps[i]],
|
||||||
|
coupon_height + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
);
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[6 + i * spacing, coupon_width / 2],
|
||||||
|
rocker_cutout_size + [coupon_steps[i], coupon_steps[i]]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,6 +1221,10 @@ else if (part == "body")
|
|||||||
body();
|
body();
|
||||||
else if (part == "lid")
|
else if (part == "lid")
|
||||||
lid();
|
lid();
|
||||||
|
else if (part == "lid_base")
|
||||||
|
lid_base();
|
||||||
|
else if (part == "lid_accent")
|
||||||
|
lid_accent();
|
||||||
else if (part == "buttons")
|
else if (part == "buttons")
|
||||||
buttons();
|
buttons();
|
||||||
else if (part == "insert_coupon")
|
else if (part == "insert_coupon")
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Render aligned lid-base and accent STLs with distinct materials."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
|
||||||
|
def import_mesh(path, name, color):
|
||||||
|
bpy.ops.wm.stl_import(filepath=os.path.abspath(path))
|
||||||
|
model = bpy.context.selected_objects[0]
|
||||||
|
model.name = name
|
||||||
|
material = bpy.data.materials.new(f"{name} material")
|
||||||
|
material.diffuse_color = color
|
||||||
|
material.metallic = 0.0
|
||||||
|
material.roughness = 0.45
|
||||||
|
model.data.materials.append(material)
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def look_at(camera, target):
|
||||||
|
direction = Vector(target) - camera.location
|
||||||
|
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[sys.argv.index("--") + 1 :]
|
||||||
|
if len(args) not in (4, 5):
|
||||||
|
raise SystemExit(
|
||||||
|
"expected: lid-base.stl lid-accent.stl output-directory basename [orientation]"
|
||||||
|
)
|
||||||
|
base_path, accent_path, output_dir, basename = args[:4]
|
||||||
|
orientation = args[4] if len(args) == 5 else "usb_left"
|
||||||
|
angles = {"usb_left": 0, "usb_front": 90, "usb_right": 180, "usb_rear": 270}
|
||||||
|
if orientation not in angles:
|
||||||
|
raise SystemExit(f"unknown lid orientation: {orientation}")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
models = [
|
||||||
|
import_mesh(base_path, "Lid base", (0.16, 0.42, 0.72, 1.0)),
|
||||||
|
import_mesh(accent_path, "Accent inlays", (0.95, 0.42, 0.08, 1.0)),
|
||||||
|
]
|
||||||
|
# Lid exports are print-oriented with the decorated exterior face down.
|
||||||
|
# Turn both aligned parts over and present the configured USB edge in its
|
||||||
|
# named position so previews inspect the finished face as the user will.
|
||||||
|
for model in models:
|
||||||
|
model.matrix_world = (
|
||||||
|
Matrix.Rotation(math.radians(angles[orientation]), 4, "Z")
|
||||||
|
@ Matrix.Rotation(math.pi, 4, "X")
|
||||||
|
)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
corners = [obj.matrix_world @ Vector(corner) for obj in models for corner in obj.bound_box]
|
||||||
|
mins = Vector([min(v[i] for v in corners) for i in range(3)])
|
||||||
|
maxs = Vector([max(v[i] for v in corners) for i in range(3)])
|
||||||
|
center = (mins + maxs) / 2
|
||||||
|
span = max(maxs - mins)
|
||||||
|
|
||||||
|
world = bpy.data.worlds.new("Inspection world")
|
||||||
|
world.use_nodes = True
|
||||||
|
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.035, 0.04, 0.05, 1)
|
||||||
|
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
|
||||||
|
bpy.context.scene.world = world
|
||||||
|
|
||||||
|
camera_data = bpy.data.cameras.new("Camera")
|
||||||
|
camera = bpy.data.objects.new("Camera", camera_data)
|
||||||
|
bpy.context.collection.objects.link(camera)
|
||||||
|
bpy.context.scene.camera = camera
|
||||||
|
camera.data.type = "ORTHO"
|
||||||
|
camera.data.ortho_scale = span * 1.35
|
||||||
|
|
||||||
|
key_data = bpy.data.lights.new("Key", type="AREA")
|
||||||
|
key_data.energy = 900
|
||||||
|
key_data.size = span
|
||||||
|
key = bpy.data.objects.new("Key", key_data)
|
||||||
|
key.location = center + Vector((span, -span, span * 1.5))
|
||||||
|
bpy.context.collection.objects.link(key)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.engine = "BLENDER_WORKBENCH"
|
||||||
|
scene.render.resolution_x = 800
|
||||||
|
scene.render.resolution_y = 600
|
||||||
|
scene.render.resolution_percentage = 100
|
||||||
|
scene.render.image_settings.file_format = "PNG"
|
||||||
|
scene.display.shading.light = "STUDIO"
|
||||||
|
scene.display.shading.color_type = "MATERIAL"
|
||||||
|
scene.display.shading.show_shadows = True
|
||||||
|
scene.display.shading.show_cavity = True
|
||||||
|
scene.display.shading.cavity_type = "BOTH"
|
||||||
|
|
||||||
|
for view, direction in {"iso": (1, -1, 0.8), "top": (0, 0, 1)}.items():
|
||||||
|
camera.location = center + Vector(direction).normalized() * span * 2.2
|
||||||
|
look_at(camera, center)
|
||||||
|
scene.render.filepath = os.path.join(output_dir, f"{basename}_{view}.png")
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine aligned lid meshes into one slicer-friendly multipart 3MF."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
|
||||||
|
ET.register_namespace("", CORE)
|
||||||
|
|
||||||
|
CONTENT_TYPES_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||||
|
</Types>
|
||||||
|
'''
|
||||||
|
|
||||||
|
RELATIONSHIPS_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||||
|
</Relationships>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def load_mesh(path: Path) -> ET.Element:
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
root = ET.fromstring(archive.read("3D/3dmodel.model"))
|
||||||
|
mesh = root.find(f".//{{{CORE}}}mesh")
|
||||||
|
if mesh is None:
|
||||||
|
raise ValueError(f"{path} does not contain a 3MF mesh")
|
||||||
|
return copy.deepcopy(mesh)
|
||||||
|
|
||||||
|
|
||||||
|
def model_xml(base: Path, accent: Path, title: str) -> bytes:
|
||||||
|
root = ET.Element(
|
||||||
|
f"{{{CORE}}}model",
|
||||||
|
{
|
||||||
|
"unit": "millimeter",
|
||||||
|
"{http://www.w3.org/XML/1998/namespace}lang": "en-US",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = (
|
||||||
|
"feather-case-wizard"
|
||||||
|
)
|
||||||
|
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
||||||
|
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
||||||
|
ET.SubElement(
|
||||||
|
materials, f"{{{CORE}}}base", {"name": "Lid base", "displaycolor": "#D1D5DB"}
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
materials,
|
||||||
|
f"{{{CORE}}}base",
|
||||||
|
{"name": "Accent inlays", "displaycolor": "#F97316"},
|
||||||
|
)
|
||||||
|
|
||||||
|
for object_id, name, source in (
|
||||||
|
(2, "Lid base — Filament 1", base),
|
||||||
|
(3, "Accent inlays — Filament 2", accent),
|
||||||
|
):
|
||||||
|
obj = ET.SubElement(
|
||||||
|
resources,
|
||||||
|
f"{{{CORE}}}object",
|
||||||
|
{
|
||||||
|
"id": str(object_id),
|
||||||
|
"name": name,
|
||||||
|
"type": "model",
|
||||||
|
"pid": "1",
|
||||||
|
"pindex": str(object_id - 2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
obj.append(load_mesh(source))
|
||||||
|
|
||||||
|
assembly = ET.SubElement(
|
||||||
|
resources, f"{{{CORE}}}object", {"id": "4", "name": title, "type": "model"}
|
||||||
|
)
|
||||||
|
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "2"})
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "3"})
|
||||||
|
build = ET.SubElement(root, f"{{{CORE}}}build")
|
||||||
|
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": "4"})
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def bambu_model_settings_xml(title: str) -> bytes:
|
||||||
|
root = ET.Element("config")
|
||||||
|
obj = ET.SubElement(root, "object", {"id": "4"})
|
||||||
|
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
||||||
|
for part_id, name, extruder in (
|
||||||
|
(2, "Lid base — Filament 1", 1),
|
||||||
|
(3, "Accent inlays — Filament 2", 2),
|
||||||
|
):
|
||||||
|
part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "name", "value": name})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)})
|
||||||
|
ET.SubElement(root, "assemble")
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
|
||||||
|
info = zipfile.ZipInfo(name)
|
||||||
|
info.create_system = 3
|
||||||
|
info.external_attr = 0o100644 << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
archive.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("base", type=Path)
|
||||||
|
parser.add_argument("accent", type=Path)
|
||||||
|
parser.add_argument("output", type=Path)
|
||||||
|
parser.add_argument("--title", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
||||||
|
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"3D/3dmodel.model",
|
||||||
|
model_xml(args.base, args.accent, args.title),
|
||||||
|
)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"Metadata/model_settings.config",
|
||||||
|
bambu_model_settings_xml(args.title),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Attitude IMU - Battery
|
# Attitude w/battery
|
||||||
|
|
||||||
Generated by the Feather Case Wizard.
|
Generated by the Feather Case Wizard.
|
||||||
|
|
||||||
@@ -6,19 +6,19 @@ Generated by the Feather Case Wizard.
|
|||||||
- FeatherWings: 1
|
- FeatherWings: 1
|
||||||
- Battery compartment: yes
|
- Battery compartment: yes
|
||||||
- 1/4-20 insert: yes
|
- 1/4-20 insert: yes
|
||||||
- Approximate exterior: 93.9 × 57.3 × 35.8 mm
|
- Approximate exterior: 88.6 × 61.1 × 34.4 mm
|
||||||
- Formats: 3mf
|
- Formats: 3mf
|
||||||
- Parts: `body`, `lid`
|
- Parts: `body`, `lid`
|
||||||
- Custom cutouts: 0
|
- Custom cutouts: 0
|
||||||
|
|
||||||
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
||||||
OpenSCAD and Blender must be installed. Set `OPENSCAD` or `BLENDER` to
|
OpenSCAD, Blender, and Python 3 must be installed. Set `OPENSCAD`,
|
||||||
override their executable paths.
|
`BLENDER`, or `PYTHON` to override their executable paths.
|
||||||
|
|
||||||
To open the saved Customizer values manually:
|
To open the saved Customizer values manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openscad -p parameters.json -P attitude-imu-battery source/feather_case.scad
|
openscad -p parameters.json -P attitude-w-battery source/feather_case.scad
|
||||||
```
|
```
|
||||||
|
|
||||||
The 3MF files contain portable geometry only. Choose printer, filament,
|
The 3MF files contain portable geometry only. Choose printer, filament,
|
||||||
@@ -44,13 +44,18 @@ config = YAML.safe_load_file(ROOT.join("case.yml"), permitted_classes: [],
|
|||||||
permitted_symbols: [], aliases: false)
|
permitted_symbols: [], aliases: false)
|
||||||
openscad = executable("OPENSCAD", "openscad")
|
openscad = executable("OPENSCAD", "openscad")
|
||||||
blender = executable("BLENDER", "blender")
|
blender = executable("BLENDER", "blender")
|
||||||
|
python = executable("PYTHON", "python3")
|
||||||
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
||||||
abort "Blender not found; install it or set BLENDER" unless blender
|
abort "Blender not found; install it or set BLENDER" unless blender
|
||||||
|
|
||||||
parameters = config.fetch("parameters").merge(
|
parameters = config.fetch("parameters").merge(
|
||||||
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
||||||
)
|
)
|
||||||
|
artwork = !parameters.fetch("lid_label", "").empty? ||
|
||||||
|
parameters.fetch("lid_logo_enabled", false)
|
||||||
|
abort "Python 3 not found; install it or set PYTHON" if artwork && !python
|
||||||
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
||||||
|
parts.concat(%w[lid_base lid_accent]) if artwork
|
||||||
buttons = %w[button_a button_b button_c button_reset]
|
buttons = %w[button_a button_b button_c button_reset]
|
||||||
if parameters["top_profile"] == "oled128x64" &&
|
if parameters["top_profile"] == "oled128x64" &&
|
||||||
buttons.any? { |name| parameters[name] }
|
buttons.any? { |name| parameters[name] }
|
||||||
@@ -80,6 +85,16 @@ begin
|
|||||||
puts "Exporting #{part}.#{format}…"
|
puts "Exporting #{part}.#{format}…"
|
||||||
export.call(directory.join("#{part}.#{format}"), part)
|
export.call(directory.join("#{part}.#{format}"), part)
|
||||||
end
|
end
|
||||||
|
if artwork && format == "3mf"
|
||||||
|
puts "Packaging lid_multicolor.3mf…"
|
||||||
|
run!(
|
||||||
|
python, ROOT.join("tools/package_multimaterial_3mf.py").to_s,
|
||||||
|
directory.join("lid_base.3mf").to_s,
|
||||||
|
directory.join("lid_accent.3mf").to_s,
|
||||||
|
directory.join("lid_multicolor.3mf").to_s,
|
||||||
|
"--title", "#{config.fetch("name")} Lid"
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assembly = scratch.join("assembly.stl")
|
assembly = scratch.join("assembly.stl")
|
||||||
@@ -97,6 +112,16 @@ begin
|
|||||||
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
||||||
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
||||||
end
|
end
|
||||||
|
if artwork
|
||||||
|
puts "Rendering multicolor lid preview…"
|
||||||
|
run!(
|
||||||
|
blender, "--background", "-noaudio", "--python",
|
||||||
|
ROOT.join("source/render_multicolor_lid.py").to_s, "--",
|
||||||
|
scratch.join("lid_base.stl").to_s,
|
||||||
|
scratch.join("lid_accent.stl").to_s,
|
||||||
|
previews.to_s, "lid_multicolor", parameters["lid_art_orientation"], env: render_env
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
backup = ROOT.join(".build-old-#{Process.pid}")
|
backup = ROOT.join(".build-old-#{Process.pid}")
|
||||||
FileUtils.mkdir_p(backup)
|
FileUtils.mkdir_p(backup)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
name: Attitude IMU - Battery
|
name: Attitude w/battery
|
||||||
slug: attitude-imu-battery
|
slug: attitude-w-battery
|
||||||
generator:
|
generator:
|
||||||
sha256: be611e3255e7d802719ebb0628e3d11481f8de05fc96c24d452f026d7b8e3353
|
sha256: 281917098487eeacd19a136ba27752698712e90cb4c03c6525a1ea3167c9ea13
|
||||||
created_with: feather-case-wizard
|
created_with: feather-case-wizard
|
||||||
parameters:
|
parameters:
|
||||||
top_profile: generic
|
top_profile: generic
|
||||||
@@ -36,20 +36,30 @@ parameters:
|
|||||||
battery_diameter: 18.0
|
battery_diameter: 18.0
|
||||||
battery_radial_clearance: 0.6
|
battery_radial_clearance: 0.6
|
||||||
battery_end_clearance: 1.0
|
battery_end_clearance: 1.0
|
||||||
switch_hole_diameter: 12.0
|
battery_partition_thickness: 1.8
|
||||||
switch_hole_compensation: 0.3
|
battery_board_clearance: 3.0
|
||||||
switch_cap_diameter: 8.0
|
rocker_cutout_size:
|
||||||
switch_flange_diameter: 16.2
|
- 12.9
|
||||||
switch_body_length: 24.0
|
- 19.4
|
||||||
switch_body_diameter: 12.0
|
rocker_corner_radius: 0.8
|
||||||
switch_wire_allowance: 3.0
|
rocker_body_size:
|
||||||
|
- 15.0
|
||||||
|
- 21.0
|
||||||
|
- 21.0
|
||||||
|
rocker_artwork_gap: 2.0
|
||||||
|
rocker_partition_offset: 2.0
|
||||||
|
rocker_tab_panel_thickness: 2.0
|
||||||
|
rocker_tab_land: 2.0
|
||||||
|
rocker_tab_taper_length: 0.6
|
||||||
tripod_insert_enabled: true
|
tripod_insert_enabled: true
|
||||||
tripod_insert_length: 6.4
|
tripod_insert_length: 5.0
|
||||||
tripod_insert_hole_diameter: 8.8
|
tripod_insert_hole_diameter: 7.2
|
||||||
|
tripod_insert_outer_diameter: 8.0
|
||||||
tripod_thread_clearance_diameter: 6.8
|
tripod_thread_clearance_diameter: 6.8
|
||||||
tripod_boss_diameter: 14.0
|
tripod_boss_diameter: 14.0
|
||||||
m25_insert_length: 4.0
|
m25_insert_length: 4.0
|
||||||
m25_insert_hole_diameter: 3.7
|
m25_insert_hole_diameter: 3.1
|
||||||
|
m25_insert_outer_diameter: 3.5
|
||||||
m25_screw_clearance: 2.8
|
m25_screw_clearance: 2.8
|
||||||
m25_boss_diameter: 7.0
|
m25_boss_diameter: 7.0
|
||||||
insert_entry_chamfer: 0.6
|
insert_entry_chamfer: 0.6
|
||||||
@@ -61,17 +71,31 @@ parameters:
|
|||||||
board_bottom_clearance: 5.4
|
board_bottom_clearance: 5.4
|
||||||
board_side_clearance: 7.0
|
board_side_clearance: 7.0
|
||||||
board_rear_clearance: 4.0
|
board_rear_clearance: 4.0
|
||||||
usb_board_wall_gap: 1.2
|
usb_board_wall_gap: 2.0
|
||||||
lid_lip_depth: 2.4
|
lid_lip_depth: 2.4
|
||||||
lid_lip_thickness: 1.2
|
lid_lip_thickness: 1.2
|
||||||
fit_clearance: 0.35
|
fit_clearance: 0.35
|
||||||
|
cutout_min_web: 1.2
|
||||||
hole_compensation: 0.2
|
hole_compensation: 0.2
|
||||||
lid_screw_head_diameter: 5.2
|
lid_screw_head_diameter: 5.2
|
||||||
lid_screw_head_height: 2.5
|
lid_screw_head_height: 2.5
|
||||||
usb_opening_width: 12.5
|
usb_opening_enabled: true
|
||||||
usb_opening_height: 7.0
|
usb_guide_enabled: true
|
||||||
|
usb_connector_overhang: 2.0
|
||||||
|
usb_guide_wall_thickness: 1.2
|
||||||
|
usb_guide_end_gap: 1.0
|
||||||
|
lid_label: ''
|
||||||
|
lid_logo_enabled: false
|
||||||
|
lid_art_orientation: usb_left
|
||||||
|
lid_inlay_depth: 0.6
|
||||||
|
lid_inlay_fit_clearance: 0.08
|
||||||
|
lid_label_size: 5.0
|
||||||
|
lid_label_logo_gap: 2.0
|
||||||
|
lid_art_margin: 8.5
|
||||||
|
usb_opening_width: 13.5
|
||||||
|
usb_opening_height: 8.0
|
||||||
usb_opening_radius: 1.5
|
usb_opening_radius: 1.5
|
||||||
usb_vertical_offset: 0.0
|
usb_vertical_offset: 0.5
|
||||||
cutouts: []
|
cutouts: []
|
||||||
outputs:
|
outputs:
|
||||||
formats:
|
formats:
|
||||||
+34
-15
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"parameterSets": {
|
"parameterSets": {
|
||||||
"attitude-imu-battery": {
|
"attitude-w-battery": {
|
||||||
"top_profile": "\"generic\"",
|
"top_profile": "\"generic\"",
|
||||||
"wing_count": "1",
|
"wing_count": "1",
|
||||||
"wing_pitch": "12.25",
|
"wing_pitch": "12.25",
|
||||||
@@ -27,20 +27,25 @@
|
|||||||
"battery_diameter": "18.0",
|
"battery_diameter": "18.0",
|
||||||
"battery_radial_clearance": "0.6",
|
"battery_radial_clearance": "0.6",
|
||||||
"battery_end_clearance": "1.0",
|
"battery_end_clearance": "1.0",
|
||||||
"switch_hole_diameter": "12.0",
|
"battery_partition_thickness": "1.8",
|
||||||
"switch_hole_compensation": "0.3",
|
"battery_board_clearance": "3.0",
|
||||||
"switch_cap_diameter": "8.0",
|
"rocker_cutout_size": "[12.9,19.4]",
|
||||||
"switch_flange_diameter": "16.2",
|
"rocker_corner_radius": "0.8",
|
||||||
"switch_body_length": "24.0",
|
"rocker_body_size": "[15.0,21.0,21.0]",
|
||||||
"switch_body_diameter": "12.0",
|
"rocker_artwork_gap": "2.0",
|
||||||
"switch_wire_allowance": "3.0",
|
"rocker_partition_offset": "2.0",
|
||||||
|
"rocker_tab_panel_thickness": "2.0",
|
||||||
|
"rocker_tab_land": "2.0",
|
||||||
|
"rocker_tab_taper_length": "0.6",
|
||||||
"tripod_insert_enabled": "true",
|
"tripod_insert_enabled": "true",
|
||||||
"tripod_insert_length": "6.4",
|
"tripod_insert_length": "5.0",
|
||||||
"tripod_insert_hole_diameter": "8.8",
|
"tripod_insert_hole_diameter": "7.2",
|
||||||
|
"tripod_insert_outer_diameter": "8.0",
|
||||||
"tripod_thread_clearance_diameter": "6.8",
|
"tripod_thread_clearance_diameter": "6.8",
|
||||||
"tripod_boss_diameter": "14.0",
|
"tripod_boss_diameter": "14.0",
|
||||||
"m25_insert_length": "4.0",
|
"m25_insert_length": "4.0",
|
||||||
"m25_insert_hole_diameter": "3.7",
|
"m25_insert_hole_diameter": "3.1",
|
||||||
|
"m25_insert_outer_diameter": "3.5",
|
||||||
"m25_screw_clearance": "2.8",
|
"m25_screw_clearance": "2.8",
|
||||||
"m25_boss_diameter": "7.0",
|
"m25_boss_diameter": "7.0",
|
||||||
"insert_entry_chamfer": "0.6",
|
"insert_entry_chamfer": "0.6",
|
||||||
@@ -52,17 +57,31 @@
|
|||||||
"board_bottom_clearance": "5.4",
|
"board_bottom_clearance": "5.4",
|
||||||
"board_side_clearance": "7.0",
|
"board_side_clearance": "7.0",
|
||||||
"board_rear_clearance": "4.0",
|
"board_rear_clearance": "4.0",
|
||||||
"usb_board_wall_gap": "1.2",
|
"usb_board_wall_gap": "2.0",
|
||||||
"lid_lip_depth": "2.4",
|
"lid_lip_depth": "2.4",
|
||||||
"lid_lip_thickness": "1.2",
|
"lid_lip_thickness": "1.2",
|
||||||
"fit_clearance": "0.35",
|
"fit_clearance": "0.35",
|
||||||
|
"cutout_min_web": "1.2",
|
||||||
"hole_compensation": "0.2",
|
"hole_compensation": "0.2",
|
||||||
"lid_screw_head_diameter": "5.2",
|
"lid_screw_head_diameter": "5.2",
|
||||||
"lid_screw_head_height": "2.5",
|
"lid_screw_head_height": "2.5",
|
||||||
"usb_opening_width": "12.5",
|
"usb_opening_enabled": "true",
|
||||||
"usb_opening_height": "7.0",
|
"usb_guide_enabled": "true",
|
||||||
|
"usb_connector_overhang": "2.0",
|
||||||
|
"usb_guide_wall_thickness": "1.2",
|
||||||
|
"usb_guide_end_gap": "1.0",
|
||||||
|
"lid_label": "\"\"",
|
||||||
|
"lid_logo_enabled": "false",
|
||||||
|
"lid_art_orientation": "\"usb_left\"",
|
||||||
|
"lid_inlay_depth": "0.6",
|
||||||
|
"lid_inlay_fit_clearance": "0.08",
|
||||||
|
"lid_label_size": "5.0",
|
||||||
|
"lid_label_logo_gap": "2.0",
|
||||||
|
"lid_art_margin": "8.5",
|
||||||
|
"usb_opening_width": "13.5",
|
||||||
|
"usb_opening_height": "8.0",
|
||||||
"usb_opening_radius": "1.5",
|
"usb_opening_radius": "1.5",
|
||||||
"usb_vertical_offset": "0.0"
|
"usb_vertical_offset": "0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fileFormatVersion": "1"
|
"fileFormatVersion": "1"
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="210mm"
|
||||||
|
height="297mm"
|
||||||
|
viewBox="0 0 210 297"
|
||||||
|
version="1.1"
|
||||||
|
id="svg584"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="NME-fixed.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview586"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.914906"
|
||||||
|
inkscape:cx="1166.24"
|
||||||
|
inkscape:cy="477.6447"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1390"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1080"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs581" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g772">
|
||||||
|
<path
|
||||||
|
id="path2"
|
||||||
|
d="m 97.576583,26.288608 c -0.51788,0.06491 -2.361848,0.291394 -4.044598,0.485422 -15.62735,1.747308 -32.128529,8.73619 -44.81195,18.960393 -9.803341,7.926917 -17.342201,17.309748 -23.360236,29.151792 -2.879725,5.629978 -3.170767,6.632927 -1.941336,6.535912 0.388054,-0.03245 2.556227,-0.06491 4.788604,-0.03245 l 4.0767,0.03245 4.820708,5.662083 c 2.653242,3.106208 4.918075,5.662083 5.01509,5.662083 0.09737,0 0.291394,-1.035402 0.420862,-2.297287 0.09701,-1.261888 0.517525,-3.688291 0.87348,-5.403146 3.106208,-14.559843 9.415285,-26.692928 19.186524,-36.852576 6.276975,-6.568017 12.521494,-11.098035 21.030846,-15.304204 5.597525,-2.749903 9.933164,-4.367742 15.562791,-5.823656 l 3.559172,-0.938388 -2.10291,0.03245 c -1.164868,0.03246 -2.523768,0.09701 -3.073747,0.129117"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path3"
|
||||||
|
d="m 169.08146,36.027745 c -26.62837,2.232377 -57.46291,14.33336 -85.90315,33.68181 -5.85611,3.979685 -15.530335,11.130137 -15.530335,11.453635 0,0.16157 1.3589,0.258937 3.008842,0.258937 h 2.976737 l 3.7211,-2.78271 c 4.917723,-3.688644 13.977408,-9.738784 19.574581,-13.071475 5.759445,-3.46216 18.668995,-9.997721 24.751595,-12.55395 21.90467,-9.188802 42.51501,-12.747625 55.16562,-9.576858 5.04754,1.261888 9.35073,4.432654 11.09769,8.120946 0.80891,1.747308 0.87381,2.167818 0.87381,6.406443 0,4.270727 -0.0649,4.691592 -1.06748,7.603419 -2.65325,7.700433 -8.70374,17.439568 -16.8575,27.178351 -1.26188,1.520825 -2.29693,2.814815 -2.29693,2.91183 0,0.09737 4.7886,0.129468 10.67718,0.06491 l 10.67715,-0.09701 V 90.772856 85.91934 l 2.07081,-2.814815 C 196.58332,76.860006 200.59546,69.677098 202.56925,64.11203 208.1015,48.484325 201.79243,38.616074 184.77337,36.286682 181.6996,35.86582 172.7373,35.704247 169.08146,36.027745"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path4"
|
||||||
|
d="m 19.11528,111.8358 v 26.20751 h 11.000669 v -35.91419 l 1.552926,1.71486 c 0.841375,0.93839 4.59458,5.27402 8.347782,9.64212 3.753202,4.33564 9.350727,10.80664 12.456583,14.36582 3.106208,3.55882 6.341533,7.31202 7.215362,8.34742 l 1.552929,1.84397 h 5.629979 5.62963 V 111.8358 85.628298 H 66.8066 61.079958 l 0.161573,17.957092 c 0.06491,9.86825 0.06491,17.95709 -0.03246,17.95709 -0.09666,-0.0324 -0.711552,-0.67945 -1.326443,-1.45591 -0.61454,-0.77646 -4.27073,-5.01509 -8.088842,-9.41564 C 40.857677,98.052425 37.978307,94.720085 33.998622,90.060598 l -3.7211,-4.367742 -5.565071,-0.03246 -5.597171,-0.0321 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path5"
|
||||||
|
d="m 77.030804,111.8358 v 26.20751 h 11.001023 v -35.04035 l 6.7945,9.02687 c 3.720748,4.98299 8.023933,10.67753 9.544753,12.68342 l 2.81481,3.65584 9.5123,-12.68307 9.5123,-12.68306 0.0649,13.65391 0.097,13.65355 3.85022,-3.26743 c 2.13571,-1.81186 4.56212,-3.91512 5.43595,-4.69159 l 1.55293,-1.39135 V 103.294 85.628298 h -5.56507 l -5.59753,0.0321 -9.38283,12.456935 c -5.14456,6.826957 -9.4802,12.392027 -9.57721,12.359567 -0.19438,-0.097 -4.4002,-5.56507 -14.33336,-18.798467 L 88.1934,85.628298 H 82.628329 77.030804 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path6"
|
||||||
|
d="m 170.4079,106.88563 -12.58605,0.097 -6.66538,6.56802 c -21.32189,20.93383 -39.11741,34.16723 -59.727393,44.39108 -28.796192,14.30126 -53.936193,18.6369 -68.334112,11.77749 -2.976739,-1.42381 -6.115402,-4.10916 -7.441848,-6.34154 -2.78271,-4.7565 -2.750253,-12.03642 0.06491,-18.41006 1.002946,-2.29729 1.002946,-2.3943 -0.161925,-2.3943 h -0.93839 l -0.09701,-2.84727 -0.09701,-2.81517 -1.553282,2.58869 c -3.8177601,6.40609 -6.9236165,14.00986 -7.8295498,19.18652 -1.3267981,7.50641 0.1615731,13.07183 4.6912397,17.53659 2.6856951,2.65289 5.0150871,4.04459 8.9947751,5.37068 8.088841,2.6857 18.863026,3.0413 30.608055,0.93874 l 4.238273,-0.74436 3.882673,2.39431 c 14.689312,8.96232 30.252104,13.39497 46.947304,13.39497 14.49529,0 26.66083,-2.81482 39.53828,-9.12389 14.59194,-7.18326 27.33992,-18.70146 35.91418,-32.45238 1.97345,-3.17112 5.92103,-10.80664 6.50312,-12.52185 l 0.32385,-1.0675 -0.54999,0.80892 c -8.60672,12.42447 -18.15112,20.90137 -30.5756,27.08133 -18.15112,9.05934 -39.14987,10.90366 -57.980434,5.04719 -3.268133,-1.00294 -10.839098,-4.01178 -10.806642,-4.27072 0,-0.097 3.461809,-1.90888 7.667977,-3.97969 4.206169,-2.0708 9.706679,-4.95053 12.197999,-6.43855 9.93281,-5.824 22.4222,-14.5923 31.70801,-22.26027 l 3.55918,-2.94464 0.097,1.29434 0.097,1.29399 h 23.94302 23.91021 v -5.01509 -5.01473 h -18.44215 -18.44287 v -1.22943 c 0,-1.16488 0.25929,-1.48873 4.49756,-5.50052 l 4.49721,-4.27072 h 10.54806 l 10.54769,-0.0325 v -5.17701 c 0,-2.81482 -0.0324,-5.11175 -0.0646,-5.07965 -0.0649,0.0646 -5.75944,0.12947 -12.68341,0.16193"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path7"
|
||||||
|
d="m 34.484042,128.23997 v 14.17144 l -6.018035,0.16192 -6.01839,0.16157 1.326798,2.91218 c 3.559175,7.79745 8.703382,15.82173 13.912496,21.67785 l 1.423812,1.58538 4.691592,-0.38806 c 5.079645,-0.42086 9.609312,-1.19732 15.497879,-2.6857 3.429706,-0.84102 11.518548,-3.39725 11.906604,-3.75285 0.06491,-0.0649 -0.582438,-0.74471 -1.455915,-1.48872 -9.544756,-7.95937 -17.083264,-18.15112 -21.80731,-29.44318 -1.455914,-3.39725 -1.649941,-3.68829 -7.312379,-10.25631 -3.202869,-3.7211 -5.888564,-6.7624 -5.985579,-6.7624 -0.09701,-0.0324 -0.161573,6.34154 -0.161573,14.10688"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<g
|
||||||
|
id="g475"
|
||||||
|
transform="matrix(0.14274222,0,0,0.14274222,123.03997,38.158339)">
|
||||||
|
<path
|
||||||
|
d="m 134.23594,367.14576 v 34.66667 h 69.06667 c 37.86666,0 71.06666,-0.4 73.86666,-0.8 4.4,-0.8 7.2,-3.73333 31.33333,-32.8 14.53334,-17.46667 26.53334,-32.4 26.8,-32.93333 0.26667,-0.66667 -0.93333,-1.46667 -2.66666,-2 -1.73334,-0.4 -47.06667,-0.8 -100.66667,-0.8 h -97.73333 z"
|
||||||
|
id="path251"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
<path
|
||||||
|
d="m 134.90261,432.74576 c -0.4,0.93334 -0.53333,27.2 -0.4,58.4 l 0.4,56.53333 38.4,-37.06666 38.26666,-37.06667 -0.26666,-20.93333 -0.4,-20.8 -37.73334,-0.4 c -29.46666,-0.26667 -37.86666,0 -38.26666,1.33333 z"
|
||||||
|
id="path253"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
+484
-119
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* [Output] */
|
/* [Output] */
|
||||||
part = "assembly"; // [assembly,body,lid,buttons,insert_coupon,switch_coupon,fit_coupon]
|
part = "assembly"; // [assembly,body,lid,lid_base,lid_accent,buttons,insert_coupon,switch_coupon,fit_coupon]
|
||||||
exploded_view = 8; // [0:1:20]
|
exploded_view = 8; // [0:1:20]
|
||||||
show_references = true;
|
show_references = true;
|
||||||
|
|
||||||
@@ -47,24 +47,29 @@ battery_length = 69;
|
|||||||
battery_diameter = 18;
|
battery_diameter = 18;
|
||||||
battery_radial_clearance = 0.6;
|
battery_radial_clearance = 0.6;
|
||||||
battery_end_clearance = 1;
|
battery_end_clearance = 1;
|
||||||
switch_hole_diameter = 12;
|
battery_partition_thickness = 1.8;
|
||||||
switch_hole_compensation = 0.3;
|
battery_board_clearance = 3;
|
||||||
switch_cap_diameter = 8;
|
rocker_cutout_size = [12.9, 19.4];
|
||||||
switch_flange_diameter = 16.2;
|
rocker_corner_radius = 0.8;
|
||||||
switch_body_length = 24;
|
rocker_body_size = [15, 21, 21];
|
||||||
switch_body_diameter = 12;
|
rocker_artwork_gap = 2;
|
||||||
switch_wire_allowance = 3;
|
rocker_partition_offset = 2;
|
||||||
|
rocker_tab_panel_thickness = 2;
|
||||||
|
rocker_tab_land = 2;
|
||||||
|
rocker_tab_taper_length = 0.6;
|
||||||
|
|
||||||
/* [Tripod mount] */
|
/* [Tripod mount] */
|
||||||
tripod_insert_enabled = false;
|
tripod_insert_enabled = false;
|
||||||
tripod_insert_length = 6.4;
|
tripod_insert_length = 5;
|
||||||
tripod_insert_hole_diameter = 8.8;
|
tripod_insert_hole_diameter = 7.2;
|
||||||
|
tripod_insert_outer_diameter = 8;
|
||||||
tripod_thread_clearance_diameter = 6.8;
|
tripod_thread_clearance_diameter = 6.8;
|
||||||
tripod_boss_diameter = 14;
|
tripod_boss_diameter = 14;
|
||||||
|
|
||||||
/* [M2.5 inserts] */
|
/* [M2.5 inserts] */
|
||||||
m25_insert_length = 4;
|
m25_insert_length = 4;
|
||||||
m25_insert_hole_diameter = 3.7;
|
m25_insert_hole_diameter = 3.1;
|
||||||
|
m25_insert_outer_diameter = 3.5;
|
||||||
m25_screw_clearance = 2.8;
|
m25_screw_clearance = 2.8;
|
||||||
m25_boss_diameter = 7;
|
m25_boss_diameter = 7;
|
||||||
insert_entry_chamfer = 0.6;
|
insert_entry_chamfer = 0.6;
|
||||||
@@ -78,19 +83,36 @@ corner_radius = 4;
|
|||||||
board_bottom_clearance = 5.4;
|
board_bottom_clearance = 5.4;
|
||||||
board_side_clearance = 7;
|
board_side_clearance = 7;
|
||||||
board_rear_clearance = 4;
|
board_rear_clearance = 4;
|
||||||
usb_board_wall_gap = 1.2;
|
usb_board_wall_gap = 2;
|
||||||
lid_lip_depth = 2.4;
|
lid_lip_depth = 2.4;
|
||||||
lid_lip_thickness = 1.2;
|
lid_lip_thickness = 1.2;
|
||||||
fit_clearance = 0.35;
|
fit_clearance = 0.35;
|
||||||
|
cutout_min_web = 1.2;
|
||||||
hole_compensation = 0.2;
|
hole_compensation = 0.2;
|
||||||
lid_screw_head_diameter = 5.2;
|
lid_screw_head_diameter = 5.2;
|
||||||
lid_screw_head_height = 2.5;
|
lid_screw_head_height = 2.5;
|
||||||
|
|
||||||
|
/* [Two-color lid artwork] */
|
||||||
|
lid_label = "";
|
||||||
|
lid_logo_enabled = false;
|
||||||
|
lid_art_orientation = "usb_left"; // [usb_left,usb_front,usb_right,usb_rear]
|
||||||
|
lid_inlay_depth = 0.6; // [0.4:0.2:1]
|
||||||
|
lid_inlay_fit_clearance = 0.08;
|
||||||
|
lid_label_size = 5;
|
||||||
|
lid_label_logo_gap = 2;
|
||||||
|
lid_art_margin = 8.5;
|
||||||
|
logo_file = "assets/NME-fixed.svg";
|
||||||
|
|
||||||
/* [USB-C opening] */
|
/* [USB-C opening] */
|
||||||
usb_opening_width = 12.5;
|
usb_opening_enabled = true;
|
||||||
usb_opening_height = 7;
|
usb_guide_enabled = true;
|
||||||
|
usb_connector_overhang = 2;
|
||||||
|
usb_guide_wall_thickness = 1.2;
|
||||||
|
usb_guide_end_gap = 1;
|
||||||
|
usb_opening_width = 13.5;
|
||||||
|
usb_opening_height = 8;
|
||||||
usb_opening_radius = 1.5;
|
usb_opening_radius = 1.5;
|
||||||
usb_vertical_offset = 0;
|
usb_vertical_offset = 0.5;
|
||||||
|
|
||||||
/* [Custom wall cutouts] */
|
/* [Custom wall cutouts] */
|
||||||
// Entry format: [face, shape, u, z, width, height, radius]
|
// Entry format: [face, shape, u, z, width, height, radius]
|
||||||
@@ -100,6 +122,8 @@ extra_cutouts = [];
|
|||||||
|
|
||||||
/* [Calibration] */
|
/* [Calibration] */
|
||||||
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
m25_coupon_steps = [-0.15, -0.075, 0, 0.075, 0.15];
|
||||||
|
tripod_coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
|
||||||
$fn = $preview ? 36 : 72;
|
$fn = $preview ? 36 : 72;
|
||||||
eps = 0.02;
|
eps = 0.02;
|
||||||
@@ -111,6 +135,9 @@ board_thickness = 1.6;
|
|||||||
mount_x = [2.54, 48.26];
|
mount_x = [2.54, 48.26];
|
||||||
mount_y = [2.54, 20.32];
|
mount_y = [2.54, 20.32];
|
||||||
usb_center_y = 11.43;
|
usb_center_y = 11.43;
|
||||||
|
jst_center_xy = [10.9855, 19.304];
|
||||||
|
jst_body_size = [8, 7.5, 4.5];
|
||||||
|
jst_relief_clearance = 0.5;
|
||||||
|
|
||||||
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
||||||
oled_visible_min = [11.42, 3.78];
|
oled_visible_min = [11.42, 3.78];
|
||||||
@@ -127,11 +154,34 @@ effective_board_clearance = max(
|
|||||||
board_bottom_clearance,
|
board_bottom_clearance,
|
||||||
m25_insert_length + insert_melt_relief + 0.6
|
m25_insert_length + insert_melt_relief + 0.6
|
||||||
);
|
);
|
||||||
|
lid_boss_r = m25_boss_diameter / 2;
|
||||||
|
lid_boss_wall_overlap = 0.6;
|
||||||
|
front_lid_boss_center = wall_thickness + lid_boss_r
|
||||||
|
- lid_boss_wall_overlap;
|
||||||
|
|
||||||
|
// Combined battery/tripod builds need room around the centered insert entry.
|
||||||
|
combined_tripod_board_gutter = battery_enabled && tripod_insert_enabled ? 2.4 : 0;
|
||||||
|
combined_tripod_x_shift = battery_enabled && tripod_insert_enabled ? 5 : 0;
|
||||||
|
|
||||||
board_origin_x = wall_thickness + usb_board_wall_gap;
|
board_origin_x = wall_thickness + usb_board_wall_gap;
|
||||||
board_origin_y = wall_thickness + board_side_clearance;
|
board_origin_y = wall_thickness + board_side_clearance
|
||||||
|
+ combined_tripod_board_gutter;
|
||||||
board_bottom_z = effective_floor + effective_board_clearance;
|
board_bottom_z = effective_floor + effective_board_clearance;
|
||||||
board_top_z = board_bottom_z + board_thickness;
|
board_top_z = board_bottom_z + board_thickness;
|
||||||
|
usb_center_u = board_origin_y + usb_center_y;
|
||||||
|
usb_center_z = board_top_z + 1.6 + usb_vertical_offset;
|
||||||
|
usb_inner_width = usb_opening_width + hole_compensation;
|
||||||
|
usb_inner_height = usb_opening_height + hole_compensation;
|
||||||
|
usb_guide_length = max(
|
||||||
|
0,
|
||||||
|
usb_board_wall_gap - usb_connector_overhang - usb_guide_end_gap
|
||||||
|
);
|
||||||
|
usb_guide_active = usb_opening_enabled && usb_guide_enabled
|
||||||
|
&& usb_guide_length > 0;
|
||||||
|
usb_reserved_width = usb_inner_width
|
||||||
|
+ (usb_guide_active ? 2 * usb_guide_wall_thickness : 0);
|
||||||
|
usb_reserved_height = usb_inner_height
|
||||||
|
+ (usb_guide_active ? 2 * usb_guide_wall_thickness : 0);
|
||||||
|
|
||||||
computed_top_pcb_z = board_top_z + wing_count * wing_pitch;
|
computed_top_pcb_z = board_top_z + wing_count * wing_pitch;
|
||||||
top_pcb_z = stack_height_override > 0
|
top_pcb_z = stack_height_override > 0
|
||||||
@@ -146,20 +196,17 @@ profile_top_gap =
|
|||||||
top_profile == "oled128x64" ? oled_face_gap :
|
top_profile == "oled128x64" ? oled_face_gap :
|
||||||
generic_top_gap;
|
generic_top_gap;
|
||||||
|
|
||||||
partition_y = board_origin_y + feather_width + 1;
|
partition_y = board_origin_y + feather_width + battery_board_clearance;
|
||||||
battery_center_y = partition_y + wall_thickness + battery_radial_clearance
|
battery_center_y = partition_y + battery_partition_thickness
|
||||||
+ battery_diameter / 2;
|
+ battery_radial_clearance + battery_diameter / 2;
|
||||||
battery_start_x = board_origin_x + battery_end_clearance;
|
battery_start_x = front_lid_boss_center + lid_boss_r
|
||||||
|
+ battery_end_clearance;
|
||||||
battery_end_x = battery_start_x + battery_length;
|
battery_end_x = battery_start_x + battery_length;
|
||||||
switch_center_x = battery_end_x + max(
|
|
||||||
switch_body_diameter / 2 + 0.8,
|
|
||||||
switch_flange_diameter / 2 + 0.5
|
|
||||||
);
|
|
||||||
|
|
||||||
case_length_plain = board_origin_x + feather_length
|
case_length_plain = board_origin_x + feather_length
|
||||||
+ board_rear_clearance + wall_thickness;
|
+ board_rear_clearance + wall_thickness;
|
||||||
case_length_battery = switch_center_x + switch_flange_diameter / 2
|
case_length_battery = battery_end_x + battery_end_clearance
|
||||||
+ 1.2 + wall_thickness;
|
+ wall_thickness + m25_boss_diameter - lid_boss_wall_overlap;
|
||||||
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
||||||
: case_length_plain;
|
: case_length_plain;
|
||||||
|
|
||||||
@@ -170,23 +217,74 @@ case_width_battery = battery_center_y + battery_diameter / 2
|
|||||||
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
||||||
: case_width_plain;
|
: case_width_plain;
|
||||||
|
|
||||||
|
rocker_installed_center_x = case_length - wall_thickness - fit_clearance
|
||||||
|
- lid_lip_thickness - fit_clearance - rocker_body_size[0] / 2;
|
||||||
|
rocker_installed_center_y = front_lid_boss_center + lid_boss_r + fit_clearance
|
||||||
|
+ rocker_body_size[1] / 2 + rocker_partition_offset;
|
||||||
|
// The printable lid is flipped across Y when installed, so its local cutout
|
||||||
|
// coordinate must be mirrored from the assembled switch location.
|
||||||
|
rocker_lid_center_x = rocker_installed_center_x;
|
||||||
|
rocker_lid_center_y = case_width - rocker_installed_center_y;
|
||||||
|
|
||||||
|
lid_artwork_enabled = lid_label != "" || lid_logo_enabled;
|
||||||
|
lid_art_rotation =
|
||||||
|
lid_art_orientation == "usb_left" ? 0 :
|
||||||
|
lid_art_orientation == "usb_front" ? 90 :
|
||||||
|
lid_art_orientation == "usb_right" ? 180 : 270;
|
||||||
|
lid_art_safe_max_x = battery_enabled
|
||||||
|
? rocker_lid_center_x - rocker_cutout_size[0] / 2 - rocker_artwork_gap
|
||||||
|
: case_length - lid_art_margin;
|
||||||
|
lid_art_physical_size = [
|
||||||
|
lid_art_safe_max_x - lid_art_margin,
|
||||||
|
case_width - 2 * lid_art_margin
|
||||||
|
];
|
||||||
|
lid_art_center = [
|
||||||
|
lid_art_margin + lid_art_physical_size[0] / 2,
|
||||||
|
case_width / 2
|
||||||
|
];
|
||||||
|
lid_art_canvas_size = lid_art_rotation == 0 || lid_art_rotation == 180
|
||||||
|
? lid_art_physical_size
|
||||||
|
: [lid_art_physical_size[1], lid_art_physical_size[0]];
|
||||||
|
lid_art_available_size = lid_art_canvas_size;
|
||||||
|
lid_label_height = lid_label == "" ? 0 : lid_label_size;
|
||||||
|
lid_art_gap = lid_label != "" && lid_logo_enabled ? lid_label_logo_gap : 0;
|
||||||
|
logo_native_size = [30, 25.75];
|
||||||
|
logo_max_height = lid_art_available_size[1] - lid_label_height - lid_art_gap;
|
||||||
|
logo_scale = lid_logo_enabled ? min(
|
||||||
|
lid_art_available_size[0] / logo_native_size[0],
|
||||||
|
logo_max_height / logo_native_size[1]
|
||||||
|
) : 0;
|
||||||
|
logo_size = [logo_native_size[0] * logo_scale,
|
||||||
|
logo_native_size[1] * logo_scale];
|
||||||
|
lid_art_total_height = logo_size[1] + lid_art_gap + lid_label_height;
|
||||||
|
lid_label_center_y = -lid_art_total_height / 2 + lid_label_height / 2;
|
||||||
|
lid_logo_center_y = -lid_art_total_height / 2 + lid_label_height
|
||||||
|
+ lid_art_gap + logo_size[1] / 2;
|
||||||
|
|
||||||
|
tripod_center_x = case_length / 2 - combined_tripod_x_shift;
|
||||||
|
tripod_center_y = case_width / 2;
|
||||||
|
tripod_entry_radius = tripod_insert_outer_diameter / 2;
|
||||||
|
tripod_standoff_clearance = min([
|
||||||
|
for (mx = mount_x, my = mount_y)
|
||||||
|
sqrt(
|
||||||
|
pow(board_origin_x + mx - tripod_center_x, 2)
|
||||||
|
+ pow(board_origin_y + my - tripod_center_y, 2)
|
||||||
|
) - tripod_entry_radius - m25_boss_diameter / 2
|
||||||
|
]);
|
||||||
|
tripod_partition_clearance = partition_y
|
||||||
|
- (tripod_center_y + tripod_entry_radius);
|
||||||
|
|
||||||
battery_center_z = effective_floor + battery_radial_clearance
|
battery_center_z = effective_floor + battery_radial_clearance
|
||||||
+ battery_diameter / 2;
|
+ battery_diameter / 2;
|
||||||
switch_center_z = max(
|
|
||||||
effective_floor + switch_flange_diameter / 2 + 1,
|
|
||||||
battery_center_z
|
|
||||||
);
|
|
||||||
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
||||||
battery_ceiling_z = battery_enabled
|
battery_ceiling_z = battery_enabled
|
||||||
? max(
|
? battery_center_z + battery_diameter / 2 + battery_radial_clearance
|
||||||
battery_center_z + battery_diameter / 2 + battery_radial_clearance,
|
|
||||||
switch_center_z + switch_flange_diameter / 2 + 1
|
|
||||||
)
|
|
||||||
: 0;
|
: 0;
|
||||||
body_height = max(electronics_ceiling_z, battery_ceiling_z);
|
rocker_ceiling_z = battery_enabled
|
||||||
|
? effective_floor + rocker_body_size[2] + fit_clearance
|
||||||
|
: 0;
|
||||||
|
body_height = max(electronics_ceiling_z, battery_ceiling_z, rocker_ceiling_z);
|
||||||
|
|
||||||
lid_boss_r = m25_boss_diameter / 2;
|
|
||||||
lid_boss_wall_overlap = 0.6;
|
|
||||||
lid_boss_xy = [
|
lid_boss_xy = [
|
||||||
[
|
[
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
||||||
@@ -205,19 +303,156 @@ lid_boss_xy = [
|
|||||||
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
rocker_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(
|
||||||
|
abs(p[0] - rocker_installed_center_x)
|
||||||
|
- rocker_body_size[0] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
+ pow(max(
|
||||||
|
abs(p[1] - rocker_installed_center_y)
|
||||||
|
- rocker_body_size[1] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
rocker_relief_half_size = [
|
||||||
|
rocker_cutout_size[0] / 2,
|
||||||
|
rocker_cutout_size[1] / 2 + rocker_tab_land + rocker_tab_taper_length
|
||||||
|
];
|
||||||
|
rocker_relief_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(abs(p[0] - rocker_lid_center_x)
|
||||||
|
- rocker_relief_half_size[0], 0), 2)
|
||||||
|
+ pow(max(abs(p[1] - rocker_lid_center_y)
|
||||||
|
- rocker_relief_half_size[1], 0), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
|
||||||
assert(wing_count >= 1, "wing_count must be at least one");
|
assert(wing_count >= 1, "wing_count must be at least one");
|
||||||
assert(top_profile == "generic" || top_profile == "gps"
|
assert(top_profile == "generic" || top_profile == "gps"
|
||||||
|| top_profile == "oled128x64", "Unsupported top_profile");
|
|| top_profile == "oled128x64", "Unsupported top_profile");
|
||||||
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_partition_thickness >= 1.2,
|
||||||
|
"battery_partition_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_board_clearance > 0,
|
||||||
|
"battery_board_clearance must be positive");
|
||||||
|
assert(rocker_tab_panel_thickness >= 1.2,
|
||||||
|
"rocker tab panel needs at least two 0.6 mm lines");
|
||||||
|
assert(rocker_tab_panel_thickness < lid_thickness,
|
||||||
|
"rocker tab panel must be thinner than the lid");
|
||||||
|
assert(rocker_tab_land > 0 && rocker_tab_taper_length > 0,
|
||||||
|
"rocker tab relief land and taper must be positive");
|
||||||
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
||||||
|
assert(lid_art_orientation == "usb_left"
|
||||||
|
|| lid_art_orientation == "usb_front"
|
||||||
|
|| lid_art_orientation == "usb_right"
|
||||||
|
|| lid_art_orientation == "usb_rear",
|
||||||
|
"Unsupported lid_art_orientation");
|
||||||
|
assert(!lid_artwork_enabled || top_profile != "oled128x64",
|
||||||
|
"Lid artwork is not supported with the OLED profile");
|
||||||
|
assert(!lid_artwork_enabled || lid_inlay_depth < lid_thickness,
|
||||||
|
"Lid inlay must be shallower than the lid");
|
||||||
|
assert(!lid_artwork_enabled
|
||||||
|
|| (lid_art_available_size[0] > 0 && lid_art_available_size[1] > 0),
|
||||||
|
"Lid artwork margin leaves no printable area");
|
||||||
|
assert(!lid_logo_enabled || logo_scale > 0,
|
||||||
|
"Lid logo has no printable area");
|
||||||
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
||||||
"M2.5 boss does not have enough radial material");
|
"M2.5 boss does not have enough radial material");
|
||||||
|
assert(m25_insert_outer_diameter >= m25_insert_hole_diameter,
|
||||||
|
"M2.5 insert entry must not be smaller than its pilot");
|
||||||
assert(!tripod_insert_enabled
|
assert(!tripod_insert_enabled
|
||||||
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
||||||
"Tripod insert boss does not have enough radial material");
|
"Tripod insert boss does not have enough radial material");
|
||||||
|
assert(usb_opening_enabled || !usb_guide_enabled,
|
||||||
|
"USB guide requires the USB opening");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z - usb_reserved_height / 2 >= effective_floor,
|
||||||
|
"USB opening or guide intersects the case floor");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z + usb_reserved_height / 2
|
||||||
|
<= body_height - lid_lip_depth,
|
||||||
|
"USB opening or guide intersects the lid lip");
|
||||||
|
assert(!tripod_insert_enabled
|
||||||
|
|| tripod_insert_outer_diameter >= tripod_insert_hole_diameter,
|
||||||
|
"Tripod insert entry must not be smaller than its pilot");
|
||||||
|
assert(!tripod_insert_enabled || tripod_standoff_clearance >= 1.2,
|
||||||
|
"Tripod insert entry is too close to a Feather standoff");
|
||||||
|
assert(!battery_enabled || !tripod_insert_enabled
|
||||||
|
|| tripod_partition_clearance >= 1,
|
||||||
|
"Tripod insert entry is too close to the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= board_origin_x + feather_length + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather PCB");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y + rocker_body_size[1] / 2
|
||||||
|
<= partition_y - fit_clearance,
|
||||||
|
"Rocker body overlaps the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the front lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x + rocker_body_size[0] / 2
|
||||||
|
<= case_length - wall_thickness - lid_lip_thickness - fit_clearance,
|
||||||
|
"Rocker body overlaps the rear lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y - rocker_body_size[1] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather-side lid lip");
|
||||||
|
assert(!battery_enabled || rocker_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker body overlaps a lid boss");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_relief_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker tab relief overlaps a lid boss scallop");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| abs(
|
||||||
|
case_width - rocker_lid_center_y - rocker_installed_center_y
|
||||||
|
) < eps,
|
||||||
|
"Rocker lid cutout does not mirror to the installed switch location");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| body_height - rocker_body_size[2] >= effective_floor,
|
||||||
|
"Rocker body intersects the case floor");
|
||||||
|
|
||||||
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
||||||
|
function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4];
|
||||||
|
function cutouts_separated(first, second, web) =
|
||||||
|
abs(first[2] - second[2])
|
||||||
|
>= (first[4] + second[4]) / 2 + web
|
||||||
|
|| abs(first[3] - second[3])
|
||||||
|
>= (cutout_height(first) + cutout_height(second)) / 2 + web;
|
||||||
|
function cutout_clear_of_usb(entry, web) =
|
||||||
|
abs(entry[2] - usb_center_u)
|
||||||
|
>= (entry[4] + usb_reserved_width) / 2 + web
|
||||||
|
|| abs(entry[3] - usb_center_z)
|
||||||
|
>= (cutout_height(entry) + usb_reserved_height) / 2 + web;
|
||||||
|
|
||||||
|
module validate_cutout_spacing() {
|
||||||
|
if (len(extra_cutouts) > 1)
|
||||||
|
for (first = [0 : len(extra_cutouts) - 2],
|
||||||
|
second = [first + 1 : len(extra_cutouts) - 1])
|
||||||
|
if (extra_cutouts[first][0] == extra_cutouts[second][0])
|
||||||
|
assert(
|
||||||
|
cutouts_separated(
|
||||||
|
extra_cutouts[first],
|
||||||
|
extra_cutouts[second],
|
||||||
|
cutout_min_web
|
||||||
|
),
|
||||||
|
"Custom cutouts are too close on the same wall"
|
||||||
|
);
|
||||||
|
if (usb_opening_enabled)
|
||||||
|
for (entry = extra_cutouts)
|
||||||
|
if (entry[0] == "front")
|
||||||
|
assert(
|
||||||
|
cutout_clear_of_usb(entry, cutout_min_web),
|
||||||
|
"Front cutout is too close to the USB opening or guide"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module rounded_rect_2d(size, radius) {
|
module rounded_rect_2d(size, radius) {
|
||||||
r = clamp(radius, 0.01, min(size[0], size[1]) / 2 - 0.01);
|
r = clamp(radius, 0.01, min(size[0], size[1]) / 2 - 0.01);
|
||||||
@@ -242,8 +477,8 @@ module rounded_box(size, radius) {
|
|||||||
rounded_rect_2d([size[0], size[1]], radius);
|
rounded_rect_2d([size[0], size[1]], radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
module insert_pocket(diameter, depth, entry_z, downward = true) {
|
module insert_pocket(diameter, entry_diameter, depth, entry_z, downward = true) {
|
||||||
bore_d = diameter + hole_compensation;
|
bore_d = diameter;
|
||||||
if (downward) {
|
if (downward) {
|
||||||
translate([0, 0, entry_z - depth])
|
translate([0, 0, entry_z - depth])
|
||||||
cylinder(h = depth + eps, d = bore_d);
|
cylinder(h = depth + eps, d = bore_d);
|
||||||
@@ -251,7 +486,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d,
|
d1 = bore_d,
|
||||||
d2 = bore_d + 2 * insert_entry_chamfer
|
d2 = entry_diameter
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
@@ -259,7 +494,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d + 2 * insert_entry_chamfer,
|
d1 = entry_diameter,
|
||||||
d2 = bore_d
|
d2 = bore_d
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -301,6 +536,40 @@ module face_circle_cutout(face, u, z, diameter) {
|
|||||||
rotate([-90, 0, 0]) cylinder(h = depth, d = diameter);
|
rotate([-90, 0, 0]) cylinder(h = depth, d = diameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module x_rounded_prism(x, length, u, z, width, height, radius) {
|
||||||
|
r = clamp(radius, 0.01, min(width, height) / 2 - 0.01);
|
||||||
|
hull()
|
||||||
|
for (yy = [u - width / 2 + r, u + width / 2 - r],
|
||||||
|
zz = [z - height / 2 + r, z + height / 2 - r])
|
||||||
|
translate([x, yy, zz])
|
||||||
|
rotate([0, 90, 0]) cylinder(h = length, r = r);
|
||||||
|
}
|
||||||
|
|
||||||
|
module usb_guide_shell() {
|
||||||
|
outer_width = usb_inner_width + 2 * usb_guide_wall_thickness;
|
||||||
|
outer_height = usb_inner_height + 2 * usb_guide_wall_thickness;
|
||||||
|
difference() {
|
||||||
|
x_rounded_prism(
|
||||||
|
wall_thickness - eps,
|
||||||
|
usb_guide_length + 2 * eps,
|
||||||
|
usb_center_u,
|
||||||
|
usb_center_z,
|
||||||
|
outer_width,
|
||||||
|
outer_height,
|
||||||
|
usb_opening_radius + usb_guide_wall_thickness
|
||||||
|
);
|
||||||
|
x_rounded_prism(
|
||||||
|
wall_thickness - 2 * eps,
|
||||||
|
usb_guide_length + 4 * eps,
|
||||||
|
usb_center_u,
|
||||||
|
usb_center_z,
|
||||||
|
usb_inner_width,
|
||||||
|
usb_inner_height,
|
||||||
|
usb_opening_radius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module custom_wall_cutout(entry) {
|
module custom_wall_cutout(entry) {
|
||||||
face = entry[0];
|
face = entry[0];
|
||||||
shape = entry[1];
|
shape = entry[1];
|
||||||
@@ -368,20 +637,27 @@ module battery_cradle() {
|
|||||||
battery_end_x - 4
|
battery_end_x - 4
|
||||||
];
|
];
|
||||||
|
|
||||||
// Board/battery partition, with a lead-routing notch near the JST end.
|
// One open-topped front passage provides JST plug access and enough room
|
||||||
|
// to route the battery lead beneath the Feather PCB beside the lid boss.
|
||||||
difference() {
|
difference() {
|
||||||
translate([wall_thickness, partition_y, effective_floor - eps])
|
translate([wall_thickness, partition_y, effective_floor - eps])
|
||||||
cube([
|
cube([
|
||||||
battery_end_x - wall_thickness + battery_end_clearance,
|
battery_end_x - wall_thickness + battery_end_clearance,
|
||||||
wall_thickness,
|
battery_partition_thickness,
|
||||||
partition_height - effective_floor + eps
|
partition_height - effective_floor + eps
|
||||||
]);
|
]);
|
||||||
translate([
|
translate([
|
||||||
board_origin_x + 6.5,
|
wall_thickness - eps,
|
||||||
partition_y - eps,
|
partition_y - eps,
|
||||||
effective_floor + 1.5
|
effective_floor + 1.5
|
||||||
])
|
])
|
||||||
cube([9, wall_thickness + 2 * eps, 6]);
|
cube([
|
||||||
|
board_origin_x + jst_center_xy[0]
|
||||||
|
+ (jst_body_size[0] + 2 * jst_relief_clearance) / 2
|
||||||
|
- wall_thickness + 2 * eps,
|
||||||
|
battery_partition_thickness + 2 * eps,
|
||||||
|
partition_height - effective_floor - 1.5 + eps
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three low curved ribs cradle the protected cylindrical pack.
|
// Three low curved ribs cradle the protected cylindrical pack.
|
||||||
@@ -412,29 +688,16 @@ module battery_cradle() {
|
|||||||
battery_end_x + battery_end_clearance
|
battery_end_x + battery_end_clearance
|
||||||
])
|
])
|
||||||
translate([
|
translate([
|
||||||
x,
|
x - eps,
|
||||||
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
||||||
effective_floor - eps
|
effective_floor - eps
|
||||||
])
|
])
|
||||||
cube([
|
cube([
|
||||||
wall_thickness,
|
wall_thickness + 2 * eps,
|
||||||
battery_diameter + 2 * battery_radial_clearance,
|
battery_diameter + 2 * battery_radial_clearance,
|
||||||
battery_diameter / 2 + 1 + eps
|
battery_diameter / 2 + 1 + eps
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Rear cross-strip separates the side-mounted switch body from the cell.
|
|
||||||
translate([
|
|
||||||
battery_end_x + battery_end_clearance,
|
|
||||||
partition_y,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
case_length - wall_thickness - battery_end_x - battery_end_clearance,
|
|
||||||
case_width - wall_thickness - partition_y,
|
|
||||||
min(body_height - effective_floor, switch_center_z
|
|
||||||
+ switch_body_diameter / 2 + switch_wire_allowance
|
|
||||||
- effective_floor) + eps
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module body_positive() {
|
module body_positive() {
|
||||||
@@ -453,8 +716,10 @@ module body_positive() {
|
|||||||
if (battery_enabled)
|
if (battery_enabled)
|
||||||
battery_cradle();
|
battery_cradle();
|
||||||
if (tripod_insert_enabled)
|
if (tripod_insert_enabled)
|
||||||
translate([case_length / 2, case_width / 2, 0])
|
translate([tripod_center_x, tripod_center_y, 0])
|
||||||
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
||||||
|
if (usb_guide_active)
|
||||||
|
usb_guide_shell();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +729,7 @@ module body_cutouts() {
|
|||||||
translate([board_origin_x + mx, board_origin_y + my, 0])
|
translate([board_origin_x + mx, board_origin_y + my, 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
board_bottom_z
|
board_bottom_z
|
||||||
);
|
);
|
||||||
@@ -473,44 +739,24 @@ module body_cutouts() {
|
|||||||
translate([p[0], p[1], 0])
|
translate([p[0], p[1], 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
body_height
|
body_height
|
||||||
);
|
);
|
||||||
|
|
||||||
// USB-C cable opening. Connector center is from the official ESP32 V2 PCB.
|
// USB-C cable opening. Connector center is from the official ESP32 V2 PCB.
|
||||||
usb_center_z = board_top_z + 1.6 + usb_vertical_offset;
|
if (usb_opening_enabled)
|
||||||
face_rounded_cutout(
|
face_rounded_cutout(
|
||||||
"front",
|
"front",
|
||||||
board_origin_y + usb_center_y,
|
usb_center_u,
|
||||||
usb_center_z,
|
usb_center_z,
|
||||||
usb_opening_width + hole_compensation,
|
usb_inner_width,
|
||||||
usb_opening_height + hole_compensation,
|
usb_inner_height,
|
||||||
usb_opening_radius
|
usb_opening_radius
|
||||||
);
|
);
|
||||||
|
|
||||||
if (battery_enabled) {
|
|
||||||
face_circle_cutout(
|
|
||||||
"right",
|
|
||||||
switch_center_x,
|
|
||||||
switch_center_z,
|
|
||||||
switch_hole_diameter + switch_hole_compensation
|
|
||||||
);
|
|
||||||
// Clear the switch body and its prewired terminals through the rear bay.
|
|
||||||
translate([
|
|
||||||
switch_center_x,
|
|
||||||
case_width - wall_thickness + eps,
|
|
||||||
switch_center_z
|
|
||||||
])
|
|
||||||
rotate([90, 0, 0])
|
|
||||||
cylinder(
|
|
||||||
h = switch_body_length + switch_wire_allowance
|
|
||||||
+ wall_thickness + eps,
|
|
||||||
d = switch_body_diameter + 2 * fit_clearance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tripod_insert_enabled) {
|
if (tripod_insert_enabled) {
|
||||||
translate([case_length / 2, case_width / 2, 0]) {
|
translate([tripod_center_x, tripod_center_y, 0]) {
|
||||||
translate([0, 0, -eps])
|
translate([0, 0, -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = effective_floor + 2 * eps,
|
h = effective_floor + 2 * eps,
|
||||||
@@ -518,6 +764,7 @@ module body_cutouts() {
|
|||||||
);
|
);
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter,
|
tripod_insert_hole_diameter,
|
||||||
|
tripod_insert_outer_diameter,
|
||||||
tripod_insert_length + insert_melt_relief,
|
tripod_insert_length + insert_melt_relief,
|
||||||
effective_floor
|
effective_floor
|
||||||
);
|
);
|
||||||
@@ -611,6 +858,13 @@ module oled_button_lid_cutout(xy) {
|
|||||||
module lid_cutouts() {
|
module lid_cutouts() {
|
||||||
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
||||||
for (p = lid_boss_xy) {
|
for (p = lid_boss_xy) {
|
||||||
|
// Scallop only the locating lip so it can pass over the full-height
|
||||||
|
// insert boss while leaving the lid plate supported by the boss top.
|
||||||
|
translate([p[0], p[1], lid_thickness - eps])
|
||||||
|
cylinder(
|
||||||
|
h = lid_lip_depth + 2 * eps,
|
||||||
|
d = m25_boss_diameter + 2 * fit_clearance
|
||||||
|
);
|
||||||
translate([p[0], p[1], -eps])
|
translate([p[0], p[1], -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = lid_thickness + lid_lip_depth + 2 * eps,
|
h = lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
@@ -623,6 +877,19 @@ module lid_cutouts() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (battery_enabled) {
|
||||||
|
translate([rocker_lid_center_x, rocker_lid_center_y, -eps])
|
||||||
|
centered_rounded_prism(
|
||||||
|
rocker_cutout_size,
|
||||||
|
lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[rocker_lid_center_x, rocker_lid_center_y],
|
||||||
|
rocker_cutout_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (top_profile == "oled128x64") {
|
if (top_profile == "oled128x64") {
|
||||||
beveled_oled_window();
|
beveled_oled_window();
|
||||||
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
||||||
@@ -632,13 +899,90 @@ module lid_cutouts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module lid() {
|
// Thin the horizontal top/bottom edges of the upright switch opening where the
|
||||||
|
// spring tabs latch. Each full-width land overlaps the rounded opening by its
|
||||||
|
// corner radius, then extends outward before tapering to the surrounding lid.
|
||||||
|
module rocker_tab_reliefs(center, opening_size) {
|
||||||
|
relief_depth = lid_thickness - rocker_tab_panel_thickness;
|
||||||
|
opening_overlap = rocker_corner_radius;
|
||||||
|
strip = 0.02;
|
||||||
|
for (side = [-1, 1]) {
|
||||||
|
edge_y = center[1] + side * opening_size[1] / 2;
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y - (side < 0 ? rocker_tab_land : opening_overlap),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], rocker_tab_land + opening_overlap,
|
||||||
|
relief_depth + eps]);
|
||||||
|
|
||||||
|
hull() {
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * rocker_tab_land - (side < 0 ? strip : 0),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, relief_depth + eps]);
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * (rocker_tab_land + rocker_tab_taper_length)
|
||||||
|
- (side < 0 ? strip : 0),
|
||||||
|
lid_thickness - strip
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, strip + eps]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_nominal_2d() {
|
||||||
|
translate(lid_art_center)
|
||||||
|
rotate(lid_art_rotation)
|
||||||
|
// The lid is modeled exterior-face-down; mirror the artwork so it
|
||||||
|
// reads normally after the lid is turned into service position.
|
||||||
|
mirror([0, 1, 0]) {
|
||||||
|
if (lid_logo_enabled)
|
||||||
|
translate([0, lid_logo_center_y])
|
||||||
|
resize(logo_size)
|
||||||
|
import(logo_file, center = true);
|
||||||
|
if (lid_label != "")
|
||||||
|
translate([0, lid_label_center_y])
|
||||||
|
text(
|
||||||
|
lid_label,
|
||||||
|
size = lid_label_size,
|
||||||
|
halign = "center",
|
||||||
|
valign = "center",
|
||||||
|
font = "DejaVu Sans:style=Bold"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_2d(inset = 0) {
|
||||||
|
offset(delta = inset)
|
||||||
|
lid_artwork_nominal_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_base() {
|
||||||
difference() {
|
difference() {
|
||||||
lid_plate_positive();
|
lid_plate_positive();
|
||||||
lid_cutouts();
|
lid_cutouts();
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
translate([0, 0, -eps])
|
||||||
|
linear_extrude(height = lid_inlay_depth + eps)
|
||||||
|
lid_artwork_2d(lid_inlay_fit_clearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module lid_accent() {
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
linear_extrude(height = lid_inlay_depth)
|
||||||
|
lid_artwork_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
color([0.92, 0.92, 0.95]) lid_base();
|
||||||
|
color([0.95, 0.42, 0.08]) lid_accent();
|
||||||
|
}
|
||||||
|
|
||||||
module one_oled_button() {
|
module one_oled_button() {
|
||||||
recess_depth = button_flange_thickness + button_travel;
|
recess_depth = button_flange_thickness + button_travel;
|
||||||
stem_height = lid_thickness - recess_depth + button_projection;
|
stem_height = lid_thickness - recess_depth + button_projection;
|
||||||
@@ -678,6 +1022,13 @@ module reference_board() {
|
|||||||
color([0.05, 0.25, 0.55, 0.75])
|
color([0.05, 0.25, 0.55, 0.75])
|
||||||
translate([board_origin_x, board_origin_y, board_bottom_z])
|
translate([board_origin_x, board_origin_y, board_bottom_z])
|
||||||
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
||||||
|
color([0.92, 0.92, 0.86, 0.9])
|
||||||
|
translate([
|
||||||
|
board_origin_x + jst_center_xy[0] - jst_body_size[0] / 2,
|
||||||
|
board_origin_y + jst_center_xy[1] - 4.5,
|
||||||
|
board_top_z
|
||||||
|
])
|
||||||
|
cube(jst_body_size);
|
||||||
color([0.12, 0.45, 0.75, 0.45])
|
color([0.12, 0.45, 0.75, 0.45])
|
||||||
for (i = [1 : wing_count])
|
for (i = [1 : wing_count])
|
||||||
translate([
|
translate([
|
||||||
@@ -744,19 +1095,20 @@ module reference_battery_and_switch() {
|
|||||||
translate([battery_start_x, battery_center_y, battery_center_z])
|
translate([battery_start_x, battery_center_y, battery_center_z])
|
||||||
rotate([0, 90, 0])
|
rotate([0, 90, 0])
|
||||||
cylinder(h = battery_length, d = battery_diameter);
|
cylinder(h = battery_length, d = battery_diameter);
|
||||||
color([0.8, 0.15, 0.1, 0.65])
|
color([0.08, 0.08, 0.08, 0.85])
|
||||||
translate([
|
translate([
|
||||||
switch_center_x,
|
rocker_installed_center_x - rocker_body_size[0] / 2,
|
||||||
case_width - wall_thickness,
|
rocker_installed_center_y - rocker_body_size[1] / 2,
|
||||||
switch_center_z
|
body_height + exploded_view - rocker_body_size[2]
|
||||||
])
|
])
|
||||||
rotate([90, 0, 0]) {
|
cube(rocker_body_size);
|
||||||
cylinder(h = switch_body_length, d = switch_body_diameter);
|
color([0.12, 0.12, 0.12, 0.95])
|
||||||
translate([0, 0, -1.5])
|
translate([
|
||||||
cylinder(h = 1.5, d = switch_flange_diameter);
|
rocker_installed_center_x - rocker_cutout_size[0] / 2,
|
||||||
translate([0, 0, -3])
|
rocker_installed_center_y - rocker_cutout_size[1] / 2,
|
||||||
cylinder(h = 3, d = switch_cap_diameter);
|
body_height + exploded_view + lid_thickness
|
||||||
}
|
])
|
||||||
|
cube([rocker_cutout_size[0], rocker_cutout_size[1], 2.5]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,13 +1126,9 @@ module assembly() {
|
|||||||
lid();
|
lid();
|
||||||
}
|
}
|
||||||
|
|
||||||
module coupon_hole(x, y, diameter, depth) {
|
|
||||||
translate([x, y, -eps]) cylinder(h = depth + 2 * eps, d = diameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
module insert_coupon() {
|
module insert_coupon() {
|
||||||
spacing = 15;
|
spacing = 15;
|
||||||
coupon_length = spacing * len(coupon_steps) + 8;
|
coupon_length = spacing * len(tripod_coupon_steps) + 8;
|
||||||
coupon_width = 32;
|
coupon_width = 32;
|
||||||
coupon_height = max(
|
coupon_height = max(
|
||||||
m25_insert_length + insert_melt_relief + 1.2,
|
m25_insert_length + insert_melt_relief + 1.2,
|
||||||
@@ -788,17 +1136,21 @@ module insert_coupon() {
|
|||||||
);
|
);
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1]) {
|
for (i = [0 : len(tripod_coupon_steps) - 1]) {
|
||||||
x = 7 + i * spacing;
|
x = 7 + i * spacing;
|
||||||
coupon_hole(
|
translate([x, 8, 0])
|
||||||
x, 8,
|
insert_pocket(
|
||||||
m25_insert_hole_diameter + coupon_steps[i],
|
m25_insert_hole_diameter + m25_coupon_steps[i],
|
||||||
m25_insert_length + insert_melt_relief
|
m25_insert_outer_diameter,
|
||||||
|
m25_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
coupon_hole(
|
translate([x, 23, 0])
|
||||||
x, 23,
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter + coupon_steps[i],
|
tripod_insert_hole_diameter + tripod_coupon_steps[i],
|
||||||
tripod_insert_length + insert_melt_relief
|
tripod_insert_outer_diameter,
|
||||||
|
tripod_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -807,16 +1159,23 @@ module insert_coupon() {
|
|||||||
module switch_coupon() {
|
module switch_coupon() {
|
||||||
spacing = 18;
|
spacing = 18;
|
||||||
coupon_length = spacing * len(coupon_steps) + 6;
|
coupon_length = spacing * len(coupon_steps) + 6;
|
||||||
coupon_width = 24;
|
coupon_width = rocker_cutout_size[1] + 6;
|
||||||
coupon_height = wall_thickness;
|
coupon_height = lid_thickness;
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1])
|
for (i = [0 : len(coupon_steps) - 1]) {
|
||||||
translate([6 + i * spacing, coupon_width / 2, -eps])
|
translate([6 + i * spacing, coupon_width / 2, -eps])
|
||||||
cylinder(
|
centered_rounded_prism(
|
||||||
h = coupon_height + 2 * eps,
|
rocker_cutout_size
|
||||||
d = switch_hole_diameter + coupon_steps[i]
|
+ [coupon_steps[i], coupon_steps[i]],
|
||||||
|
coupon_height + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
);
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[6 + i * spacing, coupon_width / 2],
|
||||||
|
rocker_cutout_size + [coupon_steps[i], coupon_steps[i]]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,12 +1213,18 @@ module fit_coupon() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_cutout_spacing();
|
||||||
|
|
||||||
if (part == "assembly")
|
if (part == "assembly")
|
||||||
assembly();
|
assembly();
|
||||||
else if (part == "body")
|
else if (part == "body")
|
||||||
body();
|
body();
|
||||||
else if (part == "lid")
|
else if (part == "lid")
|
||||||
lid();
|
lid();
|
||||||
|
else if (part == "lid_base")
|
||||||
|
lid_base();
|
||||||
|
else if (part == "lid_accent")
|
||||||
|
lid_accent();
|
||||||
else if (part == "buttons")
|
else if (part == "buttons")
|
||||||
buttons();
|
buttons();
|
||||||
else if (part == "insert_coupon")
|
else if (part == "insert_coupon")
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Render aligned lid-base and accent STLs with distinct materials."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
|
||||||
|
def import_mesh(path, name, color):
|
||||||
|
bpy.ops.wm.stl_import(filepath=os.path.abspath(path))
|
||||||
|
model = bpy.context.selected_objects[0]
|
||||||
|
model.name = name
|
||||||
|
material = bpy.data.materials.new(f"{name} material")
|
||||||
|
material.diffuse_color = color
|
||||||
|
material.metallic = 0.0
|
||||||
|
material.roughness = 0.45
|
||||||
|
model.data.materials.append(material)
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def look_at(camera, target):
|
||||||
|
direction = Vector(target) - camera.location
|
||||||
|
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[sys.argv.index("--") + 1 :]
|
||||||
|
if len(args) not in (4, 5):
|
||||||
|
raise SystemExit(
|
||||||
|
"expected: lid-base.stl lid-accent.stl output-directory basename [orientation]"
|
||||||
|
)
|
||||||
|
base_path, accent_path, output_dir, basename = args[:4]
|
||||||
|
orientation = args[4] if len(args) == 5 else "usb_left"
|
||||||
|
angles = {"usb_left": 0, "usb_front": 90, "usb_right": 180, "usb_rear": 270}
|
||||||
|
if orientation not in angles:
|
||||||
|
raise SystemExit(f"unknown lid orientation: {orientation}")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
models = [
|
||||||
|
import_mesh(base_path, "Lid base", (0.16, 0.42, 0.72, 1.0)),
|
||||||
|
import_mesh(accent_path, "Accent inlays", (0.95, 0.42, 0.08, 1.0)),
|
||||||
|
]
|
||||||
|
# Lid exports are print-oriented with the decorated exterior face down.
|
||||||
|
# Turn both aligned parts over and present the configured USB edge in its
|
||||||
|
# named position so previews inspect the finished face as the user will.
|
||||||
|
for model in models:
|
||||||
|
model.matrix_world = (
|
||||||
|
Matrix.Rotation(math.radians(angles[orientation]), 4, "Z")
|
||||||
|
@ Matrix.Rotation(math.pi, 4, "X")
|
||||||
|
)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
corners = [obj.matrix_world @ Vector(corner) for obj in models for corner in obj.bound_box]
|
||||||
|
mins = Vector([min(v[i] for v in corners) for i in range(3)])
|
||||||
|
maxs = Vector([max(v[i] for v in corners) for i in range(3)])
|
||||||
|
center = (mins + maxs) / 2
|
||||||
|
span = max(maxs - mins)
|
||||||
|
|
||||||
|
world = bpy.data.worlds.new("Inspection world")
|
||||||
|
world.use_nodes = True
|
||||||
|
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.035, 0.04, 0.05, 1)
|
||||||
|
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
|
||||||
|
bpy.context.scene.world = world
|
||||||
|
|
||||||
|
camera_data = bpy.data.cameras.new("Camera")
|
||||||
|
camera = bpy.data.objects.new("Camera", camera_data)
|
||||||
|
bpy.context.collection.objects.link(camera)
|
||||||
|
bpy.context.scene.camera = camera
|
||||||
|
camera.data.type = "ORTHO"
|
||||||
|
camera.data.ortho_scale = span * 1.35
|
||||||
|
|
||||||
|
key_data = bpy.data.lights.new("Key", type="AREA")
|
||||||
|
key_data.energy = 900
|
||||||
|
key_data.size = span
|
||||||
|
key = bpy.data.objects.new("Key", key_data)
|
||||||
|
key.location = center + Vector((span, -span, span * 1.5))
|
||||||
|
bpy.context.collection.objects.link(key)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.engine = "BLENDER_WORKBENCH"
|
||||||
|
scene.render.resolution_x = 800
|
||||||
|
scene.render.resolution_y = 600
|
||||||
|
scene.render.resolution_percentage = 100
|
||||||
|
scene.render.image_settings.file_format = "PNG"
|
||||||
|
scene.display.shading.light = "STUDIO"
|
||||||
|
scene.display.shading.color_type = "MATERIAL"
|
||||||
|
scene.display.shading.show_shadows = True
|
||||||
|
scene.display.shading.show_cavity = True
|
||||||
|
scene.display.shading.cavity_type = "BOTH"
|
||||||
|
|
||||||
|
for view, direction in {"iso": (1, -1, 0.8), "top": (0, 0, 1)}.items():
|
||||||
|
camera.location = center + Vector(direction).normalized() * span * 2.2
|
||||||
|
look_at(camera, center)
|
||||||
|
scene.render.filepath = os.path.join(output_dir, f"{basename}_{view}.png")
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine aligned lid meshes into one slicer-friendly multipart 3MF."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
|
||||||
|
ET.register_namespace("", CORE)
|
||||||
|
|
||||||
|
CONTENT_TYPES_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||||
|
</Types>
|
||||||
|
'''
|
||||||
|
|
||||||
|
RELATIONSHIPS_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||||
|
</Relationships>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def load_mesh(path: Path) -> ET.Element:
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
root = ET.fromstring(archive.read("3D/3dmodel.model"))
|
||||||
|
mesh = root.find(f".//{{{CORE}}}mesh")
|
||||||
|
if mesh is None:
|
||||||
|
raise ValueError(f"{path} does not contain a 3MF mesh")
|
||||||
|
return copy.deepcopy(mesh)
|
||||||
|
|
||||||
|
|
||||||
|
def model_xml(base: Path, accent: Path, title: str) -> bytes:
|
||||||
|
root = ET.Element(
|
||||||
|
f"{{{CORE}}}model",
|
||||||
|
{
|
||||||
|
"unit": "millimeter",
|
||||||
|
"{http://www.w3.org/XML/1998/namespace}lang": "en-US",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = (
|
||||||
|
"feather-case-wizard"
|
||||||
|
)
|
||||||
|
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
||||||
|
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
||||||
|
ET.SubElement(
|
||||||
|
materials, f"{{{CORE}}}base", {"name": "Lid base", "displaycolor": "#D1D5DB"}
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
materials,
|
||||||
|
f"{{{CORE}}}base",
|
||||||
|
{"name": "Accent inlays", "displaycolor": "#F97316"},
|
||||||
|
)
|
||||||
|
|
||||||
|
for object_id, name, source in (
|
||||||
|
(2, "Lid base — Filament 1", base),
|
||||||
|
(3, "Accent inlays — Filament 2", accent),
|
||||||
|
):
|
||||||
|
obj = ET.SubElement(
|
||||||
|
resources,
|
||||||
|
f"{{{CORE}}}object",
|
||||||
|
{
|
||||||
|
"id": str(object_id),
|
||||||
|
"name": name,
|
||||||
|
"type": "model",
|
||||||
|
"pid": "1",
|
||||||
|
"pindex": str(object_id - 2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
obj.append(load_mesh(source))
|
||||||
|
|
||||||
|
assembly = ET.SubElement(
|
||||||
|
resources, f"{{{CORE}}}object", {"id": "4", "name": title, "type": "model"}
|
||||||
|
)
|
||||||
|
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "2"})
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "3"})
|
||||||
|
build = ET.SubElement(root, f"{{{CORE}}}build")
|
||||||
|
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": "4"})
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def bambu_model_settings_xml(title: str) -> bytes:
|
||||||
|
root = ET.Element("config")
|
||||||
|
obj = ET.SubElement(root, "object", {"id": "4"})
|
||||||
|
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
||||||
|
for part_id, name, extruder in (
|
||||||
|
(2, "Lid base — Filament 1", 1),
|
||||||
|
(3, "Accent inlays — Filament 2", 2),
|
||||||
|
):
|
||||||
|
part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "name", "value": name})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)})
|
||||||
|
ET.SubElement(root, "assemble")
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
|
||||||
|
info = zipfile.ZipInfo(name)
|
||||||
|
info.create_system = 3
|
||||||
|
info.external_attr = 0o100644 << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
archive.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("base", type=Path)
|
||||||
|
parser.add_argument("accent", type=Path)
|
||||||
|
parser.add_argument("output", type=Path)
|
||||||
|
parser.add_argument("--title", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
||||||
|
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"3D/3dmodel.model",
|
||||||
|
model_xml(args.base, args.accent, args.title),
|
||||||
|
)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"Metadata/model_settings.config",
|
||||||
|
bambu_model_settings_xml(args.title),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+10
-4
@@ -6,14 +6,14 @@ Generated by the Feather Case Wizard.
|
|||||||
- FeatherWings: 1
|
- FeatherWings: 1
|
||||||
- Battery compartment: yes
|
- Battery compartment: yes
|
||||||
- 1/4-20 insert: yes
|
- 1/4-20 insert: yes
|
||||||
- Approximate exterior: 93.9 × 57.3 × 41.8 mm
|
- Approximate exterior: 88.6 × 61.1 × 40.4 mm
|
||||||
- Formats: 3mf
|
- Formats: 3mf
|
||||||
- Parts: `body`, `lid`, `insert_coupon`, `switch_coupon`, `fit_coupon`
|
- Parts: `body`, `lid`, `lid_base`, `lid_accent`, `insert_coupon`, `switch_coupon`, `fit_coupon`
|
||||||
- Custom cutouts: 0
|
- Custom cutouts: 0
|
||||||
|
|
||||||
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
||||||
OpenSCAD and Blender must be installed. Set `OPENSCAD` or `BLENDER` to
|
OpenSCAD, Blender, and Python 3 must be installed. Set `OPENSCAD`,
|
||||||
override their executable paths.
|
`BLENDER`, or `PYTHON` to override their executable paths.
|
||||||
|
|
||||||
To open the saved Customizer values manually:
|
To open the saved Customizer values manually:
|
||||||
|
|
||||||
@@ -23,3 +23,9 @@ openscad -p parameters.json -P gps source/feather_case.scad
|
|||||||
|
|
||||||
The 3MF files contain portable geometry only. Choose printer, filament,
|
The 3MF files contain portable geometry only. Choose printer, filament,
|
||||||
plate, and slicing settings in Bambu Studio.
|
plate, and slicing settings in Bambu Studio.
|
||||||
|
|
||||||
|
For a two-color lid, open `exports/3mf/lid_multicolor.3mf` as one
|
||||||
|
multipart object. Assign “Lid base — Filament 1” and “Accent inlays —
|
||||||
|
Filament 2” to the desired filaments. `lid.3mf` is the complete
|
||||||
|
single-color fallback; `lid_base.3mf` and `lid_accent.3mf` are also
|
||||||
|
provided separately.
|
||||||
|
|||||||
@@ -44,13 +44,18 @@ config = YAML.safe_load_file(ROOT.join("case.yml"), permitted_classes: [],
|
|||||||
permitted_symbols: [], aliases: false)
|
permitted_symbols: [], aliases: false)
|
||||||
openscad = executable("OPENSCAD", "openscad")
|
openscad = executable("OPENSCAD", "openscad")
|
||||||
blender = executable("BLENDER", "blender")
|
blender = executable("BLENDER", "blender")
|
||||||
|
python = executable("PYTHON", "python3")
|
||||||
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
||||||
abort "Blender not found; install it or set BLENDER" unless blender
|
abort "Blender not found; install it or set BLENDER" unless blender
|
||||||
|
|
||||||
parameters = config.fetch("parameters").merge(
|
parameters = config.fetch("parameters").merge(
|
||||||
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
||||||
)
|
)
|
||||||
|
artwork = !parameters.fetch("lid_label", "").empty? ||
|
||||||
|
parameters.fetch("lid_logo_enabled", false)
|
||||||
|
abort "Python 3 not found; install it or set PYTHON" if artwork && !python
|
||||||
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
||||||
|
parts.concat(%w[lid_base lid_accent]) if artwork
|
||||||
buttons = %w[button_a button_b button_c button_reset]
|
buttons = %w[button_a button_b button_c button_reset]
|
||||||
if parameters["top_profile"] == "oled128x64" &&
|
if parameters["top_profile"] == "oled128x64" &&
|
||||||
buttons.any? { |name| parameters[name] }
|
buttons.any? { |name| parameters[name] }
|
||||||
@@ -80,6 +85,16 @@ begin
|
|||||||
puts "Exporting #{part}.#{format}…"
|
puts "Exporting #{part}.#{format}…"
|
||||||
export.call(directory.join("#{part}.#{format}"), part)
|
export.call(directory.join("#{part}.#{format}"), part)
|
||||||
end
|
end
|
||||||
|
if artwork && format == "3mf"
|
||||||
|
puts "Packaging lid_multicolor.3mf…"
|
||||||
|
run!(
|
||||||
|
python, ROOT.join("tools/package_multimaterial_3mf.py").to_s,
|
||||||
|
directory.join("lid_base.3mf").to_s,
|
||||||
|
directory.join("lid_accent.3mf").to_s,
|
||||||
|
directory.join("lid_multicolor.3mf").to_s,
|
||||||
|
"--title", "#{config.fetch("name")} Lid"
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assembly = scratch.join("assembly.stl")
|
assembly = scratch.join("assembly.stl")
|
||||||
@@ -97,6 +112,16 @@ begin
|
|||||||
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
||||||
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
||||||
end
|
end
|
||||||
|
if artwork
|
||||||
|
puts "Rendering multicolor lid preview…"
|
||||||
|
run!(
|
||||||
|
blender, "--background", "-noaudio", "--python",
|
||||||
|
ROOT.join("source/render_multicolor_lid.py").to_s, "--",
|
||||||
|
scratch.join("lid_base.stl").to_s,
|
||||||
|
scratch.join("lid_accent.stl").to_s,
|
||||||
|
previews.to_s, "lid_multicolor", parameters["lid_art_orientation"], env: render_env
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
backup = ROOT.join(".build-old-#{Process.pid}")
|
backup = ROOT.join(".build-old-#{Process.pid}")
|
||||||
FileUtils.mkdir_p(backup)
|
FileUtils.mkdir_p(backup)
|
||||||
|
|||||||
+39
-15
@@ -3,7 +3,7 @@ schema_version: 1
|
|||||||
name: GPS
|
name: GPS
|
||||||
slug: gps
|
slug: gps
|
||||||
generator:
|
generator:
|
||||||
sha256: be611e3255e7d802719ebb0628e3d11481f8de05fc96c24d452f026d7b8e3353
|
sha256: 281917098487eeacd19a136ba27752698712e90cb4c03c6525a1ea3167c9ea13
|
||||||
created_with: feather-case-wizard
|
created_with: feather-case-wizard
|
||||||
parameters:
|
parameters:
|
||||||
top_profile: gps
|
top_profile: gps
|
||||||
@@ -36,20 +36,30 @@ parameters:
|
|||||||
battery_diameter: 18.0
|
battery_diameter: 18.0
|
||||||
battery_radial_clearance: 0.6
|
battery_radial_clearance: 0.6
|
||||||
battery_end_clearance: 1.0
|
battery_end_clearance: 1.0
|
||||||
switch_hole_diameter: 12.0
|
battery_partition_thickness: 1.8
|
||||||
switch_hole_compensation: 0.3
|
battery_board_clearance: 3.0
|
||||||
switch_cap_diameter: 8.0
|
rocker_cutout_size:
|
||||||
switch_flange_diameter: 16.2
|
- 12.9
|
||||||
switch_body_length: 24.0
|
- 19.4
|
||||||
switch_body_diameter: 12.0
|
rocker_corner_radius: 0.8
|
||||||
switch_wire_allowance: 3.0
|
rocker_body_size:
|
||||||
|
- 15.0
|
||||||
|
- 21.0
|
||||||
|
- 21.0
|
||||||
|
rocker_artwork_gap: 2.0
|
||||||
|
rocker_partition_offset: 2.0
|
||||||
|
rocker_tab_panel_thickness: 2.0
|
||||||
|
rocker_tab_land: 2.0
|
||||||
|
rocker_tab_taper_length: 0.6
|
||||||
tripod_insert_enabled: true
|
tripod_insert_enabled: true
|
||||||
tripod_insert_length: 6.4
|
tripod_insert_length: 5.0
|
||||||
tripod_insert_hole_diameter: 8.8
|
tripod_insert_hole_diameter: 7.2
|
||||||
|
tripod_insert_outer_diameter: 8.0
|
||||||
tripod_thread_clearance_diameter: 6.8
|
tripod_thread_clearance_diameter: 6.8
|
||||||
tripod_boss_diameter: 14.0
|
tripod_boss_diameter: 14.0
|
||||||
m25_insert_length: 4.0
|
m25_insert_length: 4.0
|
||||||
m25_insert_hole_diameter: 3.7
|
m25_insert_hole_diameter: 3.1
|
||||||
|
m25_insert_outer_diameter: 3.5
|
||||||
m25_screw_clearance: 2.8
|
m25_screw_clearance: 2.8
|
||||||
m25_boss_diameter: 7.0
|
m25_boss_diameter: 7.0
|
||||||
insert_entry_chamfer: 0.6
|
insert_entry_chamfer: 0.6
|
||||||
@@ -61,17 +71,31 @@ parameters:
|
|||||||
board_bottom_clearance: 5.4
|
board_bottom_clearance: 5.4
|
||||||
board_side_clearance: 7.0
|
board_side_clearance: 7.0
|
||||||
board_rear_clearance: 4.0
|
board_rear_clearance: 4.0
|
||||||
usb_board_wall_gap: 1.2
|
usb_board_wall_gap: 2.0
|
||||||
lid_lip_depth: 2.4
|
lid_lip_depth: 2.4
|
||||||
lid_lip_thickness: 1.2
|
lid_lip_thickness: 1.2
|
||||||
fit_clearance: 0.35
|
fit_clearance: 0.35
|
||||||
|
cutout_min_web: 1.2
|
||||||
hole_compensation: 0.2
|
hole_compensation: 0.2
|
||||||
lid_screw_head_diameter: 5.2
|
lid_screw_head_diameter: 5.2
|
||||||
lid_screw_head_height: 2.5
|
lid_screw_head_height: 2.5
|
||||||
usb_opening_width: 12.5
|
usb_opening_enabled: true
|
||||||
usb_opening_height: 7.0
|
usb_guide_enabled: true
|
||||||
|
usb_connector_overhang: 2.0
|
||||||
|
usb_guide_wall_thickness: 1.2
|
||||||
|
usb_guide_end_gap: 1.0
|
||||||
|
lid_label: GPS
|
||||||
|
lid_logo_enabled: true
|
||||||
|
lid_art_orientation: usb_left
|
||||||
|
lid_inlay_depth: 0.6
|
||||||
|
lid_inlay_fit_clearance: 0.08
|
||||||
|
lid_label_size: 5.0
|
||||||
|
lid_label_logo_gap: 2.0
|
||||||
|
lid_art_margin: 8.5
|
||||||
|
usb_opening_width: 13.5
|
||||||
|
usb_opening_height: 8.0
|
||||||
usb_opening_radius: 1.5
|
usb_opening_radius: 1.5
|
||||||
usb_vertical_offset: 0.0
|
usb_vertical_offset: 0.5
|
||||||
cutouts: []
|
cutouts: []
|
||||||
outputs:
|
outputs:
|
||||||
formats:
|
formats:
|
||||||
|
|||||||
+33
-14
@@ -27,20 +27,25 @@
|
|||||||
"battery_diameter": "18.0",
|
"battery_diameter": "18.0",
|
||||||
"battery_radial_clearance": "0.6",
|
"battery_radial_clearance": "0.6",
|
||||||
"battery_end_clearance": "1.0",
|
"battery_end_clearance": "1.0",
|
||||||
"switch_hole_diameter": "12.0",
|
"battery_partition_thickness": "1.8",
|
||||||
"switch_hole_compensation": "0.3",
|
"battery_board_clearance": "3.0",
|
||||||
"switch_cap_diameter": "8.0",
|
"rocker_cutout_size": "[12.9,19.4]",
|
||||||
"switch_flange_diameter": "16.2",
|
"rocker_corner_radius": "0.8",
|
||||||
"switch_body_length": "24.0",
|
"rocker_body_size": "[15.0,21.0,21.0]",
|
||||||
"switch_body_diameter": "12.0",
|
"rocker_artwork_gap": "2.0",
|
||||||
"switch_wire_allowance": "3.0",
|
"rocker_partition_offset": "2.0",
|
||||||
|
"rocker_tab_panel_thickness": "2.0",
|
||||||
|
"rocker_tab_land": "2.0",
|
||||||
|
"rocker_tab_taper_length": "0.6",
|
||||||
"tripod_insert_enabled": "true",
|
"tripod_insert_enabled": "true",
|
||||||
"tripod_insert_length": "6.4",
|
"tripod_insert_length": "5.0",
|
||||||
"tripod_insert_hole_diameter": "8.8",
|
"tripod_insert_hole_diameter": "7.2",
|
||||||
|
"tripod_insert_outer_diameter": "8.0",
|
||||||
"tripod_thread_clearance_diameter": "6.8",
|
"tripod_thread_clearance_diameter": "6.8",
|
||||||
"tripod_boss_diameter": "14.0",
|
"tripod_boss_diameter": "14.0",
|
||||||
"m25_insert_length": "4.0",
|
"m25_insert_length": "4.0",
|
||||||
"m25_insert_hole_diameter": "3.7",
|
"m25_insert_hole_diameter": "3.1",
|
||||||
|
"m25_insert_outer_diameter": "3.5",
|
||||||
"m25_screw_clearance": "2.8",
|
"m25_screw_clearance": "2.8",
|
||||||
"m25_boss_diameter": "7.0",
|
"m25_boss_diameter": "7.0",
|
||||||
"insert_entry_chamfer": "0.6",
|
"insert_entry_chamfer": "0.6",
|
||||||
@@ -52,17 +57,31 @@
|
|||||||
"board_bottom_clearance": "5.4",
|
"board_bottom_clearance": "5.4",
|
||||||
"board_side_clearance": "7.0",
|
"board_side_clearance": "7.0",
|
||||||
"board_rear_clearance": "4.0",
|
"board_rear_clearance": "4.0",
|
||||||
"usb_board_wall_gap": "1.2",
|
"usb_board_wall_gap": "2.0",
|
||||||
"lid_lip_depth": "2.4",
|
"lid_lip_depth": "2.4",
|
||||||
"lid_lip_thickness": "1.2",
|
"lid_lip_thickness": "1.2",
|
||||||
"fit_clearance": "0.35",
|
"fit_clearance": "0.35",
|
||||||
|
"cutout_min_web": "1.2",
|
||||||
"hole_compensation": "0.2",
|
"hole_compensation": "0.2",
|
||||||
"lid_screw_head_diameter": "5.2",
|
"lid_screw_head_diameter": "5.2",
|
||||||
"lid_screw_head_height": "2.5",
|
"lid_screw_head_height": "2.5",
|
||||||
"usb_opening_width": "12.5",
|
"usb_opening_enabled": "true",
|
||||||
"usb_opening_height": "7.0",
|
"usb_guide_enabled": "true",
|
||||||
|
"usb_connector_overhang": "2.0",
|
||||||
|
"usb_guide_wall_thickness": "1.2",
|
||||||
|
"usb_guide_end_gap": "1.0",
|
||||||
|
"lid_label": "\"GPS\"",
|
||||||
|
"lid_logo_enabled": "true",
|
||||||
|
"lid_art_orientation": "\"usb_left\"",
|
||||||
|
"lid_inlay_depth": "0.6",
|
||||||
|
"lid_inlay_fit_clearance": "0.08",
|
||||||
|
"lid_label_size": "5.0",
|
||||||
|
"lid_label_logo_gap": "2.0",
|
||||||
|
"lid_art_margin": "8.5",
|
||||||
|
"usb_opening_width": "13.5",
|
||||||
|
"usb_opening_height": "8.0",
|
||||||
"usb_opening_radius": "1.5",
|
"usb_opening_radius": "1.5",
|
||||||
"usb_vertical_offset": "0.0"
|
"usb_vertical_offset": "0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fileFormatVersion": "1"
|
"fileFormatVersion": "1"
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="210mm"
|
||||||
|
height="297mm"
|
||||||
|
viewBox="0 0 210 297"
|
||||||
|
version="1.1"
|
||||||
|
id="svg584"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="NME-fixed.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview586"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.914906"
|
||||||
|
inkscape:cx="1166.24"
|
||||||
|
inkscape:cy="477.6447"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1390"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1080"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs581" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g772">
|
||||||
|
<path
|
||||||
|
id="path2"
|
||||||
|
d="m 97.576583,26.288608 c -0.51788,0.06491 -2.361848,0.291394 -4.044598,0.485422 -15.62735,1.747308 -32.128529,8.73619 -44.81195,18.960393 -9.803341,7.926917 -17.342201,17.309748 -23.360236,29.151792 -2.879725,5.629978 -3.170767,6.632927 -1.941336,6.535912 0.388054,-0.03245 2.556227,-0.06491 4.788604,-0.03245 l 4.0767,0.03245 4.820708,5.662083 c 2.653242,3.106208 4.918075,5.662083 5.01509,5.662083 0.09737,0 0.291394,-1.035402 0.420862,-2.297287 0.09701,-1.261888 0.517525,-3.688291 0.87348,-5.403146 3.106208,-14.559843 9.415285,-26.692928 19.186524,-36.852576 6.276975,-6.568017 12.521494,-11.098035 21.030846,-15.304204 5.597525,-2.749903 9.933164,-4.367742 15.562791,-5.823656 l 3.559172,-0.938388 -2.10291,0.03245 c -1.164868,0.03246 -2.523768,0.09701 -3.073747,0.129117"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path3"
|
||||||
|
d="m 169.08146,36.027745 c -26.62837,2.232377 -57.46291,14.33336 -85.90315,33.68181 -5.85611,3.979685 -15.530335,11.130137 -15.530335,11.453635 0,0.16157 1.3589,0.258937 3.008842,0.258937 h 2.976737 l 3.7211,-2.78271 c 4.917723,-3.688644 13.977408,-9.738784 19.574581,-13.071475 5.759445,-3.46216 18.668995,-9.997721 24.751595,-12.55395 21.90467,-9.188802 42.51501,-12.747625 55.16562,-9.576858 5.04754,1.261888 9.35073,4.432654 11.09769,8.120946 0.80891,1.747308 0.87381,2.167818 0.87381,6.406443 0,4.270727 -0.0649,4.691592 -1.06748,7.603419 -2.65325,7.700433 -8.70374,17.439568 -16.8575,27.178351 -1.26188,1.520825 -2.29693,2.814815 -2.29693,2.91183 0,0.09737 4.7886,0.129468 10.67718,0.06491 l 10.67715,-0.09701 V 90.772856 85.91934 l 2.07081,-2.814815 C 196.58332,76.860006 200.59546,69.677098 202.56925,64.11203 208.1015,48.484325 201.79243,38.616074 184.77337,36.286682 181.6996,35.86582 172.7373,35.704247 169.08146,36.027745"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path4"
|
||||||
|
d="m 19.11528,111.8358 v 26.20751 h 11.000669 v -35.91419 l 1.552926,1.71486 c 0.841375,0.93839 4.59458,5.27402 8.347782,9.64212 3.753202,4.33564 9.350727,10.80664 12.456583,14.36582 3.106208,3.55882 6.341533,7.31202 7.215362,8.34742 l 1.552929,1.84397 h 5.629979 5.62963 V 111.8358 85.628298 H 66.8066 61.079958 l 0.161573,17.957092 c 0.06491,9.86825 0.06491,17.95709 -0.03246,17.95709 -0.09666,-0.0324 -0.711552,-0.67945 -1.326443,-1.45591 -0.61454,-0.77646 -4.27073,-5.01509 -8.088842,-9.41564 C 40.857677,98.052425 37.978307,94.720085 33.998622,90.060598 l -3.7211,-4.367742 -5.565071,-0.03246 -5.597171,-0.0321 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path5"
|
||||||
|
d="m 77.030804,111.8358 v 26.20751 h 11.001023 v -35.04035 l 6.7945,9.02687 c 3.720748,4.98299 8.023933,10.67753 9.544753,12.68342 l 2.81481,3.65584 9.5123,-12.68307 9.5123,-12.68306 0.0649,13.65391 0.097,13.65355 3.85022,-3.26743 c 2.13571,-1.81186 4.56212,-3.91512 5.43595,-4.69159 l 1.55293,-1.39135 V 103.294 85.628298 h -5.56507 l -5.59753,0.0321 -9.38283,12.456935 c -5.14456,6.826957 -9.4802,12.392027 -9.57721,12.359567 -0.19438,-0.097 -4.4002,-5.56507 -14.33336,-18.798467 L 88.1934,85.628298 H 82.628329 77.030804 V 111.8358"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path6"
|
||||||
|
d="m 170.4079,106.88563 -12.58605,0.097 -6.66538,6.56802 c -21.32189,20.93383 -39.11741,34.16723 -59.727393,44.39108 -28.796192,14.30126 -53.936193,18.6369 -68.334112,11.77749 -2.976739,-1.42381 -6.115402,-4.10916 -7.441848,-6.34154 -2.78271,-4.7565 -2.750253,-12.03642 0.06491,-18.41006 1.002946,-2.29729 1.002946,-2.3943 -0.161925,-2.3943 h -0.93839 l -0.09701,-2.84727 -0.09701,-2.81517 -1.553282,2.58869 c -3.8177601,6.40609 -6.9236165,14.00986 -7.8295498,19.18652 -1.3267981,7.50641 0.1615731,13.07183 4.6912397,17.53659 2.6856951,2.65289 5.0150871,4.04459 8.9947751,5.37068 8.088841,2.6857 18.863026,3.0413 30.608055,0.93874 l 4.238273,-0.74436 3.882673,2.39431 c 14.689312,8.96232 30.252104,13.39497 46.947304,13.39497 14.49529,0 26.66083,-2.81482 39.53828,-9.12389 14.59194,-7.18326 27.33992,-18.70146 35.91418,-32.45238 1.97345,-3.17112 5.92103,-10.80664 6.50312,-12.52185 l 0.32385,-1.0675 -0.54999,0.80892 c -8.60672,12.42447 -18.15112,20.90137 -30.5756,27.08133 -18.15112,9.05934 -39.14987,10.90366 -57.980434,5.04719 -3.268133,-1.00294 -10.839098,-4.01178 -10.806642,-4.27072 0,-0.097 3.461809,-1.90888 7.667977,-3.97969 4.206169,-2.0708 9.706679,-4.95053 12.197999,-6.43855 9.93281,-5.824 22.4222,-14.5923 31.70801,-22.26027 l 3.55918,-2.94464 0.097,1.29434 0.097,1.29399 h 23.94302 23.91021 v -5.01509 -5.01473 h -18.44215 -18.44287 v -1.22943 c 0,-1.16488 0.25929,-1.48873 4.49756,-5.50052 l 4.49721,-4.27072 h 10.54806 l 10.54769,-0.0325 v -5.17701 c 0,-2.81482 -0.0324,-5.11175 -0.0646,-5.07965 -0.0649,0.0646 -5.75944,0.12947 -12.68341,0.16193"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<path
|
||||||
|
id="path7"
|
||||||
|
d="m 34.484042,128.23997 v 14.17144 l -6.018035,0.16192 -6.01839,0.16157 1.326798,2.91218 c 3.559175,7.79745 8.703382,15.82173 13.912496,21.67785 l 1.423812,1.58538 4.691592,-0.38806 c 5.079645,-0.42086 9.609312,-1.19732 15.497879,-2.6857 3.429706,-0.84102 11.518548,-3.39725 11.906604,-3.75285 0.06491,-0.0649 -0.582438,-0.74471 -1.455915,-1.48872 -9.544756,-7.95937 -17.083264,-18.15112 -21.80731,-29.44318 -1.455914,-3.39725 -1.649941,-3.68829 -7.312379,-10.25631 -3.202869,-3.7211 -5.888564,-6.7624 -5.985579,-6.7624 -0.09701,-0.0324 -0.161573,6.34154 -0.161573,14.10688"
|
||||||
|
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352777" />
|
||||||
|
<g
|
||||||
|
id="g475"
|
||||||
|
transform="matrix(0.14274222,0,0,0.14274222,123.03997,38.158339)">
|
||||||
|
<path
|
||||||
|
d="m 134.23594,367.14576 v 34.66667 h 69.06667 c 37.86666,0 71.06666,-0.4 73.86666,-0.8 4.4,-0.8 7.2,-3.73333 31.33333,-32.8 14.53334,-17.46667 26.53334,-32.4 26.8,-32.93333 0.26667,-0.66667 -0.93333,-1.46667 -2.66666,-2 -1.73334,-0.4 -47.06667,-0.8 -100.66667,-0.8 h -97.73333 z"
|
||||||
|
id="path251"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
<path
|
||||||
|
d="m 134.90261,432.74576 c -0.4,0.93334 -0.53333,27.2 -0.4,58.4 l 0.4,56.53333 38.4,-37.06666 38.26666,-37.06667 -0.26666,-20.93333 -0.4,-20.8 -37.73334,-0.4 c -29.46666,-0.26667 -37.86666,0 -38.26666,1.33333 z"
|
||||||
|
id="path253"
|
||||||
|
style="fill:#000000;stroke:none;stroke-width:0.133333" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
+484
-119
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* [Output] */
|
/* [Output] */
|
||||||
part = "assembly"; // [assembly,body,lid,buttons,insert_coupon,switch_coupon,fit_coupon]
|
part = "assembly"; // [assembly,body,lid,lid_base,lid_accent,buttons,insert_coupon,switch_coupon,fit_coupon]
|
||||||
exploded_view = 8; // [0:1:20]
|
exploded_view = 8; // [0:1:20]
|
||||||
show_references = true;
|
show_references = true;
|
||||||
|
|
||||||
@@ -47,24 +47,29 @@ battery_length = 69;
|
|||||||
battery_diameter = 18;
|
battery_diameter = 18;
|
||||||
battery_radial_clearance = 0.6;
|
battery_radial_clearance = 0.6;
|
||||||
battery_end_clearance = 1;
|
battery_end_clearance = 1;
|
||||||
switch_hole_diameter = 12;
|
battery_partition_thickness = 1.8;
|
||||||
switch_hole_compensation = 0.3;
|
battery_board_clearance = 3;
|
||||||
switch_cap_diameter = 8;
|
rocker_cutout_size = [12.9, 19.4];
|
||||||
switch_flange_diameter = 16.2;
|
rocker_corner_radius = 0.8;
|
||||||
switch_body_length = 24;
|
rocker_body_size = [15, 21, 21];
|
||||||
switch_body_diameter = 12;
|
rocker_artwork_gap = 2;
|
||||||
switch_wire_allowance = 3;
|
rocker_partition_offset = 2;
|
||||||
|
rocker_tab_panel_thickness = 2;
|
||||||
|
rocker_tab_land = 2;
|
||||||
|
rocker_tab_taper_length = 0.6;
|
||||||
|
|
||||||
/* [Tripod mount] */
|
/* [Tripod mount] */
|
||||||
tripod_insert_enabled = false;
|
tripod_insert_enabled = false;
|
||||||
tripod_insert_length = 6.4;
|
tripod_insert_length = 5;
|
||||||
tripod_insert_hole_diameter = 8.8;
|
tripod_insert_hole_diameter = 7.2;
|
||||||
|
tripod_insert_outer_diameter = 8;
|
||||||
tripod_thread_clearance_diameter = 6.8;
|
tripod_thread_clearance_diameter = 6.8;
|
||||||
tripod_boss_diameter = 14;
|
tripod_boss_diameter = 14;
|
||||||
|
|
||||||
/* [M2.5 inserts] */
|
/* [M2.5 inserts] */
|
||||||
m25_insert_length = 4;
|
m25_insert_length = 4;
|
||||||
m25_insert_hole_diameter = 3.7;
|
m25_insert_hole_diameter = 3.1;
|
||||||
|
m25_insert_outer_diameter = 3.5;
|
||||||
m25_screw_clearance = 2.8;
|
m25_screw_clearance = 2.8;
|
||||||
m25_boss_diameter = 7;
|
m25_boss_diameter = 7;
|
||||||
insert_entry_chamfer = 0.6;
|
insert_entry_chamfer = 0.6;
|
||||||
@@ -78,19 +83,36 @@ corner_radius = 4;
|
|||||||
board_bottom_clearance = 5.4;
|
board_bottom_clearance = 5.4;
|
||||||
board_side_clearance = 7;
|
board_side_clearance = 7;
|
||||||
board_rear_clearance = 4;
|
board_rear_clearance = 4;
|
||||||
usb_board_wall_gap = 1.2;
|
usb_board_wall_gap = 2;
|
||||||
lid_lip_depth = 2.4;
|
lid_lip_depth = 2.4;
|
||||||
lid_lip_thickness = 1.2;
|
lid_lip_thickness = 1.2;
|
||||||
fit_clearance = 0.35;
|
fit_clearance = 0.35;
|
||||||
|
cutout_min_web = 1.2;
|
||||||
hole_compensation = 0.2;
|
hole_compensation = 0.2;
|
||||||
lid_screw_head_diameter = 5.2;
|
lid_screw_head_diameter = 5.2;
|
||||||
lid_screw_head_height = 2.5;
|
lid_screw_head_height = 2.5;
|
||||||
|
|
||||||
|
/* [Two-color lid artwork] */
|
||||||
|
lid_label = "";
|
||||||
|
lid_logo_enabled = false;
|
||||||
|
lid_art_orientation = "usb_left"; // [usb_left,usb_front,usb_right,usb_rear]
|
||||||
|
lid_inlay_depth = 0.6; // [0.4:0.2:1]
|
||||||
|
lid_inlay_fit_clearance = 0.08;
|
||||||
|
lid_label_size = 5;
|
||||||
|
lid_label_logo_gap = 2;
|
||||||
|
lid_art_margin = 8.5;
|
||||||
|
logo_file = "assets/NME-fixed.svg";
|
||||||
|
|
||||||
/* [USB-C opening] */
|
/* [USB-C opening] */
|
||||||
usb_opening_width = 12.5;
|
usb_opening_enabled = true;
|
||||||
usb_opening_height = 7;
|
usb_guide_enabled = true;
|
||||||
|
usb_connector_overhang = 2;
|
||||||
|
usb_guide_wall_thickness = 1.2;
|
||||||
|
usb_guide_end_gap = 1;
|
||||||
|
usb_opening_width = 13.5;
|
||||||
|
usb_opening_height = 8;
|
||||||
usb_opening_radius = 1.5;
|
usb_opening_radius = 1.5;
|
||||||
usb_vertical_offset = 0;
|
usb_vertical_offset = 0.5;
|
||||||
|
|
||||||
/* [Custom wall cutouts] */
|
/* [Custom wall cutouts] */
|
||||||
// Entry format: [face, shape, u, z, width, height, radius]
|
// Entry format: [face, shape, u, z, width, height, radius]
|
||||||
@@ -100,6 +122,8 @@ extra_cutouts = [];
|
|||||||
|
|
||||||
/* [Calibration] */
|
/* [Calibration] */
|
||||||
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
m25_coupon_steps = [-0.15, -0.075, 0, 0.075, 0.15];
|
||||||
|
tripod_coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
|
||||||
$fn = $preview ? 36 : 72;
|
$fn = $preview ? 36 : 72;
|
||||||
eps = 0.02;
|
eps = 0.02;
|
||||||
@@ -111,6 +135,9 @@ board_thickness = 1.6;
|
|||||||
mount_x = [2.54, 48.26];
|
mount_x = [2.54, 48.26];
|
||||||
mount_y = [2.54, 20.32];
|
mount_y = [2.54, 20.32];
|
||||||
usb_center_y = 11.43;
|
usb_center_y = 11.43;
|
||||||
|
jst_center_xy = [10.9855, 19.304];
|
||||||
|
jst_body_size = [8, 7.5, 4.5];
|
||||||
|
jst_relief_clearance = 0.5;
|
||||||
|
|
||||||
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
||||||
oled_visible_min = [11.42, 3.78];
|
oled_visible_min = [11.42, 3.78];
|
||||||
@@ -127,11 +154,34 @@ effective_board_clearance = max(
|
|||||||
board_bottom_clearance,
|
board_bottom_clearance,
|
||||||
m25_insert_length + insert_melt_relief + 0.6
|
m25_insert_length + insert_melt_relief + 0.6
|
||||||
);
|
);
|
||||||
|
lid_boss_r = m25_boss_diameter / 2;
|
||||||
|
lid_boss_wall_overlap = 0.6;
|
||||||
|
front_lid_boss_center = wall_thickness + lid_boss_r
|
||||||
|
- lid_boss_wall_overlap;
|
||||||
|
|
||||||
|
// Combined battery/tripod builds need room around the centered insert entry.
|
||||||
|
combined_tripod_board_gutter = battery_enabled && tripod_insert_enabled ? 2.4 : 0;
|
||||||
|
combined_tripod_x_shift = battery_enabled && tripod_insert_enabled ? 5 : 0;
|
||||||
|
|
||||||
board_origin_x = wall_thickness + usb_board_wall_gap;
|
board_origin_x = wall_thickness + usb_board_wall_gap;
|
||||||
board_origin_y = wall_thickness + board_side_clearance;
|
board_origin_y = wall_thickness + board_side_clearance
|
||||||
|
+ combined_tripod_board_gutter;
|
||||||
board_bottom_z = effective_floor + effective_board_clearance;
|
board_bottom_z = effective_floor + effective_board_clearance;
|
||||||
board_top_z = board_bottom_z + board_thickness;
|
board_top_z = board_bottom_z + board_thickness;
|
||||||
|
usb_center_u = board_origin_y + usb_center_y;
|
||||||
|
usb_center_z = board_top_z + 1.6 + usb_vertical_offset;
|
||||||
|
usb_inner_width = usb_opening_width + hole_compensation;
|
||||||
|
usb_inner_height = usb_opening_height + hole_compensation;
|
||||||
|
usb_guide_length = max(
|
||||||
|
0,
|
||||||
|
usb_board_wall_gap - usb_connector_overhang - usb_guide_end_gap
|
||||||
|
);
|
||||||
|
usb_guide_active = usb_opening_enabled && usb_guide_enabled
|
||||||
|
&& usb_guide_length > 0;
|
||||||
|
usb_reserved_width = usb_inner_width
|
||||||
|
+ (usb_guide_active ? 2 * usb_guide_wall_thickness : 0);
|
||||||
|
usb_reserved_height = usb_inner_height
|
||||||
|
+ (usb_guide_active ? 2 * usb_guide_wall_thickness : 0);
|
||||||
|
|
||||||
computed_top_pcb_z = board_top_z + wing_count * wing_pitch;
|
computed_top_pcb_z = board_top_z + wing_count * wing_pitch;
|
||||||
top_pcb_z = stack_height_override > 0
|
top_pcb_z = stack_height_override > 0
|
||||||
@@ -146,20 +196,17 @@ profile_top_gap =
|
|||||||
top_profile == "oled128x64" ? oled_face_gap :
|
top_profile == "oled128x64" ? oled_face_gap :
|
||||||
generic_top_gap;
|
generic_top_gap;
|
||||||
|
|
||||||
partition_y = board_origin_y + feather_width + 1;
|
partition_y = board_origin_y + feather_width + battery_board_clearance;
|
||||||
battery_center_y = partition_y + wall_thickness + battery_radial_clearance
|
battery_center_y = partition_y + battery_partition_thickness
|
||||||
+ battery_diameter / 2;
|
+ battery_radial_clearance + battery_diameter / 2;
|
||||||
battery_start_x = board_origin_x + battery_end_clearance;
|
battery_start_x = front_lid_boss_center + lid_boss_r
|
||||||
|
+ battery_end_clearance;
|
||||||
battery_end_x = battery_start_x + battery_length;
|
battery_end_x = battery_start_x + battery_length;
|
||||||
switch_center_x = battery_end_x + max(
|
|
||||||
switch_body_diameter / 2 + 0.8,
|
|
||||||
switch_flange_diameter / 2 + 0.5
|
|
||||||
);
|
|
||||||
|
|
||||||
case_length_plain = board_origin_x + feather_length
|
case_length_plain = board_origin_x + feather_length
|
||||||
+ board_rear_clearance + wall_thickness;
|
+ board_rear_clearance + wall_thickness;
|
||||||
case_length_battery = switch_center_x + switch_flange_diameter / 2
|
case_length_battery = battery_end_x + battery_end_clearance
|
||||||
+ 1.2 + wall_thickness;
|
+ wall_thickness + m25_boss_diameter - lid_boss_wall_overlap;
|
||||||
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
||||||
: case_length_plain;
|
: case_length_plain;
|
||||||
|
|
||||||
@@ -170,23 +217,74 @@ case_width_battery = battery_center_y + battery_diameter / 2
|
|||||||
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
||||||
: case_width_plain;
|
: case_width_plain;
|
||||||
|
|
||||||
|
rocker_installed_center_x = case_length - wall_thickness - fit_clearance
|
||||||
|
- lid_lip_thickness - fit_clearance - rocker_body_size[0] / 2;
|
||||||
|
rocker_installed_center_y = front_lid_boss_center + lid_boss_r + fit_clearance
|
||||||
|
+ rocker_body_size[1] / 2 + rocker_partition_offset;
|
||||||
|
// The printable lid is flipped across Y when installed, so its local cutout
|
||||||
|
// coordinate must be mirrored from the assembled switch location.
|
||||||
|
rocker_lid_center_x = rocker_installed_center_x;
|
||||||
|
rocker_lid_center_y = case_width - rocker_installed_center_y;
|
||||||
|
|
||||||
|
lid_artwork_enabled = lid_label != "" || lid_logo_enabled;
|
||||||
|
lid_art_rotation =
|
||||||
|
lid_art_orientation == "usb_left" ? 0 :
|
||||||
|
lid_art_orientation == "usb_front" ? 90 :
|
||||||
|
lid_art_orientation == "usb_right" ? 180 : 270;
|
||||||
|
lid_art_safe_max_x = battery_enabled
|
||||||
|
? rocker_lid_center_x - rocker_cutout_size[0] / 2 - rocker_artwork_gap
|
||||||
|
: case_length - lid_art_margin;
|
||||||
|
lid_art_physical_size = [
|
||||||
|
lid_art_safe_max_x - lid_art_margin,
|
||||||
|
case_width - 2 * lid_art_margin
|
||||||
|
];
|
||||||
|
lid_art_center = [
|
||||||
|
lid_art_margin + lid_art_physical_size[0] / 2,
|
||||||
|
case_width / 2
|
||||||
|
];
|
||||||
|
lid_art_canvas_size = lid_art_rotation == 0 || lid_art_rotation == 180
|
||||||
|
? lid_art_physical_size
|
||||||
|
: [lid_art_physical_size[1], lid_art_physical_size[0]];
|
||||||
|
lid_art_available_size = lid_art_canvas_size;
|
||||||
|
lid_label_height = lid_label == "" ? 0 : lid_label_size;
|
||||||
|
lid_art_gap = lid_label != "" && lid_logo_enabled ? lid_label_logo_gap : 0;
|
||||||
|
logo_native_size = [30, 25.75];
|
||||||
|
logo_max_height = lid_art_available_size[1] - lid_label_height - lid_art_gap;
|
||||||
|
logo_scale = lid_logo_enabled ? min(
|
||||||
|
lid_art_available_size[0] / logo_native_size[0],
|
||||||
|
logo_max_height / logo_native_size[1]
|
||||||
|
) : 0;
|
||||||
|
logo_size = [logo_native_size[0] * logo_scale,
|
||||||
|
logo_native_size[1] * logo_scale];
|
||||||
|
lid_art_total_height = logo_size[1] + lid_art_gap + lid_label_height;
|
||||||
|
lid_label_center_y = -lid_art_total_height / 2 + lid_label_height / 2;
|
||||||
|
lid_logo_center_y = -lid_art_total_height / 2 + lid_label_height
|
||||||
|
+ lid_art_gap + logo_size[1] / 2;
|
||||||
|
|
||||||
|
tripod_center_x = case_length / 2 - combined_tripod_x_shift;
|
||||||
|
tripod_center_y = case_width / 2;
|
||||||
|
tripod_entry_radius = tripod_insert_outer_diameter / 2;
|
||||||
|
tripod_standoff_clearance = min([
|
||||||
|
for (mx = mount_x, my = mount_y)
|
||||||
|
sqrt(
|
||||||
|
pow(board_origin_x + mx - tripod_center_x, 2)
|
||||||
|
+ pow(board_origin_y + my - tripod_center_y, 2)
|
||||||
|
) - tripod_entry_radius - m25_boss_diameter / 2
|
||||||
|
]);
|
||||||
|
tripod_partition_clearance = partition_y
|
||||||
|
- (tripod_center_y + tripod_entry_radius);
|
||||||
|
|
||||||
battery_center_z = effective_floor + battery_radial_clearance
|
battery_center_z = effective_floor + battery_radial_clearance
|
||||||
+ battery_diameter / 2;
|
+ battery_diameter / 2;
|
||||||
switch_center_z = max(
|
|
||||||
effective_floor + switch_flange_diameter / 2 + 1,
|
|
||||||
battery_center_z
|
|
||||||
);
|
|
||||||
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
||||||
battery_ceiling_z = battery_enabled
|
battery_ceiling_z = battery_enabled
|
||||||
? max(
|
? battery_center_z + battery_diameter / 2 + battery_radial_clearance
|
||||||
battery_center_z + battery_diameter / 2 + battery_radial_clearance,
|
|
||||||
switch_center_z + switch_flange_diameter / 2 + 1
|
|
||||||
)
|
|
||||||
: 0;
|
: 0;
|
||||||
body_height = max(electronics_ceiling_z, battery_ceiling_z);
|
rocker_ceiling_z = battery_enabled
|
||||||
|
? effective_floor + rocker_body_size[2] + fit_clearance
|
||||||
|
: 0;
|
||||||
|
body_height = max(electronics_ceiling_z, battery_ceiling_z, rocker_ceiling_z);
|
||||||
|
|
||||||
lid_boss_r = m25_boss_diameter / 2;
|
|
||||||
lid_boss_wall_overlap = 0.6;
|
|
||||||
lid_boss_xy = [
|
lid_boss_xy = [
|
||||||
[
|
[
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
||||||
@@ -205,19 +303,156 @@ lid_boss_xy = [
|
|||||||
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
rocker_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(
|
||||||
|
abs(p[0] - rocker_installed_center_x)
|
||||||
|
- rocker_body_size[0] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
+ pow(max(
|
||||||
|
abs(p[1] - rocker_installed_center_y)
|
||||||
|
- rocker_body_size[1] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
rocker_relief_half_size = [
|
||||||
|
rocker_cutout_size[0] / 2,
|
||||||
|
rocker_cutout_size[1] / 2 + rocker_tab_land + rocker_tab_taper_length
|
||||||
|
];
|
||||||
|
rocker_relief_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(abs(p[0] - rocker_lid_center_x)
|
||||||
|
- rocker_relief_half_size[0], 0), 2)
|
||||||
|
+ pow(max(abs(p[1] - rocker_lid_center_y)
|
||||||
|
- rocker_relief_half_size[1], 0), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
|
||||||
assert(wing_count >= 1, "wing_count must be at least one");
|
assert(wing_count >= 1, "wing_count must be at least one");
|
||||||
assert(top_profile == "generic" || top_profile == "gps"
|
assert(top_profile == "generic" || top_profile == "gps"
|
||||||
|| top_profile == "oled128x64", "Unsupported top_profile");
|
|| top_profile == "oled128x64", "Unsupported top_profile");
|
||||||
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_partition_thickness >= 1.2,
|
||||||
|
"battery_partition_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_board_clearance > 0,
|
||||||
|
"battery_board_clearance must be positive");
|
||||||
|
assert(rocker_tab_panel_thickness >= 1.2,
|
||||||
|
"rocker tab panel needs at least two 0.6 mm lines");
|
||||||
|
assert(rocker_tab_panel_thickness < lid_thickness,
|
||||||
|
"rocker tab panel must be thinner than the lid");
|
||||||
|
assert(rocker_tab_land > 0 && rocker_tab_taper_length > 0,
|
||||||
|
"rocker tab relief land and taper must be positive");
|
||||||
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
||||||
|
assert(lid_art_orientation == "usb_left"
|
||||||
|
|| lid_art_orientation == "usb_front"
|
||||||
|
|| lid_art_orientation == "usb_right"
|
||||||
|
|| lid_art_orientation == "usb_rear",
|
||||||
|
"Unsupported lid_art_orientation");
|
||||||
|
assert(!lid_artwork_enabled || top_profile != "oled128x64",
|
||||||
|
"Lid artwork is not supported with the OLED profile");
|
||||||
|
assert(!lid_artwork_enabled || lid_inlay_depth < lid_thickness,
|
||||||
|
"Lid inlay must be shallower than the lid");
|
||||||
|
assert(!lid_artwork_enabled
|
||||||
|
|| (lid_art_available_size[0] > 0 && lid_art_available_size[1] > 0),
|
||||||
|
"Lid artwork margin leaves no printable area");
|
||||||
|
assert(!lid_logo_enabled || logo_scale > 0,
|
||||||
|
"Lid logo has no printable area");
|
||||||
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
||||||
"M2.5 boss does not have enough radial material");
|
"M2.5 boss does not have enough radial material");
|
||||||
|
assert(m25_insert_outer_diameter >= m25_insert_hole_diameter,
|
||||||
|
"M2.5 insert entry must not be smaller than its pilot");
|
||||||
assert(!tripod_insert_enabled
|
assert(!tripod_insert_enabled
|
||||||
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
||||||
"Tripod insert boss does not have enough radial material");
|
"Tripod insert boss does not have enough radial material");
|
||||||
|
assert(usb_opening_enabled || !usb_guide_enabled,
|
||||||
|
"USB guide requires the USB opening");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z - usb_reserved_height / 2 >= effective_floor,
|
||||||
|
"USB opening or guide intersects the case floor");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z + usb_reserved_height / 2
|
||||||
|
<= body_height - lid_lip_depth,
|
||||||
|
"USB opening or guide intersects the lid lip");
|
||||||
|
assert(!tripod_insert_enabled
|
||||||
|
|| tripod_insert_outer_diameter >= tripod_insert_hole_diameter,
|
||||||
|
"Tripod insert entry must not be smaller than its pilot");
|
||||||
|
assert(!tripod_insert_enabled || tripod_standoff_clearance >= 1.2,
|
||||||
|
"Tripod insert entry is too close to a Feather standoff");
|
||||||
|
assert(!battery_enabled || !tripod_insert_enabled
|
||||||
|
|| tripod_partition_clearance >= 1,
|
||||||
|
"Tripod insert entry is too close to the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= board_origin_x + feather_length + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather PCB");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y + rocker_body_size[1] / 2
|
||||||
|
<= partition_y - fit_clearance,
|
||||||
|
"Rocker body overlaps the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the front lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x + rocker_body_size[0] / 2
|
||||||
|
<= case_length - wall_thickness - lid_lip_thickness - fit_clearance,
|
||||||
|
"Rocker body overlaps the rear lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y - rocker_body_size[1] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather-side lid lip");
|
||||||
|
assert(!battery_enabled || rocker_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker body overlaps a lid boss");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_relief_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker tab relief overlaps a lid boss scallop");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| abs(
|
||||||
|
case_width - rocker_lid_center_y - rocker_installed_center_y
|
||||||
|
) < eps,
|
||||||
|
"Rocker lid cutout does not mirror to the installed switch location");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| body_height - rocker_body_size[2] >= effective_floor,
|
||||||
|
"Rocker body intersects the case floor");
|
||||||
|
|
||||||
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
||||||
|
function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4];
|
||||||
|
function cutouts_separated(first, second, web) =
|
||||||
|
abs(first[2] - second[2])
|
||||||
|
>= (first[4] + second[4]) / 2 + web
|
||||||
|
|| abs(first[3] - second[3])
|
||||||
|
>= (cutout_height(first) + cutout_height(second)) / 2 + web;
|
||||||
|
function cutout_clear_of_usb(entry, web) =
|
||||||
|
abs(entry[2] - usb_center_u)
|
||||||
|
>= (entry[4] + usb_reserved_width) / 2 + web
|
||||||
|
|| abs(entry[3] - usb_center_z)
|
||||||
|
>= (cutout_height(entry) + usb_reserved_height) / 2 + web;
|
||||||
|
|
||||||
|
module validate_cutout_spacing() {
|
||||||
|
if (len(extra_cutouts) > 1)
|
||||||
|
for (first = [0 : len(extra_cutouts) - 2],
|
||||||
|
second = [first + 1 : len(extra_cutouts) - 1])
|
||||||
|
if (extra_cutouts[first][0] == extra_cutouts[second][0])
|
||||||
|
assert(
|
||||||
|
cutouts_separated(
|
||||||
|
extra_cutouts[first],
|
||||||
|
extra_cutouts[second],
|
||||||
|
cutout_min_web
|
||||||
|
),
|
||||||
|
"Custom cutouts are too close on the same wall"
|
||||||
|
);
|
||||||
|
if (usb_opening_enabled)
|
||||||
|
for (entry = extra_cutouts)
|
||||||
|
if (entry[0] == "front")
|
||||||
|
assert(
|
||||||
|
cutout_clear_of_usb(entry, cutout_min_web),
|
||||||
|
"Front cutout is too close to the USB opening or guide"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module rounded_rect_2d(size, radius) {
|
module rounded_rect_2d(size, radius) {
|
||||||
r = clamp(radius, 0.01, min(size[0], size[1]) / 2 - 0.01);
|
r = clamp(radius, 0.01, min(size[0], size[1]) / 2 - 0.01);
|
||||||
@@ -242,8 +477,8 @@ module rounded_box(size, radius) {
|
|||||||
rounded_rect_2d([size[0], size[1]], radius);
|
rounded_rect_2d([size[0], size[1]], radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
module insert_pocket(diameter, depth, entry_z, downward = true) {
|
module insert_pocket(diameter, entry_diameter, depth, entry_z, downward = true) {
|
||||||
bore_d = diameter + hole_compensation;
|
bore_d = diameter;
|
||||||
if (downward) {
|
if (downward) {
|
||||||
translate([0, 0, entry_z - depth])
|
translate([0, 0, entry_z - depth])
|
||||||
cylinder(h = depth + eps, d = bore_d);
|
cylinder(h = depth + eps, d = bore_d);
|
||||||
@@ -251,7 +486,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d,
|
d1 = bore_d,
|
||||||
d2 = bore_d + 2 * insert_entry_chamfer
|
d2 = entry_diameter
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
@@ -259,7 +494,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d + 2 * insert_entry_chamfer,
|
d1 = entry_diameter,
|
||||||
d2 = bore_d
|
d2 = bore_d
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -301,6 +536,40 @@ module face_circle_cutout(face, u, z, diameter) {
|
|||||||
rotate([-90, 0, 0]) cylinder(h = depth, d = diameter);
|
rotate([-90, 0, 0]) cylinder(h = depth, d = diameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module x_rounded_prism(x, length, u, z, width, height, radius) {
|
||||||
|
r = clamp(radius, 0.01, min(width, height) / 2 - 0.01);
|
||||||
|
hull()
|
||||||
|
for (yy = [u - width / 2 + r, u + width / 2 - r],
|
||||||
|
zz = [z - height / 2 + r, z + height / 2 - r])
|
||||||
|
translate([x, yy, zz])
|
||||||
|
rotate([0, 90, 0]) cylinder(h = length, r = r);
|
||||||
|
}
|
||||||
|
|
||||||
|
module usb_guide_shell() {
|
||||||
|
outer_width = usb_inner_width + 2 * usb_guide_wall_thickness;
|
||||||
|
outer_height = usb_inner_height + 2 * usb_guide_wall_thickness;
|
||||||
|
difference() {
|
||||||
|
x_rounded_prism(
|
||||||
|
wall_thickness - eps,
|
||||||
|
usb_guide_length + 2 * eps,
|
||||||
|
usb_center_u,
|
||||||
|
usb_center_z,
|
||||||
|
outer_width,
|
||||||
|
outer_height,
|
||||||
|
usb_opening_radius + usb_guide_wall_thickness
|
||||||
|
);
|
||||||
|
x_rounded_prism(
|
||||||
|
wall_thickness - 2 * eps,
|
||||||
|
usb_guide_length + 4 * eps,
|
||||||
|
usb_center_u,
|
||||||
|
usb_center_z,
|
||||||
|
usb_inner_width,
|
||||||
|
usb_inner_height,
|
||||||
|
usb_opening_radius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module custom_wall_cutout(entry) {
|
module custom_wall_cutout(entry) {
|
||||||
face = entry[0];
|
face = entry[0];
|
||||||
shape = entry[1];
|
shape = entry[1];
|
||||||
@@ -368,20 +637,27 @@ module battery_cradle() {
|
|||||||
battery_end_x - 4
|
battery_end_x - 4
|
||||||
];
|
];
|
||||||
|
|
||||||
// Board/battery partition, with a lead-routing notch near the JST end.
|
// One open-topped front passage provides JST plug access and enough room
|
||||||
|
// to route the battery lead beneath the Feather PCB beside the lid boss.
|
||||||
difference() {
|
difference() {
|
||||||
translate([wall_thickness, partition_y, effective_floor - eps])
|
translate([wall_thickness, partition_y, effective_floor - eps])
|
||||||
cube([
|
cube([
|
||||||
battery_end_x - wall_thickness + battery_end_clearance,
|
battery_end_x - wall_thickness + battery_end_clearance,
|
||||||
wall_thickness,
|
battery_partition_thickness,
|
||||||
partition_height - effective_floor + eps
|
partition_height - effective_floor + eps
|
||||||
]);
|
]);
|
||||||
translate([
|
translate([
|
||||||
board_origin_x + 6.5,
|
wall_thickness - eps,
|
||||||
partition_y - eps,
|
partition_y - eps,
|
||||||
effective_floor + 1.5
|
effective_floor + 1.5
|
||||||
])
|
])
|
||||||
cube([9, wall_thickness + 2 * eps, 6]);
|
cube([
|
||||||
|
board_origin_x + jst_center_xy[0]
|
||||||
|
+ (jst_body_size[0] + 2 * jst_relief_clearance) / 2
|
||||||
|
- wall_thickness + 2 * eps,
|
||||||
|
battery_partition_thickness + 2 * eps,
|
||||||
|
partition_height - effective_floor - 1.5 + eps
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three low curved ribs cradle the protected cylindrical pack.
|
// Three low curved ribs cradle the protected cylindrical pack.
|
||||||
@@ -412,29 +688,16 @@ module battery_cradle() {
|
|||||||
battery_end_x + battery_end_clearance
|
battery_end_x + battery_end_clearance
|
||||||
])
|
])
|
||||||
translate([
|
translate([
|
||||||
x,
|
x - eps,
|
||||||
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
||||||
effective_floor - eps
|
effective_floor - eps
|
||||||
])
|
])
|
||||||
cube([
|
cube([
|
||||||
wall_thickness,
|
wall_thickness + 2 * eps,
|
||||||
battery_diameter + 2 * battery_radial_clearance,
|
battery_diameter + 2 * battery_radial_clearance,
|
||||||
battery_diameter / 2 + 1 + eps
|
battery_diameter / 2 + 1 + eps
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Rear cross-strip separates the side-mounted switch body from the cell.
|
|
||||||
translate([
|
|
||||||
battery_end_x + battery_end_clearance,
|
|
||||||
partition_y,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
case_length - wall_thickness - battery_end_x - battery_end_clearance,
|
|
||||||
case_width - wall_thickness - partition_y,
|
|
||||||
min(body_height - effective_floor, switch_center_z
|
|
||||||
+ switch_body_diameter / 2 + switch_wire_allowance
|
|
||||||
- effective_floor) + eps
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module body_positive() {
|
module body_positive() {
|
||||||
@@ -453,8 +716,10 @@ module body_positive() {
|
|||||||
if (battery_enabled)
|
if (battery_enabled)
|
||||||
battery_cradle();
|
battery_cradle();
|
||||||
if (tripod_insert_enabled)
|
if (tripod_insert_enabled)
|
||||||
translate([case_length / 2, case_width / 2, 0])
|
translate([tripod_center_x, tripod_center_y, 0])
|
||||||
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
||||||
|
if (usb_guide_active)
|
||||||
|
usb_guide_shell();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +729,7 @@ module body_cutouts() {
|
|||||||
translate([board_origin_x + mx, board_origin_y + my, 0])
|
translate([board_origin_x + mx, board_origin_y + my, 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
board_bottom_z
|
board_bottom_z
|
||||||
);
|
);
|
||||||
@@ -473,44 +739,24 @@ module body_cutouts() {
|
|||||||
translate([p[0], p[1], 0])
|
translate([p[0], p[1], 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
body_height
|
body_height
|
||||||
);
|
);
|
||||||
|
|
||||||
// USB-C cable opening. Connector center is from the official ESP32 V2 PCB.
|
// USB-C cable opening. Connector center is from the official ESP32 V2 PCB.
|
||||||
usb_center_z = board_top_z + 1.6 + usb_vertical_offset;
|
if (usb_opening_enabled)
|
||||||
face_rounded_cutout(
|
face_rounded_cutout(
|
||||||
"front",
|
"front",
|
||||||
board_origin_y + usb_center_y,
|
usb_center_u,
|
||||||
usb_center_z,
|
usb_center_z,
|
||||||
usb_opening_width + hole_compensation,
|
usb_inner_width,
|
||||||
usb_opening_height + hole_compensation,
|
usb_inner_height,
|
||||||
usb_opening_radius
|
usb_opening_radius
|
||||||
);
|
);
|
||||||
|
|
||||||
if (battery_enabled) {
|
|
||||||
face_circle_cutout(
|
|
||||||
"right",
|
|
||||||
switch_center_x,
|
|
||||||
switch_center_z,
|
|
||||||
switch_hole_diameter + switch_hole_compensation
|
|
||||||
);
|
|
||||||
// Clear the switch body and its prewired terminals through the rear bay.
|
|
||||||
translate([
|
|
||||||
switch_center_x,
|
|
||||||
case_width - wall_thickness + eps,
|
|
||||||
switch_center_z
|
|
||||||
])
|
|
||||||
rotate([90, 0, 0])
|
|
||||||
cylinder(
|
|
||||||
h = switch_body_length + switch_wire_allowance
|
|
||||||
+ wall_thickness + eps,
|
|
||||||
d = switch_body_diameter + 2 * fit_clearance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tripod_insert_enabled) {
|
if (tripod_insert_enabled) {
|
||||||
translate([case_length / 2, case_width / 2, 0]) {
|
translate([tripod_center_x, tripod_center_y, 0]) {
|
||||||
translate([0, 0, -eps])
|
translate([0, 0, -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = effective_floor + 2 * eps,
|
h = effective_floor + 2 * eps,
|
||||||
@@ -518,6 +764,7 @@ module body_cutouts() {
|
|||||||
);
|
);
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter,
|
tripod_insert_hole_diameter,
|
||||||
|
tripod_insert_outer_diameter,
|
||||||
tripod_insert_length + insert_melt_relief,
|
tripod_insert_length + insert_melt_relief,
|
||||||
effective_floor
|
effective_floor
|
||||||
);
|
);
|
||||||
@@ -611,6 +858,13 @@ module oled_button_lid_cutout(xy) {
|
|||||||
module lid_cutouts() {
|
module lid_cutouts() {
|
||||||
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
||||||
for (p = lid_boss_xy) {
|
for (p = lid_boss_xy) {
|
||||||
|
// Scallop only the locating lip so it can pass over the full-height
|
||||||
|
// insert boss while leaving the lid plate supported by the boss top.
|
||||||
|
translate([p[0], p[1], lid_thickness - eps])
|
||||||
|
cylinder(
|
||||||
|
h = lid_lip_depth + 2 * eps,
|
||||||
|
d = m25_boss_diameter + 2 * fit_clearance
|
||||||
|
);
|
||||||
translate([p[0], p[1], -eps])
|
translate([p[0], p[1], -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = lid_thickness + lid_lip_depth + 2 * eps,
|
h = lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
@@ -623,6 +877,19 @@ module lid_cutouts() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (battery_enabled) {
|
||||||
|
translate([rocker_lid_center_x, rocker_lid_center_y, -eps])
|
||||||
|
centered_rounded_prism(
|
||||||
|
rocker_cutout_size,
|
||||||
|
lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[rocker_lid_center_x, rocker_lid_center_y],
|
||||||
|
rocker_cutout_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (top_profile == "oled128x64") {
|
if (top_profile == "oled128x64") {
|
||||||
beveled_oled_window();
|
beveled_oled_window();
|
||||||
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
||||||
@@ -632,13 +899,90 @@ module lid_cutouts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module lid() {
|
// Thin the horizontal top/bottom edges of the upright switch opening where the
|
||||||
|
// spring tabs latch. Each full-width land overlaps the rounded opening by its
|
||||||
|
// corner radius, then extends outward before tapering to the surrounding lid.
|
||||||
|
module rocker_tab_reliefs(center, opening_size) {
|
||||||
|
relief_depth = lid_thickness - rocker_tab_panel_thickness;
|
||||||
|
opening_overlap = rocker_corner_radius;
|
||||||
|
strip = 0.02;
|
||||||
|
for (side = [-1, 1]) {
|
||||||
|
edge_y = center[1] + side * opening_size[1] / 2;
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y - (side < 0 ? rocker_tab_land : opening_overlap),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], rocker_tab_land + opening_overlap,
|
||||||
|
relief_depth + eps]);
|
||||||
|
|
||||||
|
hull() {
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * rocker_tab_land - (side < 0 ? strip : 0),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, relief_depth + eps]);
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * (rocker_tab_land + rocker_tab_taper_length)
|
||||||
|
- (side < 0 ? strip : 0),
|
||||||
|
lid_thickness - strip
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, strip + eps]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_nominal_2d() {
|
||||||
|
translate(lid_art_center)
|
||||||
|
rotate(lid_art_rotation)
|
||||||
|
// The lid is modeled exterior-face-down; mirror the artwork so it
|
||||||
|
// reads normally after the lid is turned into service position.
|
||||||
|
mirror([0, 1, 0]) {
|
||||||
|
if (lid_logo_enabled)
|
||||||
|
translate([0, lid_logo_center_y])
|
||||||
|
resize(logo_size)
|
||||||
|
import(logo_file, center = true);
|
||||||
|
if (lid_label != "")
|
||||||
|
translate([0, lid_label_center_y])
|
||||||
|
text(
|
||||||
|
lid_label,
|
||||||
|
size = lid_label_size,
|
||||||
|
halign = "center",
|
||||||
|
valign = "center",
|
||||||
|
font = "DejaVu Sans:style=Bold"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_2d(inset = 0) {
|
||||||
|
offset(delta = inset)
|
||||||
|
lid_artwork_nominal_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_base() {
|
||||||
difference() {
|
difference() {
|
||||||
lid_plate_positive();
|
lid_plate_positive();
|
||||||
lid_cutouts();
|
lid_cutouts();
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
translate([0, 0, -eps])
|
||||||
|
linear_extrude(height = lid_inlay_depth + eps)
|
||||||
|
lid_artwork_2d(lid_inlay_fit_clearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module lid_accent() {
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
linear_extrude(height = lid_inlay_depth)
|
||||||
|
lid_artwork_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
color([0.92, 0.92, 0.95]) lid_base();
|
||||||
|
color([0.95, 0.42, 0.08]) lid_accent();
|
||||||
|
}
|
||||||
|
|
||||||
module one_oled_button() {
|
module one_oled_button() {
|
||||||
recess_depth = button_flange_thickness + button_travel;
|
recess_depth = button_flange_thickness + button_travel;
|
||||||
stem_height = lid_thickness - recess_depth + button_projection;
|
stem_height = lid_thickness - recess_depth + button_projection;
|
||||||
@@ -678,6 +1022,13 @@ module reference_board() {
|
|||||||
color([0.05, 0.25, 0.55, 0.75])
|
color([0.05, 0.25, 0.55, 0.75])
|
||||||
translate([board_origin_x, board_origin_y, board_bottom_z])
|
translate([board_origin_x, board_origin_y, board_bottom_z])
|
||||||
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
||||||
|
color([0.92, 0.92, 0.86, 0.9])
|
||||||
|
translate([
|
||||||
|
board_origin_x + jst_center_xy[0] - jst_body_size[0] / 2,
|
||||||
|
board_origin_y + jst_center_xy[1] - 4.5,
|
||||||
|
board_top_z
|
||||||
|
])
|
||||||
|
cube(jst_body_size);
|
||||||
color([0.12, 0.45, 0.75, 0.45])
|
color([0.12, 0.45, 0.75, 0.45])
|
||||||
for (i = [1 : wing_count])
|
for (i = [1 : wing_count])
|
||||||
translate([
|
translate([
|
||||||
@@ -744,19 +1095,20 @@ module reference_battery_and_switch() {
|
|||||||
translate([battery_start_x, battery_center_y, battery_center_z])
|
translate([battery_start_x, battery_center_y, battery_center_z])
|
||||||
rotate([0, 90, 0])
|
rotate([0, 90, 0])
|
||||||
cylinder(h = battery_length, d = battery_diameter);
|
cylinder(h = battery_length, d = battery_diameter);
|
||||||
color([0.8, 0.15, 0.1, 0.65])
|
color([0.08, 0.08, 0.08, 0.85])
|
||||||
translate([
|
translate([
|
||||||
switch_center_x,
|
rocker_installed_center_x - rocker_body_size[0] / 2,
|
||||||
case_width - wall_thickness,
|
rocker_installed_center_y - rocker_body_size[1] / 2,
|
||||||
switch_center_z
|
body_height + exploded_view - rocker_body_size[2]
|
||||||
])
|
])
|
||||||
rotate([90, 0, 0]) {
|
cube(rocker_body_size);
|
||||||
cylinder(h = switch_body_length, d = switch_body_diameter);
|
color([0.12, 0.12, 0.12, 0.95])
|
||||||
translate([0, 0, -1.5])
|
translate([
|
||||||
cylinder(h = 1.5, d = switch_flange_diameter);
|
rocker_installed_center_x - rocker_cutout_size[0] / 2,
|
||||||
translate([0, 0, -3])
|
rocker_installed_center_y - rocker_cutout_size[1] / 2,
|
||||||
cylinder(h = 3, d = switch_cap_diameter);
|
body_height + exploded_view + lid_thickness
|
||||||
}
|
])
|
||||||
|
cube([rocker_cutout_size[0], rocker_cutout_size[1], 2.5]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,13 +1126,9 @@ module assembly() {
|
|||||||
lid();
|
lid();
|
||||||
}
|
}
|
||||||
|
|
||||||
module coupon_hole(x, y, diameter, depth) {
|
|
||||||
translate([x, y, -eps]) cylinder(h = depth + 2 * eps, d = diameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
module insert_coupon() {
|
module insert_coupon() {
|
||||||
spacing = 15;
|
spacing = 15;
|
||||||
coupon_length = spacing * len(coupon_steps) + 8;
|
coupon_length = spacing * len(tripod_coupon_steps) + 8;
|
||||||
coupon_width = 32;
|
coupon_width = 32;
|
||||||
coupon_height = max(
|
coupon_height = max(
|
||||||
m25_insert_length + insert_melt_relief + 1.2,
|
m25_insert_length + insert_melt_relief + 1.2,
|
||||||
@@ -788,17 +1136,21 @@ module insert_coupon() {
|
|||||||
);
|
);
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1]) {
|
for (i = [0 : len(tripod_coupon_steps) - 1]) {
|
||||||
x = 7 + i * spacing;
|
x = 7 + i * spacing;
|
||||||
coupon_hole(
|
translate([x, 8, 0])
|
||||||
x, 8,
|
insert_pocket(
|
||||||
m25_insert_hole_diameter + coupon_steps[i],
|
m25_insert_hole_diameter + m25_coupon_steps[i],
|
||||||
m25_insert_length + insert_melt_relief
|
m25_insert_outer_diameter,
|
||||||
|
m25_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
coupon_hole(
|
translate([x, 23, 0])
|
||||||
x, 23,
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter + coupon_steps[i],
|
tripod_insert_hole_diameter + tripod_coupon_steps[i],
|
||||||
tripod_insert_length + insert_melt_relief
|
tripod_insert_outer_diameter,
|
||||||
|
tripod_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -807,16 +1159,23 @@ module insert_coupon() {
|
|||||||
module switch_coupon() {
|
module switch_coupon() {
|
||||||
spacing = 18;
|
spacing = 18;
|
||||||
coupon_length = spacing * len(coupon_steps) + 6;
|
coupon_length = spacing * len(coupon_steps) + 6;
|
||||||
coupon_width = 24;
|
coupon_width = rocker_cutout_size[1] + 6;
|
||||||
coupon_height = wall_thickness;
|
coupon_height = lid_thickness;
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1])
|
for (i = [0 : len(coupon_steps) - 1]) {
|
||||||
translate([6 + i * spacing, coupon_width / 2, -eps])
|
translate([6 + i * spacing, coupon_width / 2, -eps])
|
||||||
cylinder(
|
centered_rounded_prism(
|
||||||
h = coupon_height + 2 * eps,
|
rocker_cutout_size
|
||||||
d = switch_hole_diameter + coupon_steps[i]
|
+ [coupon_steps[i], coupon_steps[i]],
|
||||||
|
coupon_height + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
);
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[6 + i * spacing, coupon_width / 2],
|
||||||
|
rocker_cutout_size + [coupon_steps[i], coupon_steps[i]]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,12 +1213,18 @@ module fit_coupon() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_cutout_spacing();
|
||||||
|
|
||||||
if (part == "assembly")
|
if (part == "assembly")
|
||||||
assembly();
|
assembly();
|
||||||
else if (part == "body")
|
else if (part == "body")
|
||||||
body();
|
body();
|
||||||
else if (part == "lid")
|
else if (part == "lid")
|
||||||
lid();
|
lid();
|
||||||
|
else if (part == "lid_base")
|
||||||
|
lid_base();
|
||||||
|
else if (part == "lid_accent")
|
||||||
|
lid_accent();
|
||||||
else if (part == "buttons")
|
else if (part == "buttons")
|
||||||
buttons();
|
buttons();
|
||||||
else if (part == "insert_coupon")
|
else if (part == "insert_coupon")
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Render aligned lid-base and accent STLs with distinct materials."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
|
||||||
|
def import_mesh(path, name, color):
|
||||||
|
bpy.ops.wm.stl_import(filepath=os.path.abspath(path))
|
||||||
|
model = bpy.context.selected_objects[0]
|
||||||
|
model.name = name
|
||||||
|
material = bpy.data.materials.new(f"{name} material")
|
||||||
|
material.diffuse_color = color
|
||||||
|
material.metallic = 0.0
|
||||||
|
material.roughness = 0.45
|
||||||
|
model.data.materials.append(material)
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def look_at(camera, target):
|
||||||
|
direction = Vector(target) - camera.location
|
||||||
|
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[sys.argv.index("--") + 1 :]
|
||||||
|
if len(args) not in (4, 5):
|
||||||
|
raise SystemExit(
|
||||||
|
"expected: lid-base.stl lid-accent.stl output-directory basename [orientation]"
|
||||||
|
)
|
||||||
|
base_path, accent_path, output_dir, basename = args[:4]
|
||||||
|
orientation = args[4] if len(args) == 5 else "usb_left"
|
||||||
|
angles = {"usb_left": 0, "usb_front": 90, "usb_right": 180, "usb_rear": 270}
|
||||||
|
if orientation not in angles:
|
||||||
|
raise SystemExit(f"unknown lid orientation: {orientation}")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
models = [
|
||||||
|
import_mesh(base_path, "Lid base", (0.16, 0.42, 0.72, 1.0)),
|
||||||
|
import_mesh(accent_path, "Accent inlays", (0.95, 0.42, 0.08, 1.0)),
|
||||||
|
]
|
||||||
|
# Lid exports are print-oriented with the decorated exterior face down.
|
||||||
|
# Turn both aligned parts over and present the configured USB edge in its
|
||||||
|
# named position so previews inspect the finished face as the user will.
|
||||||
|
for model in models:
|
||||||
|
model.matrix_world = (
|
||||||
|
Matrix.Rotation(math.radians(angles[orientation]), 4, "Z")
|
||||||
|
@ Matrix.Rotation(math.pi, 4, "X")
|
||||||
|
)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
corners = [obj.matrix_world @ Vector(corner) for obj in models for corner in obj.bound_box]
|
||||||
|
mins = Vector([min(v[i] for v in corners) for i in range(3)])
|
||||||
|
maxs = Vector([max(v[i] for v in corners) for i in range(3)])
|
||||||
|
center = (mins + maxs) / 2
|
||||||
|
span = max(maxs - mins)
|
||||||
|
|
||||||
|
world = bpy.data.worlds.new("Inspection world")
|
||||||
|
world.use_nodes = True
|
||||||
|
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.035, 0.04, 0.05, 1)
|
||||||
|
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
|
||||||
|
bpy.context.scene.world = world
|
||||||
|
|
||||||
|
camera_data = bpy.data.cameras.new("Camera")
|
||||||
|
camera = bpy.data.objects.new("Camera", camera_data)
|
||||||
|
bpy.context.collection.objects.link(camera)
|
||||||
|
bpy.context.scene.camera = camera
|
||||||
|
camera.data.type = "ORTHO"
|
||||||
|
camera.data.ortho_scale = span * 1.35
|
||||||
|
|
||||||
|
key_data = bpy.data.lights.new("Key", type="AREA")
|
||||||
|
key_data.energy = 900
|
||||||
|
key_data.size = span
|
||||||
|
key = bpy.data.objects.new("Key", key_data)
|
||||||
|
key.location = center + Vector((span, -span, span * 1.5))
|
||||||
|
bpy.context.collection.objects.link(key)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.engine = "BLENDER_WORKBENCH"
|
||||||
|
scene.render.resolution_x = 800
|
||||||
|
scene.render.resolution_y = 600
|
||||||
|
scene.render.resolution_percentage = 100
|
||||||
|
scene.render.image_settings.file_format = "PNG"
|
||||||
|
scene.display.shading.light = "STUDIO"
|
||||||
|
scene.display.shading.color_type = "MATERIAL"
|
||||||
|
scene.display.shading.show_shadows = True
|
||||||
|
scene.display.shading.show_cavity = True
|
||||||
|
scene.display.shading.cavity_type = "BOTH"
|
||||||
|
|
||||||
|
for view, direction in {"iso": (1, -1, 0.8), "top": (0, 0, 1)}.items():
|
||||||
|
camera.location = center + Vector(direction).normalized() * span * 2.2
|
||||||
|
look_at(camera, center)
|
||||||
|
scene.render.filepath = os.path.join(output_dir, f"{basename}_{view}.png")
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine aligned lid meshes into one slicer-friendly multipart 3MF."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
|
||||||
|
ET.register_namespace("", CORE)
|
||||||
|
|
||||||
|
CONTENT_TYPES_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||||
|
</Types>
|
||||||
|
'''
|
||||||
|
|
||||||
|
RELATIONSHIPS_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||||
|
</Relationships>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def load_mesh(path: Path) -> ET.Element:
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
root = ET.fromstring(archive.read("3D/3dmodel.model"))
|
||||||
|
mesh = root.find(f".//{{{CORE}}}mesh")
|
||||||
|
if mesh is None:
|
||||||
|
raise ValueError(f"{path} does not contain a 3MF mesh")
|
||||||
|
return copy.deepcopy(mesh)
|
||||||
|
|
||||||
|
|
||||||
|
def model_xml(base: Path, accent: Path, title: str) -> bytes:
|
||||||
|
root = ET.Element(
|
||||||
|
f"{{{CORE}}}model",
|
||||||
|
{
|
||||||
|
"unit": "millimeter",
|
||||||
|
"{http://www.w3.org/XML/1998/namespace}lang": "en-US",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = (
|
||||||
|
"feather-case-wizard"
|
||||||
|
)
|
||||||
|
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
||||||
|
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
||||||
|
ET.SubElement(
|
||||||
|
materials, f"{{{CORE}}}base", {"name": "Lid base", "displaycolor": "#D1D5DB"}
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
materials,
|
||||||
|
f"{{{CORE}}}base",
|
||||||
|
{"name": "Accent inlays", "displaycolor": "#F97316"},
|
||||||
|
)
|
||||||
|
|
||||||
|
for object_id, name, source in (
|
||||||
|
(2, "Lid base — Filament 1", base),
|
||||||
|
(3, "Accent inlays — Filament 2", accent),
|
||||||
|
):
|
||||||
|
obj = ET.SubElement(
|
||||||
|
resources,
|
||||||
|
f"{{{CORE}}}object",
|
||||||
|
{
|
||||||
|
"id": str(object_id),
|
||||||
|
"name": name,
|
||||||
|
"type": "model",
|
||||||
|
"pid": "1",
|
||||||
|
"pindex": str(object_id - 2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
obj.append(load_mesh(source))
|
||||||
|
|
||||||
|
assembly = ET.SubElement(
|
||||||
|
resources, f"{{{CORE}}}object", {"id": "4", "name": title, "type": "model"}
|
||||||
|
)
|
||||||
|
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "2"})
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "3"})
|
||||||
|
build = ET.SubElement(root, f"{{{CORE}}}build")
|
||||||
|
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": "4"})
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def bambu_model_settings_xml(title: str) -> bytes:
|
||||||
|
root = ET.Element("config")
|
||||||
|
obj = ET.SubElement(root, "object", {"id": "4"})
|
||||||
|
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
||||||
|
for part_id, name, extruder in (
|
||||||
|
(2, "Lid base — Filament 1", 1),
|
||||||
|
(3, "Accent inlays — Filament 2", 2),
|
||||||
|
):
|
||||||
|
part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "name", "value": name})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)})
|
||||||
|
ET.SubElement(root, "assemble")
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
|
||||||
|
info = zipfile.ZipInfo(name)
|
||||||
|
info.create_system = 3
|
||||||
|
info.external_attr = 0o100644 << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
archive.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("base", type=Path)
|
||||||
|
parser.add_argument("accent", type=Path)
|
||||||
|
parser.add_argument("output", type=Path)
|
||||||
|
parser.add_argument("--title", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
||||||
|
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"3D/3dmodel.model",
|
||||||
|
model_xml(args.base, args.accent, args.title),
|
||||||
|
)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"Metadata/model_settings.config",
|
||||||
|
bambu_model_settings_xml(args.title),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Pan/Tilt Head
|
|
||||||
|
|
||||||
Generated by the Feather Case Wizard.
|
|
||||||
|
|
||||||
- Top profile: `oled128x64`
|
|
||||||
- FeatherWings: 3
|
|
||||||
- Battery compartment: no
|
|
||||||
- 1/4-20 insert: yes
|
|
||||||
- Approximate exterior: 95.6 × 41.7 × 59.8 mm
|
|
||||||
- Formats: 3mf
|
|
||||||
- Parts: `body`, `lid`, `buttons`
|
|
||||||
- Custom cutouts: 4
|
|
||||||
|
|
||||||
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
|
||||||
OpenSCAD and Blender must be installed. Set `OPENSCAD` or `BLENDER` to
|
|
||||||
override their executable paths.
|
|
||||||
|
|
||||||
To open the saved Customizer values manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openscad -p parameters.json -P pan-tilt-head source/feather_case.scad
|
|
||||||
```
|
|
||||||
|
|
||||||
The 3MF files contain portable geometry only. Choose printer, filament,
|
|
||||||
plate, and slicing settings in Bambu Studio.
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "fileutils"
|
|
||||||
require "json"
|
|
||||||
require "open3"
|
|
||||||
require "pathname"
|
|
||||||
require "tmpdir"
|
|
||||||
require "yaml"
|
|
||||||
|
|
||||||
ROOT = Pathname(__dir__).expand_path
|
|
||||||
|
|
||||||
def executable(env_name, command)
|
|
||||||
override = ENV[env_name]
|
|
||||||
return File.expand_path(override) if override && File.executable?(override)
|
|
||||||
|
|
||||||
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
|
|
||||||
candidate = File.join(directory, command)
|
|
||||||
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
||||||
end
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def literal(value)
|
|
||||||
case value
|
|
||||||
when String then JSON.generate(value)
|
|
||||||
when TrueClass then "true"
|
|
||||||
when FalseClass then "false"
|
|
||||||
when Numeric then value.to_s
|
|
||||||
when Array then "[#{value.map { |item| literal(item) }.join(",")}]"
|
|
||||||
else raise "Cannot convert #{value.inspect} to OpenSCAD"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def run!(*command, env: {})
|
|
||||||
stdout, stderr, status = Open3.capture3(env, *command)
|
|
||||||
return if status.success?
|
|
||||||
|
|
||||||
warn [stdout, stderr].reject(&:empty?).join("\n")
|
|
||||||
raise "Command failed (#{status.exitstatus}): #{command.first}"
|
|
||||||
end
|
|
||||||
|
|
||||||
config = YAML.safe_load_file(ROOT.join("case.yml"), permitted_classes: [],
|
|
||||||
permitted_symbols: [], aliases: false)
|
|
||||||
openscad = executable("OPENSCAD", "openscad")
|
|
||||||
blender = executable("BLENDER", "blender")
|
|
||||||
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
|
||||||
abort "Blender not found; install it or set BLENDER" unless blender
|
|
||||||
|
|
||||||
parameters = config.fetch("parameters").merge(
|
|
||||||
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
|
||||||
)
|
|
||||||
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
|
||||||
buttons = %w[button_a button_b button_c button_reset]
|
|
||||||
if parameters["top_profile"] == "oled128x64" &&
|
|
||||||
buttons.any? { |name| parameters[name] }
|
|
||||||
parts << "buttons"
|
|
||||||
end
|
|
||||||
|
|
||||||
stage = Pathname(Dir.mktmpdir(".rebuild-", ROOT.to_s))
|
|
||||||
begin
|
|
||||||
exports = stage.join("exports")
|
|
||||||
previews = stage.join("previews")
|
|
||||||
scratch = stage.join("meshes")
|
|
||||||
FileUtils.mkdir_p([exports, previews, scratch])
|
|
||||||
model = ROOT.join("source/feather_case.scad")
|
|
||||||
renderer = ROOT.join("source/render_stl_previews.py")
|
|
||||||
|
|
||||||
export = lambda do |output, part, overrides = {}|
|
|
||||||
values = parameters.merge(overrides).merge("part" => part)
|
|
||||||
command = [openscad, "-o", output.to_s]
|
|
||||||
values.each { |name, value| command.concat(["-D", "#{name}=#{literal(value)}"]) }
|
|
||||||
run!(*command, model.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
config.fetch("outputs").fetch("formats").each do |format|
|
|
||||||
directory = exports.join(format)
|
|
||||||
FileUtils.mkdir_p(directory)
|
|
||||||
parts.each do |part|
|
|
||||||
puts "Exporting #{part}.#{format}…"
|
|
||||||
export.call(directory.join("#{part}.#{format}"), part)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
assembly = scratch.join("assembly.stl")
|
|
||||||
puts "Preparing assembly preview…"
|
|
||||||
export.call(assembly, "assembly",
|
|
||||||
"exploded_view" => config.fetch("outputs").fetch("assembly_exploded_view"),
|
|
||||||
"show_references" => false)
|
|
||||||
render_env = {"DISPLAY" => nil, "WAYLAND_DISPLAY" => nil}
|
|
||||||
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
|
||||||
assembly.to_s, previews.to_s, "assembly", env: render_env)
|
|
||||||
parts.each do |part|
|
|
||||||
puts "Rendering #{part} preview…"
|
|
||||||
mesh = scratch.join("#{part}.stl")
|
|
||||||
export.call(mesh, part) unless mesh.file?
|
|
||||||
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
|
||||||
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
|
||||||
end
|
|
||||||
|
|
||||||
backup = ROOT.join(".build-old-#{Process.pid}")
|
|
||||||
FileUtils.mkdir_p(backup)
|
|
||||||
%w[exports previews].each do |name|
|
|
||||||
current = ROOT.join(name)
|
|
||||||
FileUtils.mv(current, backup.join(name)) if current.exist?
|
|
||||||
end
|
|
||||||
begin
|
|
||||||
FileUtils.mv(exports, ROOT.join("exports"))
|
|
||||||
FileUtils.mv(previews, ROOT.join("previews"))
|
|
||||||
FileUtils.rm_rf(backup)
|
|
||||||
rescue StandardError
|
|
||||||
FileUtils.rm_rf(ROOT.join("exports"))
|
|
||||||
FileUtils.rm_rf(ROOT.join("previews"))
|
|
||||||
%w[exports previews].each do |name|
|
|
||||||
saved = backup.join(name)
|
|
||||||
FileUtils.mv(saved, ROOT.join(name)) if saved.exist?
|
|
||||||
end
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
puts "Rebuilt #{ROOT}"
|
|
||||||
ensure
|
|
||||||
FileUtils.rm_rf(stage)
|
|
||||||
end
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
---
|
|
||||||
schema_version: 1
|
|
||||||
name: Pan/Tilt Head
|
|
||||||
slug: pan-tilt-head
|
|
||||||
generator:
|
|
||||||
sha256: 965260d4d402fd8894f30aa863224e2c9b42dc61a1496f01e2f00b509f9624db
|
|
||||||
created_with: feather-case-wizard
|
|
||||||
parameters:
|
|
||||||
top_profile: oled128x64
|
|
||||||
wing_count: 3
|
|
||||||
wing_pitch: 12.25
|
|
||||||
stack_height_override: 38.0
|
|
||||||
generic_component_height: 3.0
|
|
||||||
generic_top_gap: 2.0
|
|
||||||
gps_antenna_height: 6.0
|
|
||||||
gps_top_gap: 5.0
|
|
||||||
oled_component_height: 2.4
|
|
||||||
oled_display_height: 1.5
|
|
||||||
oled_face_gap: 0.8
|
|
||||||
button_a: true
|
|
||||||
button_b: true
|
|
||||||
button_c: true
|
|
||||||
button_reset: false
|
|
||||||
button_projection: 1.5
|
|
||||||
button_cap_size:
|
|
||||||
- 5.0
|
|
||||||
- 3.2
|
|
||||||
button_flange_size:
|
|
||||||
- 6.6
|
|
||||||
- 3.6
|
|
||||||
button_flange_thickness: 0.9
|
|
||||||
button_travel: 0.5
|
|
||||||
button_contact_extension: 0.7
|
|
||||||
battery_enabled: false
|
|
||||||
battery_length: 69.0
|
|
||||||
battery_diameter: 18.0
|
|
||||||
battery_radial_clearance: 0.6
|
|
||||||
battery_end_clearance: 1.0
|
|
||||||
switch_hole_diameter: 12.0
|
|
||||||
switch_hole_compensation: 0.3
|
|
||||||
switch_cap_diameter: 8.0
|
|
||||||
switch_flange_diameter: 16.2
|
|
||||||
switch_body_length: 24.0
|
|
||||||
switch_body_diameter: 12.0
|
|
||||||
switch_wire_allowance: 3.0
|
|
||||||
tripod_insert_enabled: true
|
|
||||||
tripod_insert_length: 6.4
|
|
||||||
tripod_insert_hole_diameter: 8.8
|
|
||||||
tripod_thread_clearance_diameter: 6.8
|
|
||||||
tripod_boss_diameter: 14.0
|
|
||||||
m25_insert_length: 4.0
|
|
||||||
m25_insert_hole_diameter: 3.7
|
|
||||||
m25_screw_clearance: 2.8
|
|
||||||
m25_boss_diameter: 7.0
|
|
||||||
insert_entry_chamfer: 0.6
|
|
||||||
insert_melt_relief: 0.8
|
|
||||||
wall_thickness: 2.4
|
|
||||||
floor_thickness: 2.4
|
|
||||||
lid_thickness: 3.2
|
|
||||||
corner_radius: 4.0
|
|
||||||
board_bottom_clearance: 5.4
|
|
||||||
board_side_clearance: 7.0
|
|
||||||
board_rear_clearance: 20.0
|
|
||||||
usb_board_wall_gap: 20.0
|
|
||||||
lid_lip_depth: 2.4
|
|
||||||
lid_lip_thickness: 1.2
|
|
||||||
fit_clearance: 0.35
|
|
||||||
cutout_min_web: 1.2
|
|
||||||
hole_compensation: 0.2
|
|
||||||
lid_screw_head_diameter: 5.2
|
|
||||||
lid_screw_head_height: 2.5
|
|
||||||
usb_opening_enabled: true
|
|
||||||
usb_guide_enabled: true
|
|
||||||
usb_connector_overhang: 2.0
|
|
||||||
usb_guide_wall_thickness: 1.2
|
|
||||||
usb_guide_end_gap: 1.0
|
|
||||||
usb_opening_width: 12.5
|
|
||||||
usb_opening_height: 7.0
|
|
||||||
usb_opening_radius: 1.5
|
|
||||||
usb_vertical_offset: 0.0
|
|
||||||
cutouts:
|
|
||||||
- name: Antenna
|
|
||||||
face: left
|
|
||||||
shape: circle
|
|
||||||
horizontal: 30.4
|
|
||||||
z: 31.299999999999997
|
|
||||||
width: 6.5
|
|
||||||
height: 6.5
|
|
||||||
radius: 3.25
|
|
||||||
- name: Servo
|
|
||||||
face: rear
|
|
||||||
shape: roundrect
|
|
||||||
horizontal: 20.83
|
|
||||||
z: 35.0
|
|
||||||
width: 10.0
|
|
||||||
height: 5.0
|
|
||||||
radius: 1.25
|
|
||||||
- name: Release
|
|
||||||
face: rear
|
|
||||||
shape: circle
|
|
||||||
horizontal: 20.83
|
|
||||||
z: 20.0
|
|
||||||
width: 6.0
|
|
||||||
height: 6.0
|
|
||||||
radius: 3.0
|
|
||||||
- name: Power
|
|
||||||
face: front
|
|
||||||
shape: circle
|
|
||||||
horizontal: 20.83
|
|
||||||
z: 31.65
|
|
||||||
width: 8.0
|
|
||||||
height: 8.0
|
|
||||||
radius: 4.0
|
|
||||||
outputs:
|
|
||||||
formats:
|
|
||||||
- 3mf
|
|
||||||
coupons: []
|
|
||||||
assembly_exploded_view: 8
|
|
||||||
resolved:
|
|
||||||
extra_cutouts:
|
|
||||||
- - left
|
|
||||||
- circle
|
|
||||||
- 30.4
|
|
||||||
- 31.299999999999997
|
|
||||||
- 6.5
|
|
||||||
- 6.5
|
|
||||||
- 3.25
|
|
||||||
- - rear
|
|
||||||
- roundrect
|
|
||||||
- 20.83
|
|
||||||
- 35.0
|
|
||||||
- 10.0
|
|
||||||
- 5.0
|
|
||||||
- 1.25
|
|
||||||
- - rear
|
|
||||||
- circle
|
|
||||||
- 20.83
|
|
||||||
- 20.0
|
|
||||||
- 6.0
|
|
||||||
- 6.0
|
|
||||||
- 3.0
|
|
||||||
- - front
|
|
||||||
- circle
|
|
||||||
- 20.83
|
|
||||||
- 31.65
|
|
||||||
- 8.0
|
|
||||||
- 8.0
|
|
||||||
- 4.0
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
"""Render six inspection views of an STL using Blender.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
blender --background --python tools/render_stl_previews.py -- \
|
|
||||||
input.stl output-directory basename [comma-separated-views]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from mathutils import Vector
|
|
||||||
|
|
||||||
|
|
||||||
def arguments():
|
|
||||||
args = sys.argv[sys.argv.index("--") + 1 :]
|
|
||||||
if len(args) not in (3, 4):
|
|
||||||
raise SystemExit(
|
|
||||||
"expected: input.stl output-directory basename [comma-separated-views]"
|
|
||||||
)
|
|
||||||
requested_views = args[3].split(",") if len(args) == 4 else None
|
|
||||||
return args[0], args[1], args[2], requested_views
|
|
||||||
|
|
||||||
|
|
||||||
def look_at(camera, target):
|
|
||||||
direction = Vector(target) - camera.location
|
|
||||||
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
|
||||||
|
|
||||||
|
|
||||||
def render_view(camera, center, span, name, direction, output_dir, basename):
|
|
||||||
distance = span * 2.2
|
|
||||||
camera.location = Vector(center) + Vector(direction).normalized() * distance
|
|
||||||
look_at(camera, center)
|
|
||||||
camera.data.type = "ORTHO"
|
|
||||||
camera.data.ortho_scale = span * 1.35
|
|
||||||
bpy.context.scene.render.filepath = os.path.join(
|
|
||||||
output_dir, f"{basename}_{name}.png"
|
|
||||||
)
|
|
||||||
bpy.ops.render.render(write_still=True)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
input_path, output_dir, basename, requested_views = arguments()
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
||||||
bpy.ops.wm.stl_import(filepath=os.path.abspath(input_path))
|
|
||||||
model = bpy.context.selected_objects[0]
|
|
||||||
model.name = "Feather case"
|
|
||||||
|
|
||||||
material = bpy.data.materials.new("Case material")
|
|
||||||
material.diffuse_color = (0.16, 0.42, 0.72, 1.0)
|
|
||||||
material.metallic = 0.0
|
|
||||||
material.roughness = 0.45
|
|
||||||
model.data.materials.append(material)
|
|
||||||
|
|
||||||
corners = [model.matrix_world @ Vector(corner) for corner in model.bound_box]
|
|
||||||
mins = Vector([min(v[i] for v in corners) for i in range(3)])
|
|
||||||
maxs = Vector([max(v[i] for v in corners) for i in range(3)])
|
|
||||||
center = (mins + maxs) / 2
|
|
||||||
span = max(maxs - mins)
|
|
||||||
|
|
||||||
world = bpy.data.worlds.new("Inspection world")
|
|
||||||
world.use_nodes = True
|
|
||||||
world.node_tree.nodes["Background"].inputs["Color"].default_value = (
|
|
||||||
0.035,
|
|
||||||
0.04,
|
|
||||||
0.05,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
|
|
||||||
bpy.context.scene.world = world
|
|
||||||
|
|
||||||
camera_data = bpy.data.cameras.new("Camera")
|
|
||||||
camera = bpy.data.objects.new("Camera", camera_data)
|
|
||||||
bpy.context.collection.objects.link(camera)
|
|
||||||
bpy.context.scene.camera = camera
|
|
||||||
|
|
||||||
key_data = bpy.data.lights.new("Key", type="AREA")
|
|
||||||
key_data.energy = 900
|
|
||||||
key_data.shape = "DISK"
|
|
||||||
key_data.size = span
|
|
||||||
key = bpy.data.objects.new("Key", key_data)
|
|
||||||
key.location = center + Vector((span, -span, span * 1.5))
|
|
||||||
bpy.context.collection.objects.link(key)
|
|
||||||
|
|
||||||
fill_data = bpy.data.lights.new("Fill", type="AREA")
|
|
||||||
fill_data.energy = 500
|
|
||||||
fill_data.size = span
|
|
||||||
fill = bpy.data.objects.new("Fill", fill_data)
|
|
||||||
fill.location = center + Vector((-span, span, span))
|
|
||||||
bpy.context.collection.objects.link(fill)
|
|
||||||
|
|
||||||
scene = bpy.context.scene
|
|
||||||
scene.render.engine = "BLENDER_WORKBENCH"
|
|
||||||
scene.render.resolution_x = 800
|
|
||||||
scene.render.resolution_y = 600
|
|
||||||
scene.render.resolution_percentage = 100
|
|
||||||
scene.render.image_settings.file_format = "PNG"
|
|
||||||
scene.render.film_transparent = False
|
|
||||||
scene.display.shading.light = "STUDIO"
|
|
||||||
scene.display.shading.color_type = "MATERIAL"
|
|
||||||
scene.display.shading.show_shadows = True
|
|
||||||
scene.display.shading.show_cavity = True
|
|
||||||
scene.display.shading.cavity_type = "BOTH"
|
|
||||||
scene.display.shading.curvature_ridge_factor = 1.5
|
|
||||||
scene.display.shading.curvature_valley_factor = 1.2
|
|
||||||
|
|
||||||
views = {
|
|
||||||
"iso": (1, -1, 0.8),
|
|
||||||
"front": (-1, 0, 0),
|
|
||||||
"back": (1, 0, 0),
|
|
||||||
"left": (0, -1, 0),
|
|
||||||
"right": (0, 1, 0),
|
|
||||||
"top": (0, 0, 1),
|
|
||||||
}
|
|
||||||
selected = requested_views or list(views)
|
|
||||||
unknown = sorted(set(selected) - set(views))
|
|
||||||
if unknown:
|
|
||||||
raise SystemExit(f"unknown views: {', '.join(unknown)}")
|
|
||||||
for name in selected:
|
|
||||||
direction = views[name]
|
|
||||||
render_view(camera, center, span, name, direction, output_dir, basename)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+386
-114
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* [Output] */
|
/* [Output] */
|
||||||
part = "assembly"; // [assembly,body,lid,buttons,insert_coupon,switch_coupon,fit_coupon]
|
part = "assembly"; // [assembly,body,lid,lid_base,lid_accent,buttons,insert_coupon,switch_coupon,fit_coupon]
|
||||||
exploded_view = 8; // [0:1:20]
|
exploded_view = 8; // [0:1:20]
|
||||||
show_references = true;
|
show_references = true;
|
||||||
|
|
||||||
@@ -47,24 +47,29 @@ battery_length = 69;
|
|||||||
battery_diameter = 18;
|
battery_diameter = 18;
|
||||||
battery_radial_clearance = 0.6;
|
battery_radial_clearance = 0.6;
|
||||||
battery_end_clearance = 1;
|
battery_end_clearance = 1;
|
||||||
switch_hole_diameter = 12;
|
battery_partition_thickness = 1.8;
|
||||||
switch_hole_compensation = 0.3;
|
battery_board_clearance = 3;
|
||||||
switch_cap_diameter = 8;
|
rocker_cutout_size = [12.9, 19.4];
|
||||||
switch_flange_diameter = 16.2;
|
rocker_corner_radius = 0.8;
|
||||||
switch_body_length = 24;
|
rocker_body_size = [15, 21, 21];
|
||||||
switch_body_diameter = 12;
|
rocker_artwork_gap = 2;
|
||||||
switch_wire_allowance = 3;
|
rocker_partition_offset = 2;
|
||||||
|
rocker_tab_panel_thickness = 2;
|
||||||
|
rocker_tab_land = 2;
|
||||||
|
rocker_tab_taper_length = 0.6;
|
||||||
|
|
||||||
/* [Tripod mount] */
|
/* [Tripod mount] */
|
||||||
tripod_insert_enabled = false;
|
tripod_insert_enabled = false;
|
||||||
tripod_insert_length = 6.4;
|
tripod_insert_length = 5;
|
||||||
tripod_insert_hole_diameter = 8.8;
|
tripod_insert_hole_diameter = 7.2;
|
||||||
|
tripod_insert_outer_diameter = 8;
|
||||||
tripod_thread_clearance_diameter = 6.8;
|
tripod_thread_clearance_diameter = 6.8;
|
||||||
tripod_boss_diameter = 14;
|
tripod_boss_diameter = 14;
|
||||||
|
|
||||||
/* [M2.5 inserts] */
|
/* [M2.5 inserts] */
|
||||||
m25_insert_length = 4;
|
m25_insert_length = 4;
|
||||||
m25_insert_hole_diameter = 3.7;
|
m25_insert_hole_diameter = 3.1;
|
||||||
|
m25_insert_outer_diameter = 3.5;
|
||||||
m25_screw_clearance = 2.8;
|
m25_screw_clearance = 2.8;
|
||||||
m25_boss_diameter = 7;
|
m25_boss_diameter = 7;
|
||||||
insert_entry_chamfer = 0.6;
|
insert_entry_chamfer = 0.6;
|
||||||
@@ -87,16 +92,27 @@ hole_compensation = 0.2;
|
|||||||
lid_screw_head_diameter = 5.2;
|
lid_screw_head_diameter = 5.2;
|
||||||
lid_screw_head_height = 2.5;
|
lid_screw_head_height = 2.5;
|
||||||
|
|
||||||
|
/* [Two-color lid artwork] */
|
||||||
|
lid_label = "";
|
||||||
|
lid_logo_enabled = false;
|
||||||
|
lid_art_orientation = "usb_left"; // [usb_left,usb_front,usb_right,usb_rear]
|
||||||
|
lid_inlay_depth = 0.6; // [0.4:0.2:1]
|
||||||
|
lid_inlay_fit_clearance = 0.08;
|
||||||
|
lid_label_size = 5;
|
||||||
|
lid_label_logo_gap = 2;
|
||||||
|
lid_art_margin = 8.5;
|
||||||
|
logo_file = "assets/NME-fixed.svg";
|
||||||
|
|
||||||
/* [USB-C opening] */
|
/* [USB-C opening] */
|
||||||
usb_opening_enabled = true;
|
usb_opening_enabled = true;
|
||||||
usb_guide_enabled = true;
|
usb_guide_enabled = true;
|
||||||
usb_connector_overhang = 2;
|
usb_connector_overhang = 2;
|
||||||
usb_guide_wall_thickness = 1.2;
|
usb_guide_wall_thickness = 1.2;
|
||||||
usb_guide_end_gap = 1;
|
usb_guide_end_gap = 1;
|
||||||
usb_opening_width = 12.5;
|
usb_opening_width = 13.5;
|
||||||
usb_opening_height = 7;
|
usb_opening_height = 8;
|
||||||
usb_opening_radius = 1.5;
|
usb_opening_radius = 1.5;
|
||||||
usb_vertical_offset = 0;
|
usb_vertical_offset = 0.5;
|
||||||
|
|
||||||
/* [Custom wall cutouts] */
|
/* [Custom wall cutouts] */
|
||||||
// Entry format: [face, shape, u, z, width, height, radius]
|
// Entry format: [face, shape, u, z, width, height, radius]
|
||||||
@@ -106,6 +122,8 @@ extra_cutouts = [];
|
|||||||
|
|
||||||
/* [Calibration] */
|
/* [Calibration] */
|
||||||
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
m25_coupon_steps = [-0.15, -0.075, 0, 0.075, 0.15];
|
||||||
|
tripod_coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];
|
||||||
|
|
||||||
$fn = $preview ? 36 : 72;
|
$fn = $preview ? 36 : 72;
|
||||||
eps = 0.02;
|
eps = 0.02;
|
||||||
@@ -117,6 +135,9 @@ board_thickness = 1.6;
|
|||||||
mount_x = [2.54, 48.26];
|
mount_x = [2.54, 48.26];
|
||||||
mount_y = [2.54, 20.32];
|
mount_y = [2.54, 20.32];
|
||||||
usb_center_y = 11.43;
|
usb_center_y = 11.43;
|
||||||
|
jst_center_xy = [10.9855, 19.304];
|
||||||
|
jst_body_size = [8, 7.5, 4.5];
|
||||||
|
jst_relief_clearance = 0.5;
|
||||||
|
|
||||||
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
// Official 128x64 OLED FeatherWing geometry in Feather-board coordinates.
|
||||||
oled_visible_min = [11.42, 3.78];
|
oled_visible_min = [11.42, 3.78];
|
||||||
@@ -133,9 +154,18 @@ effective_board_clearance = max(
|
|||||||
board_bottom_clearance,
|
board_bottom_clearance,
|
||||||
m25_insert_length + insert_melt_relief + 0.6
|
m25_insert_length + insert_melt_relief + 0.6
|
||||||
);
|
);
|
||||||
|
lid_boss_r = m25_boss_diameter / 2;
|
||||||
|
lid_boss_wall_overlap = 0.6;
|
||||||
|
front_lid_boss_center = wall_thickness + lid_boss_r
|
||||||
|
- lid_boss_wall_overlap;
|
||||||
|
|
||||||
|
// Combined battery/tripod builds need room around the centered insert entry.
|
||||||
|
combined_tripod_board_gutter = battery_enabled && tripod_insert_enabled ? 2.4 : 0;
|
||||||
|
combined_tripod_x_shift = battery_enabled && tripod_insert_enabled ? 5 : 0;
|
||||||
|
|
||||||
board_origin_x = wall_thickness + usb_board_wall_gap;
|
board_origin_x = wall_thickness + usb_board_wall_gap;
|
||||||
board_origin_y = wall_thickness + board_side_clearance;
|
board_origin_y = wall_thickness + board_side_clearance
|
||||||
|
+ combined_tripod_board_gutter;
|
||||||
board_bottom_z = effective_floor + effective_board_clearance;
|
board_bottom_z = effective_floor + effective_board_clearance;
|
||||||
board_top_z = board_bottom_z + board_thickness;
|
board_top_z = board_bottom_z + board_thickness;
|
||||||
usb_center_u = board_origin_y + usb_center_y;
|
usb_center_u = board_origin_y + usb_center_y;
|
||||||
@@ -166,20 +196,17 @@ profile_top_gap =
|
|||||||
top_profile == "oled128x64" ? oled_face_gap :
|
top_profile == "oled128x64" ? oled_face_gap :
|
||||||
generic_top_gap;
|
generic_top_gap;
|
||||||
|
|
||||||
partition_y = board_origin_y + feather_width + 1;
|
partition_y = board_origin_y + feather_width + battery_board_clearance;
|
||||||
battery_center_y = partition_y + wall_thickness + battery_radial_clearance
|
battery_center_y = partition_y + battery_partition_thickness
|
||||||
+ battery_diameter / 2;
|
+ battery_radial_clearance + battery_diameter / 2;
|
||||||
battery_start_x = board_origin_x + battery_end_clearance;
|
battery_start_x = front_lid_boss_center + lid_boss_r
|
||||||
|
+ battery_end_clearance;
|
||||||
battery_end_x = battery_start_x + battery_length;
|
battery_end_x = battery_start_x + battery_length;
|
||||||
switch_center_x = battery_end_x + max(
|
|
||||||
switch_body_diameter / 2 + 0.8,
|
|
||||||
switch_flange_diameter / 2 + 0.5
|
|
||||||
);
|
|
||||||
|
|
||||||
case_length_plain = board_origin_x + feather_length
|
case_length_plain = board_origin_x + feather_length
|
||||||
+ board_rear_clearance + wall_thickness;
|
+ board_rear_clearance + wall_thickness;
|
||||||
case_length_battery = switch_center_x + switch_flange_diameter / 2
|
case_length_battery = battery_end_x + battery_end_clearance
|
||||||
+ 1.2 + wall_thickness;
|
+ wall_thickness + m25_boss_diameter - lid_boss_wall_overlap;
|
||||||
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
case_length = battery_enabled ? max(case_length_plain, case_length_battery)
|
||||||
: case_length_plain;
|
: case_length_plain;
|
||||||
|
|
||||||
@@ -190,23 +217,74 @@ case_width_battery = battery_center_y + battery_diameter / 2
|
|||||||
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
case_width = battery_enabled ? max(case_width_plain, case_width_battery)
|
||||||
: case_width_plain;
|
: case_width_plain;
|
||||||
|
|
||||||
|
rocker_installed_center_x = case_length - wall_thickness - fit_clearance
|
||||||
|
- lid_lip_thickness - fit_clearance - rocker_body_size[0] / 2;
|
||||||
|
rocker_installed_center_y = front_lid_boss_center + lid_boss_r + fit_clearance
|
||||||
|
+ rocker_body_size[1] / 2 + rocker_partition_offset;
|
||||||
|
// The printable lid is flipped across Y when installed, so its local cutout
|
||||||
|
// coordinate must be mirrored from the assembled switch location.
|
||||||
|
rocker_lid_center_x = rocker_installed_center_x;
|
||||||
|
rocker_lid_center_y = case_width - rocker_installed_center_y;
|
||||||
|
|
||||||
|
lid_artwork_enabled = lid_label != "" || lid_logo_enabled;
|
||||||
|
lid_art_rotation =
|
||||||
|
lid_art_orientation == "usb_left" ? 0 :
|
||||||
|
lid_art_orientation == "usb_front" ? 90 :
|
||||||
|
lid_art_orientation == "usb_right" ? 180 : 270;
|
||||||
|
lid_art_safe_max_x = battery_enabled
|
||||||
|
? rocker_lid_center_x - rocker_cutout_size[0] / 2 - rocker_artwork_gap
|
||||||
|
: case_length - lid_art_margin;
|
||||||
|
lid_art_physical_size = [
|
||||||
|
lid_art_safe_max_x - lid_art_margin,
|
||||||
|
case_width - 2 * lid_art_margin
|
||||||
|
];
|
||||||
|
lid_art_center = [
|
||||||
|
lid_art_margin + lid_art_physical_size[0] / 2,
|
||||||
|
case_width / 2
|
||||||
|
];
|
||||||
|
lid_art_canvas_size = lid_art_rotation == 0 || lid_art_rotation == 180
|
||||||
|
? lid_art_physical_size
|
||||||
|
: [lid_art_physical_size[1], lid_art_physical_size[0]];
|
||||||
|
lid_art_available_size = lid_art_canvas_size;
|
||||||
|
lid_label_height = lid_label == "" ? 0 : lid_label_size;
|
||||||
|
lid_art_gap = lid_label != "" && lid_logo_enabled ? lid_label_logo_gap : 0;
|
||||||
|
logo_native_size = [30, 25.75];
|
||||||
|
logo_max_height = lid_art_available_size[1] - lid_label_height - lid_art_gap;
|
||||||
|
logo_scale = lid_logo_enabled ? min(
|
||||||
|
lid_art_available_size[0] / logo_native_size[0],
|
||||||
|
logo_max_height / logo_native_size[1]
|
||||||
|
) : 0;
|
||||||
|
logo_size = [logo_native_size[0] * logo_scale,
|
||||||
|
logo_native_size[1] * logo_scale];
|
||||||
|
lid_art_total_height = logo_size[1] + lid_art_gap + lid_label_height;
|
||||||
|
lid_label_center_y = -lid_art_total_height / 2 + lid_label_height / 2;
|
||||||
|
lid_logo_center_y = -lid_art_total_height / 2 + lid_label_height
|
||||||
|
+ lid_art_gap + logo_size[1] / 2;
|
||||||
|
|
||||||
|
tripod_center_x = case_length / 2 - combined_tripod_x_shift;
|
||||||
|
tripod_center_y = case_width / 2;
|
||||||
|
tripod_entry_radius = tripod_insert_outer_diameter / 2;
|
||||||
|
tripod_standoff_clearance = min([
|
||||||
|
for (mx = mount_x, my = mount_y)
|
||||||
|
sqrt(
|
||||||
|
pow(board_origin_x + mx - tripod_center_x, 2)
|
||||||
|
+ pow(board_origin_y + my - tripod_center_y, 2)
|
||||||
|
) - tripod_entry_radius - m25_boss_diameter / 2
|
||||||
|
]);
|
||||||
|
tripod_partition_clearance = partition_y
|
||||||
|
- (tripod_center_y + tripod_entry_radius);
|
||||||
|
|
||||||
battery_center_z = effective_floor + battery_radial_clearance
|
battery_center_z = effective_floor + battery_radial_clearance
|
||||||
+ battery_diameter / 2;
|
+ battery_diameter / 2;
|
||||||
switch_center_z = max(
|
|
||||||
effective_floor + switch_flange_diameter / 2 + 1,
|
|
||||||
battery_center_z
|
|
||||||
);
|
|
||||||
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
electronics_ceiling_z = top_pcb_z + profile_component_height + profile_top_gap;
|
||||||
battery_ceiling_z = battery_enabled
|
battery_ceiling_z = battery_enabled
|
||||||
? max(
|
? battery_center_z + battery_diameter / 2 + battery_radial_clearance
|
||||||
battery_center_z + battery_diameter / 2 + battery_radial_clearance,
|
|
||||||
switch_center_z + switch_flange_diameter / 2 + 1
|
|
||||||
)
|
|
||||||
: 0;
|
: 0;
|
||||||
body_height = max(electronics_ceiling_z, battery_ceiling_z);
|
rocker_ceiling_z = battery_enabled
|
||||||
|
? effective_floor + rocker_body_size[2] + fit_clearance
|
||||||
|
: 0;
|
||||||
|
body_height = max(electronics_ceiling_z, battery_ceiling_z, rocker_ceiling_z);
|
||||||
|
|
||||||
lid_boss_r = m25_boss_diameter / 2;
|
|
||||||
lid_boss_wall_overlap = 0.6;
|
|
||||||
lid_boss_xy = [
|
lid_boss_xy = [
|
||||||
[
|
[
|
||||||
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
wall_thickness + lid_boss_r - lid_boss_wall_overlap,
|
||||||
@@ -225,19 +303,121 @@ lid_boss_xy = [
|
|||||||
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
case_width - wall_thickness - lid_boss_r + lid_boss_wall_overlap
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
rocker_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(
|
||||||
|
abs(p[0] - rocker_installed_center_x)
|
||||||
|
- rocker_body_size[0] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
+ pow(max(
|
||||||
|
abs(p[1] - rocker_installed_center_y)
|
||||||
|
- rocker_body_size[1] / 2,
|
||||||
|
0
|
||||||
|
), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
rocker_relief_half_size = [
|
||||||
|
rocker_cutout_size[0] / 2,
|
||||||
|
rocker_cutout_size[1] / 2 + rocker_tab_land + rocker_tab_taper_length
|
||||||
|
];
|
||||||
|
rocker_relief_boss_clearance = min([
|
||||||
|
for (p = lid_boss_xy)
|
||||||
|
sqrt(
|
||||||
|
pow(max(abs(p[0] - rocker_lid_center_x)
|
||||||
|
- rocker_relief_half_size[0], 0), 2)
|
||||||
|
+ pow(max(abs(p[1] - rocker_lid_center_y)
|
||||||
|
- rocker_relief_half_size[1], 0), 2)
|
||||||
|
) - lid_boss_r
|
||||||
|
]);
|
||||||
|
|
||||||
assert(wing_count >= 1, "wing_count must be at least one");
|
assert(wing_count >= 1, "wing_count must be at least one");
|
||||||
assert(top_profile == "generic" || top_profile == "gps"
|
assert(top_profile == "generic" || top_profile == "gps"
|
||||||
|| top_profile == "oled128x64", "Unsupported top_profile");
|
|| top_profile == "oled128x64", "Unsupported top_profile");
|
||||||
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
assert(wall_thickness >= 1.2, "wall_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_partition_thickness >= 1.2,
|
||||||
|
"battery_partition_thickness is too small for 0.6 mm FDM");
|
||||||
|
assert(battery_board_clearance > 0,
|
||||||
|
"battery_board_clearance must be positive");
|
||||||
|
assert(rocker_tab_panel_thickness >= 1.2,
|
||||||
|
"rocker tab panel needs at least two 0.6 mm lines");
|
||||||
|
assert(rocker_tab_panel_thickness < lid_thickness,
|
||||||
|
"rocker tab panel must be thinner than the lid");
|
||||||
|
assert(rocker_tab_land > 0 && rocker_tab_taper_length > 0,
|
||||||
|
"rocker tab relief land and taper must be positive");
|
||||||
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
assert(lid_lip_thickness >= 1.2, "lid lip needs at least two 0.6 mm lines");
|
||||||
|
assert(lid_art_orientation == "usb_left"
|
||||||
|
|| lid_art_orientation == "usb_front"
|
||||||
|
|| lid_art_orientation == "usb_right"
|
||||||
|
|| lid_art_orientation == "usb_rear",
|
||||||
|
"Unsupported lid_art_orientation");
|
||||||
|
assert(!lid_artwork_enabled || top_profile != "oled128x64",
|
||||||
|
"Lid artwork is not supported with the OLED profile");
|
||||||
|
assert(!lid_artwork_enabled || lid_inlay_depth < lid_thickness,
|
||||||
|
"Lid inlay must be shallower than the lid");
|
||||||
|
assert(!lid_artwork_enabled
|
||||||
|
|| (lid_art_available_size[0] > 0 && lid_art_available_size[1] > 0),
|
||||||
|
"Lid artwork margin leaves no printable area");
|
||||||
|
assert(!lid_logo_enabled || logo_scale > 0,
|
||||||
|
"Lid logo has no printable area");
|
||||||
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
assert(m25_boss_diameter > m25_insert_hole_diameter + 2,
|
||||||
"M2.5 boss does not have enough radial material");
|
"M2.5 boss does not have enough radial material");
|
||||||
|
assert(m25_insert_outer_diameter >= m25_insert_hole_diameter,
|
||||||
|
"M2.5 insert entry must not be smaller than its pilot");
|
||||||
assert(!tripod_insert_enabled
|
assert(!tripod_insert_enabled
|
||||||
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
|| tripod_boss_diameter > tripod_insert_hole_diameter + 3,
|
||||||
"Tripod insert boss does not have enough radial material");
|
"Tripod insert boss does not have enough radial material");
|
||||||
assert(usb_opening_enabled || !usb_guide_enabled,
|
assert(usb_opening_enabled || !usb_guide_enabled,
|
||||||
"USB guide requires the USB opening");
|
"USB guide requires the USB opening");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z - usb_reserved_height / 2 >= effective_floor,
|
||||||
|
"USB opening or guide intersects the case floor");
|
||||||
|
assert(!usb_opening_enabled
|
||||||
|
|| usb_center_z + usb_reserved_height / 2
|
||||||
|
<= body_height - lid_lip_depth,
|
||||||
|
"USB opening or guide intersects the lid lip");
|
||||||
|
assert(!tripod_insert_enabled
|
||||||
|
|| tripod_insert_outer_diameter >= tripod_insert_hole_diameter,
|
||||||
|
"Tripod insert entry must not be smaller than its pilot");
|
||||||
|
assert(!tripod_insert_enabled || tripod_standoff_clearance >= 1.2,
|
||||||
|
"Tripod insert entry is too close to a Feather standoff");
|
||||||
|
assert(!battery_enabled || !tripod_insert_enabled
|
||||||
|
|| tripod_partition_clearance >= 1,
|
||||||
|
"Tripod insert entry is too close to the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= board_origin_x + feather_length + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather PCB");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y + rocker_body_size[1] / 2
|
||||||
|
<= partition_y - fit_clearance,
|
||||||
|
"Rocker body overlaps the battery partition");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x - rocker_body_size[0] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the front lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_x + rocker_body_size[0] / 2
|
||||||
|
<= case_length - wall_thickness - lid_lip_thickness - fit_clearance,
|
||||||
|
"Rocker body overlaps the rear lid lip");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_installed_center_y - rocker_body_size[1] / 2
|
||||||
|
>= wall_thickness + lid_lip_thickness + fit_clearance,
|
||||||
|
"Rocker body overlaps the Feather-side lid lip");
|
||||||
|
assert(!battery_enabled || rocker_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker body overlaps a lid boss");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| rocker_relief_boss_clearance >= fit_clearance - eps,
|
||||||
|
"Rocker tab relief overlaps a lid boss scallop");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| abs(
|
||||||
|
case_width - rocker_lid_center_y - rocker_installed_center_y
|
||||||
|
) < eps,
|
||||||
|
"Rocker lid cutout does not mirror to the installed switch location");
|
||||||
|
assert(!battery_enabled
|
||||||
|
|| body_height - rocker_body_size[2] >= effective_floor,
|
||||||
|
"Rocker body intersects the case floor");
|
||||||
|
|
||||||
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
||||||
function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4];
|
function cutout_height(entry) = len(entry) > 5 ? entry[5] : entry[4];
|
||||||
@@ -297,8 +477,8 @@ module rounded_box(size, radius) {
|
|||||||
rounded_rect_2d([size[0], size[1]], radius);
|
rounded_rect_2d([size[0], size[1]], radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
module insert_pocket(diameter, depth, entry_z, downward = true) {
|
module insert_pocket(diameter, entry_diameter, depth, entry_z, downward = true) {
|
||||||
bore_d = diameter + hole_compensation;
|
bore_d = diameter;
|
||||||
if (downward) {
|
if (downward) {
|
||||||
translate([0, 0, entry_z - depth])
|
translate([0, 0, entry_z - depth])
|
||||||
cylinder(h = depth + eps, d = bore_d);
|
cylinder(h = depth + eps, d = bore_d);
|
||||||
@@ -306,7 +486,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d,
|
d1 = bore_d,
|
||||||
d2 = bore_d + 2 * insert_entry_chamfer
|
d2 = entry_diameter
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
@@ -314,7 +494,7 @@ module insert_pocket(diameter, depth, entry_z, downward = true) {
|
|||||||
translate([0, 0, entry_z - eps])
|
translate([0, 0, entry_z - eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = insert_entry_chamfer + eps,
|
h = insert_entry_chamfer + eps,
|
||||||
d1 = bore_d + 2 * insert_entry_chamfer,
|
d1 = entry_diameter,
|
||||||
d2 = bore_d
|
d2 = bore_d
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -457,20 +637,27 @@ module battery_cradle() {
|
|||||||
battery_end_x - 4
|
battery_end_x - 4
|
||||||
];
|
];
|
||||||
|
|
||||||
// Board/battery partition, with a lead-routing notch near the JST end.
|
// One open-topped front passage provides JST plug access and enough room
|
||||||
|
// to route the battery lead beneath the Feather PCB beside the lid boss.
|
||||||
difference() {
|
difference() {
|
||||||
translate([wall_thickness, partition_y, effective_floor - eps])
|
translate([wall_thickness, partition_y, effective_floor - eps])
|
||||||
cube([
|
cube([
|
||||||
battery_end_x - wall_thickness + battery_end_clearance,
|
battery_end_x - wall_thickness + battery_end_clearance,
|
||||||
wall_thickness,
|
battery_partition_thickness,
|
||||||
partition_height - effective_floor + eps
|
partition_height - effective_floor + eps
|
||||||
]);
|
]);
|
||||||
translate([
|
translate([
|
||||||
board_origin_x + 6.5,
|
wall_thickness - eps,
|
||||||
partition_y - eps,
|
partition_y - eps,
|
||||||
effective_floor + 1.5
|
effective_floor + 1.5
|
||||||
])
|
])
|
||||||
cube([9, wall_thickness + 2 * eps, 6]);
|
cube([
|
||||||
|
board_origin_x + jst_center_xy[0]
|
||||||
|
+ (jst_body_size[0] + 2 * jst_relief_clearance) / 2
|
||||||
|
- wall_thickness + 2 * eps,
|
||||||
|
battery_partition_thickness + 2 * eps,
|
||||||
|
partition_height - effective_floor - 1.5 + eps
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three low curved ribs cradle the protected cylindrical pack.
|
// Three low curved ribs cradle the protected cylindrical pack.
|
||||||
@@ -501,29 +688,16 @@ module battery_cradle() {
|
|||||||
battery_end_x + battery_end_clearance
|
battery_end_x + battery_end_clearance
|
||||||
])
|
])
|
||||||
translate([
|
translate([
|
||||||
x,
|
x - eps,
|
||||||
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
battery_center_y - battery_diameter / 2 - battery_radial_clearance,
|
||||||
effective_floor - eps
|
effective_floor - eps
|
||||||
])
|
])
|
||||||
cube([
|
cube([
|
||||||
wall_thickness,
|
wall_thickness + 2 * eps,
|
||||||
battery_diameter + 2 * battery_radial_clearance,
|
battery_diameter + 2 * battery_radial_clearance,
|
||||||
battery_diameter / 2 + 1 + eps
|
battery_diameter / 2 + 1 + eps
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Rear cross-strip separates the side-mounted switch body from the cell.
|
|
||||||
translate([
|
|
||||||
battery_end_x + battery_end_clearance,
|
|
||||||
partition_y,
|
|
||||||
effective_floor - eps
|
|
||||||
])
|
|
||||||
cube([
|
|
||||||
case_length - wall_thickness - battery_end_x - battery_end_clearance,
|
|
||||||
case_width - wall_thickness - partition_y,
|
|
||||||
min(body_height - effective_floor, switch_center_z
|
|
||||||
+ switch_body_diameter / 2 + switch_wire_allowance
|
|
||||||
- effective_floor) + eps
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module body_positive() {
|
module body_positive() {
|
||||||
@@ -542,7 +716,7 @@ module body_positive() {
|
|||||||
if (battery_enabled)
|
if (battery_enabled)
|
||||||
battery_cradle();
|
battery_cradle();
|
||||||
if (tripod_insert_enabled)
|
if (tripod_insert_enabled)
|
||||||
translate([case_length / 2, case_width / 2, 0])
|
translate([tripod_center_x, tripod_center_y, 0])
|
||||||
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
cylinder(h = effective_floor, d = tripod_boss_diameter);
|
||||||
if (usb_guide_active)
|
if (usb_guide_active)
|
||||||
usb_guide_shell();
|
usb_guide_shell();
|
||||||
@@ -555,6 +729,7 @@ module body_cutouts() {
|
|||||||
translate([board_origin_x + mx, board_origin_y + my, 0])
|
translate([board_origin_x + mx, board_origin_y + my, 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
board_bottom_z
|
board_bottom_z
|
||||||
);
|
);
|
||||||
@@ -564,6 +739,7 @@ module body_cutouts() {
|
|||||||
translate([p[0], p[1], 0])
|
translate([p[0], p[1], 0])
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
m25_insert_hole_diameter,
|
m25_insert_hole_diameter,
|
||||||
|
m25_insert_outer_diameter,
|
||||||
m25_insert_length + insert_melt_relief,
|
m25_insert_length + insert_melt_relief,
|
||||||
body_height
|
body_height
|
||||||
);
|
);
|
||||||
@@ -579,29 +755,8 @@ module body_cutouts() {
|
|||||||
usb_opening_radius
|
usb_opening_radius
|
||||||
);
|
);
|
||||||
|
|
||||||
if (battery_enabled) {
|
|
||||||
face_circle_cutout(
|
|
||||||
"right",
|
|
||||||
switch_center_x,
|
|
||||||
switch_center_z,
|
|
||||||
switch_hole_diameter + switch_hole_compensation
|
|
||||||
);
|
|
||||||
// Clear the switch body and its prewired terminals through the rear bay.
|
|
||||||
translate([
|
|
||||||
switch_center_x,
|
|
||||||
case_width - wall_thickness + eps,
|
|
||||||
switch_center_z
|
|
||||||
])
|
|
||||||
rotate([90, 0, 0])
|
|
||||||
cylinder(
|
|
||||||
h = switch_body_length + switch_wire_allowance
|
|
||||||
+ wall_thickness + eps,
|
|
||||||
d = switch_body_diameter + 2 * fit_clearance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tripod_insert_enabled) {
|
if (tripod_insert_enabled) {
|
||||||
translate([case_length / 2, case_width / 2, 0]) {
|
translate([tripod_center_x, tripod_center_y, 0]) {
|
||||||
translate([0, 0, -eps])
|
translate([0, 0, -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = effective_floor + 2 * eps,
|
h = effective_floor + 2 * eps,
|
||||||
@@ -609,6 +764,7 @@ module body_cutouts() {
|
|||||||
);
|
);
|
||||||
insert_pocket(
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter,
|
tripod_insert_hole_diameter,
|
||||||
|
tripod_insert_outer_diameter,
|
||||||
tripod_insert_length + insert_melt_relief,
|
tripod_insert_length + insert_melt_relief,
|
||||||
effective_floor
|
effective_floor
|
||||||
);
|
);
|
||||||
@@ -702,6 +858,13 @@ module oled_button_lid_cutout(xy) {
|
|||||||
module lid_cutouts() {
|
module lid_cutouts() {
|
||||||
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
// Through holes plus shallow counterbores for flush M2.5 socket heads.
|
||||||
for (p = lid_boss_xy) {
|
for (p = lid_boss_xy) {
|
||||||
|
// Scallop only the locating lip so it can pass over the full-height
|
||||||
|
// insert boss while leaving the lid plate supported by the boss top.
|
||||||
|
translate([p[0], p[1], lid_thickness - eps])
|
||||||
|
cylinder(
|
||||||
|
h = lid_lip_depth + 2 * eps,
|
||||||
|
d = m25_boss_diameter + 2 * fit_clearance
|
||||||
|
);
|
||||||
translate([p[0], p[1], -eps])
|
translate([p[0], p[1], -eps])
|
||||||
cylinder(
|
cylinder(
|
||||||
h = lid_thickness + lid_lip_depth + 2 * eps,
|
h = lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
@@ -714,6 +877,19 @@ module lid_cutouts() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (battery_enabled) {
|
||||||
|
translate([rocker_lid_center_x, rocker_lid_center_y, -eps])
|
||||||
|
centered_rounded_prism(
|
||||||
|
rocker_cutout_size,
|
||||||
|
lid_thickness + lid_lip_depth + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[rocker_lid_center_x, rocker_lid_center_y],
|
||||||
|
rocker_cutout_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (top_profile == "oled128x64") {
|
if (top_profile == "oled128x64") {
|
||||||
beveled_oled_window();
|
beveled_oled_window();
|
||||||
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
if (button_a) oled_button_lid_cutout(oled_button_a_xy);
|
||||||
@@ -723,13 +899,90 @@ module lid_cutouts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module lid() {
|
// Thin the horizontal top/bottom edges of the upright switch opening where the
|
||||||
|
// spring tabs latch. Each full-width land overlaps the rounded opening by its
|
||||||
|
// corner radius, then extends outward before tapering to the surrounding lid.
|
||||||
|
module rocker_tab_reliefs(center, opening_size) {
|
||||||
|
relief_depth = lid_thickness - rocker_tab_panel_thickness;
|
||||||
|
opening_overlap = rocker_corner_radius;
|
||||||
|
strip = 0.02;
|
||||||
|
for (side = [-1, 1]) {
|
||||||
|
edge_y = center[1] + side * opening_size[1] / 2;
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y - (side < 0 ? rocker_tab_land : opening_overlap),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], rocker_tab_land + opening_overlap,
|
||||||
|
relief_depth + eps]);
|
||||||
|
|
||||||
|
hull() {
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * rocker_tab_land - (side < 0 ? strip : 0),
|
||||||
|
rocker_tab_panel_thickness
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, relief_depth + eps]);
|
||||||
|
translate([
|
||||||
|
center[0] - opening_size[0] / 2,
|
||||||
|
edge_y + side * (rocker_tab_land + rocker_tab_taper_length)
|
||||||
|
- (side < 0 ? strip : 0),
|
||||||
|
lid_thickness - strip
|
||||||
|
])
|
||||||
|
cube([opening_size[0], strip, strip + eps]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_nominal_2d() {
|
||||||
|
translate(lid_art_center)
|
||||||
|
rotate(lid_art_rotation)
|
||||||
|
// The lid is modeled exterior-face-down; mirror the artwork so it
|
||||||
|
// reads normally after the lid is turned into service position.
|
||||||
|
mirror([0, 1, 0]) {
|
||||||
|
if (lid_logo_enabled)
|
||||||
|
translate([0, lid_logo_center_y])
|
||||||
|
resize(logo_size)
|
||||||
|
import(logo_file, center = true);
|
||||||
|
if (lid_label != "")
|
||||||
|
translate([0, lid_label_center_y])
|
||||||
|
text(
|
||||||
|
lid_label,
|
||||||
|
size = lid_label_size,
|
||||||
|
halign = "center",
|
||||||
|
valign = "center",
|
||||||
|
font = "DejaVu Sans:style=Bold"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_artwork_2d(inset = 0) {
|
||||||
|
offset(delta = inset)
|
||||||
|
lid_artwork_nominal_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid_base() {
|
||||||
difference() {
|
difference() {
|
||||||
lid_plate_positive();
|
lid_plate_positive();
|
||||||
lid_cutouts();
|
lid_cutouts();
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
translate([0, 0, -eps])
|
||||||
|
linear_extrude(height = lid_inlay_depth + eps)
|
||||||
|
lid_artwork_2d(lid_inlay_fit_clearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module lid_accent() {
|
||||||
|
if (lid_artwork_enabled)
|
||||||
|
linear_extrude(height = lid_inlay_depth)
|
||||||
|
lid_artwork_2d();
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
color([0.92, 0.92, 0.95]) lid_base();
|
||||||
|
color([0.95, 0.42, 0.08]) lid_accent();
|
||||||
|
}
|
||||||
|
|
||||||
module one_oled_button() {
|
module one_oled_button() {
|
||||||
recess_depth = button_flange_thickness + button_travel;
|
recess_depth = button_flange_thickness + button_travel;
|
||||||
stem_height = lid_thickness - recess_depth + button_projection;
|
stem_height = lid_thickness - recess_depth + button_projection;
|
||||||
@@ -769,6 +1022,13 @@ module reference_board() {
|
|||||||
color([0.05, 0.25, 0.55, 0.75])
|
color([0.05, 0.25, 0.55, 0.75])
|
||||||
translate([board_origin_x, board_origin_y, board_bottom_z])
|
translate([board_origin_x, board_origin_y, board_bottom_z])
|
||||||
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
rounded_box([feather_length, feather_width, board_thickness], 2.54);
|
||||||
|
color([0.92, 0.92, 0.86, 0.9])
|
||||||
|
translate([
|
||||||
|
board_origin_x + jst_center_xy[0] - jst_body_size[0] / 2,
|
||||||
|
board_origin_y + jst_center_xy[1] - 4.5,
|
||||||
|
board_top_z
|
||||||
|
])
|
||||||
|
cube(jst_body_size);
|
||||||
color([0.12, 0.45, 0.75, 0.45])
|
color([0.12, 0.45, 0.75, 0.45])
|
||||||
for (i = [1 : wing_count])
|
for (i = [1 : wing_count])
|
||||||
translate([
|
translate([
|
||||||
@@ -835,19 +1095,20 @@ module reference_battery_and_switch() {
|
|||||||
translate([battery_start_x, battery_center_y, battery_center_z])
|
translate([battery_start_x, battery_center_y, battery_center_z])
|
||||||
rotate([0, 90, 0])
|
rotate([0, 90, 0])
|
||||||
cylinder(h = battery_length, d = battery_diameter);
|
cylinder(h = battery_length, d = battery_diameter);
|
||||||
color([0.8, 0.15, 0.1, 0.65])
|
color([0.08, 0.08, 0.08, 0.85])
|
||||||
translate([
|
translate([
|
||||||
switch_center_x,
|
rocker_installed_center_x - rocker_body_size[0] / 2,
|
||||||
case_width - wall_thickness,
|
rocker_installed_center_y - rocker_body_size[1] / 2,
|
||||||
switch_center_z
|
body_height + exploded_view - rocker_body_size[2]
|
||||||
])
|
])
|
||||||
rotate([90, 0, 0]) {
|
cube(rocker_body_size);
|
||||||
cylinder(h = switch_body_length, d = switch_body_diameter);
|
color([0.12, 0.12, 0.12, 0.95])
|
||||||
translate([0, 0, -1.5])
|
translate([
|
||||||
cylinder(h = 1.5, d = switch_flange_diameter);
|
rocker_installed_center_x - rocker_cutout_size[0] / 2,
|
||||||
translate([0, 0, -3])
|
rocker_installed_center_y - rocker_cutout_size[1] / 2,
|
||||||
cylinder(h = 3, d = switch_cap_diameter);
|
body_height + exploded_view + lid_thickness
|
||||||
}
|
])
|
||||||
|
cube([rocker_cutout_size[0], rocker_cutout_size[1], 2.5]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,13 +1126,9 @@ module assembly() {
|
|||||||
lid();
|
lid();
|
||||||
}
|
}
|
||||||
|
|
||||||
module coupon_hole(x, y, diameter, depth) {
|
|
||||||
translate([x, y, -eps]) cylinder(h = depth + 2 * eps, d = diameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
module insert_coupon() {
|
module insert_coupon() {
|
||||||
spacing = 15;
|
spacing = 15;
|
||||||
coupon_length = spacing * len(coupon_steps) + 8;
|
coupon_length = spacing * len(tripod_coupon_steps) + 8;
|
||||||
coupon_width = 32;
|
coupon_width = 32;
|
||||||
coupon_height = max(
|
coupon_height = max(
|
||||||
m25_insert_length + insert_melt_relief + 1.2,
|
m25_insert_length + insert_melt_relief + 1.2,
|
||||||
@@ -879,17 +1136,21 @@ module insert_coupon() {
|
|||||||
);
|
);
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1]) {
|
for (i = [0 : len(tripod_coupon_steps) - 1]) {
|
||||||
x = 7 + i * spacing;
|
x = 7 + i * spacing;
|
||||||
coupon_hole(
|
translate([x, 8, 0])
|
||||||
x, 8,
|
insert_pocket(
|
||||||
m25_insert_hole_diameter + coupon_steps[i],
|
m25_insert_hole_diameter + m25_coupon_steps[i],
|
||||||
m25_insert_length + insert_melt_relief
|
m25_insert_outer_diameter,
|
||||||
|
m25_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
coupon_hole(
|
translate([x, 23, 0])
|
||||||
x, 23,
|
insert_pocket(
|
||||||
tripod_insert_hole_diameter + coupon_steps[i],
|
tripod_insert_hole_diameter + tripod_coupon_steps[i],
|
||||||
tripod_insert_length + insert_melt_relief
|
tripod_insert_outer_diameter,
|
||||||
|
tripod_insert_length + insert_melt_relief,
|
||||||
|
coupon_height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -898,16 +1159,23 @@ module insert_coupon() {
|
|||||||
module switch_coupon() {
|
module switch_coupon() {
|
||||||
spacing = 18;
|
spacing = 18;
|
||||||
coupon_length = spacing * len(coupon_steps) + 6;
|
coupon_length = spacing * len(coupon_steps) + 6;
|
||||||
coupon_width = 24;
|
coupon_width = rocker_cutout_size[1] + 6;
|
||||||
coupon_height = wall_thickness;
|
coupon_height = lid_thickness;
|
||||||
difference() {
|
difference() {
|
||||||
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
rounded_box([coupon_length, coupon_width, coupon_height], 3);
|
||||||
for (i = [0 : len(coupon_steps) - 1])
|
for (i = [0 : len(coupon_steps) - 1]) {
|
||||||
translate([6 + i * spacing, coupon_width / 2, -eps])
|
translate([6 + i * spacing, coupon_width / 2, -eps])
|
||||||
cylinder(
|
centered_rounded_prism(
|
||||||
h = coupon_height + 2 * eps,
|
rocker_cutout_size
|
||||||
d = switch_hole_diameter + coupon_steps[i]
|
+ [coupon_steps[i], coupon_steps[i]],
|
||||||
|
coupon_height + 2 * eps,
|
||||||
|
rocker_corner_radius
|
||||||
);
|
);
|
||||||
|
rocker_tab_reliefs(
|
||||||
|
[6 + i * spacing, coupon_width / 2],
|
||||||
|
rocker_cutout_size + [coupon_steps[i], coupon_steps[i]]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,6 +1221,10 @@ else if (part == "body")
|
|||||||
body();
|
body();
|
||||||
else if (part == "lid")
|
else if (part == "lid")
|
||||||
lid();
|
lid();
|
||||||
|
else if (part == "lid_base")
|
||||||
|
lid_base();
|
||||||
|
else if (part == "lid_accent")
|
||||||
|
lid_accent();
|
||||||
else if (part == "buttons")
|
else if (part == "buttons")
|
||||||
buttons();
|
buttons();
|
||||||
else if (part == "insert_coupon")
|
else if (part == "insert_coupon")
|
||||||
|
|||||||
@@ -23,4 +23,7 @@ module FeatherCaseWizard
|
|||||||
CASES_ROOT = ROOT.join("cases").freeze
|
CASES_ROOT = ROOT.join("cases").freeze
|
||||||
MODEL_SOURCE = ROOT.join("feather_case.scad").freeze
|
MODEL_SOURCE = ROOT.join("feather_case.scad").freeze
|
||||||
RENDERER_SOURCE = ROOT.join("tools/render_stl_previews.py").freeze
|
RENDERER_SOURCE = ROOT.join("tools/render_stl_previews.py").freeze
|
||||||
|
MULTICOLOR_RENDERER_SOURCE = ROOT.join("tools/render_multicolor_lid.py").freeze
|
||||||
|
MULTICOLOR_PACKAGER_SOURCE = ROOT.join("tools/package_multimaterial_3mf.py").freeze
|
||||||
|
LOGO_SOURCE = ROOT.join("assets/NME-fixed.svg").freeze
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ module FeatherCaseWizard
|
|||||||
FACES = %w[front rear left right].freeze
|
FACES = %w[front rear left right].freeze
|
||||||
SHAPES = %w[circle rect roundrect slot].freeze
|
SHAPES = %w[circle rect roundrect slot].freeze
|
||||||
COUPONS = %w[insert_coupon switch_coupon fit_coupon].freeze
|
COUPONS = %w[insert_coupon switch_coupon fit_coupon].freeze
|
||||||
|
LID_ART_ORIENTATIONS = %w[usb_left usb_front usb_right usb_rear].freeze
|
||||||
|
LEGACY_SWITCH_FIELDS = %w[switch_hole_diameter switch_hole_compensation
|
||||||
|
switch_cap_diameter switch_flange_diameter
|
||||||
|
switch_body_length switch_body_diameter
|
||||||
|
switch_wire_allowance].freeze
|
||||||
|
|
||||||
DEFAULT_PARAMETERS = {
|
DEFAULT_PARAMETERS = {
|
||||||
"top_profile" => "generic",
|
"top_profile" => "generic",
|
||||||
@@ -38,20 +43,25 @@ module FeatherCaseWizard
|
|||||||
"battery_diameter" => 18.0,
|
"battery_diameter" => 18.0,
|
||||||
"battery_radial_clearance" => 0.6,
|
"battery_radial_clearance" => 0.6,
|
||||||
"battery_end_clearance" => 1.0,
|
"battery_end_clearance" => 1.0,
|
||||||
"switch_hole_diameter" => 12.0,
|
"battery_partition_thickness" => 1.8,
|
||||||
"switch_hole_compensation" => 0.3,
|
"battery_board_clearance" => 3.0,
|
||||||
"switch_cap_diameter" => 8.0,
|
"rocker_cutout_size" => [12.9, 19.4],
|
||||||
"switch_flange_diameter" => 16.2,
|
"rocker_corner_radius" => 0.8,
|
||||||
"switch_body_length" => 24.0,
|
"rocker_body_size" => [15.0, 21.0, 21.0],
|
||||||
"switch_body_diameter" => 12.0,
|
"rocker_artwork_gap" => 2.0,
|
||||||
"switch_wire_allowance" => 3.0,
|
"rocker_partition_offset" => 2.0,
|
||||||
|
"rocker_tab_panel_thickness" => 2.0,
|
||||||
|
"rocker_tab_land" => 2.0,
|
||||||
|
"rocker_tab_taper_length" => 0.6,
|
||||||
"tripod_insert_enabled" => false,
|
"tripod_insert_enabled" => false,
|
||||||
"tripod_insert_length" => 6.4,
|
"tripod_insert_length" => 5.0,
|
||||||
"tripod_insert_hole_diameter" => 8.8,
|
"tripod_insert_hole_diameter" => 7.2,
|
||||||
|
"tripod_insert_outer_diameter" => 8.0,
|
||||||
"tripod_thread_clearance_diameter" => 6.8,
|
"tripod_thread_clearance_diameter" => 6.8,
|
||||||
"tripod_boss_diameter" => 14.0,
|
"tripod_boss_diameter" => 14.0,
|
||||||
"m25_insert_length" => 4.0,
|
"m25_insert_length" => 4.0,
|
||||||
"m25_insert_hole_diameter" => 3.7,
|
"m25_insert_hole_diameter" => 3.1,
|
||||||
|
"m25_insert_outer_diameter" => 3.5,
|
||||||
"m25_screw_clearance" => 2.8,
|
"m25_screw_clearance" => 2.8,
|
||||||
"m25_boss_diameter" => 7.0,
|
"m25_boss_diameter" => 7.0,
|
||||||
"insert_entry_chamfer" => 0.6,
|
"insert_entry_chamfer" => 0.6,
|
||||||
@@ -76,10 +86,18 @@ module FeatherCaseWizard
|
|||||||
"usb_connector_overhang" => 2.0,
|
"usb_connector_overhang" => 2.0,
|
||||||
"usb_guide_wall_thickness" => 1.2,
|
"usb_guide_wall_thickness" => 1.2,
|
||||||
"usb_guide_end_gap" => 1.0,
|
"usb_guide_end_gap" => 1.0,
|
||||||
"usb_opening_width" => 12.5,
|
"lid_label" => "",
|
||||||
"usb_opening_height" => 7.0,
|
"lid_logo_enabled" => false,
|
||||||
|
"lid_art_orientation" => "usb_left",
|
||||||
|
"lid_inlay_depth" => 0.6,
|
||||||
|
"lid_inlay_fit_clearance" => 0.08,
|
||||||
|
"lid_label_size" => 5.0,
|
||||||
|
"lid_label_logo_gap" => 2.0,
|
||||||
|
"lid_art_margin" => 8.5,
|
||||||
|
"usb_opening_width" => 13.5,
|
||||||
|
"usb_opening_height" => 8.0,
|
||||||
"usb_opening_radius" => 1.5,
|
"usb_opening_radius" => 1.5,
|
||||||
"usb_vertical_offset" => 0.0
|
"usb_vertical_offset" => 0.5
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
ADVANCED_FIELDS = {
|
ADVANCED_FIELDS = {
|
||||||
@@ -87,19 +105,27 @@ module FeatherCaseWizard
|
|||||||
gps_antenna_height gps_top_gap oled_component_height
|
gps_antenna_height gps_top_gap oled_component_height
|
||||||
oled_display_height oled_face_gap],
|
oled_display_height oled_face_gap],
|
||||||
"Shell and fit" => %w[wall_thickness floor_thickness lid_thickness corner_radius
|
"Shell and fit" => %w[wall_thickness floor_thickness lid_thickness corner_radius
|
||||||
board_bottom_clearance board_side_clearance lid_lip_depth
|
board_bottom_clearance board_side_clearance board_rear_clearance
|
||||||
lid_lip_thickness fit_clearance cutout_min_web hole_compensation
|
usb_board_wall_gap lid_lip_depth lid_lip_thickness fit_clearance
|
||||||
lid_screw_head_diameter lid_screw_head_height],
|
cutout_min_web hole_compensation lid_screw_head_diameter
|
||||||
|
lid_screw_head_height],
|
||||||
|
"Lid artwork" => %w[lid_inlay_depth lid_inlay_fit_clearance lid_label_size
|
||||||
|
lid_label_logo_gap lid_art_margin],
|
||||||
"M2.5 inserts" => %w[m25_insert_length m25_insert_hole_diameter
|
"M2.5 inserts" => %w[m25_insert_length m25_insert_hole_diameter
|
||||||
|
m25_insert_outer_diameter
|
||||||
m25_screw_clearance m25_boss_diameter
|
m25_screw_clearance m25_boss_diameter
|
||||||
insert_entry_chamfer insert_melt_relief],
|
insert_entry_chamfer insert_melt_relief],
|
||||||
"Tripod insert" => %w[tripod_insert_length tripod_insert_hole_diameter
|
"Tripod insert" => %w[tripod_insert_length tripod_insert_hole_diameter
|
||||||
|
tripod_insert_outer_diameter
|
||||||
tripod_thread_clearance_diameter tripod_boss_diameter],
|
tripod_thread_clearance_diameter tripod_boss_diameter],
|
||||||
"Battery and switch" => %w[battery_length battery_diameter battery_radial_clearance
|
"Battery and switch" => %w[battery_length battery_diameter battery_radial_clearance
|
||||||
battery_end_clearance switch_hole_diameter
|
battery_end_clearance battery_partition_thickness
|
||||||
switch_hole_compensation switch_cap_diameter
|
battery_board_clearance
|
||||||
switch_flange_diameter switch_body_length
|
rocker_cutout_size rocker_corner_radius
|
||||||
switch_body_diameter switch_wire_allowance],
|
rocker_body_size rocker_artwork_gap
|
||||||
|
rocker_partition_offset
|
||||||
|
rocker_tab_panel_thickness rocker_tab_land
|
||||||
|
rocker_tab_taper_length],
|
||||||
"USB-C opening" => %w[usb_opening_width usb_opening_height
|
"USB-C opening" => %w[usb_opening_width usb_opening_height
|
||||||
usb_opening_radius usb_vertical_offset usb_connector_overhang
|
usb_opening_radius usb_vertical_offset usb_connector_overhang
|
||||||
usb_guide_wall_thickness usb_guide_end_gap],
|
usb_guide_wall_thickness usb_guide_end_gap],
|
||||||
@@ -158,6 +184,7 @@ module FeatherCaseWizard
|
|||||||
|
|
||||||
def printable_parts
|
def printable_parts
|
||||||
parts = %w[body lid]
|
parts = %w[body lid]
|
||||||
|
parts.concat(%w[lid_base lid_accent]) if lid_artwork?
|
||||||
if parameters["top_profile"] == "oled128x64" &&
|
if parameters["top_profile"] == "oled128x64" &&
|
||||||
%w[button_a button_b button_c button_reset].any? { |key| parameters[key] }
|
%w[button_a button_b button_c button_reset].any? { |key| parameters[key] }
|
||||||
parts << "buttons"
|
parts << "buttons"
|
||||||
@@ -165,6 +192,10 @@ module FeatherCaseWizard
|
|||||||
parts + outputs["coupons"]
|
parts + outputs["coupons"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lid_artwork?
|
||||||
|
!parameters["lid_label"].empty? || parameters["lid_logo_enabled"]
|
||||||
|
end
|
||||||
|
|
||||||
def model_parameters
|
def model_parameters
|
||||||
parameters.merge("extra_cutouts" => scad_cutouts)
|
parameters.merge("extra_cutouts" => scad_cutouts)
|
||||||
end
|
end
|
||||||
@@ -179,7 +210,10 @@ module FeatherCaseWizard
|
|||||||
p["m25_insert_length"] + p["insert_melt_relief"] + 0.6
|
p["m25_insert_length"] + p["insert_melt_relief"] + 0.6
|
||||||
].max
|
].max
|
||||||
board_origin_x = p["wall_thickness"] + p["usb_board_wall_gap"]
|
board_origin_x = p["wall_thickness"] + p["usb_board_wall_gap"]
|
||||||
board_origin_y = p["wall_thickness"] + p["board_side_clearance"]
|
combined_tripod_board_gutter = p["battery_enabled"] && p["tripod_insert_enabled"] ?
|
||||||
|
2.4 : 0.0
|
||||||
|
board_origin_y = p["wall_thickness"] + p["board_side_clearance"] +
|
||||||
|
combined_tripod_board_gutter
|
||||||
board_top_z = effective_floor + effective_board_clearance + 1.6
|
board_top_z = effective_floor + effective_board_clearance + 1.6
|
||||||
top_pcb_z = board_top_z + (
|
top_pcb_z = board_top_z + (
|
||||||
p["stack_height_override"].positive? ?
|
p["stack_height_override"].positive? ?
|
||||||
@@ -198,41 +232,41 @@ module FeatherCaseWizard
|
|||||||
plain_width = board_origin_y + 22.86 + p["board_side_clearance"] +
|
plain_width = board_origin_y + 22.86 + p["board_side_clearance"] +
|
||||||
p["wall_thickness"]
|
p["wall_thickness"]
|
||||||
if p["battery_enabled"]
|
if p["battery_enabled"]
|
||||||
partition_y = board_origin_y + 22.86 + 1
|
partition_y = board_origin_y + 22.86 + p["battery_board_clearance"]
|
||||||
battery_y = partition_y + p["wall_thickness"] +
|
battery_y = partition_y + p["battery_partition_thickness"] +
|
||||||
p["battery_radial_clearance"] + p["battery_diameter"] / 2
|
p["battery_radial_clearance"] + p["battery_diameter"] / 2
|
||||||
battery_start_x = board_origin_x + p["battery_end_clearance"]
|
lid_boss_wall_overlap = 0.6
|
||||||
|
lid_boss_front_x = p["wall_thickness"] + p["m25_boss_diameter"] / 2 -
|
||||||
|
lid_boss_wall_overlap
|
||||||
|
battery_start_x = lid_boss_front_x + p["m25_boss_diameter"] / 2 +
|
||||||
|
p["battery_end_clearance"]
|
||||||
battery_end_x = battery_start_x + p["battery_length"]
|
battery_end_x = battery_start_x + p["battery_length"]
|
||||||
switch_x = battery_end_x + [
|
battery_length_required = battery_end_x + p["battery_end_clearance"] +
|
||||||
p["switch_body_diameter"] / 2 + 0.8,
|
p["wall_thickness"] + p["m25_boss_diameter"] -
|
||||||
p["switch_flange_diameter"] / 2 + 0.5
|
lid_boss_wall_overlap
|
||||||
].max
|
length = [plain_length, battery_length_required].max
|
||||||
length = [plain_length, switch_x + p["switch_flange_diameter"] / 2 +
|
|
||||||
1.2 + p["wall_thickness"]].max
|
|
||||||
width = [plain_width, battery_y + p["battery_diameter"] / 2 +
|
width = [plain_width, battery_y + p["battery_diameter"] / 2 +
|
||||||
p["battery_radial_clearance"] + p["wall_thickness"]].max
|
p["battery_radial_clearance"] + p["wall_thickness"]].max
|
||||||
battery_z = effective_floor + p["battery_radial_clearance"] +
|
battery_z = effective_floor + p["battery_radial_clearance"] +
|
||||||
p["battery_diameter"] / 2
|
p["battery_diameter"] / 2
|
||||||
switch_z = [
|
battery_ceiling = battery_z + p["battery_diameter"] / 2 +
|
||||||
effective_floor + p["switch_flange_diameter"] / 2 + 1,
|
p["battery_radial_clearance"]
|
||||||
battery_z
|
rocker_ceiling = effective_floor + p["rocker_body_size"][2] +
|
||||||
].max
|
p["fit_clearance"]
|
||||||
battery_ceiling = [
|
|
||||||
battery_z + p["battery_diameter"] / 2 + p["battery_radial_clearance"],
|
|
||||||
switch_z + p["switch_flange_diameter"] / 2 + 1
|
|
||||||
].max
|
|
||||||
else
|
else
|
||||||
length = plain_length
|
length = plain_length
|
||||||
width = plain_width
|
width = plain_width
|
||||||
battery_ceiling = 0
|
battery_ceiling = 0
|
||||||
|
rocker_ceiling = 0
|
||||||
end
|
end
|
||||||
{
|
{
|
||||||
"length" => length,
|
"length" => length,
|
||||||
"width" => width,
|
"width" => width,
|
||||||
"height" => [top_pcb_z + component_height + top_gap, battery_ceiling].max,
|
"height" => [top_pcb_z + component_height + top_gap,
|
||||||
|
battery_ceiling, rocker_ceiling].max,
|
||||||
"floor" => effective_floor,
|
"floor" => effective_floor,
|
||||||
"lip_bottom" => [top_pcb_z + component_height + top_gap, battery_ceiling].max -
|
"lip_bottom" => [top_pcb_z + component_height + top_gap,
|
||||||
p["lid_lip_depth"]
|
battery_ceiling, rocker_ceiling].max - p["lid_lip_depth"]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -265,6 +299,15 @@ module FeatherCaseWizard
|
|||||||
raise ConfigError, "Slug must contain lowercase letters, numbers, and single hyphens"
|
raise ConfigError, "Slug must contain lowercase letters, numbers, and single hyphens"
|
||||||
end
|
end
|
||||||
raise ConfigError, "Unsupported top profile" unless PROFILES.include?(parameters["top_profile"])
|
raise ConfigError, "Unsupported top profile" unless PROFILES.include?(parameters["top_profile"])
|
||||||
|
unless LID_ART_ORIENTATIONS.include?(parameters["lid_art_orientation"])
|
||||||
|
raise ConfigError, "Unsupported lid art orientation"
|
||||||
|
end
|
||||||
|
unless parameters["lid_label"].is_a?(String)
|
||||||
|
raise ConfigError, "lid_label must be text"
|
||||||
|
end
|
||||||
|
if lid_artwork? && parameters["top_profile"] == "oled128x64"
|
||||||
|
raise ConfigError, "Lid artwork is not supported with the OLED profile"
|
||||||
|
end
|
||||||
raise ConfigError, "wing_count must be between 1 and 8" unless (1..8).cover?(parameters["wing_count"])
|
raise ConfigError, "wing_count must be between 1 and 8" unless (1..8).cover?(parameters["wing_count"])
|
||||||
raise ConfigError, "At least one export format is required" if outputs["formats"].empty?
|
raise ConfigError, "At least one export format is required" if outputs["formats"].empty?
|
||||||
unless (outputs["formats"] - FORMATS).empty?
|
unless (outputs["formats"] - FORMATS).empty?
|
||||||
@@ -289,9 +332,27 @@ module FeatherCaseWizard
|
|||||||
parameters["stack_height_override"].negative?
|
parameters["stack_height_override"].negative?
|
||||||
raise ConfigError, "wall_thickness must be at least 1.2 mm" if
|
raise ConfigError, "wall_thickness must be at least 1.2 mm" if
|
||||||
parameters["wall_thickness"] < 1.2
|
parameters["wall_thickness"] < 1.2
|
||||||
|
raise ConfigError, "battery_partition_thickness must be at least 1.2 mm" if
|
||||||
|
parameters["battery_partition_thickness"] < 1.2
|
||||||
|
raise ConfigError, "rocker_tab_panel_thickness must be at least 1.2 mm" if
|
||||||
|
parameters["rocker_tab_panel_thickness"] < 1.2
|
||||||
|
if parameters["rocker_tab_panel_thickness"] >= parameters["lid_thickness"]
|
||||||
|
raise ConfigError, "rocker_tab_panel_thickness must be less than lid_thickness"
|
||||||
|
end
|
||||||
raise ConfigError, "lid_lip_thickness must be at least 1.2 mm" if
|
raise ConfigError, "lid_lip_thickness must be at least 1.2 mm" if
|
||||||
parameters["lid_lip_thickness"] < 1.2
|
parameters["lid_lip_thickness"] < 1.2
|
||||||
|
raise ConfigError, "lid_inlay_depth must be less than lid_thickness" if
|
||||||
|
parameters["lid_inlay_depth"] >= parameters["lid_thickness"]
|
||||||
|
if parameters["m25_insert_outer_diameter"] < parameters["m25_insert_hole_diameter"]
|
||||||
|
raise ConfigError, "m25_insert_outer_diameter must not be smaller than the pilot"
|
||||||
|
end
|
||||||
|
if parameters["tripod_insert_outer_diameter"] <
|
||||||
|
parameters["tripod_insert_hole_diameter"]
|
||||||
|
raise ConfigError,
|
||||||
|
"tripod_insert_outer_diameter must not be smaller than the pilot"
|
||||||
|
end
|
||||||
|
|
||||||
|
validate_usb_opening!
|
||||||
validate_cutouts!
|
validate_cutouts!
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
@@ -305,6 +366,7 @@ module FeatherCaseWizard
|
|||||||
legacy_usb_guide = !supplied_parameters.key?("usb_guide_enabled")
|
legacy_usb_guide = !supplied_parameters.key?("usb_guide_enabled")
|
||||||
data["parameters"] = self.class.deep_copy(DEFAULT_PARAMETERS).merge(supplied_parameters)
|
data["parameters"] = self.class.deep_copy(DEFAULT_PARAMETERS).merge(supplied_parameters)
|
||||||
data["parameters"]["usb_guide_enabled"] = false if legacy_usb_guide
|
data["parameters"]["usb_guide_enabled"] = false if legacy_usb_guide
|
||||||
|
LEGACY_SWITCH_FIELDS.each { |key| data["parameters"].delete(key) }
|
||||||
data["cutouts"] ||= []
|
data["cutouts"] ||= []
|
||||||
data["outputs"] ||= {}
|
data["outputs"] ||= {}
|
||||||
data["outputs"]["formats"] ||= ["3mf"]
|
data["outputs"]["formats"] ||= ["3mf"]
|
||||||
@@ -355,6 +417,19 @@ module FeatherCaseWizard
|
|||||||
validate_cutout_spacing!(bounds, dims)
|
validate_cutout_spacing!(bounds, dims)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_usb_opening!
|
||||||
|
return unless parameters["usb_opening_enabled"]
|
||||||
|
|
||||||
|
dims = dimensions
|
||||||
|
bounds = usb_wall_bounds(dims)
|
||||||
|
if bounds["bottom"] < dims["floor"]
|
||||||
|
raise ConfigError, "USB opening or guide intersects the case floor"
|
||||||
|
end
|
||||||
|
if bounds["top"] > dims["lip_bottom"]
|
||||||
|
raise ConfigError, "USB opening or guide intersects the lid lip"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate_cutout_spacing!(bounds, dims)
|
def validate_cutout_spacing!(bounds, dims)
|
||||||
web = parameters["cutout_min_web"]
|
web = parameters["cutout_min_web"]
|
||||||
bounds.combination(2) do |first, second|
|
bounds.combination(2) do |first, second|
|
||||||
|
|||||||
@@ -2,16 +2,23 @@
|
|||||||
|
|
||||||
module FeatherCaseWizard
|
module FeatherCaseWizard
|
||||||
class PackageBuilder
|
class PackageBuilder
|
||||||
attr_reader :config, :root, :toolchain, :model_source, :renderer_source
|
attr_reader :config, :root, :toolchain, :model_source, :renderer_source,
|
||||||
|
:multicolor_renderer_source, :multicolor_packager_source, :logo_source
|
||||||
|
|
||||||
def initialize(config, root: CASES_ROOT, toolchain: Toolchain.new,
|
def initialize(config, root: CASES_ROOT, toolchain: Toolchain.new,
|
||||||
model_source: MODEL_SOURCE, renderer_source: RENDERER_SOURCE,
|
model_source: MODEL_SOURCE, renderer_source: RENDERER_SOURCE,
|
||||||
|
multicolor_renderer_source: MULTICOLOR_RENDERER_SOURCE,
|
||||||
|
multicolor_packager_source: MULTICOLOR_PACKAGER_SOURCE,
|
||||||
|
logo_source: LOGO_SOURCE,
|
||||||
reporter: nil)
|
reporter: nil)
|
||||||
@config = config
|
@config = config
|
||||||
@root = Pathname(root)
|
@root = Pathname(root)
|
||||||
@toolchain = toolchain
|
@toolchain = toolchain
|
||||||
@model_source = Pathname(model_source)
|
@model_source = Pathname(model_source)
|
||||||
@renderer_source = Pathname(renderer_source)
|
@renderer_source = Pathname(renderer_source)
|
||||||
|
@multicolor_renderer_source = Pathname(multicolor_renderer_source)
|
||||||
|
@multicolor_packager_source = Pathname(multicolor_packager_source)
|
||||||
|
@logo_source = Pathname(logo_source)
|
||||||
@reporter = reporter || ->(_message) {}
|
@reporter = reporter || ->(_message) {}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -37,13 +44,22 @@ module FeatherCaseWizard
|
|||||||
def verify_sources!
|
def verify_sources!
|
||||||
raise ToolError, "Missing model source: #{model_source}" unless model_source.file?
|
raise ToolError, "Missing model source: #{model_source}" unless model_source.file?
|
||||||
raise ToolError, "Missing preview renderer: #{renderer_source}" unless renderer_source.file?
|
raise ToolError, "Missing preview renderer: #{renderer_source}" unless renderer_source.file?
|
||||||
|
raise ToolError, "Missing multicolor renderer: #{multicolor_renderer_source}" unless
|
||||||
|
multicolor_renderer_source.file?
|
||||||
|
raise ToolError, "Missing multicolor packager: #{multicolor_packager_source}" unless
|
||||||
|
multicolor_packager_source.file?
|
||||||
|
raise ToolError, "Missing lid logo: #{logo_source}" unless logo_source.file?
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_package(stage, previous)
|
def prepare_package(stage, previous)
|
||||||
source_dir = stage.join("source")
|
source_dir = stage.join("source")
|
||||||
FileUtils.mkdir_p(source_dir)
|
tools_dir = stage.join("tools")
|
||||||
|
FileUtils.mkdir_p([source_dir, source_dir.join("assets"), tools_dir])
|
||||||
FileUtils.cp(model_source, source_dir.join("feather_case.scad"))
|
FileUtils.cp(model_source, source_dir.join("feather_case.scad"))
|
||||||
FileUtils.cp(renderer_source, source_dir.join("render_stl_previews.py"))
|
FileUtils.cp(renderer_source, source_dir.join("render_stl_previews.py"))
|
||||||
|
FileUtils.cp(multicolor_renderer_source, source_dir.join("render_multicolor_lid.py"))
|
||||||
|
FileUtils.cp(multicolor_packager_source, tools_dir.join("package_multimaterial_3mf.py"))
|
||||||
|
FileUtils.cp(logo_source, source_dir.join("assets/NME-fixed.svg"))
|
||||||
FileUtils.cp(ROOT.join("templates/standalone_build.rb"), stage.join("build.rb"))
|
FileUtils.cp(ROOT.join("templates/standalone_build.rb"), stage.join("build.rb"))
|
||||||
|
|
||||||
copy_history(previous, stage)
|
copy_history(previous, stage)
|
||||||
@@ -91,9 +107,26 @@ module FeatherCaseWizard
|
|||||||
report("Exporting #{part}.#{format}")
|
report("Exporting #{part}.#{format}")
|
||||||
export_model(stage, directory.join("#{part}.#{format}"), part)
|
export_model(stage, directory.join("#{part}.#{format}"), part)
|
||||||
end
|
end
|
||||||
|
package_multicolor_lid(stage, directory) if format == "3mf" && config.lid_artwork?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def package_multicolor_lid(stage, directory)
|
||||||
|
report("Packaging lid_multicolor.3mf")
|
||||||
|
command = [
|
||||||
|
toolchain.python,
|
||||||
|
stage.join("tools/package_multimaterial_3mf.py").to_s,
|
||||||
|
directory.join("lid_base.3mf").to_s,
|
||||||
|
directory.join("lid_accent.3mf").to_s,
|
||||||
|
directory.join("lid_multicolor.3mf").to_s,
|
||||||
|
"--title", "#{config.name} Lid"
|
||||||
|
]
|
||||||
|
toolchain.run!(*command)
|
||||||
|
output = directory.join("lid_multicolor.3mf")
|
||||||
|
raise ToolError, "Multicolor packager did not create #{output}" unless
|
||||||
|
output.file? && output.size.positive?
|
||||||
|
end
|
||||||
|
|
||||||
def generate_previews(stage)
|
def generate_previews(stage)
|
||||||
previews = stage.join("previews")
|
previews = stage.join("previews")
|
||||||
scratch = stage.join(".preview-meshes")
|
scratch = stage.join(".preview-meshes")
|
||||||
@@ -111,9 +144,22 @@ module FeatherCaseWizard
|
|||||||
export_model(stage, mesh, part) unless mesh.file?
|
export_model(stage, mesh, part) unless mesh.file?
|
||||||
render(stage, mesh, previews, part, ["iso"])
|
render(stage, mesh, previews, part, ["iso"])
|
||||||
end
|
end
|
||||||
|
render_multicolor_lid(stage, scratch, previews) if config.lid_artwork?
|
||||||
FileUtils.rm_rf(scratch)
|
FileUtils.rm_rf(scratch)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_multicolor_lid(stage, scratch, previews)
|
||||||
|
report("Rendering multicolor lid preview")
|
||||||
|
command = [
|
||||||
|
toolchain.blender, "--background", "-noaudio", "--python",
|
||||||
|
stage.join("source/render_multicolor_lid.py").to_s, "--",
|
||||||
|
scratch.join("lid_base.stl").to_s,
|
||||||
|
scratch.join("lid_accent.stl").to_s,
|
||||||
|
previews.to_s, "lid_multicolor", config.parameters["lid_art_orientation"]
|
||||||
|
]
|
||||||
|
toolchain.run!(*command, env: {"DISPLAY" => nil, "WAYLAND_DISPLAY" => nil})
|
||||||
|
end
|
||||||
|
|
||||||
def export_model(stage, output, part, overrides = {})
|
def export_model(stage, output, part, overrides = {})
|
||||||
model = stage.join("source/feather_case.scad")
|
model = stage.join("source/feather_case.scad")
|
||||||
parameters = config.model_parameters.merge(overrides).merge("part" => part)
|
parameters = config.model_parameters.merge(overrides).merge("part" => part)
|
||||||
@@ -139,6 +185,18 @@ module FeatherCaseWizard
|
|||||||
def write_summary(stage)
|
def write_summary(stage)
|
||||||
dims = config.dimensions
|
dims = config.dimensions
|
||||||
parts = config.printable_parts.map { |part| "`#{part}`" }.join(", ")
|
parts = config.printable_parts.map { |part| "`#{part}`" }.join(", ")
|
||||||
|
artwork = if config.lid_artwork?
|
||||||
|
<<~ARTWORK.chomp
|
||||||
|
|
||||||
|
For a two-color lid, open `exports/3mf/lid_multicolor.3mf` as one
|
||||||
|
multipart object. Assign “Lid base — Filament 1” and “Accent inlays —
|
||||||
|
Filament 2” to the desired filaments. `lid.3mf` is the complete
|
||||||
|
single-color fallback; `lid_base.3mf` and `lid_accent.3mf` are also
|
||||||
|
provided separately.
|
||||||
|
ARTWORK
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
text = <<~MARKDOWN
|
text = <<~MARKDOWN
|
||||||
# #{config.name}
|
# #{config.name}
|
||||||
|
|
||||||
@@ -154,8 +212,8 @@ module FeatherCaseWizard
|
|||||||
- Custom cutouts: #{config.cutouts.length}
|
- Custom cutouts: #{config.cutouts.length}
|
||||||
|
|
||||||
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
Run `ruby build.rb` in this directory to regenerate exports and previews.
|
||||||
OpenSCAD and Blender must be installed. Set `OPENSCAD` or `BLENDER` to
|
OpenSCAD, Blender, and Python 3 must be installed. Set `OPENSCAD`,
|
||||||
override their executable paths.
|
`BLENDER`, or `PYTHON` to override their executable paths.
|
||||||
|
|
||||||
To open the saved Customizer values manually:
|
To open the saved Customizer values manually:
|
||||||
|
|
||||||
@@ -165,8 +223,9 @@ module FeatherCaseWizard
|
|||||||
|
|
||||||
The 3MF files contain portable geometry only. Choose printer, filament,
|
The 3MF files contain portable geometry only. Choose printer, filament,
|
||||||
plate, and slicing settings in Bambu Studio.
|
plate, and slicing settings in Bambu Studio.
|
||||||
|
#{artwork}
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
File.write(stage.join("README.md"), text)
|
File.write(stage.join("README.md"), text.rstrip + "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
def install_atomically(stage, destination)
|
def install_atomically(stage, destination)
|
||||||
|
|||||||
@@ -4,18 +4,20 @@ module FeatherCaseWizard
|
|||||||
class ToolError < StandardError; end
|
class ToolError < StandardError; end
|
||||||
|
|
||||||
class Toolchain
|
class Toolchain
|
||||||
attr_reader :openscad, :blender
|
attr_reader :openscad, :blender, :python
|
||||||
|
|
||||||
def initialize(env: ENV)
|
def initialize(env: ENV)
|
||||||
@env = env
|
@env = env
|
||||||
@openscad = discover("OPENSCAD", "openscad")
|
@openscad = discover("OPENSCAD", "openscad")
|
||||||
@blender = discover("BLENDER", "blender")
|
@blender = discover("BLENDER", "blender")
|
||||||
|
@python = discover("PYTHON", "python3")
|
||||||
end
|
end
|
||||||
|
|
||||||
def check!
|
def check!
|
||||||
missing = []
|
missing = []
|
||||||
missing << "OpenSCAD (`sudo apt-get install openscad` or set OPENSCAD)" unless openscad
|
missing << "OpenSCAD (`sudo apt-get install openscad` or set OPENSCAD)" unless openscad
|
||||||
missing << "Blender (`sudo apt-get install blender` or set BLENDER)" unless blender
|
missing << "Blender (`sudo apt-get install blender` or set BLENDER)" unless blender
|
||||||
|
missing << "Python 3 (`sudo apt-get install python3` or set PYTHON)" unless python
|
||||||
return self if missing.empty?
|
return self if missing.empty?
|
||||||
|
|
||||||
raise ToolError, "Missing required tools:\n - #{missing.join("\n - ")}"
|
raise ToolError, "Missing required tools:\n - #{missing.join("\n - ")}"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ module FeatherCaseWizard
|
|||||||
choose_stack(config)
|
choose_stack(config)
|
||||||
choose_end_clearance(config)
|
choose_end_clearance(config)
|
||||||
choose_features(config)
|
choose_features(config)
|
||||||
|
choose_lid_artwork(config)
|
||||||
manage_cutouts(config)
|
manage_cutouts(config)
|
||||||
choose_coupons(config)
|
choose_coupons(config)
|
||||||
advanced(config) if ui.agree("Adjust advanced dimensions? (y/n) ") do |question|
|
advanced(config) if ui.agree("Adjust advanced dimensions? (y/n) ") do |question|
|
||||||
@@ -143,6 +144,26 @@ module FeatherCaseWizard
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def choose_lid_artwork(config)
|
||||||
|
p = config.parameters
|
||||||
|
label_enabled = agreement("Add a two-color device title to the lid?",
|
||||||
|
!p["lid_label"].empty?)
|
||||||
|
p["lid_label"] = label_enabled ? text(
|
||||||
|
"Device title", p["lid_label"].empty? ? config.name : p["lid_label"]
|
||||||
|
) : ""
|
||||||
|
p["lid_logo_enabled"] = agreement("Add the two-color NME logo to the lid?",
|
||||||
|
p["lid_logo_enabled"])
|
||||||
|
return unless !p["lid_label"].empty? || p["lid_logo_enabled"]
|
||||||
|
|
||||||
|
current = p["lid_art_orientation"]
|
||||||
|
p["lid_art_orientation"] = ui.choose("USB position when the lid text is upright") do |menu|
|
||||||
|
Config::LID_ART_ORIENTATIONS.each do |orientation|
|
||||||
|
menu.choice(orientation.delete_prefix("usb_").capitalize) { orientation }
|
||||||
|
end
|
||||||
|
menu.default = (Config::LID_ART_ORIENTATIONS.index(current).to_i + 1).to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def manage_cutouts(config)
|
def manage_cutouts(config)
|
||||||
loop do
|
loop do
|
||||||
ui.say("Custom cable cutouts: #{config.cutouts.empty? ? "none" :
|
ui.say("Custom cable cutouts: #{config.cutouts.empty? ? "none" :
|
||||||
|
|||||||
@@ -44,13 +44,18 @@ config = YAML.safe_load_file(ROOT.join("case.yml"), permitted_classes: [],
|
|||||||
permitted_symbols: [], aliases: false)
|
permitted_symbols: [], aliases: false)
|
||||||
openscad = executable("OPENSCAD", "openscad")
|
openscad = executable("OPENSCAD", "openscad")
|
||||||
blender = executable("BLENDER", "blender")
|
blender = executable("BLENDER", "blender")
|
||||||
|
python = executable("PYTHON", "python3")
|
||||||
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
abort "OpenSCAD not found; install it or set OPENSCAD" unless openscad
|
||||||
abort "Blender not found; install it or set BLENDER" unless blender
|
abort "Blender not found; install it or set BLENDER" unless blender
|
||||||
|
|
||||||
parameters = config.fetch("parameters").merge(
|
parameters = config.fetch("parameters").merge(
|
||||||
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
"extra_cutouts" => config.fetch("resolved").fetch("extra_cutouts")
|
||||||
)
|
)
|
||||||
|
artwork = !parameters.fetch("lid_label", "").empty? ||
|
||||||
|
parameters.fetch("lid_logo_enabled", false)
|
||||||
|
abort "Python 3 not found; install it or set PYTHON" if artwork && !python
|
||||||
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
parts = %w[body lid] + config.fetch("outputs").fetch("coupons")
|
||||||
|
parts.concat(%w[lid_base lid_accent]) if artwork
|
||||||
buttons = %w[button_a button_b button_c button_reset]
|
buttons = %w[button_a button_b button_c button_reset]
|
||||||
if parameters["top_profile"] == "oled128x64" &&
|
if parameters["top_profile"] == "oled128x64" &&
|
||||||
buttons.any? { |name| parameters[name] }
|
buttons.any? { |name| parameters[name] }
|
||||||
@@ -80,6 +85,16 @@ begin
|
|||||||
puts "Exporting #{part}.#{format}…"
|
puts "Exporting #{part}.#{format}…"
|
||||||
export.call(directory.join("#{part}.#{format}"), part)
|
export.call(directory.join("#{part}.#{format}"), part)
|
||||||
end
|
end
|
||||||
|
if artwork && format == "3mf"
|
||||||
|
puts "Packaging lid_multicolor.3mf…"
|
||||||
|
run!(
|
||||||
|
python, ROOT.join("tools/package_multimaterial_3mf.py").to_s,
|
||||||
|
directory.join("lid_base.3mf").to_s,
|
||||||
|
directory.join("lid_accent.3mf").to_s,
|
||||||
|
directory.join("lid_multicolor.3mf").to_s,
|
||||||
|
"--title", "#{config.fetch("name")} Lid"
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assembly = scratch.join("assembly.stl")
|
assembly = scratch.join("assembly.stl")
|
||||||
@@ -97,6 +112,16 @@ begin
|
|||||||
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
run!(blender, "--background", "-noaudio", "--python", renderer.to_s, "--",
|
||||||
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
mesh.to_s, previews.to_s, part, "iso", env: render_env)
|
||||||
end
|
end
|
||||||
|
if artwork
|
||||||
|
puts "Rendering multicolor lid preview…"
|
||||||
|
run!(
|
||||||
|
blender, "--background", "-noaudio", "--python",
|
||||||
|
ROOT.join("source/render_multicolor_lid.py").to_s, "--",
|
||||||
|
scratch.join("lid_base.stl").to_s,
|
||||||
|
scratch.join("lid_accent.stl").to_s,
|
||||||
|
previews.to_s, "lid_multicolor", parameters["lid_art_orientation"], env: render_env
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
backup = ROOT.join(".build-old-#{Process.pid}")
|
backup = ROOT.join(".build-old-#{Process.pid}")
|
||||||
FileUtils.mkdir_p(backup)
|
FileUtils.mkdir_p(backup)
|
||||||
|
|||||||
+168
-1
@@ -72,6 +72,26 @@ class ConfigTest < Minitest::Test
|
|||||||
assert_match(/guide requires/, error.message)
|
assert_match(/guide requires/, error.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_usb_opening_expands_upward_without_moving_bottom_edge
|
||||||
|
parameters = config.parameters
|
||||||
|
compensation = parameters["hole_compensation"]
|
||||||
|
board_top = config.dimensions["floor"] +
|
||||||
|
parameters["board_bottom_clearance"] + 1.6
|
||||||
|
old_center = board_top + 1.6
|
||||||
|
old_width = 12.5 + compensation
|
||||||
|
old_height = 7.0 + compensation
|
||||||
|
new_center = old_center + parameters["usb_vertical_offset"]
|
||||||
|
new_width = parameters["usb_opening_width"] + compensation
|
||||||
|
new_height = parameters["usb_opening_height"] + compensation
|
||||||
|
|
||||||
|
assert_in_delta 1.0, new_width - old_width, 0.001
|
||||||
|
assert_in_delta old_center - old_height / 2,
|
||||||
|
new_center - new_height / 2, 0.001
|
||||||
|
assert_in_delta 1.0,
|
||||||
|
(new_center + new_height / 2) -
|
||||||
|
(old_center + old_height / 2), 0.001
|
||||||
|
end
|
||||||
|
|
||||||
def test_end_clearances_expand_case_length
|
def test_end_clearances_expand_case_length
|
||||||
subject = config
|
subject = config
|
||||||
subject.parameters["usb_board_wall_gap"] = 20.0
|
subject.parameters["usb_board_wall_gap"] = 20.0
|
||||||
@@ -80,6 +100,48 @@ class ConfigTest < Minitest::Test
|
|||||||
assert_in_delta 95.6, subject.dimensions["length"], 0.01
|
assert_in_delta 95.6, subject.dimensions["length"], 0.01
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_insert_defaults_match_selected_hardware
|
||||||
|
parameters = config.parameters
|
||||||
|
|
||||||
|
assert_in_delta 4.0, parameters["m25_insert_length"], 0.001
|
||||||
|
assert_in_delta 3.1, parameters["m25_insert_hole_diameter"], 0.001
|
||||||
|
assert_in_delta 3.5, parameters["m25_insert_outer_diameter"], 0.001
|
||||||
|
assert_in_delta 5.0, parameters["tripod_insert_length"], 0.001
|
||||||
|
assert_in_delta 7.2, parameters["tripod_insert_hole_diameter"], 0.001
|
||||||
|
assert_in_delta 8.0, parameters["tripod_insert_outer_diameter"], 0.001
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tripod_floor_tracks_insert_length
|
||||||
|
subject = config
|
||||||
|
subject.parameters["tripod_insert_enabled"] = true
|
||||||
|
|
||||||
|
assert_in_delta 7.0, subject.dimensions["floor"], 0.001
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_insert_coupon_uses_finished_pilot_ranges
|
||||||
|
source = FeatherCaseWizard::MODEL_SOURCE.read
|
||||||
|
|
||||||
|
assert_includes source,
|
||||||
|
"m25_coupon_steps = [-0.15, -0.075, 0, 0.075, 0.15];"
|
||||||
|
assert_includes source,
|
||||||
|
"tripod_coupon_steps = [-0.3, -0.15, 0, 0.15, 0.3];"
|
||||||
|
assert_includes source, "bore_d = diameter;"
|
||||||
|
refute_includes source, "bore_d = diameter + hole_compensation;"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_lid_rocker_mirrors_to_installed_feather_side
|
||||||
|
source = FeatherCaseWizard::MODEL_SOURCE.read
|
||||||
|
|
||||||
|
assert_includes source,
|
||||||
|
"rocker_lid_center_y = case_width - rocker_installed_center_y;"
|
||||||
|
assert_includes source,
|
||||||
|
"translate([rocker_lid_center_x, rocker_lid_center_y, -eps])"
|
||||||
|
assert_includes source,
|
||||||
|
"rocker_installed_center_y - rocker_body_size[1] / 2"
|
||||||
|
assert_includes source,
|
||||||
|
"+ rocker_body_size[1] / 2 + rocker_partition_offset;"
|
||||||
|
end
|
||||||
|
|
||||||
def test_oled_buttons_and_selected_coupons_are_printable
|
def test_oled_buttons_and_selected_coupons_are_printable
|
||||||
subject = config
|
subject = config
|
||||||
subject.parameters["top_profile"] = "oled128x64"
|
subject.parameters["top_profile"] = "oled128x64"
|
||||||
@@ -90,6 +152,111 @@ class ConfigTest < Minitest::Test
|
|||||||
assert_equal %w[body lid buttons fit_coupon], subject.printable_parts
|
assert_equal %w[body lid buttons fit_coupon], subject.printable_parts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_combined_battery_and_tripod_case_adds_partition_clearance
|
||||||
|
subject = config
|
||||||
|
subject.parameters["battery_enabled"] = true
|
||||||
|
subject.parameters["tripod_insert_enabled"] = true
|
||||||
|
|
||||||
|
assert_in_delta 88.6, subject.dimensions["length"], 0.01
|
||||||
|
assert_in_delta 61.06, subject.dimensions["width"], 0.01
|
||||||
|
assert_in_delta 31.25, subject.dimensions["height"], 0.01
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_battery_board_clearance_moves_the_whole_bay_outward
|
||||||
|
subject = config
|
||||||
|
subject.parameters["battery_enabled"] = true
|
||||||
|
original_width = subject.dimensions["width"]
|
||||||
|
|
||||||
|
subject.parameters["battery_board_clearance"] += 2.0
|
||||||
|
|
||||||
|
assert_in_delta 2.0, subject.dimensions["width"] - original_width, 0.001
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_switch_tab_relief_uses_two_mm_lands_in_lid_and_coupon
|
||||||
|
source = FeatherCaseWizard::MODEL_SOURCE.read
|
||||||
|
|
||||||
|
assert_in_delta 2.0, config.parameters["rocker_tab_panel_thickness"], 0.001
|
||||||
|
assert_in_delta 2.0, config.parameters["rocker_tab_land"], 0.001
|
||||||
|
assert_includes source, "module rocker_tab_reliefs(center, opening_size)"
|
||||||
|
assert_equal 2, source.scan(/rocker_tab_reliefs\(/).length - 1
|
||||||
|
assert_includes source,
|
||||||
|
"edge_y = center[1] + side * opening_size[1] / 2;"
|
||||||
|
assert_includes source,
|
||||||
|
"opening_overlap = rocker_corner_radius;"
|
||||||
|
assert_includes source,
|
||||||
|
"cube([opening_size[0], rocker_tab_land + opening_overlap,"
|
||||||
|
refute_includes source,
|
||||||
|
"edge_x = center[0] + side * opening_size[0] / 2;"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_switch_moves_two_mm_toward_partition_on_installed_lid
|
||||||
|
assert_in_delta 2.0, config.parameters["rocker_partition_offset"], 0.001
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_switch_tab_panel_must_be_thinner_than_lid
|
||||||
|
subject = config
|
||||||
|
subject.parameters["rocker_tab_panel_thickness"] =
|
||||||
|
subject.parameters["lid_thickness"]
|
||||||
|
|
||||||
|
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
|
||||||
|
assert_match(/rocker_tab_panel_thickness/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_legacy_round_switch_fields_migrate_to_rocker_defaults
|
||||||
|
data = config.data
|
||||||
|
data["parameters"]["switch_hole_diameter"] = 12.0
|
||||||
|
data["parameters"]["switch_body_length"] = 24.0
|
||||||
|
|
||||||
|
subject = FeatherCaseWizard::Config.new(data)
|
||||||
|
|
||||||
|
refute subject.parameters.key?("switch_hole_diameter")
|
||||||
|
refute subject.parameters.key?("switch_body_length")
|
||||||
|
assert_equal [12.9, 19.4], subject.parameters["rocker_cutout_size"]
|
||||||
|
assert_equal [15.0, 21.0, 21.0], subject.parameters["rocker_body_size"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_battery_partition_requires_three_06_mm_lines
|
||||||
|
subject = config
|
||||||
|
subject.parameters["battery_partition_thickness"] = 1.19
|
||||||
|
|
||||||
|
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
|
||||||
|
assert_match(/battery_partition_thickness/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_insert_entry_cannot_be_smaller_than_pilot
|
||||||
|
subject = config
|
||||||
|
subject.parameters["m25_insert_outer_diameter"] = 3.0
|
||||||
|
|
||||||
|
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
|
||||||
|
assert_match(/m25_insert_outer_diameter/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_lid_artwork_adds_base_and_accent_parts
|
||||||
|
subject = config
|
||||||
|
subject.parameters["lid_label"] = "GPS"
|
||||||
|
subject.parameters["lid_logo_enabled"] = true
|
||||||
|
|
||||||
|
assert subject.lid_artwork?
|
||||||
|
assert_equal %w[body lid lid_base lid_accent], subject.printable_parts
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_lid_artwork_rejects_oled_profile
|
||||||
|
subject = config
|
||||||
|
subject.parameters["top_profile"] = "oled128x64"
|
||||||
|
subject.parameters["lid_label"] = "OLED"
|
||||||
|
|
||||||
|
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
|
||||||
|
assert_match(/not supported with the OLED profile/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_lid_artwork_orientation_is_named
|
||||||
|
subject = config
|
||||||
|
subject.parameters["lid_art_orientation"] = "diagonal"
|
||||||
|
|
||||||
|
error = assert_raises(FeatherCaseWizard::ConfigError) { subject.validate! }
|
||||||
|
assert_match(/orientation/, error.message)
|
||||||
|
end
|
||||||
|
|
||||||
def test_wall_local_cutouts_map_to_model_coordinates
|
def test_wall_local_cutouts_map_to_model_coordinates
|
||||||
subject = config
|
subject = config
|
||||||
length = subject.dimensions["length"]
|
length = subject.dimensions["length"]
|
||||||
@@ -138,7 +305,7 @@ class ConfigTest < Minitest::Test
|
|||||||
|
|
||||||
def test_front_cutout_can_clear_usb_reservation
|
def test_front_cutout_can_clear_usb_reservation
|
||||||
subject = config
|
subject = config
|
||||||
subject.data["cutouts"] = [cutout("front", 10.0).merge("z" => 11.0)]
|
subject.data["cutouts"] = [cutout("front", 8.0).merge("z" => 11.0)]
|
||||||
|
|
||||||
assert_same subject, subject.validate!
|
assert_same subject, subject.validate!
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class PackageBuilderTest < Minitest::Test
|
|||||||
%w[iso front back left right top].each do |view|
|
%w[iso front back left right top].each do |view|
|
||||||
assert destination.join("previews/assembly_#{view}.png").file?
|
assert destination.join("previews/assembly_#{view}.png").file?
|
||||||
end
|
end
|
||||||
|
refute_match(/\n\n\z/, destination.join("README.md").read)
|
||||||
|
|
||||||
config.parameters["button_reset"] = true
|
config.parameters["button_reset"] = true
|
||||||
build(config, root.join("cases"), tools, model, renderer)
|
build(config, root.join("cases"), tools, model, renderer)
|
||||||
@@ -68,6 +69,30 @@ class PackageBuilderTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_builds_aligned_multicolor_lid_package_when_artwork_is_enabled
|
||||||
|
Dir.mktmpdir do |directory|
|
||||||
|
root = Pathname(directory)
|
||||||
|
tools = create_fake_tools(root)
|
||||||
|
model = root.join("model.scad")
|
||||||
|
renderer = root.join("renderer.py")
|
||||||
|
File.write(model, "cube([1,1,1]);\n")
|
||||||
|
File.write(renderer, "# test renderer\n")
|
||||||
|
config = FeatherCaseWizard::Config.create(name: "IMU", slug: "imu")
|
||||||
|
config.parameters["lid_label"] = "IMU"
|
||||||
|
config.parameters["lid_logo_enabled"] = true
|
||||||
|
|
||||||
|
destination = build(config, root.join("cases"), tools, model, renderer)
|
||||||
|
|
||||||
|
%w[lid lid_base lid_accent].each do |part|
|
||||||
|
assert destination.join("exports/3mf/#{part}.3mf").file?
|
||||||
|
end
|
||||||
|
assert destination.join("exports/3mf/lid_multicolor.3mf").file?
|
||||||
|
assert destination.join("source/assets/NME-fixed.svg").file?
|
||||||
|
assert destination.join("tools/package_multimaterial_3mf.py").file?
|
||||||
|
assert destination.join("previews/lid_multicolor_top.png").file?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build(config, root, tools, model, renderer)
|
def build(config, root, tools, model, renderer)
|
||||||
@@ -93,14 +118,26 @@ class PackageBuilderTest < Minitest::Test
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
require "fileutils"
|
require "fileutils"
|
||||||
args = ARGV[(ARGV.index("--") + 1)..]
|
args = ARGV[(ARGV.index("--") + 1)..]
|
||||||
|
multicolor = ARGV[ARGV.index("--python") + 1].include?("render_multicolor")
|
||||||
|
if multicolor
|
||||||
|
_base, _accent, output, basename = args
|
||||||
|
views = %w[iso top]
|
||||||
|
else
|
||||||
_mesh, output, basename, selected = args
|
_mesh, output, basename, selected = args
|
||||||
views = selected ? selected.split(",") : %w[iso front back left right top]
|
views = selected ? selected.split(",") : %w[iso front back left right top]
|
||||||
|
end
|
||||||
FileUtils.mkdir_p(output)
|
FileUtils.mkdir_p(output)
|
||||||
views.each { |view| File.binwrite(File.join(output, "\#{basename}_\#{view}.png"), "png") }
|
views.each { |view| File.binwrite(File.join(output, "\#{basename}_\#{view}.png"), "png") }
|
||||||
RUBY
|
RUBY
|
||||||
FileUtils.chmod(0o755, [openscad, blender])
|
python = bin.join("python3")
|
||||||
|
File.write(python, <<~RUBY)
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
File.binwrite(ARGV[3], "multipart")
|
||||||
|
RUBY
|
||||||
|
FileUtils.chmod(0o755, [openscad, blender, python])
|
||||||
FeatherCaseWizard::Toolchain.new(
|
FeatherCaseWizard::Toolchain.new(
|
||||||
env: {"PATH" => bin.to_s, "OPENSCAD" => openscad.to_s, "BLENDER" => blender.to_s}
|
env: {"PATH" => bin.to_s, "OPENSCAD" => openscad.to_s,
|
||||||
|
"BLENDER" => blender.to_s, "PYTHON" => python.to_s}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class ToolchainTest < Minitest::Test
|
|||||||
|
|
||||||
assert_match(/OpenSCAD/, error.message)
|
assert_match(/OpenSCAD/, error.message)
|
||||||
assert_match(/Blender/, error.message)
|
assert_match(/Blender/, error.message)
|
||||||
|
assert_match(/Python 3/, error.message)
|
||||||
assert_match(/apt-get/, error.message)
|
assert_match(/apt-get/, error.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -18,13 +19,14 @@ class ToolchainTest < Minitest::Test
|
|||||||
File.write(executable, "#!/bin/sh\nexit 0\n")
|
File.write(executable, "#!/bin/sh\nexit 0\n")
|
||||||
FileUtils.chmod(0o755, executable)
|
FileUtils.chmod(0o755, executable)
|
||||||
toolchain = FeatherCaseWizard::Toolchain.new(
|
toolchain = FeatherCaseWizard::Toolchain.new(
|
||||||
env: {"PATH" => "", "OPENSCAD" => executable, "BLENDER" => executable}
|
env: {"PATH" => "", "OPENSCAD" => executable, "BLENDER" => executable,
|
||||||
|
"PYTHON" => executable}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_equal executable, toolchain.openscad
|
assert_equal executable, toolchain.openscad
|
||||||
assert_equal executable, toolchain.blender
|
assert_equal executable, toolchain.blender
|
||||||
|
assert_equal executable, toolchain.python
|
||||||
assert_same toolchain, toolchain.check!
|
assert_same toolchain, toolchain.check!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -6,8 +6,8 @@ require_relative "test_helper"
|
|||||||
class WizardTest < Minitest::Test
|
class WizardTest < Minitest::Test
|
||||||
def test_minimal_new_case_flow_and_confirmation
|
def test_minimal_new_case_flow_and_confirmation
|
||||||
input = StringIO.new(
|
input = StringIO.new(
|
||||||
["Demo Case", "", "1", "1", "", "n", "n", "", "", "n", "n", "1",
|
["Demo Case", "", "1", "1", "", "n", "n", "", "", "n", "n", "n", "n",
|
||||||
"n", "n", "n", "n", "y"].join("\n") + "\n"
|
"1", "n", "n", "n", "n", "y"].join("\n") + "\n"
|
||||||
)
|
)
|
||||||
output = StringIO.new
|
output = StringIO.new
|
||||||
wizard = FeatherCaseWizard::Wizard.new(ui: HighLine.new(input, output))
|
wizard = FeatherCaseWizard::Wizard.new(ui: HighLine.new(input, output))
|
||||||
@@ -24,8 +24,8 @@ class WizardTest < Minitest::Test
|
|||||||
config = FeatherCaseWizard::Config.create(name: "GPS", slug: "gps")
|
config = FeatherCaseWizard::Config.create(name: "GPS", slug: "gps")
|
||||||
config.parameters["top_profile"] = "gps"
|
config.parameters["top_profile"] = "gps"
|
||||||
input = StringIO.new(
|
input = StringIO.new(
|
||||||
["", "", "", "", "n", "n", "", "", "n", "n", "1", "n", "n", "n",
|
["", "", "", "", "n", "n", "", "", "n", "n", "n", "n", "1", "n",
|
||||||
"n"].join("\n") + "\n"
|
"n", "n", "n"].join("\n") + "\n"
|
||||||
)
|
)
|
||||||
wizard = FeatherCaseWizard::Wizard.new(ui: HighLine.new(input, StringIO.new))
|
wizard = FeatherCaseWizard::Wizard.new(ui: HighLine.new(input, StringIO.new))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine aligned lid meshes into one slicer-friendly multipart 3MF."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
CORE = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
|
||||||
|
ET.register_namespace("", CORE)
|
||||||
|
|
||||||
|
CONTENT_TYPES_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||||
|
</Types>
|
||||||
|
'''
|
||||||
|
|
||||||
|
RELATIONSHIPS_XML = b'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||||
|
</Relationships>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def load_mesh(path: Path) -> ET.Element:
|
||||||
|
with zipfile.ZipFile(path) as archive:
|
||||||
|
root = ET.fromstring(archive.read("3D/3dmodel.model"))
|
||||||
|
mesh = root.find(f".//{{{CORE}}}mesh")
|
||||||
|
if mesh is None:
|
||||||
|
raise ValueError(f"{path} does not contain a 3MF mesh")
|
||||||
|
return copy.deepcopy(mesh)
|
||||||
|
|
||||||
|
|
||||||
|
def model_xml(base: Path, accent: Path, title: str) -> bytes:
|
||||||
|
root = ET.Element(
|
||||||
|
f"{{{CORE}}}model",
|
||||||
|
{
|
||||||
|
"unit": "millimeter",
|
||||||
|
"{http://www.w3.org/XML/1998/namespace}lang": "en-US",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Title"}).text = title
|
||||||
|
ET.SubElement(root, f"{{{CORE}}}metadata", {"name": "Generator"}).text = (
|
||||||
|
"feather-case-wizard"
|
||||||
|
)
|
||||||
|
resources = ET.SubElement(root, f"{{{CORE}}}resources")
|
||||||
|
materials = ET.SubElement(resources, f"{{{CORE}}}basematerials", {"id": "1"})
|
||||||
|
ET.SubElement(
|
||||||
|
materials, f"{{{CORE}}}base", {"name": "Lid base", "displaycolor": "#D1D5DB"}
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
materials,
|
||||||
|
f"{{{CORE}}}base",
|
||||||
|
{"name": "Accent inlays", "displaycolor": "#F97316"},
|
||||||
|
)
|
||||||
|
|
||||||
|
for object_id, name, source in (
|
||||||
|
(2, "Lid base — Filament 1", base),
|
||||||
|
(3, "Accent inlays — Filament 2", accent),
|
||||||
|
):
|
||||||
|
obj = ET.SubElement(
|
||||||
|
resources,
|
||||||
|
f"{{{CORE}}}object",
|
||||||
|
{
|
||||||
|
"id": str(object_id),
|
||||||
|
"name": name,
|
||||||
|
"type": "model",
|
||||||
|
"pid": "1",
|
||||||
|
"pindex": str(object_id - 2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
obj.append(load_mesh(source))
|
||||||
|
|
||||||
|
assembly = ET.SubElement(
|
||||||
|
resources, f"{{{CORE}}}object", {"id": "4", "name": title, "type": "model"}
|
||||||
|
)
|
||||||
|
components = ET.SubElement(assembly, f"{{{CORE}}}components")
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "2"})
|
||||||
|
ET.SubElement(components, f"{{{CORE}}}component", {"objectid": "3"})
|
||||||
|
build = ET.SubElement(root, f"{{{CORE}}}build")
|
||||||
|
ET.SubElement(build, f"{{{CORE}}}item", {"objectid": "4"})
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def bambu_model_settings_xml(title: str) -> bytes:
|
||||||
|
root = ET.Element("config")
|
||||||
|
obj = ET.SubElement(root, "object", {"id": "4"})
|
||||||
|
ET.SubElement(obj, "metadata", {"key": "name", "value": title})
|
||||||
|
for part_id, name, extruder in (
|
||||||
|
(2, "Lid base — Filament 1", 1),
|
||||||
|
(3, "Accent inlays — Filament 2", 2),
|
||||||
|
):
|
||||||
|
part = ET.SubElement(obj, "part", {"id": str(part_id), "subtype": "normal_part"})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "name", "value": name})
|
||||||
|
ET.SubElement(part, "metadata", {"key": "extruder", "value": str(extruder)})
|
||||||
|
ET.SubElement(root, "assemble")
|
||||||
|
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_entry(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
|
||||||
|
info = zipfile.ZipInfo(name)
|
||||||
|
info.create_system = 3
|
||||||
|
info.external_attr = 0o100644 << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
archive.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("base", type=Path)
|
||||||
|
parser.add_argument("accent", type=Path)
|
||||||
|
parser.add_argument("output", type=Path)
|
||||||
|
parser.add_argument("--title", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
write_entry(archive, "[Content_Types].xml", CONTENT_TYPES_XML)
|
||||||
|
write_entry(archive, "_rels/.rels", RELATIONSHIPS_XML)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"3D/3dmodel.model",
|
||||||
|
model_xml(args.base, args.accent, args.title),
|
||||||
|
)
|
||||||
|
write_entry(
|
||||||
|
archive,
|
||||||
|
"Metadata/model_settings.config",
|
||||||
|
bambu_model_settings_xml(args.title),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Render aligned lid-base and accent STLs with distinct materials."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
|
|
||||||
|
def import_mesh(path, name, color):
|
||||||
|
bpy.ops.wm.stl_import(filepath=os.path.abspath(path))
|
||||||
|
model = bpy.context.selected_objects[0]
|
||||||
|
model.name = name
|
||||||
|
material = bpy.data.materials.new(f"{name} material")
|
||||||
|
material.diffuse_color = color
|
||||||
|
material.metallic = 0.0
|
||||||
|
material.roughness = 0.45
|
||||||
|
model.data.materials.append(material)
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def look_at(camera, target):
|
||||||
|
direction = Vector(target) - camera.location
|
||||||
|
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[sys.argv.index("--") + 1 :]
|
||||||
|
if len(args) not in (4, 5):
|
||||||
|
raise SystemExit(
|
||||||
|
"expected: lid-base.stl lid-accent.stl output-directory basename [orientation]"
|
||||||
|
)
|
||||||
|
base_path, accent_path, output_dir, basename = args[:4]
|
||||||
|
orientation = args[4] if len(args) == 5 else "usb_left"
|
||||||
|
angles = {"usb_left": 0, "usb_front": 90, "usb_right": 180, "usb_rear": 270}
|
||||||
|
if orientation not in angles:
|
||||||
|
raise SystemExit(f"unknown lid orientation: {orientation}")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
models = [
|
||||||
|
import_mesh(base_path, "Lid base", (0.16, 0.42, 0.72, 1.0)),
|
||||||
|
import_mesh(accent_path, "Accent inlays", (0.95, 0.42, 0.08, 1.0)),
|
||||||
|
]
|
||||||
|
# Lid exports are print-oriented with the decorated exterior face down.
|
||||||
|
# Turn both aligned parts over and present the configured USB edge in its
|
||||||
|
# named position so previews inspect the finished face as the user will.
|
||||||
|
for model in models:
|
||||||
|
model.matrix_world = (
|
||||||
|
Matrix.Rotation(math.radians(angles[orientation]), 4, "Z")
|
||||||
|
@ Matrix.Rotation(math.pi, 4, "X")
|
||||||
|
)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
corners = [obj.matrix_world @ Vector(corner) for obj in models for corner in obj.bound_box]
|
||||||
|
mins = Vector([min(v[i] for v in corners) for i in range(3)])
|
||||||
|
maxs = Vector([max(v[i] for v in corners) for i in range(3)])
|
||||||
|
center = (mins + maxs) / 2
|
||||||
|
span = max(maxs - mins)
|
||||||
|
|
||||||
|
world = bpy.data.worlds.new("Inspection world")
|
||||||
|
world.use_nodes = True
|
||||||
|
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.035, 0.04, 0.05, 1)
|
||||||
|
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
|
||||||
|
bpy.context.scene.world = world
|
||||||
|
|
||||||
|
camera_data = bpy.data.cameras.new("Camera")
|
||||||
|
camera = bpy.data.objects.new("Camera", camera_data)
|
||||||
|
bpy.context.collection.objects.link(camera)
|
||||||
|
bpy.context.scene.camera = camera
|
||||||
|
camera.data.type = "ORTHO"
|
||||||
|
camera.data.ortho_scale = span * 1.35
|
||||||
|
|
||||||
|
key_data = bpy.data.lights.new("Key", type="AREA")
|
||||||
|
key_data.energy = 900
|
||||||
|
key_data.size = span
|
||||||
|
key = bpy.data.objects.new("Key", key_data)
|
||||||
|
key.location = center + Vector((span, -span, span * 1.5))
|
||||||
|
bpy.context.collection.objects.link(key)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.engine = "BLENDER_WORKBENCH"
|
||||||
|
scene.render.resolution_x = 800
|
||||||
|
scene.render.resolution_y = 600
|
||||||
|
scene.render.resolution_percentage = 100
|
||||||
|
scene.render.image_settings.file_format = "PNG"
|
||||||
|
scene.display.shading.light = "STUDIO"
|
||||||
|
scene.display.shading.color_type = "MATERIAL"
|
||||||
|
scene.display.shading.show_shadows = True
|
||||||
|
scene.display.shading.show_cavity = True
|
||||||
|
scene.display.shading.cavity_type = "BOTH"
|
||||||
|
|
||||||
|
for view, direction in {"iso": (1, -1, 0.8), "top": (0, 0, 1)}.items():
|
||||||
|
camera.location = center + Vector(direction).normalized() * span * 2.2
|
||||||
|
look_at(camera, center)
|
||||||
|
scene.render.filepath = os.path.join(output_dir, f"{basename}_{view}.png")
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user