Publish curated OpenSCAD generator collection
9
LICENSE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Creative Commons Attribution-NonCommercial 4.0 International
|
||||
|
||||
Copyright © 2026 Christian Johannesen.
|
||||
|
||||
The OpenSCAD source files, exported STL models, digital renders, and documentation in this repository are licensed under the Creative Commons Attribution-NonCommercial 4.0 International license (CC BY-NC 4.0).
|
||||
|
||||
You may share and adapt the material with appropriate credit, a link to the license, and an indication of changes. Commercial use is not permitted.
|
||||
|
||||
License deed and legal code: https://creativecommons.org/licenses/by-nc/4.0/
|
||||
56
README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# OpenSCAD Generators
|
||||
|
||||
A curated public collection of editable parametric OpenSCAD generators, ready-to-slice STL examples, and matching digital renders.
|
||||
|
||||
> The images in this repository are digital OpenSCAD renders. They are not photographs of physical prints. Each project README states its validation and physical-test status.
|
||||
|
||||
## Generator gallery
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="cr80-card-holder/README.md"><img src="cr80-card-holder/images/default-flex-tabs.png" width="300" alt="CR80 / ID-1 Card Holder render"><br><strong>CR80 / ID-1 Card Holder</strong></a></td>
|
||||
<td align="center"><a href="cable-clips/README.md"><img src="cable-clips/images/small.png" width="300" alt="Adjustable Cable Clips render"><br><strong>Adjustable Cable Clips</strong></a></td>
|
||||
<td align="center"><a href="decorative-insects/README.md"><img src="decorative-insects/images/butterfly-relief.png" width="300" alt="Decorative Insects render"><br><strong>Decorative Insects</strong></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="parametric-box/README.md"><img src="parametric-box/images/inner-lip.png" width="300" alt="Parametric Box render"><br><strong>Parametric Box</strong></a></td>
|
||||
<td align="center"><a href="parametric-keyring/README.md"><img src="parametric-keyring/images/raised-text.png" width="300" alt="Parametric Keyring render"><br><strong>Parametric Keyring</strong></a></td>
|
||||
<td align="center"><a href="s-hook/README.md"><img src="s-hook/images/round.png" width="300" alt="Parametric S-Hook render"><br><strong>Parametric S-Hook</strong></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="storage-system/README.md"><img src="storage-system/images/standard-box.png" width="300" alt="Parametric Storage System render"><br><strong>Parametric Storage System</strong></a></td>
|
||||
<td align="center"><a href="tea-bag-dispenser/README.md"><img src="tea-bag-dispenser/images/front-pull.png" width="300" alt="Tea-Bag Dispenser render"><br><strong>Tea-Bag Dispenser</strong></a></td>
|
||||
<td align="center"><a href="text-generator/README.md"><img src="text-generator/images/standing-text.png" width="300" alt="3D Text Generator render"><br><strong>3D Text Generator</strong></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="universal-clock-dial/README.md"><img src="universal-clock-dial/images/round.png" width="300" alt="Universal Clock Dial render"><br><strong>Universal Clock Dial</strong></a></td>
|
||||
<td align="center"><a href="universal-sign/README.md"><img src="universal-sign/images/flat-plate.png" width="300" alt="Universal Sign Generator render"><br><strong>Universal Sign Generator</strong></a></td>
|
||||
<td align="center"><a href="usb-stick-holder/README.md"><img src="usb-stick-holder/images/rectangular.png" width="300" alt="USB Stick Holder render"><br><strong>USB Stick Holder</strong></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Install OpenSCAD Nightly
|
||||
|
||||
These generators target a recent **OpenSCAD development snapshot / Nightly**. Download the appropriate snapshot from the official [OpenSCAD downloads page](https://openscad.org/downloads.html#snapshots).
|
||||
|
||||
Some text-based generators use the experimental `textmetrics` feature. Enable it in **Edit → Preferences → Features → textmetrics**, then restart or reopen the model if required.
|
||||
|
||||
## Use a generator
|
||||
|
||||
1. Clone or download this repository.
|
||||
2. Open the generator `.scad` file in OpenSCAD Nightly.
|
||||
3. Open **Window → Customizer**, change the parameters, and preview with **F5**.
|
||||
4. Press **F6** for the final render and export STL from the **File** menu.
|
||||
5. Slice the STL with a profile for your printer and material. Calibrate fit-sensitive parts before a full print.
|
||||
|
||||
Each project folder also contains curated STL examples under `stl-files/` and matching digital renders under `images/`.
|
||||
|
||||
## Validation and provenance
|
||||
|
||||
The public tree is generated from an explicit whitelist in the private canonical source repository. Every published SCAD file is rendered in isolation; every STL is checked for closed topology and bed bounds and is smoke-sliced with PrusaSlicer.
|
||||
|
||||
Generated from canonical source commit `481b1feae297b11311978708135ffef607aba5b6` using OpenSCAD Nightly. Public assets are updated in place rather than manually edited here.
|
||||
|
||||
## License
|
||||
|
||||
All SCAD sources, STL models, renders, and documentation in this repository are licensed under [Creative Commons Attribution-NonCommercial 4.0 International](LICENSE.md).
|
||||
47
cable-clips/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Adjustable Cable Clips
|
||||
|
||||

|
||||
|
||||
**Status:** Prototype; digitally validated but not cycle- or adhesive-tested.
|
||||
|
||||
The editable generator is [cable-clips.scad](cable-clips.scad). FDM printing; PETG is preferred where the retaining lips flex repeatedly.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
35 mm long clip for a nominal 6 mm cable with a 2.4 mm adhesive base.
|
||||
|
||||
**Fit and tolerance:** Measure the cable and use cable_clearance rather than forcing an oversized cable.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| 3 mm cable | [Download](stl-files/small.stl) | [Preview](images/small.png) |
|
||||
| 6 mm cable | [Download](stl-files/standard.stl) | [Preview](images/standard.png) |
|
||||
| 12 mm cable | [Download](stl-files/large.stl) | [Preview](images/large.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print with the flat adhesive base on the build plate.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Adhesive performance depends on the tape, plastic, and mounting surface.
|
||||
- Do not use for hazardous or load-bearing wiring.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
32
cable-clips/cable-clips.scad
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Cable Clips v1 prototype: adhesive-backed, open-top cable clip.
|
||||
// Kept separate from the storage family's dimensions.
|
||||
|
||||
/* [Cable and clip, mm] */
|
||||
cable_diameter = 6; // [3:0.5:12]
|
||||
clip_length = 35; // [15:1:70]
|
||||
base_thickness = 2.4; // [1.6:0.2:5]
|
||||
wall_thickness = 2.0; // [1.6:0.2:5]
|
||||
grip_lip = 1.2; // [0.6:0.2:2]
|
||||
cable_clearance = 0.4; // [0.2:0.1:1]
|
||||
|
||||
channel = cable_diameter + 2 * cable_clearance;
|
||||
width = channel + 2 * wall_thickness;
|
||||
wall_height = cable_diameter + cable_clearance + 2;
|
||||
|
||||
assert(cable_diameter >= 3 && cable_diameter <= 12, "Cable diameter must be 3..12 mm");
|
||||
assert(clip_length >= 15 && clip_length <= 70, "Clip length must be 15..70 mm");
|
||||
assert(base_thickness >= 1.6 && wall_thickness >= 1.6, "Clip walls/base are too thin");
|
||||
assert(grip_lip >= 0.6 && 2 * grip_lip < channel - 1, "Lip leaves too little cable entry");
|
||||
|
||||
union() {
|
||||
cube([clip_length, width, base_thickness]);
|
||||
translate([0, 0, base_thickness])
|
||||
cube([clip_length, wall_thickness, wall_height]);
|
||||
translate([0, width - wall_thickness, base_thickness])
|
||||
cube([clip_length, wall_thickness, wall_height]);
|
||||
translate([0, wall_thickness, base_thickness + wall_height - grip_lip])
|
||||
cube([clip_length, grip_lip, grip_lip]);
|
||||
translate([0, width - wall_thickness - grip_lip,
|
||||
base_thickness + wall_height - grip_lip])
|
||||
cube([clip_length, grip_lip, grip_lip]);
|
||||
}
|
||||
BIN
cable-clips/images/large.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
cable-clips/images/small.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cable-clips/images/standard.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cable-clips/stl-files/large.stl
Normal file
BIN
cable-clips/stl-files/small.stl
Normal file
BIN
cable-clips/stl-files/standard.stl
Normal file
47
cr80-card-holder/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# CR80 / ID-1 Card Holder
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated; physical card fit and flex life still require testing.
|
||||
|
||||
The editable generator is [cr80-card-holder.scad](cr80-card-holder.scad). FDM printing in PLA or PETG; PETG is preferred for repeated flexing.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
Portrait CR80 card, PETG Durable profile, flex tabs, top thumb notch, and top slot attachment.
|
||||
|
||||
**Fit and tolerance:** Defaults use 0.30 mm XY and 0.20 mm Z clearance. Test with a sacrificial card.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default portrait flex tabs | [Download](stl-files/default-flex-tabs.stl) | [Preview](images/default-flex-tabs.png) |
|
||||
| Landscape abuse-proof security rails | [Download](stl-files/landscape-security-rails.stl) | [Preview](images/landscape-security-rails.png) |
|
||||
| Full bezel with round attachment | [Download](stl-files/full-bezel-round-attachment.stl) | [Preview](images/full-bezel-round-attachment.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print with the broad back plate flat on the bed.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Card thickness and printer extrusion vary.
|
||||
- Retention tabs and attachments need physical fatigue and load testing.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
466
cr80-card-holder/cr80-card-holder.scad
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
/*
|
||||
CR80 / ID-1 CARD HOLDER GENERATOR — V2
|
||||
--------------------------------------
|
||||
One-piece FDM-printable holders for CR80 / ISO ID-1 cards.
|
||||
Designed for OpenSCAD Nightly, but deliberately avoids fragile/obscure
|
||||
experimental syntax so files remain easy to maintain and batch-render.
|
||||
|
||||
Nominal card size: 85.60 x 53.98 x 0.76 mm.
|
||||
Recommended print orientation: back plate flat on the bed.
|
||||
|
||||
V2 highlights:
|
||||
- multiple retention systems, including XY-flex snap tabs
|
||||
- slide-in and semi-permanent security variants
|
||||
- durable / abuse-proof construction profiles
|
||||
- rounded bumper frame
|
||||
- reinforced lanyard / chain attachments
|
||||
- optional dual attachments
|
||||
- tuneable clearances for PLA / PETG
|
||||
- support-free geometry as far as practical
|
||||
*/
|
||||
|
||||
/* [Card] */
|
||||
orientation = "Portrait"; // [Portrait,Landscape]
|
||||
card_width = 85.60; // [80:0.01:90]
|
||||
card_height = 53.98; // [50:0.01:60]
|
||||
card_thickness = 0.76; // [0.50:0.01:1.50]
|
||||
card_corner_radius = 3.18; // [2.0:0.01:4.0]
|
||||
xy_clearance = 0.30; // [0.10:0.05:0.80]
|
||||
z_clearance = 0.20; // [0.05:0.05:0.60]
|
||||
|
||||
/* [Material / Construction Profile] */
|
||||
material_profile = "PETG"; // [PLA,PETG,Custom]
|
||||
construction = "Durable"; // [Light,Standard,Durable,Abuse Proof]
|
||||
wall_width = 2.40; // [1.20:0.10:6.00]
|
||||
back_thickness = 1.60; // [0.80:0.10:4.00]
|
||||
outer_corner_radius = 5.00; // [2.0:0.25:12.0]
|
||||
front_edge_height = 0.70; // [0.30:0.10:2.00]
|
||||
bumper_enabled = true; // [true,false]
|
||||
bumper_extra_width = 1.20; // [0.40:0.10:3.50]
|
||||
bumper_height = 1.20; // [0.40:0.10:3.00]
|
||||
|
||||
/* [Retention] */
|
||||
retention = "Flex Tabs"; // [Corner Clips,Edge Clips,Side Detents,Flex Tabs,Slide Rails,Security Rails,Full Bezel,Friction Only]
|
||||
clip_overhang = 1.35; // [0.50:0.05:2.80]
|
||||
clip_length = 10.0; // [5:0.5:22]
|
||||
clip_thickness = 0.90; // [0.50:0.05:1.80]
|
||||
clip_entry_chamfer = 0.55; // [0.20:0.05:1.20]
|
||||
detent_depth = 0.45; // [0.15:0.05:1.00]
|
||||
detent_diameter = 3.0; // [1.5:0.25:5.0]
|
||||
slide_rail_overhang = 1.40; // [0.50:0.05:3.00]
|
||||
|
||||
/* [Flex Tabs] */
|
||||
flex_tab_length = 15.0; // [8:0.5:28]
|
||||
flex_tab_width = 2.20; // [1.20:0.10:4.00]
|
||||
flex_relief_gap = 0.75; // [0.40:0.05:1.50]
|
||||
flex_hook_depth = 0.70; // [0.25:0.05:1.40]
|
||||
flex_hook_length = 3.20; // [1.5:0.10:6.0]
|
||||
flex_tabs_per_side = 1; // [1,2]
|
||||
|
||||
/* [Security] */
|
||||
security_capture = 1.80; // [0.80:0.10:3.50]
|
||||
security_entry_gap = 0.45; // [0.20:0.05:1.20]
|
||||
security_release_notch = true; // [true,false]
|
||||
security_release_width = 7.0; // [4:0.5:14]
|
||||
|
||||
/* [Access / Removal] */
|
||||
thumb_notch = true; // [true,false]
|
||||
thumb_notch_side = "Top"; // [Top,Bottom,Left,Right]
|
||||
thumb_notch_diameter = 16.0; // [8:1:28]
|
||||
back_push_hole = false; // [true,false]
|
||||
back_push_hole_diameter = 18.0; // [10:1:30]
|
||||
|
||||
/* [Attachment] */
|
||||
attachment = "Slot"; // [None,Round,Slot,Dual Round,Dual Slot,Round + Slot]
|
||||
attachment_side = "Top"; // [Top,Bottom,Left,Right]
|
||||
second_attachment = false; // [true,false]
|
||||
attachment_width = 24.0; // [14:1:44]
|
||||
attachment_extension = 12.0; // [8:1:28]
|
||||
round_hole_diameter = 5.0; // [3:0.25:10]
|
||||
slot_length = 14.0; // [8:0.5:26]
|
||||
slot_width = 4.5; // [3:0.25:9]
|
||||
dual_hole_spacing = 10.0; // [6:0.5:22]
|
||||
attachment_reinforcement = 3.0; // [1.5:0.25:7]
|
||||
attachment_gussets = true; // [true,false]
|
||||
|
||||
/* [Preview / Quality] */
|
||||
show_card_preview = true; // [true,false]
|
||||
preview_explode = 0.0; // [0:0.5:12]
|
||||
quality = "Normal"; // [Draft,Normal,Fine]
|
||||
|
||||
// ---------- Quality ----------
|
||||
$fn = quality == "Draft" ? 28 : quality == "Fine" ? 96 : 56;
|
||||
|
||||
// ---------- Material-derived tuning ----------
|
||||
// These do not override your explicit dimensions; they set safe lower bounds.
|
||||
MAT_FLEX_MIN = material_profile == "PLA" ? 2.40 : material_profile == "PETG" ? 1.80 : 1.20;
|
||||
MAT_GAP_MIN = material_profile == "PLA" ? 0.70 : material_profile == "PETG" ? 0.55 : 0.40;
|
||||
|
||||
// ---------- Construction profile ----------
|
||||
PROFILE_WALL = construction == "Light" ? 1.60 :
|
||||
construction == "Standard" ? 2.20 :
|
||||
construction == "Durable" ? 3.00 : 3.80;
|
||||
PROFILE_BACK = construction == "Light" ? 1.00 :
|
||||
construction == "Standard" ? 1.40 :
|
||||
construction == "Durable" ? 2.00 : 2.60;
|
||||
PROFILE_RADIUS = construction == "Light" ? 3.50 :
|
||||
construction == "Standard" ? 4.50 :
|
||||
construction == "Durable" ? 5.50 : 6.50;
|
||||
PROFILE_ATTACH = construction == "Abuse Proof" ? 14.0 : construction == "Durable" ? 12.0 : 9.0;
|
||||
|
||||
WALL = max(wall_width, PROFILE_WALL);
|
||||
BACK = max(back_thickness, PROFILE_BACK);
|
||||
R_OUT = max(outer_corner_radius, PROFILE_RADIUS);
|
||||
FLEX_W = max(flex_tab_width, MAT_FLEX_MIN);
|
||||
FLEX_GAP = max(flex_relief_gap, MAT_GAP_MIN);
|
||||
ATT_EXT = max(attachment_extension, PROFILE_ATTACH);
|
||||
ATT_REINF = construction == "Abuse Proof" ? max(attachment_reinforcement, 4.2) :
|
||||
construction == "Durable" ? max(attachment_reinforcement, 3.4) : attachment_reinforcement;
|
||||
|
||||
// ---------- Derived card / holder geometry ----------
|
||||
raw_w = orientation == "Landscape" ? card_width : card_height;
|
||||
raw_h = orientation == "Landscape" ? card_height : card_width;
|
||||
pocket_w = raw_w + 2 * xy_clearance;
|
||||
pocket_h = raw_h + 2 * xy_clearance;
|
||||
outer_w = pocket_w + 2 * WALL;
|
||||
outer_h = pocket_h + 2 * WALL;
|
||||
|
||||
card_top_z = BACK + card_thickness + z_clearance;
|
||||
wall_top_z = card_top_z + front_edge_height;
|
||||
attach_t = max(wall_top_z, construction == "Abuse Proof" ? 3.6 : 2.8);
|
||||
bumper_w = construction == "Abuse Proof" ? max(bumper_extra_width, 1.8) : bumper_extra_width;
|
||||
bumper_h = construction == "Abuse Proof" ? max(bumper_height, 1.6) : bumper_height;
|
||||
|
||||
assert(xy_clearance >= 0.10, "xy_clearance is probably too small for FDM.");
|
||||
assert(WALL > max(clip_overhang, flex_hook_depth), "Wall is too narrow for selected retention geometry.");
|
||||
assert(slot_length > slot_width, "slot_length must be greater than slot_width.");
|
||||
assert(FLEX_W < WALL + 1.5, "Flex tab is unusually thick. Reduce flex_tab_width or increase wall_width.");
|
||||
|
||||
// ---------- 2D primitives ----------
|
||||
module rounded_rect_2d(w, h, r) {
|
||||
rr = min(r, min(w,h)/2 - 0.01);
|
||||
offset(r=rr)
|
||||
square([max(0.01,w - 2*rr), max(0.01,h - 2*rr)], center=true);
|
||||
}
|
||||
|
||||
module slot_2d(len, wid) {
|
||||
hull() {
|
||||
translate([-(len-wid)/2, 0]) circle(d=wid);
|
||||
translate([ +(len-wid)/2, 0]) circle(d=wid);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Attachment positioning ----------
|
||||
module at_attachment_side(side=attachment_side) {
|
||||
if (side == "Top")
|
||||
translate([0, outer_h/2 + ATT_EXT/2 - 1.0, 0]) children();
|
||||
else if (side == "Bottom")
|
||||
translate([0, -outer_h/2 - ATT_EXT/2 + 1.0, 0]) rotate([0,0,180]) children();
|
||||
else if (side == "Left")
|
||||
translate([-outer_w/2 - ATT_EXT/2 + 1.0, 0, 0]) rotate([0,0,90]) children();
|
||||
else
|
||||
translate([outer_w/2 + ATT_EXT/2 - 1.0, 0, 0]) rotate([0,0,-90]) children();
|
||||
}
|
||||
|
||||
function opposite_side(s) = s == "Top" ? "Bottom" : s == "Bottom" ? "Top" : s == "Left" ? "Right" : "Left";
|
||||
|
||||
module attachment_plate_2d() {
|
||||
rounded_rect_2d(attachment_width, ATT_EXT + 4.0,
|
||||
min(R_OUT, min(attachment_width, ATT_EXT+4)/2 - 0.1));
|
||||
}
|
||||
|
||||
module attachment_holes_2d() {
|
||||
if (attachment == "Round") {
|
||||
circle(d=round_hole_diameter);
|
||||
} else if (attachment == "Slot") {
|
||||
slot_2d(slot_length, slot_width);
|
||||
} else if (attachment == "Dual Round") {
|
||||
translate([-dual_hole_spacing/2,0]) circle(d=round_hole_diameter);
|
||||
translate([ dual_hole_spacing/2,0]) circle(d=round_hole_diameter);
|
||||
} else if (attachment == "Dual Slot") {
|
||||
translate([-dual_hole_spacing/2,0]) rotate(90) slot_2d(slot_length*0.65, slot_width);
|
||||
translate([ dual_hole_spacing/2,0]) rotate(90) slot_2d(slot_length*0.65, slot_width);
|
||||
} else if (attachment == "Round + Slot") {
|
||||
translate([0,-3.5]) slot_2d(slot_length, slot_width);
|
||||
translate([0, 4.5]) circle(d=round_hole_diameter);
|
||||
}
|
||||
}
|
||||
|
||||
module one_attachment(side=attachment_side) {
|
||||
if (attachment != "None") {
|
||||
at_attachment_side(side)
|
||||
difference() {
|
||||
union() {
|
||||
linear_extrude(attach_t) attachment_plate_2d();
|
||||
|
||||
// Local reinforcement rings distribute load around the opening.
|
||||
if (construction == "Durable" || construction == "Abuse Proof") {
|
||||
if (attachment == "Round")
|
||||
cylinder(h=attach_t, d=round_hole_diameter + 2*ATT_REINF);
|
||||
else if (attachment == "Slot")
|
||||
linear_extrude(attach_t) offset(r=ATT_REINF) slot_2d(slot_length, slot_width);
|
||||
else if (attachment == "Dual Round") {
|
||||
translate([-dual_hole_spacing/2,0,0]) cylinder(h=attach_t, d=round_hole_diameter + 2*ATT_REINF);
|
||||
translate([ dual_hole_spacing/2,0,0]) cylinder(h=attach_t, d=round_hole_diameter + 2*ATT_REINF);
|
||||
}
|
||||
}
|
||||
}
|
||||
translate([0,0,-0.1]) linear_extrude(attach_t + 0.2) attachment_holes_2d();
|
||||
}
|
||||
|
||||
// Broad triangular-ish roots reduce stress concentration where tab meets body.
|
||||
if (attachment_gussets)
|
||||
at_attachment_side(side)
|
||||
for (sx=[-1,1])
|
||||
hull() {
|
||||
translate([sx*(attachment_width/2-2.0), -ATT_EXT/2+2.0, 0])
|
||||
cylinder(h=min(attach_t, BACK+1.0), d=3.2);
|
||||
translate([sx*(outer_w < outer_h ? outer_w : outer_h)*0.10, -ATT_EXT/2-3.5, 0])
|
||||
cylinder(h=min(attach_t, BACK+1.0), d=3.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module attachments() {
|
||||
one_attachment(attachment_side);
|
||||
if (second_attachment && attachment != "None")
|
||||
one_attachment(opposite_side(attachment_side));
|
||||
}
|
||||
|
||||
// ---------- Base shell ----------
|
||||
module back_plate() {
|
||||
linear_extrude(BACK)
|
||||
rounded_rect_2d(outer_w, outer_h, R_OUT);
|
||||
}
|
||||
|
||||
module perimeter_walls(open_top=false) {
|
||||
difference() {
|
||||
translate([0,0,BACK-0.08])
|
||||
linear_extrude(wall_top_z - BACK + 0.08)
|
||||
rounded_rect_2d(outer_w, outer_h, R_OUT);
|
||||
|
||||
translate([0,0,BACK-0.05])
|
||||
linear_extrude(wall_top_z - BACK + 0.10)
|
||||
rounded_rect_2d(pocket_w, pocket_h, card_corner_radius + xy_clearance);
|
||||
|
||||
if (open_top)
|
||||
translate([0, outer_h/2 - WALL/2, BACK-0.1])
|
||||
cube([pocket_w + 0.6, WALL + 0.8, wall_top_z-BACK+0.3], center=true);
|
||||
}
|
||||
}
|
||||
|
||||
module bumper_frame() {
|
||||
if (bumper_enabled) {
|
||||
bh = min(BACK + bumper_h, wall_top_z);
|
||||
|
||||
if (retention == "Flex Tabs") {
|
||||
// Flex tabs need free space to deflect outward in XY. A continuous
|
||||
// side bumper would mechanically block them, so this mode uses
|
||||
// heavy top/bottom impact rails instead of a closed bumper ring.
|
||||
for (sy=[-1,1])
|
||||
translate([0, sy*(outer_h/2 + bumper_w/2 - 0.20), 0])
|
||||
linear_extrude(bh)
|
||||
rounded_rect_2d(outer_w + 2*bumper_w, bumper_w + 0.80,
|
||||
min((bumper_w+0.80)/2-0.01, R_OUT));
|
||||
} else {
|
||||
// Closed low bumper for rigid retention systems.
|
||||
difference() {
|
||||
linear_extrude(bh)
|
||||
rounded_rect_2d(outer_w + 2*bumper_w, outer_h + 2*bumper_w, R_OUT + bumper_w);
|
||||
translate([0,0,-0.1])
|
||||
linear_extrude(bh+0.2)
|
||||
rounded_rect_2d(outer_w - 0.20, outer_h - 0.20, max(0.5,R_OUT-0.10));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Traditional roof-style retainers ----------
|
||||
module edge_roof_x(y, len=clip_length) {
|
||||
translate([0, y, card_top_z + clip_thickness/2])
|
||||
cube([len, WALL + clip_overhang, clip_thickness], center=true);
|
||||
}
|
||||
|
||||
module edge_roof_y(x, len=clip_length) {
|
||||
translate([x, 0, card_top_z + clip_thickness/2])
|
||||
cube([WALL + clip_overhang, len, clip_thickness], center=true);
|
||||
}
|
||||
|
||||
module corner_clips() {
|
||||
x = pocket_w/2 + (WALL-clip_overhang)/2;
|
||||
y = pocket_h/2 + (WALL-clip_overhang)/2;
|
||||
for (sx=[-1,1], sy=[-1,1])
|
||||
translate([sx*x, sy*y, card_top_z + clip_thickness/2])
|
||||
cube([WALL + clip_overhang, WALL + clip_overhang, clip_thickness], center=true);
|
||||
}
|
||||
|
||||
module edge_clips() {
|
||||
edge_roof_x( pocket_h/2 + (WALL-clip_overhang)/2);
|
||||
edge_roof_x(-pocket_h/2 - (WALL-clip_overhang)/2);
|
||||
edge_roof_y( pocket_w/2 + (WALL-clip_overhang)/2);
|
||||
edge_roof_y(-pocket_w/2 - (WALL-clip_overhang)/2);
|
||||
}
|
||||
|
||||
// ---------- Side detents ----------
|
||||
module side_detents() {
|
||||
d = detent_depth;
|
||||
for (sy=[-0.24,0.24]) {
|
||||
translate([ pocket_w/2 + detent_diameter/2 - d, pocket_h*sy, BACK + card_thickness*0.58])
|
||||
rotate([90,0,0]) cylinder(h=detent_diameter, d=detent_diameter, center=true);
|
||||
translate([-pocket_w/2 - detent_diameter/2 + d, pocket_h*sy, BACK + card_thickness*0.58])
|
||||
rotate([90,0,0]) cylinder(h=detent_diameter, d=detent_diameter, center=true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- XY flex tabs ----------
|
||||
// A long leaf is isolated from each side wall by relief cuts. The rounded hook
|
||||
// protrudes into the pocket. During insertion the leaf bends OUTWARD in XY,
|
||||
// so flexing does not rely primarily on Z-layer adhesion.
|
||||
module flex_tab_positive(side=1, y0=0) {
|
||||
xwall = side*(pocket_w/2 + FLEX_W/2);
|
||||
translate([xwall, y0, BACK])
|
||||
cube([FLEX_W, flex_tab_length, wall_top_z-BACK], center=false);
|
||||
}
|
||||
|
||||
module flex_hook(side=1, y0=0) {
|
||||
x = side*(pocket_w/2 + flex_hook_depth/2 - 0.08);
|
||||
y = y0 + flex_tab_length/2 - flex_hook_length/2;
|
||||
translate([x,y,BACK + card_thickness*0.58])
|
||||
rotate([90,0,0]) cylinder(h=flex_hook_length, d=2*flex_hook_depth, center=true);
|
||||
}
|
||||
|
||||
module flex_tab_relief_cuts() {
|
||||
tab_positions = flex_tabs_per_side == 2 ? [-pocket_h*0.24-flex_tab_length/2, pocket_h*0.24-flex_tab_length/2] : [-flex_tab_length/2];
|
||||
for (side=[-1,1], y0=tab_positions) {
|
||||
// Along the inner/outer side of the cantilever: leave its root connected.
|
||||
x = side*(pocket_w/2 + WALL/2);
|
||||
translate([x, y0-flex_relief_gap/2, BACK-0.1])
|
||||
cube([WALL+1.0, FLEX_GAP, wall_top_z-BACK+0.3], center=true);
|
||||
translate([x, y0+flex_tab_length+flex_relief_gap/2, BACK-0.1])
|
||||
cube([WALL+1.0, FLEX_GAP, wall_top_z-BACK+0.3], center=true);
|
||||
// Outer clearance pocket lets the leaf deflect outward.
|
||||
translate([side*(pocket_w/2 + WALL - FLEX_GAP/2), y0+flex_tab_length*0.58, BACK-0.1])
|
||||
cube([FLEX_GAP, flex_tab_length*0.78, wall_top_z-BACK+0.3], center=true);
|
||||
}
|
||||
}
|
||||
|
||||
module flex_tabs() {
|
||||
tab_positions = flex_tabs_per_side == 2 ? [-pocket_h*0.24-flex_tab_length/2, pocket_h*0.24-flex_tab_length/2] : [-flex_tab_length/2];
|
||||
for (side=[-1,1], y0=tab_positions)
|
||||
flex_hook(side,y0);
|
||||
}
|
||||
|
||||
// ---------- Slide rails ----------
|
||||
module slide_rails(security=false) {
|
||||
over = security ? max(slide_rail_overhang, security_capture) : slide_rail_overhang;
|
||||
rail_h = max(clip_thickness, 0.8);
|
||||
x = pocket_w/2 + (WALL-over)/2;
|
||||
|
||||
for (sx=[-1,1])
|
||||
translate([sx*x, -WALL*0.15, card_top_z + rail_h/2])
|
||||
cube([WALL + over, pocket_h - 3.5, rail_h], center=true);
|
||||
|
||||
// Bottom capture / stop.
|
||||
translate([0, -pocket_h/2 - (WALL-over)/2, card_top_z + rail_h/2])
|
||||
cube([pocket_w*0.60, WALL + over, rail_h], center=true);
|
||||
|
||||
// Entrance retention.
|
||||
if (!security) {
|
||||
for (sx=[-1,1])
|
||||
translate([sx*(pocket_w/2 - 5), pocket_h/2 - 2.0, BACK + card_thickness*0.58])
|
||||
sphere(d=max(1.6, detent_diameter*0.62));
|
||||
} else {
|
||||
// Stronger shoulders at entry: semi-permanent but serviceable.
|
||||
for (sx=[-1,1])
|
||||
translate([sx*(pocket_w/2 - 5.5), pocket_h/2 - 2.4, BACK + card_thickness*0.60])
|
||||
sphere(d=max(2.0, security_capture*1.55));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Full bezel ----------
|
||||
module full_bezel() {
|
||||
// Continuous front frame. Card is intended to slide in from the top opening.
|
||||
bezel_t = max(clip_thickness, 0.9);
|
||||
difference() {
|
||||
translate([0,0,card_top_z])
|
||||
linear_extrude(bezel_t)
|
||||
rounded_rect_2d(outer_w, outer_h, R_OUT);
|
||||
translate([0,0,card_top_z-0.05])
|
||||
linear_extrude(bezel_t+0.10)
|
||||
rounded_rect_2d(max(1,pocket_w-2*security_capture), max(1,pocket_h-2*security_capture), max(0.5,card_corner_radius-0.4));
|
||||
// top loading opening
|
||||
translate([0, outer_h/2-WALL/2, card_top_z-0.1])
|
||||
cube([pocket_w*0.78, WALL+1.0, bezel_t+0.3], center=true);
|
||||
}
|
||||
}
|
||||
|
||||
module retention_features() {
|
||||
if (retention == "Corner Clips") corner_clips();
|
||||
else if (retention == "Edge Clips") edge_clips();
|
||||
else if (retention == "Side Detents") side_detents();
|
||||
else if (retention == "Flex Tabs") flex_tabs();
|
||||
else if (retention == "Slide Rails") slide_rails(false);
|
||||
else if (retention == "Security Rails") slide_rails(true);
|
||||
else if (retention == "Full Bezel") full_bezel();
|
||||
}
|
||||
|
||||
// ---------- Access cuts ----------
|
||||
module thumb_notch_cut() {
|
||||
if (thumb_notch) {
|
||||
h = wall_top_z-BACK + clip_thickness + 0.6;
|
||||
if (thumb_notch_side == "Top")
|
||||
translate([0, pocket_h/2 + WALL*0.30, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter);
|
||||
else if (thumb_notch_side == "Bottom")
|
||||
translate([0, -pocket_h/2 - WALL*0.30, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter);
|
||||
else if (thumb_notch_side == "Left")
|
||||
translate([-pocket_w/2 - WALL*0.30, 0, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter);
|
||||
else
|
||||
translate([pocket_w/2 + WALL*0.30, 0, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter);
|
||||
}
|
||||
}
|
||||
|
||||
module back_push_cut() {
|
||||
if (back_push_hole)
|
||||
translate([0,0,-0.1]) cylinder(h=BACK+0.2, d=back_push_hole_diameter);
|
||||
}
|
||||
|
||||
module security_release_cut() {
|
||||
if (retention == "Security Rails" && security_release_notch) {
|
||||
// Narrow service notch at entry. Enough for a fingernail/small plastic tool,
|
||||
// but much less likely to release accidentally than an open rail system.
|
||||
translate([0, pocket_h/2 + WALL*0.15, BACK + card_thickness*0.35])
|
||||
cube([security_release_width, WALL+1.2, wall_top_z-BACK+clip_thickness+0.6], center=true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Final holder ----------
|
||||
module holder() {
|
||||
open_top = retention == "Slide Rails" || retention == "Security Rails" || retention == "Full Bezel";
|
||||
|
||||
difference() {
|
||||
union() {
|
||||
back_plate();
|
||||
perimeter_walls(open_top);
|
||||
bumper_frame();
|
||||
retention_features();
|
||||
attachments();
|
||||
}
|
||||
|
||||
// Relief slots are subtractive and belong to Flex Tabs only.
|
||||
if (retention == "Flex Tabs") flex_tab_relief_cuts();
|
||||
|
||||
thumb_notch_cut();
|
||||
back_push_cut();
|
||||
security_release_cut();
|
||||
}
|
||||
}
|
||||
|
||||
module card_preview() {
|
||||
if (show_card_preview)
|
||||
%translate([0,0,BACK + preview_explode])
|
||||
linear_extrude(card_thickness)
|
||||
rounded_rect_2d(raw_w, raw_h, card_corner_radius);
|
||||
}
|
||||
|
||||
holder();
|
||||
card_preview();
|
||||
BIN
cr80-card-holder/images/default-flex-tabs.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
cr80-card-holder/images/full-bezel-round-attachment.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
cr80-card-holder/images/landscape-security-rails.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
cr80-card-holder/stl-files/default-flex-tabs.stl
Normal file
BIN
cr80-card-holder/stl-files/full-bezel-round-attachment.stl
Normal file
BIN
cr80-card-holder/stl-files/landscape-security-rails.stl
Normal file
46
decorative-insects/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Decorative Insects
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated for representative 3D output.
|
||||
|
||||
The editable generator is [decorative-insects.scad](decorative-insects.scad). FDM reliefs and flat models; the generator also supports laser, vinyl, and engraving output.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
Free-form butterfly relief at approximately 100 mm wingspan and 52 mm body length.
|
||||
|
||||
**Fit and tolerance:** Adapt Min_Feature to the nozzle, kerf, or cutting tool.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default butterfly relief | [Download](stl-files/butterfly-relief.stl) | [Preview](images/butterfly-relief.png) |
|
||||
| Flat emperor dragonfly | [Download](stl-files/dragonfly-flat.stl) | [Preview](images/dragonfly-flat.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print flat or relief models with the broad rear face on the bed.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Fine veins and stencil bridges require process-specific testing.
|
||||
- The included public package contains STL examples only.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
1051
decorative-insects/decorative-insects.scad
Normal file
BIN
decorative-insects/images/butterfly-relief.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
decorative-insects/images/dragonfly-flat.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
decorative-insects/stl-files/butterfly-relief.stl
Normal file
BIN
decorative-insects/stl-files/dragonfly-flat.stl
Normal file
47
parametric-box/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Parametric Box
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated for three lid systems; physical mechanisms require calibration.
|
||||
|
||||
The editable generator is [parametric-box.scad](parametric-box.scad). FDM printing in PLA or PETG.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
100 × 70 × 40 mm inside volume, 2.4 mm walls, rounded corners, and inner-lip lid.
|
||||
|
||||
**Fit and tolerance:** Lid clearance defaults to 0.25 mm; sliding, hinge, snap, and magnet fits are separate controls.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default inner-lip layout | [Download](stl-files/inner-lip.stl) | [Preview](images/inner-lip.png) |
|
||||
| Sliding lid with pull tab | [Download](stl-files/sliding-lid.stl) | [Preview](images/sliding-lid.png) |
|
||||
| Hinged lid with front snap | [Download](stl-files/hinged-snap.stl) | [Preview](images/hinged-snap.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print the box on its bottom and detached lids on their largest flat face.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Combined layouts can exceed a nominal 220 mm usable bed width.
|
||||
- Hinges, snaps, and magnets require physical fit testing.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
parametric-box/images/hinged-snap.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
parametric-box/images/inner-lip.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
parametric-box/images/sliding-lid.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
627
parametric-box/parametric-box.scad
Normal file
|
|
@ -0,0 +1,627 @@
|
|||
/*
|
||||
===============================================================================
|
||||
PARAMETRIC BOX GENERATOR v1.0
|
||||
Target: OpenSCAD Nightly (kept dependency-free and largely 2021.01 compatible)
|
||||
Purpose: Advanced configurable boxes/enclosures for FDM 3D printing.
|
||||
|
||||
Design goals:
|
||||
- Clear distinction between internal and external dimensions
|
||||
- Independent fit tolerances for lids, sliders, hinges and magnets
|
||||
- Multiple lid systems
|
||||
- Rounded or chamfered corners
|
||||
- Dividers, finger notches, floor patterns, ribs, feet, labels, magnets
|
||||
- Print/export layouts and assembled previews
|
||||
- Sensible minimum guards and diagnostics
|
||||
===============================================================================
|
||||
*/
|
||||
|
||||
/* [01 - Output / visning] */
|
||||
Output = "Both - print layout"; // [Box only,Lid only,Both - print layout,Assembled preview,Exploded preview]
|
||||
Show_Diagnostics = true;
|
||||
Preview_Quality = 48; // [12:4:128]
|
||||
Render_Quality = 96; // [24:8:256]
|
||||
Print_Gap = 12; // [2:1:50]
|
||||
Explode_Distance = 18; // [5:1:80]
|
||||
|
||||
/* [02 - Grundmål] */
|
||||
Dimension_Mode = "Inside"; // [Inside,Outside]
|
||||
Box_Width = 100; // [10:1:400]
|
||||
Box_Depth = 70; // [10:1:400]
|
||||
Box_Height = 40; // [5:1:250]
|
||||
Wall_Thickness = 2.4; // [0.8:0.1:10]
|
||||
Bottom_Thickness = 2.4; // [0.8:0.1:12]
|
||||
|
||||
/* [03 - Hjørner og vægge] */
|
||||
Corner_Style = "Rounded"; // [Rounded,Chamfered,Square]
|
||||
Corner_Radius = 7; // [0:0.5:50]
|
||||
Chamfer_Size = 5; // [0:0.5:30]
|
||||
Top_Rim_Thickness = 0; // [0:0.2:5]
|
||||
Top_Rim_Height = 0; // [0:0.2:8]
|
||||
Outer_Ribs = false;
|
||||
Rib_Width = 2.0; // [0.8:0.2:8]
|
||||
Rib_Depth = 0.8; // [0.2:0.1:4]
|
||||
Rib_Spacing = 15; // [5:1:50]
|
||||
|
||||
/* [04 - Bund] */
|
||||
Floor_Style = "Solid"; // [Solid,Round holes,Hex-ish holes,Slots]
|
||||
Floor_Hole_Size = 5; // [1:0.5:20]
|
||||
Floor_Hole_Spacing = 9;// [3:0.5:30]
|
||||
Floor_Edge_Margin = 4; // [1:0.5:20]
|
||||
Feet = "None"; // [None,Four pads,Full perimeter]
|
||||
Foot_Height = 1.2; // [0.4:0.2:5]
|
||||
Foot_Size = 8; // [3:1:25]
|
||||
|
||||
/* [05 - Lågtype] */
|
||||
Lid_Type = "Inner lip"; // [None,Inner lip,Outer cap,Sliding,Hinged]
|
||||
Lid_Thickness = 2.4; // [0.8:0.1:10]
|
||||
Lid_Clearance = 0.25; // [0:0.05:1.5]
|
||||
Lid_Overhang = 0.8; // [0:0.2:8]
|
||||
Lip_Height = 5; // [1:0.5:20]
|
||||
Lip_Thickness = 1.6; // [0.8:0.1:5]
|
||||
Lid_Corner_Radius_Adjust = 0; // [-5:0.5:10]
|
||||
|
||||
/* [06 - Skydelåg] */
|
||||
Slide_Direction = "Width"; // [Width,Depth]
|
||||
Slide_Clearance = 0.30; // [0.05:0.05:1.5]
|
||||
Slide_Rail_Height = 2.2; // [1:0.2:6]
|
||||
Slide_Rail_Depth = 1.5; // [0.8:0.1:5]
|
||||
Slide_Stop = true;
|
||||
Slide_Grip = "Finger notch"; // [None,Finger notch,Pull tab]
|
||||
Slide_Grip_Size = 14; // [5:1:40]
|
||||
|
||||
/* [07 - Hængslet låg] */
|
||||
Hinge_Knuckles = 5; // [3:2:9]
|
||||
Hinge_Outer_Diameter = 6; // [3:0.5:14]
|
||||
Hinge_Pin_Diameter = 2.0; // [1:0.1:6]
|
||||
Hinge_Pin_Clearance = 0.25; // [0.05:0.05:1]
|
||||
Hinge_Gap = 0.35; // [0.1:0.05:1.5]
|
||||
Hinge_Axis_Offset = 1.0; // [-3:0.5:5]
|
||||
Hinge_Lid_Angle = 110; // [0:5:180]
|
||||
Hinge_Add_Pin_Model = true;
|
||||
|
||||
/* [08 - Lukning] */
|
||||
Closure = "None"; // [None,Front snap,Magnets]
|
||||
Snap_Width = 14; // [5:1:35]
|
||||
Snap_Thickness = 1.2; // [0.6:0.1:3]
|
||||
Snap_Engagement = 0.7; // [0.2:0.1:2]
|
||||
Snap_Clearance = 0.25; // [0.05:0.05:1]
|
||||
Magnet_Diameter = 6.2; // [2:0.1:15]
|
||||
Magnet_Thickness = 2.2;// [1:0.1:8]
|
||||
Magnet_Clearance = 0.15; // [0:0.05:0.8]
|
||||
Magnet_Count = 2; // [1:1:4]
|
||||
Magnet_Edge_Offset = 10; // [4:1:30]
|
||||
|
||||
/* [09 - Rumdelere] */
|
||||
Dividers = "None"; // [None,Grid X,Grid Y,Grid X+Y]
|
||||
Divider_Count_X = 2; // [1:1:12]
|
||||
Divider_Count_Y = 2; // [1:1:12]
|
||||
Divider_Thickness = 1.6; // [0.8:0.1:5]
|
||||
Divider_Height = 25; // [2:1:200]
|
||||
Divider_Bottom_Gap = 0; // [0:0.2:5]
|
||||
Divider_Top_Gap = 2; // [0:0.5:20]
|
||||
|
||||
/* [10 - Fingerudskæringer] */
|
||||
Finger_Notch = "None"; // [None,Front,Front+Back,All walls]
|
||||
Finger_Notch_Width = 24; // [6:1:80]
|
||||
Finger_Notch_Depth = 8; // [2:1:30]
|
||||
Finger_Notch_Radius = 10;// [3:1:30]
|
||||
|
||||
/* [11 - Label / tekst] */
|
||||
Label_Mode = "None"; // [None,Embossed lid,Debossed lid,Embossed front,Debossed front]
|
||||
Label_Text = "BOX";
|
||||
Label_Font = "Liberation Sans:style=Bold";
|
||||
Label_Size = 12; // [3:1:60]
|
||||
Label_Depth = 0.6; // [0.2:0.1:3]
|
||||
Label_Rotation = 0; // [0:90:270]
|
||||
|
||||
/* [12 - Generelle printerparametre] */
|
||||
General_Clearance = 0.25; // [0:0.05:1]
|
||||
Elephant_Foot_Compensation = 0.20; // [0:0.05:1]
|
||||
Min_Feature = 0.8; // [0.4:0.1:2]
|
||||
Epsilon = 0.02;
|
||||
|
||||
/* [Hidden] */
|
||||
$fn = $preview ? Preview_Quality : Render_Quality;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Derived dimensions
|
||||
// ---------------------------------------------------------------------------
|
||||
IW = Dimension_Mode == "Inside" ? Box_Width : max(Min_Feature, Box_Width - 2*Wall_Thickness);
|
||||
ID = Dimension_Mode == "Inside" ? Box_Depth : max(Min_Feature, Box_Depth - 2*Wall_Thickness);
|
||||
IH = Dimension_Mode == "Inside" ? Box_Height : max(Min_Feature, Box_Height - Bottom_Thickness);
|
||||
OW = Dimension_Mode == "Inside" ? Box_Width + 2*Wall_Thickness : Box_Width;
|
||||
OD = Dimension_Mode == "Inside" ? Box_Depth + 2*Wall_Thickness : Box_Depth;
|
||||
OH = Dimension_Mode == "Inside" ? Box_Height + Bottom_Thickness : Box_Height;
|
||||
|
||||
OR = Corner_Style == "Rounded" ? min(Corner_Radius, min(OW,OD)/2-Epsilon) : 0;
|
||||
IR = max(0, OR - Wall_Thickness);
|
||||
LID_OR = max(0, OR + Lid_Corner_Radius_Adjust);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility functions/modules
|
||||
// ---------------------------------------------------------------------------
|
||||
function clamp(v,lo,hi) = min(max(v,lo),hi);
|
||||
function bool_has(s, token) = len(search(token,s)) > 0;
|
||||
|
||||
module rounded_rect_2d(w,d,r){
|
||||
rr = clamp(r,0,min(w,d)/2-Epsilon);
|
||||
if(rr <= Epsilon)
|
||||
square([w,d],center=true);
|
||||
else
|
||||
offset(r=rr) square([w-2*rr,d-2*rr],center=true);
|
||||
}
|
||||
|
||||
module chamfer_rect_2d(w,d,c){
|
||||
cc=clamp(c,0,min(w,d)/2-Epsilon);
|
||||
polygon(points=[
|
||||
[-w/2+cc,-d/2], [w/2-cc,-d/2], [w/2,-d/2+cc],
|
||||
[w/2,d/2-cc], [w/2-cc,d/2], [-w/2+cc,d/2],
|
||||
[-w/2,d/2-cc], [-w/2,-d/2+cc]
|
||||
]);
|
||||
}
|
||||
|
||||
module footprint_2d(w,d,r=OR){
|
||||
if(Corner_Style=="Rounded") rounded_rect_2d(w,d,r);
|
||||
else if(Corner_Style=="Chamfered") chamfer_rect_2d(w,d,Chamfer_Size);
|
||||
else square([w,d],center=true);
|
||||
}
|
||||
|
||||
module prism(w,d,h,r=OR){
|
||||
linear_extrude(height=h) footprint_2d(w,d,r);
|
||||
}
|
||||
|
||||
module rounded_prism_custom(w,d,h,r){
|
||||
linear_extrude(height=h) rounded_rect_2d(w,d,r);
|
||||
}
|
||||
|
||||
module shell_body(){
|
||||
difference(){
|
||||
union(){
|
||||
prism(OW,OD,OH,OR);
|
||||
if(Top_Rim_Thickness>0 && Top_Rim_Height>0)
|
||||
translate([0,0,OH-Top_Rim_Height])
|
||||
linear_extrude(Top_Rim_Height)
|
||||
difference(){
|
||||
offset(delta=Top_Rim_Thickness) footprint_2d(OW,OD,OR);
|
||||
footprint_2d(OW,OD,OR);
|
||||
}
|
||||
}
|
||||
translate([0,0,Bottom_Thickness])
|
||||
linear_extrude(OH-Bottom_Thickness+Epsilon)
|
||||
if(Corner_Style=="Rounded") rounded_rect_2d(IW,ID,IR);
|
||||
else if(Corner_Style=="Chamfered") chamfer_rect_2d(IW,ID,max(0,Chamfer_Size-Wall_Thickness));
|
||||
else square([IW,ID],center=true);
|
||||
finger_notch_cuts();
|
||||
floor_pattern_cuts();
|
||||
magnet_body_cuts();
|
||||
if(Label_Mode=="Debossed front") front_label(cut=true);
|
||||
if(Elephant_Foot_Compensation>0)
|
||||
elephant_foot_cut();
|
||||
}
|
||||
}
|
||||
|
||||
module elephant_foot_cut(){
|
||||
// Tiny 45-ish relief around bottom perimeter; improves first-layer fit/appearance.
|
||||
h=min(Elephant_Foot_Compensation,Bottom_Thickness/2);
|
||||
if(h>0)
|
||||
difference(){
|
||||
linear_extrude(h) offset(delta=Epsilon) footprint_2d(OW,OD,OR);
|
||||
translate([0,0,-Epsilon])
|
||||
linear_extrude(h+2*Epsilon) offset(delta=-Elephant_Foot_Compensation) footprint_2d(OW,OD,OR);
|
||||
}
|
||||
}
|
||||
|
||||
module floor_pattern_cuts(){
|
||||
if(Floor_Style!="Solid"){
|
||||
usable_w=max(0,IW-2*Floor_Edge_Margin);
|
||||
usable_d=max(0,ID-2*Floor_Edge_Margin);
|
||||
nx=max(1,floor(usable_w/Floor_Hole_Spacing));
|
||||
ny=max(1,floor(usable_d/Floor_Hole_Spacing));
|
||||
for(ix=[0:nx-1], iy=[0:ny-1]){
|
||||
x=(ix-(nx-1)/2)*Floor_Hole_Spacing;
|
||||
y=(iy-(ny-1)/2)*Floor_Hole_Spacing;
|
||||
stagger = Floor_Style=="Hex-ish holes" && (iy%2==1) ? Floor_Hole_Spacing/2 : 0;
|
||||
translate([x+stagger,y,-Epsilon])
|
||||
if(Floor_Style=="Slots")
|
||||
linear_extrude(Bottom_Thickness+2*Epsilon)
|
||||
hull(){
|
||||
translate([-Floor_Hole_Size/2,0]) circle(d=Floor_Hole_Size/2);
|
||||
translate([ Floor_Hole_Size/2,0]) circle(d=Floor_Hole_Size/2);
|
||||
}
|
||||
else
|
||||
cylinder(h=Bottom_Thickness+2*Epsilon,d=Floor_Hole_Size,$fn=Floor_Style=="Hex-ish holes"?6:$fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module finger_notch_one(side){
|
||||
z0=OH-Finger_Notch_Depth;
|
||||
if(side=="front")
|
||||
translate([0,-OD/2-Epsilon,z0+Finger_Notch_Radius]) rotate([90,0,0])
|
||||
hull(){
|
||||
cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius,center=false);
|
||||
translate([Finger_Notch_Width/2-Finger_Notch_Radius,0,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([-Finger_Notch_Width/2+Finger_Notch_Radius,0,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
}
|
||||
if(side=="back")
|
||||
translate([0,OD/2+Epsilon,z0+Finger_Notch_Radius]) rotate([-90,0,0])
|
||||
hull(){
|
||||
cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([Finger_Notch_Width/2-Finger_Notch_Radius,0,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([-Finger_Notch_Width/2+Finger_Notch_Radius,0,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
}
|
||||
if(side=="left")
|
||||
translate([-OW/2-Epsilon,0,z0+Finger_Notch_Radius]) rotate([0,90,0])
|
||||
hull(){
|
||||
cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([0,Finger_Notch_Width/2-Finger_Notch_Radius,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([0,-Finger_Notch_Width/2+Finger_Notch_Radius,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
}
|
||||
if(side=="right")
|
||||
translate([OW/2+Epsilon,0,z0+Finger_Notch_Radius]) rotate([0,-90,0])
|
||||
hull(){
|
||||
cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([0,Finger_Notch_Width/2-Finger_Notch_Radius,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
translate([0,-Finger_Notch_Width/2+Finger_Notch_Radius,0]) cylinder(h=Wall_Thickness+2*Epsilon,r=Finger_Notch_Radius);
|
||||
}
|
||||
}
|
||||
|
||||
module finger_notch_cuts(){
|
||||
if(Finger_Notch=="Front") finger_notch_one("front");
|
||||
if(Finger_Notch=="Front+Back") { finger_notch_one("front"); finger_notch_one("back"); }
|
||||
if(Finger_Notch=="All walls") { finger_notch_one("front"); finger_notch_one("back"); finger_notch_one("left"); finger_notch_one("right"); }
|
||||
}
|
||||
|
||||
module divider_geometry(){
|
||||
dh=min(Divider_Height,IH-Divider_Top_Gap);
|
||||
z0=Bottom_Thickness+Divider_Bottom_Gap;
|
||||
if(dh>0){
|
||||
if(Dividers=="Grid X" || Dividers=="Grid X+Y")
|
||||
for(i=[1:Divider_Count_X]){
|
||||
x=-IW/2 + i*IW/(Divider_Count_X+1);
|
||||
translate([x,0,z0]) cube([Divider_Thickness,ID,dh],center=false);
|
||||
}
|
||||
if(Dividers=="Grid Y" || Dividers=="Grid X+Y")
|
||||
for(i=[1:Divider_Count_Y]){
|
||||
y=-ID/2 + i*ID/(Divider_Count_Y+1);
|
||||
translate([-IW/2,y,z0]) cube([IW,Divider_Thickness,dh],center=false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module outer_ribs(){
|
||||
if(Outer_Ribs){
|
||||
nx=max(1,floor((OW-2*OR)/Rib_Spacing));
|
||||
ny=max(1,floor((OD-2*OR)/Rib_Spacing));
|
||||
for(i=[0:nx]){
|
||||
x=-OW/2+OR+i*(OW-2*OR)/max(1,nx);
|
||||
translate([x,-OD/2-Rib_Depth,Bottom_Thickness]) cube([Rib_Width,Rib_Depth,OH-Bottom_Thickness],center=false);
|
||||
translate([x,OD/2,Bottom_Thickness]) cube([Rib_Width,Rib_Depth,OH-Bottom_Thickness],center=false);
|
||||
}
|
||||
for(i=[0:ny]){
|
||||
y=-OD/2+OR+i*(OD-2*OR)/max(1,ny);
|
||||
translate([-OW/2-Rib_Depth,y,Bottom_Thickness]) cube([Rib_Depth,Rib_Width,OH-Bottom_Thickness],center=false);
|
||||
translate([OW/2,y,Bottom_Thickness]) cube([Rib_Depth,Rib_Width,OH-Bottom_Thickness],center=false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module feet(){
|
||||
if(Feet=="Four pads"){
|
||||
for(x=[-1,1],y=[-1,1])
|
||||
translate([x*(OW/2-Foot_Size/2-OR/3),y*(OD/2-Foot_Size/2-OR/3),-Foot_Height])
|
||||
rounded_prism_custom(Foot_Size,Foot_Size,Foot_Height,min(Foot_Size/3,OR/2));
|
||||
} else if(Feet=="Full perimeter"){
|
||||
translate([0,0,-Foot_Height]) linear_extrude(Foot_Height)
|
||||
difference(){
|
||||
footprint_2d(OW,OD,OR);
|
||||
offset(delta=-Foot_Size/2) footprint_2d(OW,OD,OR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module front_label(cut=false){
|
||||
d=cut ? Label_Depth+Epsilon : Label_Depth;
|
||||
// Text lies on front Y face.
|
||||
translate([0,-OD/2-(cut?-Epsilon:d),OH/2]) rotate([90,0,0]) rotate([0,0,Label_Rotation])
|
||||
linear_extrude(d+Epsilon)
|
||||
text(Label_Text,size=Label_Size,font=Label_Font,halign="center",valign="center");
|
||||
}
|
||||
|
||||
module lid_label(cut=false, top_z=0){
|
||||
d=cut ? Label_Depth+Epsilon : Label_Depth;
|
||||
translate([0,0,top_z-(cut?Label_Depth:0)]) rotate([0,0,Label_Rotation])
|
||||
linear_extrude(d+Epsilon)
|
||||
text(Label_Text,size=Label_Size,font=Label_Font,halign="center",valign="center");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Magnet pockets
|
||||
// ---------------------------------------------------------------------------
|
||||
function magnet_x(i,n) = n<=1 ? 0 : -OW/2+Magnet_Edge_Offset + i*(OW-2*Magnet_Edge_Offset)/(n-1);
|
||||
|
||||
module magnet_body_cuts(){
|
||||
if(Closure=="Magnets" && (Lid_Type=="Inner lip" || Lid_Type=="Outer cap" || Lid_Type=="Hinged"))
|
||||
for(i=[0:Magnet_Count-1])
|
||||
translate([magnet_x(i,Magnet_Count),-OD/2+Wall_Thickness/2,OH-Magnet_Thickness/2])
|
||||
rotate([90,0,0]) cylinder(h=Wall_Thickness+2*Epsilon,d=Magnet_Diameter+2*Magnet_Clearance,center=true);
|
||||
}
|
||||
|
||||
module magnet_lid_cuts(lid_y=-OD/2+Wall_Thickness/2,lid_z=0){
|
||||
if(Closure=="Magnets")
|
||||
for(i=[0:Magnet_Count-1])
|
||||
translate([magnet_x(i,Magnet_Count),lid_y,lid_z])
|
||||
rotate([90,0,0]) cylinder(h=max(Lip_Thickness,Wall_Thickness)+2*Epsilon,d=Magnet_Diameter+2*Magnet_Clearance,center=true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Box-specific lid interfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
module sliding_box_rails(){
|
||||
if(Lid_Type=="Sliding"){
|
||||
z=OH-Slide_Rail_Height-Lid_Thickness-Slide_Clearance;
|
||||
if(Slide_Direction=="Width"){
|
||||
// rails on front/back; lid moves in X
|
||||
translate([0,-OD/2-Slide_Rail_Depth,z]) cube([OW,Slide_Rail_Depth,Slide_Rail_Height],center=false);
|
||||
translate([0, OD/2,z]) cube([OW,Slide_Rail_Depth,Slide_Rail_Height],center=false);
|
||||
if(Slide_Stop)
|
||||
translate([OW/2-Wall_Thickness,-OD/2-Slide_Rail_Depth,z]) cube([Wall_Thickness,OD+2*Slide_Rail_Depth,Slide_Rail_Height+Lid_Thickness],center=false);
|
||||
} else {
|
||||
translate([-OW/2-Slide_Rail_Depth,0,z]) cube([Slide_Rail_Depth,OD,Slide_Rail_Height],center=false);
|
||||
translate([ OW/2,0,z]) cube([Slide_Rail_Depth,OD,Slide_Rail_Height],center=false);
|
||||
if(Slide_Stop)
|
||||
translate([-OW/2-Slide_Rail_Depth,OD/2-Wall_Thickness,z]) cube([OW+2*Slide_Rail_Depth,Wall_Thickness,Slide_Rail_Height+Lid_Thickness],center=false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module snap_body_receiver(){
|
||||
if(Closure=="Front snap" && Lid_Type!="None" && Lid_Type!="Sliding"){
|
||||
// small catch block at front outside
|
||||
translate([-Snap_Width/2,-OD/2-Snap_Engagement,OH-Snap_Thickness*2])
|
||||
cube([Snap_Width,Snap_Engagement+Wall_Thickness/2,Snap_Thickness],center=false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hinge geometry
|
||||
// ---------------------------------------------------------------------------
|
||||
function hinge_total_len() = OW - 2*max(OR,Wall_Thickness*2);
|
||||
function hinge_seg_len() = (hinge_total_len()-(Hinge_Knuckles-1)*Hinge_Gap)/Hinge_Knuckles;
|
||||
|
||||
module hinge_knuckle(x,len,is_lid=false){
|
||||
od=Hinge_Outer_Diameter;
|
||||
id=Hinge_Pin_Diameter+2*Hinge_Pin_Clearance;
|
||||
difference(){
|
||||
translate([x,-OD/2-od/2+Hinge_Axis_Offset,OH+od/2-Lid_Thickness]) rotate([0,90,0]) cylinder(h=len,d=od,center=false);
|
||||
translate([x-Epsilon,-OD/2-od/2+Hinge_Axis_Offset,OH+od/2-Lid_Thickness]) rotate([0,90,0]) cylinder(h=len+2*Epsilon,d=id,center=false);
|
||||
}
|
||||
}
|
||||
|
||||
module box_hinges(){
|
||||
if(Lid_Type=="Hinged"){
|
||||
seg=hinge_seg_len();
|
||||
start=-hinge_total_len()/2;
|
||||
for(i=[0:Hinge_Knuckles-1]) if(i%2==0)
|
||||
hinge_knuckle(start+i*(seg+Hinge_Gap),seg,false);
|
||||
}
|
||||
}
|
||||
|
||||
module lid_hinges_local(){
|
||||
if(Lid_Type=="Hinged"){
|
||||
seg=hinge_seg_len();
|
||||
start=-hinge_total_len()/2;
|
||||
od=Hinge_Outer_Diameter;
|
||||
id=Hinge_Pin_Diameter+2*Hinge_Pin_Clearance;
|
||||
for(i=[0:Hinge_Knuckles-1]) if(i%2==1)
|
||||
difference(){
|
||||
translate([start+i*(seg+Hinge_Gap),-OD/2-od/2+Hinge_Axis_Offset,od/2]) rotate([0,90,0]) cylinder(h=seg,d=od);
|
||||
translate([start+i*(seg+Hinge_Gap)-Epsilon,-OD/2-od/2+Hinge_Axis_Offset,od/2]) rotate([0,90,0]) cylinder(h=seg+2*Epsilon,d=id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module hinge_pin(){
|
||||
if(Hinge_Add_Pin_Model && Lid_Type=="Hinged")
|
||||
rotate([0,90,0]) cylinder(h=hinge_total_len()+4,d=Hinge_Pin_Diameter,center=true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lid modules
|
||||
// ---------------------------------------------------------------------------
|
||||
module lid_panel(w=OW,d=OD,r=LID_OR){
|
||||
difference(){
|
||||
rounded_prism_custom(w,d,Lid_Thickness,r);
|
||||
if(Label_Mode=="Debossed lid") lid_label(cut=true,top_z=Lid_Thickness+Epsilon);
|
||||
}
|
||||
if(Label_Mode=="Embossed lid") lid_label(cut=false,top_z=Lid_Thickness);
|
||||
}
|
||||
|
||||
module inner_lip_lid(){
|
||||
lip_w=IW-2*Lid_Clearance;
|
||||
lip_d=ID-2*Lid_Clearance;
|
||||
panel_w=OW+2*Lid_Overhang;
|
||||
panel_d=OD+2*Lid_Overhang;
|
||||
difference(){
|
||||
union(){
|
||||
lid_panel(panel_w,panel_d,LID_OR+Lid_Overhang);
|
||||
translate([0,0,-Lip_Height])
|
||||
linear_extrude(Lip_Height)
|
||||
difference(){
|
||||
rounded_rect_2d(lip_w,lip_d,max(0,IR-Lid_Clearance));
|
||||
rounded_rect_2d(max(Min_Feature,lip_w-2*Lip_Thickness),max(Min_Feature,lip_d-2*Lip_Thickness),max(0,IR-Lid_Clearance-Lip_Thickness));
|
||||
}
|
||||
if(Closure=="Front snap") lid_snap_local();
|
||||
}
|
||||
magnet_lid_cuts(-lip_d/2+Lip_Thickness/2,-Lip_Height/2);
|
||||
}
|
||||
}
|
||||
|
||||
module outer_cap_lid(){
|
||||
inner_w=OW+2*Lid_Clearance;
|
||||
inner_d=OD+2*Lid_Clearance;
|
||||
outer_w=inner_w+2*Lip_Thickness;
|
||||
outer_d=inner_d+2*Lip_Thickness;
|
||||
difference(){
|
||||
union(){
|
||||
lid_panel(outer_w,outer_d,LID_OR+Lip_Thickness+Lid_Clearance);
|
||||
translate([0,0,-Lip_Height])
|
||||
linear_extrude(Lip_Height)
|
||||
difference(){
|
||||
rounded_rect_2d(outer_w,outer_d,LID_OR+Lip_Thickness+Lid_Clearance);
|
||||
rounded_rect_2d(inner_w,inner_d,LID_OR+Lid_Clearance);
|
||||
}
|
||||
if(Closure=="Front snap") lid_snap_local(y=-outer_d/2);
|
||||
}
|
||||
magnet_lid_cuts(-inner_d/2-Lip_Thickness/2,-Lip_Height/2);
|
||||
}
|
||||
}
|
||||
|
||||
module lid_snap_local(y=-OD/2){
|
||||
translate([-Snap_Width/2,y-Snap_Thickness,-Lip_Height+Snap_Clearance])
|
||||
cube([Snap_Width,Snap_Thickness,Lip_Height-Snap_Clearance],center=false);
|
||||
translate([-Snap_Width/2,y-Snap_Thickness-Snap_Engagement,-Lip_Height+Snap_Clearance])
|
||||
cube([Snap_Width,Snap_Engagement,Snap_Thickness],center=false);
|
||||
}
|
||||
|
||||
module sliding_lid(){
|
||||
if(Slide_Direction=="Width"){
|
||||
w=OW-(Slide_Stop?Wall_Thickness:0)-2*Slide_Clearance;
|
||||
d=OD+2*Slide_Rail_Depth-2*Slide_Clearance;
|
||||
difference(){
|
||||
lid_panel(w,d,max(0,LID_OR));
|
||||
if(Slide_Grip=="Finger notch")
|
||||
translate([-w/2,0,-Epsilon]) cylinder(h=Lid_Thickness+2*Epsilon,d=Slide_Grip_Size);
|
||||
}
|
||||
if(Slide_Grip=="Pull tab")
|
||||
translate([-w/2-Slide_Grip_Size/2,0,0]) rounded_prism_custom(Slide_Grip_Size,Slide_Grip_Size,Lid_Thickness,min(3,Slide_Grip_Size/4));
|
||||
} else {
|
||||
w=OW+2*Slide_Rail_Depth-2*Slide_Clearance;
|
||||
d=OD-(Slide_Stop?Wall_Thickness:0)-2*Slide_Clearance;
|
||||
difference(){
|
||||
lid_panel(w,d,max(0,LID_OR));
|
||||
if(Slide_Grip=="Finger notch")
|
||||
translate([0,-d/2,-Epsilon]) cylinder(h=Lid_Thickness+2*Epsilon,d=Slide_Grip_Size);
|
||||
}
|
||||
if(Slide_Grip=="Pull tab")
|
||||
translate([0,-d/2-Slide_Grip_Size/2,0]) rounded_prism_custom(Slide_Grip_Size,Slide_Grip_Size,Lid_Thickness,min(3,Slide_Grip_Size/4));
|
||||
}
|
||||
}
|
||||
|
||||
module hinged_lid(){
|
||||
// flat-ish lid with shallow locating lip, plus alternating hinge knuckles
|
||||
union(){
|
||||
difference(){
|
||||
union(){
|
||||
lid_panel(OW+2*Lid_Overhang,OD+2*Lid_Overhang,LID_OR+Lid_Overhang);
|
||||
translate([0,0,-min(Lip_Height,3)])
|
||||
linear_extrude(min(Lip_Height,3))
|
||||
difference(){
|
||||
rounded_rect_2d(IW-2*Lid_Clearance,ID-2*Lid_Clearance,max(0,IR-Lid_Clearance));
|
||||
rounded_rect_2d(IW-2*Lid_Clearance-2*Lip_Thickness,ID-2*Lid_Clearance-2*Lip_Thickness,max(0,IR-Lid_Clearance-Lip_Thickness));
|
||||
}
|
||||
lid_hinges_local();
|
||||
if(Closure=="Front snap") lid_snap_local();
|
||||
}
|
||||
magnet_lid_cuts(-ID/2+Lip_Thickness/2,-min(Lip_Height,3)/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module lid_model(){
|
||||
if(Lid_Type=="Inner lip") inner_lip_lid();
|
||||
else if(Lid_Type=="Outer cap") outer_cap_lid();
|
||||
else if(Lid_Type=="Sliding") sliding_lid();
|
||||
else if(Lid_Type=="Hinged") hinged_lid();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Full box assembly
|
||||
// ---------------------------------------------------------------------------
|
||||
module box_model(){
|
||||
union(){
|
||||
shell_body();
|
||||
intersection(){
|
||||
divider_geometry();
|
||||
translate([0,0,Bottom_Thickness])
|
||||
linear_extrude(max(0,IH))
|
||||
if(Corner_Style=="Rounded") rounded_rect_2d(IW,ID,IR);
|
||||
else if(Corner_Style=="Chamfered") chamfer_rect_2d(IW,ID,max(0,Chamfer_Size-Wall_Thickness));
|
||||
else square([IW,ID],center=true);
|
||||
}
|
||||
outer_ribs();
|
||||
feet();
|
||||
sliding_box_rails();
|
||||
box_hinges();
|
||||
snap_body_receiver();
|
||||
if(Label_Mode=="Embossed front") front_label(cut=false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout / preview
|
||||
// ---------------------------------------------------------------------------
|
||||
module lid_print_oriented(){
|
||||
// Lid top face down for common FDM-friendly print orientation where practical.
|
||||
if(Lid_Type=="Inner lip" || Lid_Type=="Outer cap" || Lid_Type=="Hinged")
|
||||
translate([0,0,Lid_Thickness]) rotate([180,0,0]) lid_model();
|
||||
else lid_model();
|
||||
}
|
||||
|
||||
module assembled(){
|
||||
box_model();
|
||||
if(Lid_Type=="Inner lip" || Lid_Type=="Outer cap")
|
||||
translate([0,0,OH+Lip_Height]) lid_model();
|
||||
else if(Lid_Type=="Sliding")
|
||||
translate([0,0,OH-Lid_Thickness-Slide_Clearance]) lid_model();
|
||||
else if(Lid_Type=="Hinged"){
|
||||
// approximate visual rotation about rear hinge axis
|
||||
axis_y=-OD/2-Hinge_Outer_Diameter/2+Hinge_Axis_Offset;
|
||||
axis_z=OH+Hinge_Outer_Diameter/2-Lid_Thickness;
|
||||
translate([0,axis_y,axis_z]) rotate([Hinge_Lid_Angle,0,0]) translate([0,-axis_y,-Hinge_Outer_Diameter/2]) hinged_lid();
|
||||
}
|
||||
}
|
||||
|
||||
module exploded(){
|
||||
box_model();
|
||||
if(Lid_Type!="None") translate([0,0,OH+Explode_Distance]) lid_model();
|
||||
}
|
||||
|
||||
module print_layout(){
|
||||
box_model();
|
||||
if(Lid_Type!="None")
|
||||
translate([OW/2 + max(OW,OD)/2 + Print_Gap,0,0]) lid_print_oriented();
|
||||
if(Lid_Type=="Hinged" && Hinge_Add_Pin_Model)
|
||||
translate([0,-OD/2-Hinge_Outer_Diameter-Print_Gap, Hinge_Pin_Diameter/2]) hinge_pin();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostics
|
||||
// ---------------------------------------------------------------------------
|
||||
module diagnostics(){
|
||||
if(Show_Diagnostics){
|
||||
echo("=== PARAMETRIC BOX GENERATOR ===");
|
||||
echo("Dimension mode:",Dimension_Mode);
|
||||
echo("Inside W/D/H:",IW,ID,IH);
|
||||
echo("Outside W/D/H:",OW,OD,OH);
|
||||
echo("Wall / bottom:",Wall_Thickness,Bottom_Thickness);
|
||||
echo("Lid:",Lid_Type," clearance=",Lid_Clearance);
|
||||
if(Lid_Type=="Sliding") echo("Slide clearance:",Slide_Clearance);
|
||||
if(Lid_Type=="Hinged") echo("Hinge pin / hole:",Hinge_Pin_Diameter,Hinge_Pin_Diameter+2*Hinge_Pin_Clearance);
|
||||
if(Closure=="Magnets") echo("Magnet pocket diameter:",Magnet_Diameter+2*Magnet_Clearance," depth:",Magnet_Thickness);
|
||||
if(Wall_Thickness<Min_Feature) echo("WARNING: Wall thickness below Min_Feature");
|
||||
if(Bottom_Thickness<Min_Feature) echo("WARNING: Bottom thickness below Min_Feature");
|
||||
if(Lip_Thickness<Min_Feature && Lid_Type!="None" && Lid_Type!="Sliding") echo("WARNING: Lid lip thinner than Min_Feature");
|
||||
if(Divider_Thickness<Min_Feature && Dividers!="None") echo("WARNING: Divider thinner than Min_Feature");
|
||||
if(IW<=2*Lid_Clearance || ID<=2*Lid_Clearance) echo("WARNING: Lid clearance too large for opening");
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics();
|
||||
|
||||
if(Output=="Box only") box_model();
|
||||
else if(Output=="Lid only" && Lid_Type!="None") lid_print_oriented();
|
||||
else if(Output=="Both - print layout") print_layout();
|
||||
else if(Output=="Assembled preview") assembled();
|
||||
else if(Output=="Exploded preview") exploded();
|
||||
else box_model();
|
||||
BIN
parametric-box/stl-files/hinged-snap.stl
Normal file
BIN
parametric-box/stl-files/inner-lip.stl
Normal file
BIN
parametric-box/stl-files/sliding-lid.stl
Normal file
47
parametric-keyring/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Parametric Keyring
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated; lug strength and inlay fit require physical tests.
|
||||
|
||||
The editable generator is [parametric-keyring.scad](parametric-keyring.scad). FDM printing; the generator also provides intentional 2D output.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
58 × 30 × 3.2 mm rounded rectangle, reinforced right lug, and two-line raised text.
|
||||
|
||||
**Fit and tolerance:** The default inlay clearance is 0.12 mm and must be calibrated per printer.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator requires a recent OpenSCAD Nightly build with `textmetrics` enabled.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default raised text | [Download](stl-files/raised-text.stl) | [Preview](images/raised-text.png) |
|
||||
| Heart with two lugs | [Download](stl-files/heart-two-lugs.stl) | [Preview](images/heart-two-lugs.png) |
|
||||
| Separate inlay text insert | [Download](stl-files/inlay-insert.stl) | [Preview](images/inlay-insert.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print 3D keyrings flat on the bed.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Requires OpenSCAD Nightly with textmetrics enabled.
|
||||
- Small text and ring holes need visual and physical inspection.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
parametric-keyring/images/heart-two-lugs.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
parametric-keyring/images/inlay-insert.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
parametric-keyring/images/raised-text.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
1103
parametric-keyring/parametric-keyring.scad
Normal file
BIN
parametric-keyring/stl-files/heart-two-lugs.stl
Normal file
BIN
parametric-keyring/stl-files/inlay-insert.stl
Normal file
BIN
parametric-keyring/stl-files/raised-text.stl
Normal file
47
s-hook/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Parametric S-Hook
|
||||
|
||||

|
||||
|
||||
**Status:** Reconstructed, corrected, and digitally validated; not load-rated.
|
||||
|
||||
The editable generator is [s-hook.scad](s-hook.scad). FDM printing in PETG or another material appropriate to the expected flex and creep.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
22/18 mm hook radii, 290° sweeps, 22 mm neck, and 5 mm round section.
|
||||
|
||||
**Fit and tolerance:** Choose hook radii and section size for the actual objects; leave installation clearance.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default round section | [Download](stl-files/round.stl) | [Preview](images/round.png) |
|
||||
| Flat section with bulb tips | [Download](stl-files/flat-bulb.stl) | [Preview](images/flat-bulb.png) |
|
||||
| Curved reinforced oval section | [Download](stl-files/reinforced-oval.stl) | [Preview](images/reinforced-oval.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print flat in the generated orientation.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Not certified or rated for overhead, valuable, or safety-related loads.
|
||||
- Long-term creep and layer-direction strength require physical testing.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
s-hook/images/flat-bulb.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
s-hook/images/reinforced-oval.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
s-hook/images/round.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
225
s-hook/s-hook.scad
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
PARAMETRIC S-HOOK GENERATOR — reconstructed repository edition
|
||||
---------------------------------------------------------------------------
|
||||
This file reconstructs the final corrected S-hook design discussed on
|
||||
2026-09-23. The original final source was delivered inline in chat and was
|
||||
not preserved as a downloadable artifact.
|
||||
|
||||
The critical geometry correction is preserved:
|
||||
- the upper hook joins the neck at the RIGHT side of its ellipse (0 deg)
|
||||
- the lower hook joins the neck at the LEFT side of its ellipse (180 deg)
|
||||
- the upper arc therefore terminates at 0 deg
|
||||
- the lower arc begins at 180 deg
|
||||
|
||||
This removes the earlier 90-degree neck/arc orientation error.
|
||||
|
||||
Units: millimetres.
|
||||
Intended for recent OpenSCAD Nightly, but uses conservative syntax.
|
||||
*/
|
||||
|
||||
/* [Hook geometry] */
|
||||
top_radius = 22; // [5:0.5:80]
|
||||
bottom_radius = 18; // [5:0.5:80]
|
||||
top_sweep = 290; // [190:1:340]
|
||||
bottom_sweep = 290; // [190:1:340]
|
||||
neck_length = 22; // [0:0.5:100]
|
||||
neck_curve = 0; // [-20:0.5:20]
|
||||
overall_scale_x = 1.0; // [0.25:0.05:3]
|
||||
overall_scale_y = 1.0; // [0.25:0.05:3]
|
||||
|
||||
/* [Section] */
|
||||
profile = "Round"; // [Round,Oval,Flat]
|
||||
rod_diameter = 5.0; // [1.5:0.1:16]
|
||||
oval_z_ratio = 0.70; // [0.25:0.05:1.5]
|
||||
flat_thickness = 4.0; // [0.8:0.1:12]
|
||||
flat_width = 6.0; // [1.5:0.1:20]
|
||||
|
||||
/* [Tips] */
|
||||
tip_style = "Rounded"; // [Plain,Rounded,Bulb]
|
||||
tip_bulb_scale = 1.30; // [1:0.05:2]
|
||||
tip_extension = 0; // [0:0.5:15]
|
||||
|
||||
/* [Reinforcement] */
|
||||
neck_reinforcement = 0; // [0:0.1:5]
|
||||
|
||||
/* [Quality] */
|
||||
quality = 72; // [16:4:180]
|
||||
path_steps = 72; // [20:2:180]
|
||||
show_path_markers = false;
|
||||
|
||||
$fn = quality;
|
||||
EPS = 0.01;
|
||||
|
||||
function lerp(a,b,t) = a + (b-a)*t;
|
||||
function bez2(p0,p1,p2,t) =
|
||||
let(u=1-t)
|
||||
[u*u*p0[0] + 2*u*t*p1[0] + t*t*p2[0],
|
||||
u*u*p0[1] + 2*u*t*p1[1] + t*t*p2[1]];
|
||||
|
||||
function point_on(center, rx, ry, a) =
|
||||
[center[0] + rx*cos(a), center[1] + ry*sin(a)];
|
||||
|
||||
top_center = [-top_radius, neck_length/2];
|
||||
bottom_center = [bottom_radius, -neck_length/2];
|
||||
|
||||
top_connection = [0, neck_length/2];
|
||||
bottom_connection = [0, -neck_length/2];
|
||||
|
||||
// A clockwise sweep from top_sweep to 0° approaches the right tangent from
|
||||
// above, then continues down the neck. The earlier reconstruction used
|
||||
// 360-top_sweep..360, which put the opening and incoming arc on the wrong side.
|
||||
top_start_angle = top_sweep;
|
||||
top_points = [
|
||||
for (i=[0:path_steps])
|
||||
let(a = lerp(top_start_angle, 0, i/path_steps))
|
||||
point_on(top_center, top_radius, top_radius, a)
|
||||
];
|
||||
|
||||
// Neck runs from upper right tangent to lower left tangent.
|
||||
// A quadratic Bezier allows an optional sideways bow. Larger neck_curve values
|
||||
// deliberately turn the join tangent while preserving its downward direction.
|
||||
neck_control = [neck_curve, 0];
|
||||
neck_points = [
|
||||
for (i=[1:path_steps-1])
|
||||
bez2(top_connection, neck_control, bottom_connection, i/path_steps)
|
||||
];
|
||||
|
||||
// Lower path deliberately STARTS at 180° (left tangent point).
|
||||
bottom_points = [
|
||||
for (i=[0:path_steps])
|
||||
let(a = lerp(180, 180 + bottom_sweep, i/path_steps))
|
||||
point_on(bottom_center, bottom_radius, bottom_radius, a)
|
||||
];
|
||||
|
||||
hook_path = concat(top_points, neck_points, bottom_points);
|
||||
|
||||
function vsub(a,b) = [a[0]-b[0], a[1]-b[1]];
|
||||
function vlen(v) = sqrt(v[0]*v[0] + v[1]*v[1]);
|
||||
function vnorm(v) = let(l=max(EPS,vlen(v))) [v[0]/l,v[1]/l];
|
||||
function vdot(a,b) = a[0]*b[0] + a[1]*b[1];
|
||||
|
||||
// Discrete direction checks protect the reconstructed 0°/180° joins. The
|
||||
// vectors need to continue in the same general direction; neck_curve is allowed
|
||||
// to change the exact tangent as part of its documented shape control.
|
||||
top_incoming = vsub(top_points[len(top_points)-1], top_points[len(top_points)-2]);
|
||||
neck_outgoing = vsub(neck_points[0], top_connection);
|
||||
neck_incoming = vsub(bottom_connection, neck_points[len(neck_points)-1]);
|
||||
bottom_outgoing = vsub(bottom_points[1], bottom_points[0]);
|
||||
assert(vlen(vsub(top_points[len(top_points)-1], top_connection)) < EPS,
|
||||
"Upper arc must end at the neck connection");
|
||||
assert(vlen(vsub(bottom_points[0], bottom_connection)) < EPS,
|
||||
"Lower arc must start at the neck connection");
|
||||
assert(vdot(vnorm(top_incoming), vnorm(neck_outgoing)) > 0,
|
||||
"Upper arc and neck must continue in the same general direction");
|
||||
assert(vdot(vnorm(neck_incoming), vnorm(bottom_outgoing)) > 0,
|
||||
"Neck and lower arc must continue in the same general direction");
|
||||
|
||||
function tip_extended_point(p0,p1,amount) =
|
||||
let(v=vnorm(vsub(p0,p1)))
|
||||
[p0[0]+v[0]*amount,p0[1]+v[1]*amount];
|
||||
|
||||
path_with_tips =
|
||||
tip_extension > 0
|
||||
? concat(
|
||||
[tip_extended_point(hook_path[0],hook_path[1],tip_extension)],
|
||||
hook_path,
|
||||
[tip_extended_point(
|
||||
hook_path[len(hook_path)-1],
|
||||
hook_path[len(hook_path)-2],
|
||||
tip_extension
|
||||
)]
|
||||
)
|
||||
: hook_path;
|
||||
|
||||
module flat_node(p, d=flat_width) {
|
||||
translate([p[0],p[1],0]) circle(d=d);
|
||||
}
|
||||
|
||||
module flat_path(points) {
|
||||
linear_extrude(height=flat_thickness)
|
||||
union()
|
||||
for (i=[0:len(points)-2])
|
||||
hull() {
|
||||
flat_node(points[i]);
|
||||
flat_node(points[i+1]);
|
||||
}
|
||||
}
|
||||
|
||||
module round_node(p, d=rod_diameter, zratio=1) {
|
||||
translate([p[0],p[1],rod_diameter*zratio/2])
|
||||
scale([1,1,zratio])
|
||||
sphere(d=d);
|
||||
}
|
||||
|
||||
module round_path(points, zratio=1) {
|
||||
union()
|
||||
for (i=[0:len(points)-2])
|
||||
hull() {
|
||||
round_node(points[i], rod_diameter, zratio);
|
||||
round_node(points[i+1], rod_diameter, zratio);
|
||||
}
|
||||
}
|
||||
|
||||
module tip_features(points) {
|
||||
if (tip_style == "Bulb") {
|
||||
if (profile == "Flat") {
|
||||
linear_extrude(height=flat_thickness) {
|
||||
translate(points[0]) circle(d=flat_width*tip_bulb_scale);
|
||||
translate(points[len(points)-1]) circle(d=flat_width*tip_bulb_scale);
|
||||
}
|
||||
} else {
|
||||
zr = profile == "Oval" ? oval_z_ratio : 1;
|
||||
round_node(points[0], rod_diameter*tip_bulb_scale, zr);
|
||||
round_node(points[len(points)-1], rod_diameter*tip_bulb_scale, zr);
|
||||
}
|
||||
} else if (tip_style == "Rounded") {
|
||||
// Base path already ends in round nodes/caps.
|
||||
}
|
||||
}
|
||||
|
||||
module reinforcement() {
|
||||
if (neck_reinforcement > 0) {
|
||||
if (profile == "Flat") {
|
||||
linear_extrude(height=flat_thickness)
|
||||
hull() {
|
||||
translate(top_connection) circle(d=flat_width + 2*neck_reinforcement);
|
||||
translate(bottom_connection) circle(d=flat_width + 2*neck_reinforcement);
|
||||
}
|
||||
} else {
|
||||
zr = profile == "Oval" ? oval_z_ratio : 1;
|
||||
for (p=[top_connection,bottom_connection])
|
||||
round_node(p,rod_diameter+2*neck_reinforcement,zr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module hook() {
|
||||
scale([overall_scale_x,overall_scale_y,1])
|
||||
union() {
|
||||
if (profile == "Flat")
|
||||
flat_path(path_with_tips);
|
||||
else if (profile == "Oval")
|
||||
round_path(path_with_tips,oval_z_ratio);
|
||||
else
|
||||
round_path(path_with_tips,1);
|
||||
|
||||
tip_features(path_with_tips);
|
||||
reinforcement();
|
||||
}
|
||||
}
|
||||
|
||||
module markers() {
|
||||
if (show_path_markers)
|
||||
color("red")
|
||||
for (p=hook_path)
|
||||
translate([p[0],p[1],rod_diameter+2])
|
||||
sphere(d=max(0.8,rod_diameter*0.18));
|
||||
}
|
||||
|
||||
echo("S-Hook Generator — reconstructed corrected edition");
|
||||
echo("Upper connection", top_connection, "upper arc terminates at 0 degrees");
|
||||
echo("Lower connection", bottom_connection, "lower arc begins at 180 degrees");
|
||||
echo("Profile", profile, "Top sweep", top_sweep, "Bottom sweep", bottom_sweep);
|
||||
|
||||
hook();
|
||||
markers();
|
||||
BIN
s-hook/stl-files/flat-bulb.stl
Normal file
BIN
s-hook/stl-files/reinforced-oval.stl
Normal file
BIN
s-hook/stl-files/round.stl
Normal file
47
storage-system/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Parametric Storage System
|
||||
|
||||

|
||||
|
||||
**Status:** Digital release candidate; representative physical fit test and photos are still required.
|
||||
|
||||
The editable generator is [storage-system.scad](storage-system.scad). FDM printing in PLA or PETG with a common 0.4 mm nozzle.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
120 × 80 × 50 mm outside dimensions with box, cap lid, and removable two-compartment divider.
|
||||
|
||||
**Fit and tolerance:** fit_clearance is per side and defaults to 0.35 mm. Calibrate before a complete set.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Standard box | [Download](stl-files/standard-box.stl) | [Preview](images/standard-box.png) |
|
||||
| Standard cap lid | [Download](stl-files/standard-lid.stl) | [Preview](images/standard-lid.png) |
|
||||
| Standard two-compartment divider | [Download](stl-files/standard-divider.stl) | [Preview](images/standard-divider.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print the box and divider upright; print the lid with its top face on the bed.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Digital validation does not prove lid or divider fit.
|
||||
- The public single-file bundle contains the v1 box, lid, and divider only.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
storage-system/images/standard-box.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
storage-system/images/standard-divider.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
storage-system/images/standard-lid.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
storage-system/stl-files/standard-box.stl
Normal file
BIN
storage-system/stl-files/standard-divider.stl
Normal file
BIN
storage-system/stl-files/standard-lid.stl
Normal file
99
storage-system/storage-system.scad
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Generated public single-file edition of the OpenSCAD Collection storage system.
|
||||
// Canonical files: core.scad + storage.scad. Do not edit this generated copy.
|
||||
// OpenSCAD Collection: shared dimensions for the storage family.
|
||||
// All dimensions are millimetres. Clearance is radial/per side.
|
||||
|
||||
function storage_inner_x(x, wall) = x - 2 * wall;
|
||||
function storage_inner_y(y, wall) = y - 2 * wall;
|
||||
function storage_lid_outer_x(x, lid_wall, fit) = x + 2 * (lid_wall + fit);
|
||||
function storage_lid_outer_y(y, lid_wall, fit) = y + 2 * (lid_wall + fit);
|
||||
|
||||
module storage_validate(x, y, h, wall, floor_thickness, fit, lid_wall, lid_top, skirt, divider_height, divider_floor) {
|
||||
assert(x >= 40 && x <= 200, "Outer X must be 40..200 mm");
|
||||
assert(y >= 40 && y <= 200, "Outer Y must be 40..200 mm");
|
||||
assert(h >= 20 && h <= 150, "Outer height must be 20..150 mm");
|
||||
assert(wall >= 1.6 && wall <= 6, "Wall must be 1.6..6 mm");
|
||||
assert(floor_thickness >= 1.6 && floor_thickness <= 6, "Floor must be 1.6..6 mm");
|
||||
assert(fit >= 0.15 && fit <= 0.8, "Clearance must be 0.15..0.8 mm per side");
|
||||
assert(lid_wall >= 1.6 && lid_wall <= 6, "Lid wall must be 1.6..6 mm");
|
||||
assert(lid_top >= 1.6 && lid_top <= 6, "Lid top must be 1.6..6 mm");
|
||||
assert(skirt >= 3 && skirt <= 20, "Lid skirt must be 3..20 mm");
|
||||
assert(divider_floor >= 1.2 && divider_floor <= 5, "Divider floor must be 1.2..5 mm");
|
||||
assert(storage_inner_x(x, wall) > 12 && storage_inner_y(y, wall) > 12, "Box interior is too small");
|
||||
assert(h > floor_thickness + skirt + 2, "Box is too short for lid skirt");
|
||||
assert(divider_height >= divider_floor + 5 && divider_height <= h - floor_thickness - 2,
|
||||
"Divider must fit below the box rim");
|
||||
children();
|
||||
}
|
||||
|
||||
module storage_box(x, y, h, wall, floor_thickness) {
|
||||
difference() {
|
||||
cube([x, y, h]);
|
||||
translate([wall, wall, floor_thickness])
|
||||
cube([storage_inner_x(x, wall), storage_inner_y(y, wall), h - floor_thickness + 0.01]);
|
||||
}
|
||||
}
|
||||
|
||||
// Print top face on the bed. Turn the lid over to put it on the box.
|
||||
module storage_lid(x, y, fit, lid_wall, lid_top, skirt) {
|
||||
outer_x = storage_lid_outer_x(x, lid_wall, fit);
|
||||
outer_y = storage_lid_outer_y(y, lid_wall, fit);
|
||||
difference() {
|
||||
cube([outer_x, outer_y, lid_top + skirt]);
|
||||
translate([lid_wall, lid_wall, lid_top])
|
||||
cube([x + 2 * fit, y + 2 * fit, skirt + 0.01]);
|
||||
}
|
||||
}
|
||||
|
||||
// A removable two-compartment insert. Its floor keeps the partition upright.
|
||||
module storage_divider(x, y, wall, fit, divider_floor, divider_height, divider_wall) {
|
||||
insert_x = storage_inner_x(x, wall) - 2 * fit;
|
||||
insert_y = storage_inner_y(y, wall) - 2 * fit;
|
||||
assert(insert_x > 2 * divider_wall && insert_y > 2 * divider_wall,
|
||||
"Divider has no usable compartments");
|
||||
union() {
|
||||
cube([insert_x, insert_y, divider_floor]);
|
||||
translate([0, (insert_y - divider_wall) / 2, divider_floor])
|
||||
cube([insert_x, divider_wall, divider_height - divider_floor]);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenSCAD Customizer entrypoint. Export each part separately.
|
||||
// Keep identical settings for box, lid, and divider.
|
||||
|
||||
/* [Part] */
|
||||
part = "box"; // [box, lid, divider]
|
||||
|
||||
/* [Outer dimensions, mm] */
|
||||
outer_x = 120; // [40:1:200]
|
||||
outer_y = 80; // [40:1:200]
|
||||
outer_h = 50; // [20:1:150]
|
||||
|
||||
/* [Print and fit, mm] */
|
||||
wall = 2.4; // [1.6:0.2:6]
|
||||
floor_thickness = 2.4; // [1.6:0.2:6]
|
||||
fit_clearance = 0.35; // [0.15:0.05:0.8] Per side; increase for tight printers.
|
||||
|
||||
/* [Lid, mm] */
|
||||
lid_wall = 2.0; // [1.6:0.2:6]
|
||||
lid_top = 2.4; // [1.6:0.2:6]
|
||||
lid_skirt = 8; // [3:1:20]
|
||||
|
||||
/* [Divider, mm] */
|
||||
divider_floor = 1.6; // [1.2:0.2:5]
|
||||
divider_wall = 2.0; // [1.6:0.2:6]
|
||||
divider_height = 30; // [7:1:140]
|
||||
effective_divider_height = min(divider_height, outer_h - floor_thickness - 2);
|
||||
storage_validate(outer_x, outer_y, outer_h, wall, floor_thickness,
|
||||
fit_clearance, lid_wall, lid_top, lid_skirt,
|
||||
effective_divider_height, divider_floor) {
|
||||
if (part == "box")
|
||||
storage_box(outer_x, outer_y, outer_h, wall, floor_thickness);
|
||||
else if (part == "lid")
|
||||
storage_lid(outer_x, outer_y, fit_clearance, lid_wall, lid_top, lid_skirt);
|
||||
else if (part == "divider")
|
||||
storage_divider(outer_x, outer_y, wall, fit_clearance,
|
||||
divider_floor, effective_divider_height, divider_wall);
|
||||
else
|
||||
assert(false, str("Unknown part: ", part));
|
||||
}
|
||||
47
tea-bag-dispenser/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Tea-Bag Dispenser
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated; packet feeding and mounting require physical tests.
|
||||
|
||||
The editable generator is [tea-bag-dispenser.scad](tea-bag-dispenser.scad). FDM printing in PLA or PETG; use at least three perimeters for a durable kitchen part.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
Front-pull magazine for 60 × 70 × 1.6 mm packets with capacity 24.
|
||||
|
||||
**Fit and tolerance:** Measure a real packet stack because seams and wrapper thickness vary by brand.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator is maintained and validated with a recent OpenSCAD Nightly build.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default front-pull magazine | [Download](stl-files/front-pull.stl) | [Preview](images/front-pull.png) |
|
||||
| Angled counter tray | [Download](stl-files/angled-tray.stl) | [Preview](images/angled-tray.png) |
|
||||
| Open organizer with magnetic back | [Download](stl-files/open-organizer.stl) | [Preview](images/open-organizer.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print the front-pull magazine with its back flat on the bed.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Packet feeding, jamming, and cleanability require physical testing.
|
||||
- Wall fasteners and magnets must be selected for the installation.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
tea-bag-dispenser/images/angled-tray.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
tea-bag-dispenser/images/front-pull.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
tea-bag-dispenser/images/open-organizer.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
tea-bag-dispenser/stl-files/angled-tray.stl
Normal file
BIN
tea-bag-dispenser/stl-files/front-pull.stl
Normal file
BIN
tea-bag-dispenser/stl-files/open-organizer.stl
Normal file
189
tea-bag-dispenser/tea-bag-dispenser.scad
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
SMART TEA-BAG DISPENSER GENERATOR — OpenSCAD Nightly
|
||||
-----------------------------------------------------------------------
|
||||
Designed around individually wrapped, flat tea envelopes. The default
|
||||
envelope is 60 x 70 mm. The default model is a front-pull gravity
|
||||
magazine: envelopes stand on their bottom edge, lean toward the front,
|
||||
and are held by a low retaining lip plus two rounded side fingers.
|
||||
|
||||
Print orientation: place the back flat on the print bed. No supports are
|
||||
needed in the standard configuration. Use 3+ perimeters for a durable
|
||||
kitchen part. Measure the actual sachets: their width/height may differ
|
||||
noticeably between brands.
|
||||
|
||||
Key design choices:
|
||||
- clearance around paper packets prevents jamming from seams and wrinkles;
|
||||
- a low front lip retains the stack, while a central thumb opening gives a
|
||||
deliberate, one-at-a-time grasp point;
|
||||
- two tapered side fingers stop the next packet following the first one;
|
||||
- a rearward lean uses gravity to keep the front packet presented;
|
||||
- open top and open side windows make refilling and stock level obvious;
|
||||
- optional drain/crumb slot and either countertop feet or wall screw holes.
|
||||
*/
|
||||
|
||||
// [Tea envelope]
|
||||
packet_width = 60; // [30:0.5:120]
|
||||
packet_height = 70; // [35:0.5:150]
|
||||
packet_thickness = 1.6; // [0.5:0.1:5]
|
||||
capacity = 24; // [4:1:80]
|
||||
side_clearance = 1.5; // [0.4:0.1:5]
|
||||
height_clearance = 3; // [0.5:0.5:15]
|
||||
depth_clearance = 3; // [0.5:0.5:12]
|
||||
|
||||
// [Format and mounting]
|
||||
dispenser_style = "Front Pull Magazine"; // [Front Pull Magazine,Angled Counter Tray,Open Organizer]
|
||||
mounting = "Countertop"; // [Countertop,Wall Mount,Magnetic Back]
|
||||
back_lean = 10; // [0:1:18]
|
||||
show_front_label = false; // [true,false]
|
||||
label_text = "TEA";
|
||||
|
||||
// [Dispensing geometry]
|
||||
front_lip_height = 15; // [6:0.5:30]
|
||||
thumb_opening_width = 34; // [18:0.5:55]
|
||||
thumb_opening_depth = 13; // [5:0.5:32]
|
||||
side_fingers = true; // [true,false]
|
||||
finger_width = 7; // [3:0.5:16]
|
||||
finger_height = 28; // [10:0.5:45]
|
||||
finger_overlap = 2.2; // [0.5:0.1:6]
|
||||
front_window = true; // [true,false]
|
||||
window_height = 34; // [10:1:80]
|
||||
|
||||
// [Construction]
|
||||
wall = 2.4; // [1.2:0.1:5]
|
||||
corner_radius = 4; // [0:0.5:12]
|
||||
bottom_thickness = 2.4; // [1.2:0.1:6]
|
||||
foot_height = 3; // [0:0.5:10]
|
||||
add_drain_slot = true; // [true,false]
|
||||
drain_slot_width = 22; // [5:1:50]
|
||||
wall_hole_diameter = 4.5; // [3:0.1:8]
|
||||
wall_hole_spacing = 54; // [20:1:150]
|
||||
magnet_diameter = 10; // [6:0.5:20]
|
||||
magnet_depth = 2.2; // [1:0.1:5]
|
||||
$fn = 48;
|
||||
|
||||
// Derived sizes. Y is depth (front to back); Z is height.
|
||||
inner_w = packet_width + 2*side_clearance;
|
||||
inner_d = capacity*packet_thickness + depth_clearance;
|
||||
inner_h = packet_height + height_clearance;
|
||||
outer_w = inner_w + 2*wall;
|
||||
outer_d = inner_d + 2*wall;
|
||||
outer_h = inner_h + bottom_thickness + wall;
|
||||
front_y = -outer_d/2;
|
||||
rear_y = outer_d/2;
|
||||
|
||||
assert(packet_width > 0 && packet_height > 0 && packet_thickness > 0, "Packet dimensions must be positive.");
|
||||
assert(thumb_opening_width < inner_w - 2*finger_width, "Thumb opening is too wide for the chosen dispenser.");
|
||||
assert(front_lip_height < inner_h - 5, "Front lip must be lower than the packets.");
|
||||
|
||||
module rounded_box(size=[10,10,10], r=2) {
|
||||
rr = min(r, min(size[0],size[1])/2);
|
||||
hull() for (x=[-size[0]/2+rr,size[0]/2-rr]) for (y=[-size[1]/2+rr,size[1]/2-rr])
|
||||
translate([x,y,0]) cylinder(h=size[2],r=rr);
|
||||
}
|
||||
|
||||
// A large horizontal capsule cutout, gentler on fingers than a rectangular slot.
|
||||
module capsule_2d(length, diameter) {
|
||||
hull() for (x=[-length/2+diameter/2,length/2-diameter/2]) translate([x,0]) circle(d=diameter);
|
||||
}
|
||||
|
||||
module front_gate() {
|
||||
// Low continuous lip with a deep centre thumb notch.
|
||||
difference() {
|
||||
translate([0,front_y+wall/2,bottom_thickness]) rounded_box([outer_w,wall,front_lip_height],corner_radius);
|
||||
translate([0,front_y-1,bottom_thickness+front_lip_height]) rotate([90,0,0])
|
||||
linear_extrude(height=wall+2) capsule_2d(thumb_opening_width,thumb_opening_depth*2);
|
||||
}
|
||||
// Tapered fingers prevent a second sachet from simply following the first.
|
||||
if (side_fingers) for (sx=[-1,1])
|
||||
translate([sx*(inner_w/2-finger_width/2+wall-finger_overlap),front_y+wall/2,bottom_thickness])
|
||||
hull() {
|
||||
translate([0,0,0]) rounded_box([finger_width,wall,finger_height],min(2,corner_radius));
|
||||
translate([sx*1.5,0,finger_height-7]) rounded_box([finger_width-1,wall,7],min(2,corner_radius));
|
||||
}
|
||||
}
|
||||
|
||||
module gravity_shelf() {
|
||||
// The packet bottoms rest on this gentle front-down ramp. It biases the
|
||||
// stack into the gate without requiring a spring, moving parts, or support.
|
||||
rise = min(tan(back_lean)*inner_d, front_lip_height*0.45);
|
||||
translate([0,0,bottom_thickness])
|
||||
polyhedron(
|
||||
points=[[-inner_w/2,front_y+wall,0],[inner_w/2,front_y+wall,0],
|
||||
[inner_w/2,rear_y-wall,0],[-inner_w/2,rear_y-wall,0],
|
||||
[-inner_w/2,front_y+wall,0],[inner_w/2,front_y+wall,0],
|
||||
[inner_w/2,rear_y-wall,rise],[-inner_w/2,rear_y-wall,rise]],
|
||||
faces=[[0,1,2,3],[4,7,6,5],[0,4,5,1],[1,5,6,2],[2,6,7,3],[3,7,4,0]]);
|
||||
}
|
||||
|
||||
module magazine_body() {
|
||||
difference() {
|
||||
union() {
|
||||
// Back panel and side rails; leave top fully open for comfortable refilling.
|
||||
translate([0,rear_y-wall/2,0]) rounded_box([outer_w,wall,outer_h],corner_radius);
|
||||
for (sx=[-1,1]) translate([sx*(outer_w-wall)/2,0,0]) rounded_box([wall,outer_d,outer_h],corner_radius);
|
||||
translate([0,0,0]) rounded_box([outer_w,outer_d,bottom_thickness],corner_radius);
|
||||
gravity_shelf();
|
||||
front_gate();
|
||||
if (mounting == "Countertop" && foot_height > 0)
|
||||
for (x=[-outer_w/2+10,outer_w/2-10]) for (y=[front_y+10,rear_y-10])
|
||||
translate([x,y,-foot_height]) cylinder(h=foot_height,r=4);
|
||||
}
|
||||
// Large side windows: see stock, grab a jammed packet, and save material.
|
||||
if (front_window) for (sx=[-1,1])
|
||||
translate([sx*(outer_w/2-wall/2),0,bottom_thickness+front_lip_height+8]) rotate([0,90,0])
|
||||
linear_extrude(height=wall+2,center=true)
|
||||
capsule_2d(min(inner_d-14, window_height), min(inner_h-front_lip_height-16, window_height));
|
||||
if (add_drain_slot)
|
||||
translate([0,front_y+wall/2,-1]) rotate([90,0,0]) linear_extrude(height=wall+2,center=true)
|
||||
capsule_2d(drain_slot_width,4);
|
||||
if (mounting == "Wall Mount") for (x=[-wall_hole_spacing/2,wall_hole_spacing/2])
|
||||
translate([x,rear_y+1,outer_h-16]) rotate([90,0,0]) cylinder(h=wall+3,d=wall_hole_diameter);
|
||||
if (mounting == "Magnetic Back") for (x=[-wall_hole_spacing/2,wall_hole_spacing/2])
|
||||
translate([x,rear_y-wall+magnet_depth,outer_h-16]) rotate([90,0,0]) cylinder(h=magnet_depth+0.1,d=magnet_diameter);
|
||||
}
|
||||
}
|
||||
|
||||
module angled_counter_tray() {
|
||||
// A shallow first-in/first-out gravity runway. Fill from the elevated rear;
|
||||
// the retaining gate presents one packet at the low front.
|
||||
tray_len = max(110, capacity*packet_thickness*1.35 + 45);
|
||||
tray_w = inner_w + 2*wall;
|
||||
rise = tan(back_lean)*tray_len;
|
||||
rotate([back_lean,0,0]) {
|
||||
difference() {
|
||||
union() {
|
||||
rounded_box([tray_w,tray_len,bottom_thickness],corner_radius);
|
||||
for (sx=[-1,1]) translate([sx*(tray_w-wall)/2,0,bottom_thickness]) rounded_box([wall,tray_len,packet_height*0.38],corner_radius);
|
||||
translate([0,-tray_len/2+wall/2,bottom_thickness]) rounded_box([tray_w,wall,front_lip_height],corner_radius);
|
||||
}
|
||||
translate([0,-tray_len/2-1,bottom_thickness+front_lip_height]) rotate([90,0,0]) linear_extrude(height=wall+2)
|
||||
capsule_2d(thumb_opening_width,thumb_opening_depth*2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module open_organizer() {
|
||||
// A simple open caddy with the same intelligent front gate for quick sorting.
|
||||
difference() {
|
||||
union() {
|
||||
rounded_box([outer_w,outer_d,bottom_thickness],corner_radius);
|
||||
for (sx=[-1,1]) translate([sx*(outer_w-wall)/2,0,bottom_thickness]) rounded_box([wall,outer_d,inner_h*0.65],corner_radius);
|
||||
translate([0,rear_y-wall/2,bottom_thickness]) rounded_box([outer_w,wall,inner_h*0.65],corner_radius);
|
||||
front_gate();
|
||||
}
|
||||
translate([0,front_y-1,bottom_thickness+front_lip_height]) rotate([90,0,0]) linear_extrude(height=wall+2)
|
||||
capsule_2d(thumb_opening_width,thumb_opening_depth*2);
|
||||
}
|
||||
}
|
||||
|
||||
module label() {
|
||||
if (show_front_label)
|
||||
translate([0,front_y-wall/2-0.05,bottom_thickness+front_lip_height+11]) rotate([90,0,0])
|
||||
linear_extrude(height=0.6) text(label_text,size=10,halign="center",valign="center",font="Liberation Sans:style=Bold");
|
||||
}
|
||||
|
||||
// Main output. Front Pull is the recommended all-round design.
|
||||
if (dispenser_style == "Front Pull Magazine") {
|
||||
magazine_body(); label();
|
||||
} else if (dispenser_style == "Angled Counter Tray") angled_counter_tray();
|
||||
else { open_organizer(); label(); }
|
||||
47
text-generator/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# 3D Text Generator
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated with current OpenSCAD Nightly.
|
||||
|
||||
The editable generator is [text-generator.scad](text-generator.scad). FDM printing for free-standing text, plaques, and plinth signs.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
3D PRINT in Impact Regular at 30 mm, 5 mm deep, on an auto-sized convex base.
|
||||
|
||||
**Fit and tolerance:** Font availability changes measured geometry; inspect the resolved-font diagnostics.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator requires a recent OpenSCAD Nightly build with `textmetrics` enabled.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default standing text | [Download](stl-files/standing-text.stl) | [Preview](images/standing-text.png) |
|
||||
| Engraved plaque | [Download](stl-files/engraved-plaque.stl) | [Preview](images/engraved-plaque.png) |
|
||||
| Multiline plinth | [Download](stl-files/multiline-stand.stl) | [Preview](images/multiline-stand.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print plaques and plinths on a broad flat surface; inspect standing exports before slicing.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Requires OpenSCAD Nightly with textmetrics enabled.
|
||||
- Very small text, missing fonts, and shallow engraving need visual review.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
text-generator/images/engraved-plaque.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
text-generator/images/multiline-stand.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
text-generator/images/standing-text.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
text-generator/stl-files/engraved-plaque.stl
Normal file
BIN
text-generator/stl-files/multiline-stand.stl
Normal file
BIN
text-generator/stl-files/standing-text.stl
Normal file
499
text-generator/text-generator.scad
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
/*
|
||||
OpenSCAD 3D Text Generator — Nightly v2
|
||||
----------------------------------------
|
||||
Requires an OpenSCAD Development Snapshot / Nightly with:
|
||||
- textmetrics()
|
||||
- fontmetrics()
|
||||
|
||||
Main improvements over the original:
|
||||
* Real text bounding boxes via textmetrics()
|
||||
* Real multi-line layout (up to 5 Customizer-friendly lines)
|
||||
* Functional left/center/right line alignment
|
||||
* Optional true mm-based character tracking (no text(spacing!=1))
|
||||
* Automatic line spacing from fontmetrics().interline
|
||||
* Base sizing/placement based on actual rendered glyph extents
|
||||
* Auto rounded rectangular sign and automatically positioned stand
|
||||
* Separate engraving depth, safety floor and through-cut option
|
||||
* Optional chamfer on engraved text as well as embossed text
|
||||
* Correct upright placement on Z=0 using measured geometry
|
||||
* Diagnostics for requested/resolved font and model dimensions
|
||||
|
||||
Units: millimetres unless otherwise stated.
|
||||
*/
|
||||
|
||||
/* [Tekst] */
|
||||
// Første tekstlinje
|
||||
Tekst = "3D PRINT";
|
||||
// Ekstra linjer. Tom streng = linjen ignoreres.
|
||||
Linje_2 = "";
|
||||
Linje_3 = "";
|
||||
Linje_4 = "";
|
||||
Linje_5 = "";
|
||||
|
||||
// Skrifttypens familienavn, som vist i OpenSCAD Font List
|
||||
Font_Navn = "Impact";
|
||||
// Skrifttype-stil
|
||||
Font_Stil = "Regular"; // ["Regular", "Bold", "Italic", "Bold Italic"]
|
||||
// Nominel fontstørrelse
|
||||
Tekst_Stoerrelse = 30; // [5:1:200]
|
||||
// Dybde/højde på embossed eller fritstående tekst
|
||||
Tekst_Dybde = 5; // [0.5:0.5:50]
|
||||
|
||||
// Ekstra afstand MELLEM tegn i mm. 0 bruger fontens normale shaping/kerning.
|
||||
// Når værdien er forskellig fra 0, placeres glyffer individuelt for forudsigelig fysisk tracking.
|
||||
Bogstav_Afstand_mm = 0; // [-2:0.1:10]
|
||||
|
||||
// Baseline-afstand mellem linjer som faktor af fontens designede interline.
|
||||
Linje_Afstand = 1.0; // [0.5:0.05:3]
|
||||
|
||||
// Justering af linjer inden for den bredeste tekstlinje
|
||||
Justering = "center"; // ["left", "center", "right"]
|
||||
|
||||
// Sprog/script sendes videre til OpenSCADs tekst-shaping
|
||||
Sprog = "en";
|
||||
Script = "latin";
|
||||
|
||||
/* [Base & Bagplade] */
|
||||
// 0: Ingen, 1: Boble/outline, 2: Konveks hull, 3: Rektangulært skilt, 4: Stand/plint under teksten
|
||||
Base_Type = 2; // [0, 1, 2, 3, 4]
|
||||
|
||||
// 0: Embossed, 1: Engraved
|
||||
Print_Mode = 0; // [0, 1]
|
||||
|
||||
// Margin omkring tekst for base type 1/2 og ved auto-dimensionering
|
||||
Base_Margin = 6; // [0:0.5:50]
|
||||
// Basens tykkelse
|
||||
Base_Dybde = 3; // [0.5:0.5:50]
|
||||
|
||||
/* [Rektangulært skilt - Type 3] */
|
||||
// Automatisk størrelse efter tekstens faktiske bounding box
|
||||
Rektangel_Auto = true;
|
||||
// Manuelle dimensioner bruges når Rektangel_Auto = false
|
||||
Manuel_Base_Bredde = 150; // [10:1:400]
|
||||
Manuel_Base_Hoejde = 50; // [10:1:300]
|
||||
// Vertikal forskydning af rektanglet
|
||||
Manuel_Base_Y_Forskydning = 0; // [-100:1:100]
|
||||
// Radius på skiltets hjørner
|
||||
Base_Hjoerne_Radius = 3; // [0:0.5:30]
|
||||
|
||||
/* [Stand / plint - Type 4] */
|
||||
// Automatisk bredde efter teksten
|
||||
Stand_Auto_Bredde = true;
|
||||
// Bruges som bredde hvis Stand_Auto_Bredde = false
|
||||
Stand_Bredde = 150; // [10:1:400]
|
||||
// Plintens højde i modellens 2D-plan
|
||||
Stand_Hoejde = 12; // [2:0.5:100]
|
||||
// Hvor meget plinten overlapper tekstens nederste del for sikker union
|
||||
Stand_Overlap = 2.0; // [0.2:0.2:20]
|
||||
// Ekstra Y-forskydning af plinten
|
||||
Stand_Y_Forskydning = 0; // [-50:0.5:50]
|
||||
// Afrunding på plinten
|
||||
Stand_Hjoerne_Radius = 2; // [0:0.5:20]
|
||||
|
||||
/* [Embossed Bevel] */
|
||||
// Top-chamfer på embossed/fritstående tekst. 0 = fra.
|
||||
Bevel_Tykkelse = 0.8; // [0:0.1:5]
|
||||
// Antal stepped offset-lag
|
||||
Bevel_Trin = 4; // [1:1:16]
|
||||
|
||||
/* [Engraving] */
|
||||
// Faktisk graveringsdybde. Uafhængig af Tekst_Dybde.
|
||||
Graverings_Dybde = 1.2; // [0.1:0.1:20]
|
||||
// false = behold minimum bundtykkelse, true = skær helt igennem basen
|
||||
Gravering_Gennem = false;
|
||||
// Sikker bundtykkelse når Gravering_Gennem = false
|
||||
Minimum_Bundtykkelse = 0.8; // [0:0.1:10]
|
||||
// Chamfer omkring graveringsåbningen. 0 = lodrette kanter.
|
||||
Gravering_Bevel = 0.4; // [0:0.1:5]
|
||||
Gravering_Bevel_Trin = 3; // [1:1:16]
|
||||
|
||||
/* [Effekter & Orientering] */
|
||||
// Falsk italic via shear i grader
|
||||
Shear_Vinkel = 0; // [-45:1:45]
|
||||
// 0 = fladt, 1 = stående med nederste målte kant præcist på Z=0
|
||||
Orientering = 1; // [0, 1]
|
||||
|
||||
/* [Kvalitet & System] */
|
||||
// Kurveopløsning for tekst og afrundinger
|
||||
Kurve_Oploesning = 64; // [12:4:160]
|
||||
// Lille overlap/tolerance mod coplanar/non-manifold geometri
|
||||
Epsilon = 0.02; // [0.005:0.005:0.1]
|
||||
// Vis diagnostiske echo()-linjer i konsollen
|
||||
Diagnostik = true;
|
||||
|
||||
$fn = Kurve_Oploesning;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// NIGHTLY METRICS & HJÆLPEFUNKTIONER
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
_Fuld_Font = str(Font_Navn, ":style=", Font_Stil);
|
||||
_Linjer_Raa = [for (s = [Tekst, Linje_2, Linje_3, Linje_4, Linje_5]) if (s != "") s];
|
||||
// Sørger for defineret geometri/metrics selv hvis alle tekstfelter tømmes.
|
||||
_Linjer = len(_Linjer_Raa) > 0 ? _Linjer_Raa : [" "];
|
||||
_FontMetrics = fontmetrics(size = Tekst_Stoerrelse, font = _Fuld_Font);
|
||||
|
||||
function _tm(s) = textmetrics(
|
||||
text = s,
|
||||
size = Tekst_Stoerrelse,
|
||||
font = _Fuld_Font,
|
||||
halign = "left",
|
||||
valign = "baseline",
|
||||
spacing = 1,
|
||||
direction = "ltr",
|
||||
language = Sprog,
|
||||
script = Script
|
||||
);
|
||||
|
||||
// Position af tegn nr. i ved ægte fysisk tracking.
|
||||
// Hvert tegn placeres efter forrige tegns advance + Bogstav_Afstand_mm.
|
||||
function _glyph_x(s, i) =
|
||||
i <= 0 ? 0 :
|
||||
_glyph_x(s, i - 1) + _tm(s[i - 1]).advance.x + Bogstav_Afstand_mm;
|
||||
|
||||
function _glyph_bbox(s, i) =
|
||||
let(t = _tm(s[i]), x = _glyph_x(s, i))
|
||||
[
|
||||
x + t.position.x,
|
||||
t.position.y,
|
||||
x + t.position.x + t.size.x,
|
||||
t.position.y + t.size.y
|
||||
];
|
||||
|
||||
// Tight ink-bounding-box for én linje: [xmin, ymin, xmax, ymax]
|
||||
function _line_bbox(s) =
|
||||
Bogstav_Afstand_mm == 0
|
||||
? let(t = _tm(s))
|
||||
[t.position.x, t.position.y,
|
||||
t.position.x + t.size.x, t.position.y + t.size.y]
|
||||
: len(s) == 0
|
||||
? [0, 0, 0, 0]
|
||||
: [
|
||||
min([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[0]]),
|
||||
min([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[1]]),
|
||||
max([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[2]]),
|
||||
max([for (i = [0 : len(s) - 1]) _glyph_bbox(s, i)[3]])
|
||||
];
|
||||
|
||||
function _line_width(s) = let(b = _line_bbox(s)) b[2] - b[0];
|
||||
function _line_step() = _FontMetrics.interline * Linje_Afstand;
|
||||
function _block_width() = max([for (s = _Linjer) _line_width(s)]);
|
||||
|
||||
function _raw_line_y(i) = -i * _line_step();
|
||||
function _raw_block_ymin() = min([
|
||||
for (i = [0 : len(_Linjer) - 1])
|
||||
_line_bbox(_Linjer[i])[1] + _raw_line_y(i)
|
||||
]);
|
||||
function _raw_block_ymax() = max([
|
||||
for (i = [0 : len(_Linjer) - 1])
|
||||
_line_bbox(_Linjer[i])[3] + _raw_line_y(i)
|
||||
]);
|
||||
function _block_y_shift() = -(_raw_block_ymin() + _raw_block_ymax()) / 2;
|
||||
|
||||
// Placér hver linje inden for bredeste linjes centrerede blok.
|
||||
function _line_x_shift(s) =
|
||||
let(b = _line_bbox(s), bw = _block_width())
|
||||
Justering == "left" ? -bw / 2 - b[0] :
|
||||
Justering == "right" ? bw / 2 - b[2] :
|
||||
-(b[0] + b[2]) / 2;
|
||||
|
||||
function _placed_line_bbox(i) =
|
||||
let(
|
||||
s = _Linjer[i],
|
||||
b = _line_bbox(s),
|
||||
dx = _line_x_shift(s),
|
||||
dy = _raw_line_y(i) + _block_y_shift()
|
||||
)
|
||||
[b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy];
|
||||
|
||||
// Bounding box efter shear. Y ændres ikke af denne shear-transform.
|
||||
function _sheared_line_bbox(i) =
|
||||
let(
|
||||
b = _placed_line_bbox(i),
|
||||
k = tan(Shear_Vinkel),
|
||||
xs = [
|
||||
b[0] + k * b[1],
|
||||
b[0] + k * b[3],
|
||||
b[2] + k * b[1],
|
||||
b[2] + k * b[3]
|
||||
]
|
||||
)
|
||||
[min(xs), b[1], max(xs), b[3]];
|
||||
|
||||
function _text_bbox() = [
|
||||
min([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[0]]),
|
||||
min([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[1]]),
|
||||
max([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[2]]),
|
||||
max([for (i = [0 : len(_Linjer) - 1]) _sheared_line_bbox(i)[3]])
|
||||
];
|
||||
|
||||
function _text_width() = let(b = _text_bbox()) b[2] - b[0];
|
||||
function _text_height() = let(b = _text_bbox()) b[3] - b[1];
|
||||
|
||||
function _rect_width() = Rektangel_Auto
|
||||
? _text_width() + 2 * Base_Margin
|
||||
: Manuel_Base_Bredde;
|
||||
function _rect_height() = Rektangel_Auto
|
||||
? _text_height() + 2 * Base_Margin
|
||||
: Manuel_Base_Hoejde;
|
||||
|
||||
function _stand_width() = Stand_Auto_Bredde
|
||||
? _text_width() + 2 * Base_Margin
|
||||
: Stand_Bredde;
|
||||
function _stand_top() = _text_bbox()[1] + Stand_Overlap + Stand_Y_Forskydning;
|
||||
function _stand_center_y() = _stand_top() - Stand_Hoejde / 2;
|
||||
|
||||
// Approksimeret/sikker 2D-bbox for basen.
|
||||
function _base_bbox() =
|
||||
let(tb = _text_bbox())
|
||||
Base_Type == 1 || Base_Type == 2
|
||||
? [tb[0] - Base_Margin, tb[1] - Base_Margin,
|
||||
tb[2] + Base_Margin, tb[3] + Base_Margin]
|
||||
: Base_Type == 3
|
||||
? [-_rect_width()/2,
|
||||
Manuel_Base_Y_Forskydning - _rect_height()/2,
|
||||
_rect_width()/2,
|
||||
Manuel_Base_Y_Forskydning + _rect_height()/2]
|
||||
: Base_Type == 4
|
||||
? [-_stand_width()/2,
|
||||
_stand_top() - Stand_Hoejde,
|
||||
_stand_width()/2,
|
||||
_stand_top()]
|
||||
: tb;
|
||||
|
||||
function _scene_ymin() =
|
||||
Base_Type == 0
|
||||
? _text_bbox()[1]
|
||||
: Print_Mode == 0
|
||||
? min(_base_bbox()[1], _text_bbox()[1])
|
||||
: _base_bbox()[1];
|
||||
|
||||
function _eff_text_bevel() = min(max(Bevel_Tykkelse, 0), max(Tekst_Dybde - Epsilon, 0));
|
||||
function _max_safe_engrave() = max(Base_Dybde - Minimum_Bundtykkelse, Epsilon);
|
||||
function _eff_engrave_depth() = Gravering_Gennem
|
||||
? Base_Dybde + 2 * Epsilon
|
||||
: min(Graverings_Dybde, _max_safe_engrave());
|
||||
function _eff_engrave_bevel() = min(max(Gravering_Bevel, 0), _eff_engrave_depth());
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 2D TEKST
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module shear_transform(vinkel) {
|
||||
multmatrix([
|
||||
[1, tan(vinkel), 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
]) children();
|
||||
}
|
||||
|
||||
module _draw_line(s) {
|
||||
if (Bogstav_Afstand_mm == 0) {
|
||||
// Bevarer fontens normale HarfBuzz shaping, kerning og ligaturer.
|
||||
text(
|
||||
text = s,
|
||||
size = Tekst_Stoerrelse,
|
||||
font = _Fuld_Font,
|
||||
halign = "left",
|
||||
valign = "baseline",
|
||||
spacing = 1,
|
||||
direction = "ltr",
|
||||
language = Sprog,
|
||||
script = Script
|
||||
);
|
||||
} else {
|
||||
// Forudsigelig ekstra fysisk tracking i mm.
|
||||
// Tradeoff: tegn shaper ikke længere som én samlet streng.
|
||||
for (i = [0 : len(s) - 1]) {
|
||||
translate([_glyph_x(s, i), 0])
|
||||
text(
|
||||
text = s[i],
|
||||
size = Tekst_Stoerrelse,
|
||||
font = _Fuld_Font,
|
||||
halign = "left",
|
||||
valign = "baseline",
|
||||
spacing = 1,
|
||||
direction = "ltr",
|
||||
language = Sprog,
|
||||
script = Script
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module draw_2d_text() {
|
||||
shear_transform(Shear_Vinkel) {
|
||||
for (i = [0 : len(_Linjer) - 1]) {
|
||||
s = _Linjer[i];
|
||||
translate([
|
||||
_line_x_shift(s),
|
||||
_raw_line_y(i) + _block_y_shift()
|
||||
]) _draw_line(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 3D TEKST / EMBOSS BEVEL
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module draw_3d_text() {
|
||||
b = _eff_text_bevel();
|
||||
|
||||
if (b > Epsilon && Bevel_Trin > 0) {
|
||||
body_h = max(Epsilon, Tekst_Dybde - b);
|
||||
step_h = b / Bevel_Trin;
|
||||
|
||||
// Hovedkrop
|
||||
linear_extrude(height = body_h + Epsilon, convexity = 10)
|
||||
draw_2d_text();
|
||||
|
||||
// Stepped top-chamfer: konstant lateral/vertikal ratio ~1:1.
|
||||
for (i = [0 : Bevel_Trin - 1]) {
|
||||
d = (i + 1) * b / Bevel_Trin;
|
||||
translate([0, 0, body_h + i * step_h])
|
||||
linear_extrude(height = step_h + Epsilon, convexity = 10)
|
||||
offset(delta = -d)
|
||||
draw_2d_text();
|
||||
}
|
||||
} else {
|
||||
linear_extrude(height = Tekst_Dybde, convexity = 10)
|
||||
draw_2d_text();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BASE
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module rounded_rect_2d(w, h, r) {
|
||||
rr = min(max(r, 0), max(min(w, h) / 2 - Epsilon, 0));
|
||||
if (rr > Epsilon) {
|
||||
offset(r = rr)
|
||||
square([max(w - 2*rr, Epsilon), max(h - 2*rr, Epsilon)], center = true);
|
||||
} else {
|
||||
square([w, h], center = true);
|
||||
}
|
||||
}
|
||||
|
||||
module draw_2d_base() {
|
||||
if (Base_Type == 1) {
|
||||
// Boble/outline følger den faktiske tekstgeometri.
|
||||
offset(r = Base_Margin)
|
||||
draw_2d_text();
|
||||
}
|
||||
else if (Base_Type == 2) {
|
||||
// Stram konveks plade rundt om alle tegn/linjer.
|
||||
offset(r = Base_Margin)
|
||||
hull()
|
||||
draw_2d_text();
|
||||
}
|
||||
else if (Base_Type == 3) {
|
||||
translate([0, Manuel_Base_Y_Forskydning])
|
||||
rounded_rect_2d(_rect_width(), _rect_height(), Base_Hjoerne_Radius);
|
||||
}
|
||||
else if (Base_Type == 4) {
|
||||
// Plintens top ligger ved tekstens FAKTISKE nederste ink-kant + overlap.
|
||||
translate([0, _stand_center_y()])
|
||||
rounded_rect_2d(_stand_width(), Stand_Hoejde, Stand_Hjoerne_Radius);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ENGRAVING CUTTER
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module engraving_cutter() {
|
||||
depth = _eff_engrave_depth();
|
||||
bevel = _eff_engrave_bevel();
|
||||
bottom_z = Gravering_Gennem ? -Epsilon : Base_Dybde - depth;
|
||||
|
||||
// Nominel tekstprofil gennem hele den valgte dybde.
|
||||
translate([0, 0, bottom_z - Epsilon])
|
||||
linear_extrude(height = depth + 2*Epsilon, convexity = 10)
|
||||
draw_2d_text();
|
||||
|
||||
// Ekstra udvidelse nær overfladen giver en chamfer/countersink-effekt.
|
||||
if (bevel > Epsilon && Gravering_Bevel_Trin > 0) {
|
||||
step_h = bevel / Gravering_Bevel_Trin;
|
||||
for (i = [0 : Gravering_Bevel_Trin - 1]) {
|
||||
d = (i + 1) * bevel / Gravering_Bevel_Trin;
|
||||
translate([0, 0, Base_Dybde - bevel + i * step_h])
|
||||
linear_extrude(height = step_h + Epsilon, convexity = 10)
|
||||
offset(delta = d)
|
||||
draw_2d_text();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ASSEMBLY
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module assemble_scene() {
|
||||
if (Base_Type == 0) {
|
||||
// Engraving giver ingen mening uden base; vi genererer derfor ren tekst.
|
||||
draw_3d_text();
|
||||
}
|
||||
else if (Print_Mode == 0) {
|
||||
union() {
|
||||
linear_extrude(height = Base_Dybde, convexity = 10)
|
||||
draw_2d_base();
|
||||
|
||||
// Minimal overlap gør union robust uden synlig geometrisk ændring.
|
||||
translate([0, 0, Base_Dybde - Epsilon])
|
||||
draw_3d_text();
|
||||
}
|
||||
}
|
||||
else {
|
||||
difference() {
|
||||
linear_extrude(height = Base_Dybde, convexity = 10)
|
||||
draw_2d_base();
|
||||
engraving_cutter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Korrekt placering ved stående print:
|
||||
// Efter rotate([90,0,0]) bliver oprindelig Y til ny Z.
|
||||
// Derfor flyttes modellen med -scene_ymin efter rotationen.
|
||||
module apply_orientation() {
|
||||
if (Orientering == 1) {
|
||||
translate([0, 0, -_scene_ymin()])
|
||||
rotate([90, 0, 0])
|
||||
children();
|
||||
} else {
|
||||
children();
|
||||
}
|
||||
}
|
||||
|
||||
module diagnostics() {
|
||||
if (Diagnostik) {
|
||||
echo("--- 3D TEXT GENERATOR / NIGHTLY v2 ---");
|
||||
echo(Requested_Font = _Fuld_Font);
|
||||
echo(Resolved_Font_Family = _FontMetrics.font.family);
|
||||
echo(Resolved_Font_Style = _FontMetrics.font.style);
|
||||
echo(Active_Lines = _Linjer);
|
||||
echo(Text_BBox_mm = _text_bbox());
|
||||
echo(Text_Size_mm = [_text_width(), _text_height()]);
|
||||
if (Base_Type != 0)
|
||||
echo(Base_BBox_mm = _base_bbox());
|
||||
if (Bogstav_Afstand_mm != 0)
|
||||
echo("NOTE: mm-tracking placerer tegn individuelt; ligaturer og par-kerning fra den samlede streng bruges derfor ikke.");
|
||||
if (Bevel_Tykkelse > _eff_text_bevel() + Epsilon)
|
||||
echo("NOTE: Bevel_Tykkelse er automatisk begrænset af Tekst_Dybde.");
|
||||
if (!Gravering_Gennem && Graverings_Dybde > _eff_engrave_depth() + Epsilon)
|
||||
echo("NOTE: Graverings_Dybde er automatisk reduceret for at bevare Minimum_Bundtykkelse.");
|
||||
if (Base_Type == 0 && Print_Mode == 1)
|
||||
echo("NOTE: Print_Mode=Engraved ignoreres når Base_Type=0.");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MAIN
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
diagnostics();
|
||||
apply_orientation()
|
||||
assemble_scene();
|
||||
46
universal-clock-dial/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Universal Clock Dial
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated; movement fit and hand clearance require physical checks.
|
||||
|
||||
The editable generator is [universal-clock-dial.scad](universal-clock-dial.scad). FDM printing plus optional laser, CNC, vinyl, or plotter workflows.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
180 mm round, 3 mm thick dial with 8.2 mm centre hole, rings, track, numbers, and text.
|
||||
|
||||
**Fit and tolerance:** Measure the clock movement shaft, recess, and hand stack before manufacturing.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator requires a recent OpenSCAD Nightly build with `textmetrics` enabled.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default round dial | [Download](stl-files/round.stl) | [Preview](images/round.png) |
|
||||
| Star dial with subdial | [Download](stl-files/star-subdial.stl) | [Preview](images/star-subdial.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print the dial flat on its rear face.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Requires OpenSCAD Nightly with textmetrics enabled.
|
||||
- Fine marks and text must match the nozzle or tool resolution.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
universal-clock-dial/images/round.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
universal-clock-dial/images/star-subdial.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
universal-clock-dial/stl-files/round.stl
Normal file
BIN
universal-clock-dial/stl-files/star-subdial.stl
Normal file
694
universal-clock-dial/universal-clock-dial.scad
Normal file
|
|
@ -0,0 +1,694 @@
|
|||
/*
|
||||
===============================================================================
|
||||
UNIVERSAL CLOCK DIAL GENERATOR — OpenSCAD Nightly
|
||||
Version 1.0 — 2026-09
|
||||
|
||||
Requires a recent OpenSCAD Nightly / development snapshot because text layout
|
||||
deliberately uses textmetrics(). No external libraries are required.
|
||||
|
||||
Designed for:
|
||||
• 3D printing
|
||||
• laser cutting / engraving
|
||||
• CNC routing / engraving
|
||||
• vinyl / plotter workflows via 2D export
|
||||
|
||||
Recommended workflow:
|
||||
1. Select parameters in the Customizer.
|
||||
2. Use Output_Mode = "3D" for a printable dial.
|
||||
3. Use 2D_CUT / 2D_ENGRAVE / 2D_MARKS / 2D_TEXT for SVG/DXF export.
|
||||
4. Use F6 render before final export.
|
||||
|
||||
textmetrics() notes:
|
||||
We do not assume text() starts at [0,0], and we do not use string length as
|
||||
a width estimate. The helpers below use textmetrics().position and .size to
|
||||
center and fit the actual ink bounds of each rendered string.
|
||||
===============================================================================
|
||||
*/
|
||||
|
||||
/* [Output] */
|
||||
Output_Mode = "3D"; // [3D,2D_CUT,2D_ENGRAVE,2D_MARKS,2D_TEXT,2D_PREVIEW]
|
||||
Preview_Colors = true;
|
||||
Explode_3D = false;
|
||||
Explode_Distance = 8; // [0:1:40]
|
||||
|
||||
/* [Dial shape] */
|
||||
Dial_Shape = "Round"; // [Round,Oval,Rectangle,RoundedRectangle,Polygon,Star]
|
||||
Dial_Diameter = 180; // [30:1:500]
|
||||
Dial_Width = 180; // [30:1:500]
|
||||
Dial_Height = 180; // [30:1:500]
|
||||
Corner_Radius = 18; // [0:0.5:100]
|
||||
Polygon_Sides = 8; // [3:1:24]
|
||||
Polygon_Rotation = 22.5; // [-180:1:180]
|
||||
Star_Points = 12; // [3:1:36]
|
||||
Star_Inner_Ratio = 0.88; // [0.1:0.01:0.99]
|
||||
Shape_Margin = 0; // [-20:0.5:40]
|
||||
|
||||
/* [3D body] */
|
||||
Dial_Thickness = 3.0; // [0.4:0.1:20]
|
||||
Back_Recess_Enable = false;
|
||||
Back_Recess_Diameter = 58; // [5:1:200]
|
||||
Back_Recess_Depth = 1.2; // [0:0.1:10]
|
||||
Edge_Bevel_Enable = false;
|
||||
Edge_Bevel = 0.8; // [0:0.1:5]
|
||||
|
||||
/* [Center / movement hole] */
|
||||
Center_Hole_Enable = true;
|
||||
Center_Hole_Type = "Round"; // [Round,Square,D_Shaft,None]
|
||||
Center_Hole_Diameter = 8.2; // [0.5:0.1:30]
|
||||
Center_Hole_Square = 8.2; // [0.5:0.1:30]
|
||||
D_Shaft_Flat = 1.8; // [0:0.1:10]
|
||||
Center_Hole_X = 0; // [-50:0.1:50]
|
||||
Center_Hole_Y = 0; // [-50:0.1:50]
|
||||
|
||||
/* [Outer rings and borders] */
|
||||
Outer_Rim_Enable = true;
|
||||
Outer_Rim_Inset = 4; // [0:0.5:40]
|
||||
Outer_Rim_Width = 1.2; // [0.2:0.1:10]
|
||||
Outer_Rim_Mode = "Raised"; // [Raised,Engraved,None]
|
||||
Outer_Rim_Height = 0.7; // [0.1:0.1:5]
|
||||
Outer_Rim_Depth = 0.5; // [0.1:0.1:5]
|
||||
Inner_Ring_Enable = true;
|
||||
Inner_Ring_Radius = 58; // [5:0.5:240]
|
||||
Inner_Ring_Width = 0.7; // [0.1:0.1:8]
|
||||
Inner_Ring_Mode = "Engraved"; // [Raised,Engraved,None]
|
||||
|
||||
/* [Minute / second track] */
|
||||
Track_Enable = true;
|
||||
Track_Count = 60; // [1:1:240]
|
||||
Track_Radius = 78; // [5:0.5:240]
|
||||
Track_Major_Every = 5; // [1:1:60]
|
||||
Track_Major_Style = "Line"; // [Line,Dot,Pill,Triangle,Diamond,None]
|
||||
Track_Minor_Style = "Line"; // [Line,Dot,Pill,Triangle,Diamond,None]
|
||||
Track_Major_Length = 6; // [0.5:0.25:30]
|
||||
Track_Minor_Length = 3; // [0.25:0.25:20]
|
||||
Track_Major_Width = 1.6; // [0.2:0.1:8]
|
||||
Track_Minor_Width = 0.7; // [0.1:0.1:5]
|
||||
Track_Major_Diameter = 3.2; // [0.5:0.1:12]
|
||||
Track_Minor_Diameter = 1.5; // [0.3:0.1:8]
|
||||
Track_Rotation = 0; // [-180:1:180]
|
||||
Track_Mode = "Raised"; // [Raised,Engraved,Through,None]
|
||||
Track_Height = 0.7; // [0.1:0.1:5]
|
||||
Track_Depth = 0.5; // [0.1:0.1:5]
|
||||
|
||||
/* [Hour markers] */
|
||||
Hour_Markers_Enable = false;
|
||||
Hour_Marker_Count = 12; // [1:1:36]
|
||||
Hour_Marker_Radius = 66; // [5:0.5:240]
|
||||
Hour_Marker_Style = "Pill"; // [Line,Dot,Pill,Triangle,Diamond,None]
|
||||
Hour_Marker_Length = 8; // [0.5:0.25:30]
|
||||
Hour_Marker_Width = 2.4; // [0.2:0.1:12]
|
||||
Hour_Marker_Diameter = 4; // [0.5:0.1:15]
|
||||
Hour_Marker_Rotation = 0; // [-180:1:180]
|
||||
Hour_Marker_Mode = "Raised"; // [Raised,Engraved,Through,None]
|
||||
Hour_Marker_Height = 0.8; // [0.1:0.1:5]
|
||||
Hour_Marker_Depth = 0.5; // [0.1:0.1:5]
|
||||
|
||||
/* [Hour numerals] */
|
||||
Numbers_Enable = true;
|
||||
Number_Set = "Arabic_1_12"; // [Arabic_1_12,Arabic_0_11,Roman,Quarter_Arabic,Quarter_Roman,Words,None]
|
||||
Number_Radius = 61; // [5:0.5:240]
|
||||
Number_Font = "Liberation Sans:style=Bold";
|
||||
Number_Size = 18; // [2:0.5:80]
|
||||
Number_Max_Width = 22; // [2:0.5:80]
|
||||
Number_Max_Height = 18; // [2:0.5:80]
|
||||
Number_Spacing = 1.0; // [0.5:0.05:2]
|
||||
Number_Orientation = "Upright"; // [Upright,Radial,Tangent]
|
||||
Number_Rotation = 0; // [-180:1:180]
|
||||
Number_Mode = "Raised"; // [Raised,Engraved,Through,None]
|
||||
Number_Height = 0.8; // [0.1:0.1:5]
|
||||
Number_Depth = 0.5; // [0.1:0.1:5]
|
||||
|
||||
/* [Custom text] */
|
||||
Top_Text_Enable = true;
|
||||
Top_Text = "MAKERSPACE";
|
||||
Top_Text_Y = 27; // [-200:0.5:200]
|
||||
Top_Text_Font = "Liberation Sans:style=Bold";
|
||||
Top_Text_Size = 8; // [1:0.5:50]
|
||||
Top_Text_Max_Width = 70; // [5:1:300]
|
||||
Top_Text_Max_Height = 12; // [2:0.5:100]
|
||||
Top_Text_Mode = "Engraved"; // [Raised,Engraved,Through,None]
|
||||
|
||||
Bottom_Text_Enable = true;
|
||||
Bottom_Text = "NÆSTVED";
|
||||
Bottom_Text_Y = -30; // [-200:0.5:200]
|
||||
Bottom_Text_Font = "Liberation Sans";
|
||||
Bottom_Text_Size = 6; // [1:0.5:50]
|
||||
Bottom_Text_Max_Width = 55; // [5:1:300]
|
||||
Bottom_Text_Max_Height = 10; // [2:0.5:100]
|
||||
Bottom_Text_Mode = "Engraved"; // [Raised,Engraved,Through,None]
|
||||
|
||||
Center_Text_Enable = false;
|
||||
Center_Text = "QUARTZ";
|
||||
Center_Text_X = 0; // [-200:0.5:200]
|
||||
Center_Text_Y = -15; // [-200:0.5:200]
|
||||
Center_Text_Font = "Liberation Sans";
|
||||
Center_Text_Size = 5; // [1:0.5:50]
|
||||
Center_Text_Max_Width = 45; // [5:1:300]
|
||||
Center_Text_Max_Height = 8; // [2:0.5:100]
|
||||
Center_Text_Mode = "Engraved"; // [Raised,Engraved,Through,None]
|
||||
|
||||
Text_Raised_Height = 0.7; // [0.1:0.1:5]
|
||||
Text_Engrave_Depth = 0.45; // [0.1:0.1:5]
|
||||
|
||||
/* [Subdial 1] */
|
||||
Subdial1_Enable = false;
|
||||
Subdial1_X = 0; // [-200:0.5:200]
|
||||
Subdial1_Y = -35; // [-200:0.5:200]
|
||||
Subdial1_Radius = 19; // [4:0.5:80]
|
||||
Subdial1_Count = 60; // [1:1:120]
|
||||
Subdial1_Major_Every = 5; // [1:1:60]
|
||||
Subdial1_Label = "SECONDS";
|
||||
Subdial1_Label_Size = 3.5; // [1:0.25:20]
|
||||
Subdial1_Center_Hole = 2.2; // [0:0.1:20]
|
||||
|
||||
/* [Subdial 2] */
|
||||
Subdial2_Enable = false;
|
||||
Subdial2_X = -38; // [-200:0.5:200]
|
||||
Subdial2_Y = 0; // [-200:0.5:200]
|
||||
Subdial2_Radius = 17; // [4:0.5:80]
|
||||
Subdial2_Count = 24; // [1:1:120]
|
||||
Subdial2_Major_Every = 4; // [1:1:60]
|
||||
Subdial2_Label = "24 H";
|
||||
Subdial2_Label_Size = 3.5; // [1:0.25:20]
|
||||
Subdial2_Center_Hole = 2.2; // [0:0.1:20]
|
||||
|
||||
/* [Subdial 3] */
|
||||
Subdial3_Enable = false;
|
||||
Subdial3_X = 38; // [-200:0.5:200]
|
||||
Subdial3_Y = 0; // [-200:0.5:200]
|
||||
Subdial3_Radius = 17; // [4:0.5:80]
|
||||
Subdial3_Count = 31; // [1:1:120]
|
||||
Subdial3_Major_Every = 5; // [1:1:60]
|
||||
Subdial3_Label = "DATE";
|
||||
Subdial3_Label_Size = 3.5; // [1:0.25:20]
|
||||
Subdial3_Center_Hole = 2.2; // [0:0.1:20]
|
||||
|
||||
/* [Subdial appearance] */
|
||||
Subdial_Ring_Width = 0.65; // [0.1:0.05:5]
|
||||
Subdial_Tick_Width = 0.45; // [0.1:0.05:3]
|
||||
Subdial_Tick_Length = 2.0; // [0.2:0.1:8]
|
||||
Subdial_Major_Length = 3.2; // [0.2:0.1:10]
|
||||
Subdial_Mode = "Engraved"; // [Raised,Engraved,Through,None]
|
||||
Subdial_Depth = 0.35; // [0.1:0.05:3]
|
||||
Subdial_Height = 0.5; // [0.1:0.05:3]
|
||||
Subdial_Font = "Liberation Sans";
|
||||
|
||||
/* [Decorative center] */
|
||||
Center_Ring_Enable = true;
|
||||
Center_Ring_Radius = 8; // [1:0.5:50]
|
||||
Center_Ring_Width = 0.8; // [0.1:0.1:6]
|
||||
Center_Ring_Mode = "Engraved"; // [Raised,Engraved,None]
|
||||
|
||||
/* [Mounting / auxiliary holes] */
|
||||
Aux_Holes_Enable = false;
|
||||
Aux_Hole_Diameter = 3.2; // [0.5:0.1:20]
|
||||
Aux_Holes = [[0,75],[0,-75]];
|
||||
|
||||
/* [Manufacturing compensation] */
|
||||
Cut_Kerf_Compensation = 0; // [-1:0.01:1]
|
||||
Engrave_Expansion = 0; // [-1:0.01:1]
|
||||
Min_Feature = 0.2; // [0.05:0.05:3]
|
||||
|
||||
/* [Quality] */
|
||||
Curve_Fragments = 128; // [24:8:512]
|
||||
Text_Fragments = 24; // [8:4:128]
|
||||
$fn = Curve_Fragments;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function clamp(v, lo, hi) = min(max(v, lo), hi);
|
||||
function safe_div(a,b,fallback=1) = abs(b) < 0.000001 ? fallback : a/b;
|
||||
function polar(r,a) = [r*sin(a), r*cos(a)];
|
||||
function is_major(i,every) = every > 0 && i % every == 0;
|
||||
|
||||
ROMAN = ["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];
|
||||
WORDS = ["TWELVE","ONE","TWO","THREE","FOUR","FIVE","SIX","SEVEN","EIGHT","NINE","TEN","ELEVEN"];
|
||||
|
||||
function hour_label(i) =
|
||||
Number_Set == "Arabic_1_12" ? str(i == 0 ? 12 : i) :
|
||||
Number_Set == "Arabic_0_11" ? str(i) :
|
||||
Number_Set == "Roman" ? ROMAN[i] :
|
||||
Number_Set == "Quarter_Arabic" ? (i==0?"12":i==3?"3":i==6?"6":i==9?"9":"") :
|
||||
Number_Set == "Quarter_Roman" ? (i==0?"XII":i==3?"III":i==6?"VI":i==9?"IX":"") :
|
||||
Number_Set == "Words" ? WORDS[i] : "";
|
||||
|
||||
function star_pts(n, ro, ri, rot=0) =
|
||||
[for(i=[0:2*n-1])
|
||||
let(a = rot + i*180/n, r = (i%2==0 ? ro : ri))
|
||||
[r*sin(a), r*cos(a)]];
|
||||
|
||||
// Nightly textmetrics(): tight ink-bounds fit scale.
|
||||
function text_fit_scale(s, nominal_size, font, max_w, max_h, spacing=1) =
|
||||
let(m = textmetrics(text=s, size=nominal_size, font=font,
|
||||
halign="left", valign="baseline", spacing=spacing),
|
||||
sx = safe_div(max_w, max(m.size.x, 0.0001), 1),
|
||||
sy = safe_div(max_h, max(m.size.y, 0.0001), 1))
|
||||
min(1, sx, sy);
|
||||
|
||||
module tight_text_2d(s, nominal_size=10, font="Liberation Sans",
|
||||
max_w=1000, max_h=1000, spacing=1) {
|
||||
if (s != "") {
|
||||
m = textmetrics(text=s, size=nominal_size, font=font,
|
||||
halign="left", valign="baseline", spacing=spacing);
|
||||
fit = min(1,
|
||||
safe_div(max_w, max(m.size.x,0.0001),1),
|
||||
safe_div(max_h, max(m.size.y,0.0001),1));
|
||||
scale([fit,fit])
|
||||
translate([-m.position.x - m.size.x/2,
|
||||
-m.position.y - m.size.y/2])
|
||||
text(text=s, size=nominal_size, font=font,
|
||||
halign="left", valign="baseline", spacing=spacing,
|
||||
$fn=Text_Fragments);
|
||||
}
|
||||
}
|
||||
|
||||
module ring2d(radius, width) {
|
||||
w = max(width, Min_Feature);
|
||||
difference() {
|
||||
circle(r=radius + w/2);
|
||||
circle(r=max(0.001, radius - w/2));
|
||||
}
|
||||
}
|
||||
|
||||
module rounded_rect_2d(w,h,r) {
|
||||
rr = min(max(r,0), min(w,h)/2);
|
||||
if (rr <= 0) square([w,h], center=true);
|
||||
else offset(r=rr) square([max(0.01,w-2*rr), max(0.01,h-2*rr)], center=true);
|
||||
}
|
||||
|
||||
module raw_dial_shape_2d(delta=0) {
|
||||
if (Dial_Shape == "Round")
|
||||
circle(r=Dial_Diameter/2 + Shape_Margin + delta);
|
||||
else if (Dial_Shape == "Oval")
|
||||
scale([(Dial_Width/2 + Shape_Margin + delta),
|
||||
(Dial_Height/2 + Shape_Margin + delta)]) circle(r=1);
|
||||
else if (Dial_Shape == "Rectangle")
|
||||
square([Dial_Width + 2*(Shape_Margin+delta),
|
||||
Dial_Height + 2*(Shape_Margin+delta)], center=true);
|
||||
else if (Dial_Shape == "RoundedRectangle")
|
||||
rounded_rect_2d(Dial_Width + 2*(Shape_Margin+delta),
|
||||
Dial_Height + 2*(Shape_Margin+delta),
|
||||
max(0,Corner_Radius + delta));
|
||||
else if (Dial_Shape == "Polygon")
|
||||
rotate(Polygon_Rotation)
|
||||
circle(r=Dial_Diameter/2 + Shape_Margin + delta, $fn=Polygon_Sides);
|
||||
else if (Dial_Shape == "Star")
|
||||
polygon(star_pts(Star_Points,
|
||||
Dial_Diameter/2 + Shape_Margin + delta,
|
||||
(Dial_Diameter/2 + Shape_Margin + delta)*Star_Inner_Ratio,
|
||||
Polygon_Rotation));
|
||||
}
|
||||
|
||||
module dial_cut_outline_2d() {
|
||||
delta = Cut_Kerf_Compensation/2;
|
||||
raw_dial_shape_2d(delta);
|
||||
}
|
||||
|
||||
module center_hole_2d(extra=0) {
|
||||
translate([Center_Hole_X,Center_Hole_Y]) {
|
||||
if (Center_Hole_Enable && Center_Hole_Type == "Round")
|
||||
circle(d=max(0.01,Center_Hole_Diameter + 2*extra));
|
||||
else if (Center_Hole_Enable && Center_Hole_Type == "Square")
|
||||
square([Center_Hole_Square+2*extra,Center_Hole_Square+2*extra],center=true);
|
||||
else if (Center_Hole_Enable && Center_Hole_Type == "D_Shaft") {
|
||||
d = Center_Hole_Diameter + 2*extra;
|
||||
flat = clamp(D_Shaft_Flat,0,d*0.9);
|
||||
intersection() {
|
||||
circle(d=d);
|
||||
translate([-d/2,-d/2]) square([d-flat,d]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module aux_holes_2d(extra=0) {
|
||||
if (Aux_Holes_Enable)
|
||||
for(p=Aux_Holes)
|
||||
translate(p) circle(d=max(0.01,Aux_Hole_Diameter + 2*extra));
|
||||
}
|
||||
|
||||
module all_through_holes_2d() {
|
||||
center_hole_2d(Cut_Kerf_Compensation/2);
|
||||
aux_holes_2d(Cut_Kerf_Compensation/2);
|
||||
subdial_holes_2d(Cut_Kerf_Compensation/2);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Marker primitives
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module marker_shape_2d(style="Line", len=5, width=1, diameter=2) {
|
||||
w = max(width, Min_Feature);
|
||||
d = max(diameter, Min_Feature);
|
||||
if (style == "Line")
|
||||
square([w,max(len,Min_Feature)],center=true);
|
||||
else if (style == "Pill")
|
||||
hull() {
|
||||
translate([0,-max(0,len-d)/2]) circle(d=d);
|
||||
translate([0, max(0,len-d)/2]) circle(d=d);
|
||||
}
|
||||
else if (style == "Dot")
|
||||
circle(d=d);
|
||||
else if (style == "Triangle")
|
||||
polygon([[0,len/2],[-w,-len/2],[w,-len/2]]);
|
||||
else if (style == "Diamond")
|
||||
polygon([[0,len/2],[w,0],[0,-len/2],[-w,0]]);
|
||||
}
|
||||
|
||||
module radial_marker_2d(radius, angle, style, len, width, diameter) {
|
||||
p = polar(radius,angle);
|
||||
translate(p) rotate(-angle) marker_shape_2d(style,len,width,diameter);
|
||||
}
|
||||
|
||||
module track_geometry_2d() {
|
||||
if (Track_Enable && Track_Mode != "None")
|
||||
for(i=[0:Track_Count-1]) {
|
||||
major = is_major(i,Track_Major_Every);
|
||||
a = Track_Rotation + i*360/Track_Count;
|
||||
style = major ? Track_Major_Style : Track_Minor_Style;
|
||||
len = major ? Track_Major_Length : Track_Minor_Length;
|
||||
wid = major ? Track_Major_Width : Track_Minor_Width;
|
||||
dia = major ? Track_Major_Diameter : Track_Minor_Diameter;
|
||||
radial_marker_2d(Track_Radius,a,style,len,wid,dia);
|
||||
}
|
||||
}
|
||||
|
||||
module hour_marker_geometry_2d() {
|
||||
if (Hour_Markers_Enable && Hour_Marker_Mode != "None")
|
||||
for(i=[0:Hour_Marker_Count-1])
|
||||
radial_marker_2d(Hour_Marker_Radius,
|
||||
Hour_Marker_Rotation + i*360/Hour_Marker_Count,
|
||||
Hour_Marker_Style,
|
||||
Hour_Marker_Length,
|
||||
Hour_Marker_Width,
|
||||
Hour_Marker_Diameter);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Numerals / text
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module numeral_at(i) {
|
||||
s = hour_label(i);
|
||||
if (s != "") {
|
||||
a = Number_Rotation + i*30;
|
||||
p = polar(Number_Radius,a);
|
||||
orient = Number_Orientation == "Radial" ? -a :
|
||||
Number_Orientation == "Tangent" ? -a+90 : 0;
|
||||
translate(p)
|
||||
rotate(orient)
|
||||
tight_text_2d(s, Number_Size, Number_Font,
|
||||
Number_Max_Width, Number_Max_Height,
|
||||
Number_Spacing);
|
||||
}
|
||||
}
|
||||
|
||||
module number_geometry_2d() {
|
||||
if (Numbers_Enable && Number_Set != "None" && Number_Mode != "None")
|
||||
for(i=[0:11]) numeral_at(i);
|
||||
}
|
||||
|
||||
module top_text_2d() {
|
||||
if (Top_Text_Enable && Top_Text_Mode != "None")
|
||||
translate([0,Top_Text_Y])
|
||||
tight_text_2d(Top_Text,Top_Text_Size,Top_Text_Font,
|
||||
Top_Text_Max_Width,Top_Text_Max_Height);
|
||||
}
|
||||
|
||||
module bottom_text_2d() {
|
||||
if (Bottom_Text_Enable && Bottom_Text_Mode != "None")
|
||||
translate([0,Bottom_Text_Y])
|
||||
tight_text_2d(Bottom_Text,Bottom_Text_Size,Bottom_Text_Font,
|
||||
Bottom_Text_Max_Width,Bottom_Text_Max_Height);
|
||||
}
|
||||
|
||||
module center_text_2d() {
|
||||
if (Center_Text_Enable && Center_Text_Mode != "None")
|
||||
translate([Center_Text_X,Center_Text_Y])
|
||||
tight_text_2d(Center_Text,Center_Text_Size,Center_Text_Font,
|
||||
Center_Text_Max_Width,Center_Text_Max_Height);
|
||||
}
|
||||
|
||||
module all_custom_text_2d() {
|
||||
top_text_2d();
|
||||
bottom_text_2d();
|
||||
center_text_2d();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rings / subdials
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module outer_rim_2d() {
|
||||
// Generalized inset border: offset inward from actual dial outline.
|
||||
if (Outer_Rim_Enable && Outer_Rim_Mode != "None")
|
||||
difference() {
|
||||
offset(delta=-Outer_Rim_Inset + Outer_Rim_Width/2) raw_dial_shape_2d();
|
||||
offset(delta=-Outer_Rim_Inset - Outer_Rim_Width/2) raw_dial_shape_2d();
|
||||
}
|
||||
}
|
||||
|
||||
module inner_ring_2d() {
|
||||
if (Inner_Ring_Enable && Inner_Ring_Mode != "None")
|
||||
ring2d(Inner_Ring_Radius,Inner_Ring_Width);
|
||||
}
|
||||
|
||||
module center_ring_2d() {
|
||||
if (Center_Ring_Enable && Center_Ring_Mode != "None")
|
||||
translate([Center_Hole_X,Center_Hole_Y]) ring2d(Center_Ring_Radius,Center_Ring_Width);
|
||||
}
|
||||
|
||||
module subdial_2d(x,y,r,count,major_every,label,label_size,hole_d) {
|
||||
translate([x,y]) {
|
||||
ring2d(r,Subdial_Ring_Width);
|
||||
for(i=[0:count-1]) {
|
||||
major = is_major(i,major_every);
|
||||
a = i*360/count;
|
||||
rr = r - (major ? Subdial_Major_Length : Subdial_Tick_Length)/2;
|
||||
radial_marker_2d(rr,a,"Line",
|
||||
major ? Subdial_Major_Length : Subdial_Tick_Length,
|
||||
Subdial_Tick_Width,Subdial_Tick_Width);
|
||||
}
|
||||
if (label != "")
|
||||
translate([0,-r*0.42])
|
||||
tight_text_2d(label,label_size,Subdial_Font,r*1.35,label_size*1.35);
|
||||
}
|
||||
}
|
||||
|
||||
module subdial_holes_2d(extra=0) {
|
||||
if (Subdial1_Enable && Subdial1_Center_Hole > 0)
|
||||
translate([Subdial1_X,Subdial1_Y]) circle(d=Subdial1_Center_Hole+2*extra);
|
||||
if (Subdial2_Enable && Subdial2_Center_Hole > 0)
|
||||
translate([Subdial2_X,Subdial2_Y]) circle(d=Subdial2_Center_Hole+2*extra);
|
||||
if (Subdial3_Enable && Subdial3_Center_Hole > 0)
|
||||
translate([Subdial3_X,Subdial3_Y]) circle(d=Subdial3_Center_Hole+2*extra);
|
||||
}
|
||||
|
||||
module subdial_geometry_2d() {
|
||||
if (Subdial_Mode != "None") {
|
||||
if (Subdial1_Enable) subdial_2d(Subdial1_X,Subdial1_Y,Subdial1_Radius,Subdial1_Count,Subdial1_Major_Every,Subdial1_Label,Subdial1_Label_Size,Subdial1_Center_Hole);
|
||||
if (Subdial2_Enable) subdial_2d(Subdial2_X,Subdial2_Y,Subdial2_Radius,Subdial2_Count,Subdial2_Major_Every,Subdial2_Label,Subdial2_Label_Size,Subdial2_Center_Hole);
|
||||
if (Subdial3_Enable) subdial_2d(Subdial3_X,Subdial3_Y,Subdial3_Radius,Subdial3_Count,Subdial3_Major_Every,Subdial3_Label,Subdial3_Label_Size,Subdial3_Center_Hole);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Group by manufacturing treatment
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module raised_geometry_2d() {
|
||||
if (Track_Mode == "Raised") track_geometry_2d();
|
||||
if (Hour_Marker_Mode == "Raised") hour_marker_geometry_2d();
|
||||
if (Number_Mode == "Raised") number_geometry_2d();
|
||||
if (Outer_Rim_Mode == "Raised") outer_rim_2d();
|
||||
if (Inner_Ring_Mode == "Raised") inner_ring_2d();
|
||||
if (Center_Ring_Mode == "Raised") center_ring_2d();
|
||||
if (Subdial_Mode == "Raised") subdial_geometry_2d();
|
||||
if (Top_Text_Mode == "Raised") top_text_2d();
|
||||
if (Bottom_Text_Mode == "Raised") bottom_text_2d();
|
||||
if (Center_Text_Mode == "Raised") center_text_2d();
|
||||
}
|
||||
|
||||
module engraved_geometry_2d() {
|
||||
if (Track_Mode == "Engraved") track_geometry_2d();
|
||||
if (Hour_Marker_Mode == "Engraved") hour_marker_geometry_2d();
|
||||
if (Number_Mode == "Engraved") number_geometry_2d();
|
||||
if (Outer_Rim_Mode == "Engraved") outer_rim_2d();
|
||||
if (Inner_Ring_Mode == "Engraved") inner_ring_2d();
|
||||
if (Center_Ring_Mode == "Engraved") center_ring_2d();
|
||||
if (Subdial_Mode == "Engraved") subdial_geometry_2d();
|
||||
if (Top_Text_Mode == "Engraved") top_text_2d();
|
||||
if (Bottom_Text_Mode == "Engraved") bottom_text_2d();
|
||||
if (Center_Text_Mode == "Engraved") center_text_2d();
|
||||
}
|
||||
|
||||
module through_decoration_2d() {
|
||||
if (Track_Mode == "Through") track_geometry_2d();
|
||||
if (Hour_Marker_Mode == "Through") hour_marker_geometry_2d();
|
||||
if (Number_Mode == "Through") number_geometry_2d();
|
||||
if (Subdial_Mode == "Through") subdial_geometry_2d();
|
||||
if (Top_Text_Mode == "Through") top_text_2d();
|
||||
if (Bottom_Text_Mode == "Through") bottom_text_2d();
|
||||
if (Center_Text_Mode == "Through") center_text_2d();
|
||||
}
|
||||
|
||||
module all_cutouts_2d() {
|
||||
all_through_holes_2d();
|
||||
through_decoration_2d();
|
||||
}
|
||||
|
||||
module all_marks_2d() {
|
||||
track_geometry_2d();
|
||||
hour_marker_geometry_2d();
|
||||
outer_rim_2d();
|
||||
inner_ring_2d();
|
||||
center_ring_2d();
|
||||
subdial_geometry_2d();
|
||||
}
|
||||
|
||||
module all_text_2d() {
|
||||
number_geometry_2d();
|
||||
all_custom_text_2d();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 3D construction
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module raw_body_3d() {
|
||||
b = min(max(Edge_Bevel,0), Dial_Thickness/2);
|
||||
if (Edge_Bevel_Enable && b > 0.01) {
|
||||
// True geometric top/bottom bevel using hulls between an inset profile
|
||||
// and the full profile; works for all supported dial outlines.
|
||||
hull() {
|
||||
linear_extrude(height=0.01) offset(delta=-b) raw_dial_shape_2d();
|
||||
translate([0,0,b]) linear_extrude(height=0.01) raw_dial_shape_2d();
|
||||
}
|
||||
translate([0,0,b])
|
||||
linear_extrude(height=max(0.01,Dial_Thickness-2*b)) raw_dial_shape_2d();
|
||||
hull() {
|
||||
translate([0,0,Dial_Thickness-b-0.01])
|
||||
linear_extrude(height=0.01) raw_dial_shape_2d();
|
||||
translate([0,0,Dial_Thickness-0.01])
|
||||
linear_extrude(height=0.01) offset(delta=-b) raw_dial_shape_2d();
|
||||
}
|
||||
} else
|
||||
linear_extrude(height=Dial_Thickness) raw_dial_shape_2d();
|
||||
}
|
||||
|
||||
module base_body_3d() {
|
||||
difference() {
|
||||
raw_body_3d();
|
||||
|
||||
// Through holes and through-decoration get a small Z overcut.
|
||||
translate([0,0,-0.2])
|
||||
linear_extrude(height=Dial_Thickness+0.4)
|
||||
all_cutouts_2d();
|
||||
|
||||
if (Back_Recess_Enable && Back_Recess_Depth > 0)
|
||||
translate([Center_Hole_X,Center_Hole_Y,-0.01])
|
||||
cylinder(d=Back_Recess_Diameter,
|
||||
h=min(Back_Recess_Depth,Dial_Thickness-0.05));
|
||||
|
||||
// Engraving cut from front face.
|
||||
translate([0,0,max(0,Dial_Thickness-max(Track_Depth,Hour_Marker_Depth,Number_Depth,Outer_Rim_Depth,Subdial_Depth,Text_Engrave_Depth)-0.01)])
|
||||
linear_extrude(height=max(Track_Depth,Hour_Marker_Depth,Number_Depth,Outer_Rim_Depth,Subdial_Depth,Text_Engrave_Depth)+0.02)
|
||||
engraved_geometry_2d();
|
||||
}
|
||||
}
|
||||
|
||||
module raised_features_3d() {
|
||||
// Features share one user-visible top height policy: each category may
|
||||
// request its own height, so they are extruded separately.
|
||||
z0 = Dial_Thickness + (Explode_3D ? Explode_Distance : 0);
|
||||
|
||||
if (Track_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Track_Height) track_geometry_2d();
|
||||
if (Hour_Marker_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Hour_Marker_Height) hour_marker_geometry_2d();
|
||||
if (Number_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Number_Height) number_geometry_2d();
|
||||
if (Outer_Rim_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Outer_Rim_Height) outer_rim_2d();
|
||||
if (Inner_Ring_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Outer_Rim_Height) inner_ring_2d();
|
||||
if (Center_Ring_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Outer_Rim_Height) center_ring_2d();
|
||||
if (Subdial_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Subdial_Height) subdial_geometry_2d();
|
||||
if (Top_Text_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Text_Raised_Height) top_text_2d();
|
||||
if (Bottom_Text_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Text_Raised_Height) bottom_text_2d();
|
||||
if (Center_Text_Mode == "Raised")
|
||||
translate([0,0,z0]) linear_extrude(height=Text_Raised_Height) center_text_2d();
|
||||
}
|
||||
|
||||
module clock_dial_3d() {
|
||||
if (Preview_Colors) {
|
||||
color([0.82,0.82,0.82]) base_body_3d();
|
||||
color([0.12,0.12,0.12]) raised_features_3d();
|
||||
} else {
|
||||
base_body_3d();
|
||||
raised_features_3d();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 2D export views
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
module export_2d_cut() {
|
||||
difference() {
|
||||
dial_cut_outline_2d();
|
||||
all_cutouts_2d();
|
||||
}
|
||||
}
|
||||
|
||||
module export_2d_engrave() {
|
||||
if (Engrave_Expansion == 0)
|
||||
engraved_geometry_2d();
|
||||
else
|
||||
offset(delta=Engrave_Expansion) engraved_geometry_2d();
|
||||
}
|
||||
|
||||
module export_2d_marks() { all_marks_2d(); }
|
||||
module export_2d_text() { all_text_2d(); }
|
||||
|
||||
module export_2d_preview() {
|
||||
color([0.85,0.85,0.85]) export_2d_cut();
|
||||
color([0.15,0.15,0.15]) export_2d_engrave();
|
||||
color([0.05,0.25,0.75]) raised_geometry_2d();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Diagnostics
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
echo("Universal Clock Dial Generator — Nightly textmetrics edition");
|
||||
echo("Output",Output_Mode,"Shape",Dial_Shape);
|
||||
echo("Center hole",Center_Hole_Enable,Center_Hole_Type,Center_Hole_Diameter);
|
||||
if (Numbers_Enable && Number_Set != "None") {
|
||||
_m12 = textmetrics(text=hour_label(0), size=Number_Size, font=Number_Font,
|
||||
halign="left",valign="baseline",spacing=Number_Spacing);
|
||||
echo("12-label textmetrics position",_m12.position,"size",_m12.size,"advance",_m12.advance);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
if (Output_Mode == "3D")
|
||||
clock_dial_3d();
|
||||
else if (Output_Mode == "2D_CUT")
|
||||
export_2d_cut();
|
||||
else if (Output_Mode == "2D_ENGRAVE")
|
||||
export_2d_engrave();
|
||||
else if (Output_Mode == "2D_MARKS")
|
||||
export_2d_marks();
|
||||
else if (Output_Mode == "2D_TEXT")
|
||||
export_2d_text();
|
||||
else if (Output_Mode == "2D_PREVIEW")
|
||||
export_2d_preview();
|
||||
47
universal-sign/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Universal Sign Generator
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated for representative 3D constructions.
|
||||
|
||||
The editable generator is [universal-sign.scad](universal-sign.scad). FDM printing plus optional CNC, laser cutting, and engraving output.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
Auto-sized rounded rectangular 3 mm plate with raised text and straight edges.
|
||||
|
||||
**Fit and tolerance:** Verify installed fonts and calibrate inlay, fold, kerf, and mounting dimensions.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator requires a recent OpenSCAD Nightly build with `textmetrics` enabled.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default flat plate | [Download](stl-files/flat-plate.stl) | [Preview](images/flat-plate.png) |
|
||||
| Desk L-stand | [Download](stl-files/desk-stand.stl) | [Preview](images/desk-stand.png) |
|
||||
| Separate inlay text insert | [Download](stl-files/inlay-insert.stl) | [Preview](images/inlay-insert.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print flat plates face-up or face-down as appropriate; inspect supports for desk assemblies.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Accurate sizing requires OpenSCAD Nightly with textmetrics enabled.
|
||||
- Desk joinery and inlays require physical calibration.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
universal-sign/images/desk-stand.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
universal-sign/images/flat-plate.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
universal-sign/images/inlay-insert.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
universal-sign/stl-files/desk-stand.stl
Normal file
BIN
universal-sign/stl-files/flat-plate.stl
Normal file
BIN
universal-sign/stl-files/inlay-insert.stl
Normal file
1358
universal-sign/universal-sign.scad
Normal file
47
usb-stick-holder/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# USB Stick Holder
|
||||
|
||||

|
||||
|
||||
**Status:** Digitally validated; connector retention requires physical calibration.
|
||||
|
||||
The editable generator is [usb-stick-holder.scad](usb-stick-holder.scad). FDM printing in PLA or PETG.
|
||||
|
||||
## Defaults and important controls
|
||||
|
||||
Eight-position rectangular holder, 18 mm high, for nominal 12.0 × 4.5 mm USB-A plugs.
|
||||
|
||||
**Fit and tolerance:** Print the clearance test before a large holder; real USB-A shells vary.
|
||||
|
||||
Open the SCAD file in OpenSCAD and use **Window → Customizer** to change the documented parameters. Dimensions are millimetres.
|
||||
|
||||
## OpenSCAD and export
|
||||
|
||||
This generator requires a recent OpenSCAD Nightly build with `textmetrics` enabled.
|
||||
|
||||
1. Open the SCAD file and select parameters in Customizer.
|
||||
2. Press **F6** for a full render.
|
||||
3. Use **File → Export → Export as STL** for printable 3D output.
|
||||
4. Slice with the actual printer, nozzle, material, and layer profile.
|
||||
|
||||
## Included examples
|
||||
|
||||
| Preset | STL | Digital render |
|
||||
| --- | --- | --- |
|
||||
| Default rectangular holder | [Download](stl-files/rectangular.stl) | [Preview](images/rectangular.png) |
|
||||
| Circular holder | [Download](stl-files/circular.stl) | [Preview](images/circular.png) |
|
||||
| Five-step clearance test | [Download](stl-files/fit-test.stl) | [Preview](images/fit-test.png) |
|
||||
|
||||
## Printing and fabrication
|
||||
|
||||
Print holders and fit tests on their flat base.
|
||||
|
||||
The included STL files passed digital mesh and slicer checks. Digital validation does not establish physical fit, strength, fatigue life, or material suitability.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Requires OpenSCAD Nightly with textmetrics enabled for measured labels.
|
||||
- Body spacing and wall mounting require physical checks.
|
||||
|
||||
## License
|
||||
|
||||
The SCAD source, STL models, renders, and documentation in this public package are licensed under **CC BY-NC 4.0**. See the repository [LICENSE](../LICENSE.md).
|
||||
BIN
usb-stick-holder/images/circular.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
usb-stick-holder/images/fit-test.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
usb-stick-holder/images/rectangular.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
usb-stick-holder/stl-files/circular.stl
Normal file
BIN
usb-stick-holder/stl-files/fit-test.stl
Normal file
BIN
usb-stick-holder/stl-files/rectangular.stl
Normal file
352
usb-stick-holder/usb-stick-holder.scad
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
// USB-A Stick Holder Generator v2.1 - string and wall warning fixes
|
||||
// OpenSCAD Nightly. All dimensions in millimetres.
|
||||
// F6 -> File -> Export -> Export as STL.
|
||||
// Holes accept the metal part of a USB-A male connector, not the plastic body.
|
||||
// Measure your own connectors. Print the fit test before a large holder.
|
||||
|
||||
/* [Holder] */
|
||||
Holder_Type = "Rectangular"; // [Circular, Rectangular, Wall, Fit Test]
|
||||
Count = 8; // [1:1:48]
|
||||
Rows = 2; // [1:1:12]
|
||||
Columns = 0; // [0:1:48]
|
||||
Holder_Height = 18; // [6:1:60]
|
||||
Edge_Margin = 5; // [2:0.5:20]
|
||||
Hole_Wall = 3; // [1.2:0.2:12]
|
||||
Outer_Corner_Radius = 5; // [0:0.5:30]
|
||||
|
||||
/* [USB-A fit] */
|
||||
USB_Width = 12.0; // [10:0.1:15]
|
||||
USB_Thickness = 4.5; // [3:0.1:7]
|
||||
Clearance_Width = 0.25; // [0:0.05:1.5]
|
||||
Clearance_Thickness = 0.25; // [0:0.05:1.5]
|
||||
Hole_Depth = 12; // [3:0.5:40]
|
||||
Bottom_Thickness = 1.6; // [1:0.2:5]
|
||||
Hole_Corner_Radius = 0.4; // [0:0.1:2]
|
||||
Entry_Chamfer = 0.8; // [0:0.1:3]
|
||||
Entry_Clearance = 0.15; // [0:0.05:0.8]
|
||||
|
||||
/* [Stick body spacing] */
|
||||
Use_Body_Spacing = true;
|
||||
Body_Width = 20; // [12:0.5:50]
|
||||
Body_Thickness = 8; // [4:0.5:30]
|
||||
Body_Gap = 3; // [0:0.5:30]
|
||||
|
||||
/* [Circular] */
|
||||
Circular_Min_Diameter = 45; // [25:1:200]
|
||||
Circular_Orientation = "Radial"; // [Radial, Tangential, Parallel]
|
||||
Center_Clearance = 0; // [0:1:100]
|
||||
Circular_Text_Position = "Center"; // [Center, Ring, Side]
|
||||
Circular_Ring_Width = 12; // [4:1:40]
|
||||
Circular_Text_Start_Angle = 180; // [-360:1:360]
|
||||
Circular_Text_Direction = "CCW"; // [CCW, CW]
|
||||
|
||||
/* [Rectangular] */
|
||||
Rectangular_Orientation = "Horizontal"; // [Horizontal, Vertical]
|
||||
|
||||
/* [Wall mounting] */
|
||||
Wall_Columns = 4; // [1:1:24]
|
||||
Wall_Plate_Thickness = 6; // [3:0.5:20]
|
||||
Wall_Margin = 8; // [4:1:30]
|
||||
Wall_Angle = 15; // [0:1:45]
|
||||
Screw_Diameter = 4.5; // [2:0.5:8]
|
||||
Screw_Head_Diameter = 8.5; // [4:0.5:16]
|
||||
Screw_Head_Depth = 2; // [0:0.5:6]
|
||||
Screw_Spacing = 0; // [0:1:300]
|
||||
Screw_Edge_Offset = 10; // [5:1:30]
|
||||
|
||||
/* [Text] */
|
||||
Show_Text = true;
|
||||
Text = "USB";
|
||||
Text_Font = "Liberation Sans:style=Bold";
|
||||
Text_Size = 8; // [3:0.5:30]
|
||||
Text_Spacing = 1; // [0.5:0.1:2]
|
||||
Text_Mode = "Engraved"; // [Engraved, Raised]
|
||||
Text_Depth = 0.6; // [0.2:0.1:3]
|
||||
Text_Auto_Fit = true;
|
||||
Text_Allow_Enlarge = false;
|
||||
Text_Max_Scale = 2; // [1:0.1:5]
|
||||
Text_Band = 14; // [0:1:40]
|
||||
Text_Margin = 3; // [0:0.5:10]
|
||||
Text_Position = "Front"; // [Front, Back]
|
||||
Text_Rotation = 0; // [-180:1:180]
|
||||
Text_Offset_X = 0; // [-100:0.5:100]
|
||||
Text_Offset_Y = 0; // [-100:0.5:100]
|
||||
|
||||
/* [Hole numbering] */
|
||||
Show_Numbers = false;
|
||||
Number_Start = 1; // [0:1:999]
|
||||
Number_Font = "Liberation Sans:style=Bold";
|
||||
Number_Size = 4; // [2:0.5:12]
|
||||
Number_Position = "Front"; // [Front, Back]
|
||||
Number_Margin = 1.5; // [0:0.5:5]
|
||||
|
||||
/* [Fit Test] */
|
||||
Test_Count = 5; // [1:1:12]
|
||||
Test_Clearance_Start = 0.10; // [0:0.05:1]
|
||||
Test_Clearance_Step = 0.05; // [0:0.01:0.3]
|
||||
Test_Label = true;
|
||||
|
||||
/* [STL quality] */
|
||||
Quality = "High"; // [Draft, Normal, High, Ultra, Custom]
|
||||
Custom_FN = 96; // [12:1:360]
|
||||
Show_Info = true;
|
||||
Show_Body_Preview = false;
|
||||
|
||||
/* [Hidden] */
|
||||
$fn = Quality == "Draft" ? 24 : Quality == "Normal" ? 48 : Quality == "High" ? 96 : Quality == "Ultra" ? 180 : Custom_FN;
|
||||
eps = 0.02;
|
||||
function clamp(x,a,b) = min(max(x,a),b);
|
||||
function rr(w,h,r) = clamp(r,0,min(w,h)/2-0.001);
|
||||
function max2(a,b) = max(a,b);
|
||||
function ncols(n,rows,cols) = cols > 0 ? min(n,cols) : ceil(n/rows);
|
||||
function rot_w(w,h,a) = abs(w*cos(a)) + abs(h*sin(a));
|
||||
function rot_h(w,h,a) = abs(w*sin(a)) + abs(h*cos(a));
|
||||
function prefix(s,n) = n<=0 ? "" : str(prefix(s,n-1),s[n-1]);
|
||||
function txt_m(s,size,font,spacing) = textmetrics(text=s,size=size,font=font,spacing=spacing);
|
||||
function txt_w(s,size,font,spacing) = txt_m(s,size,font,spacing).size.x;
|
||||
function txt_h(s,size,font,spacing) = txt_m(s,size,font,spacing).size.y;
|
||||
function fit_factor(w,h,maxw,maxh,allowup,maxscale) = min(allowup ? maxscale : 1, maxw/max(w,0.001), maxh/max(h,0.001));
|
||||
function txt_fit(s,size,font,spacing,maxw,maxh,angle) = fit_factor(rot_w(txt_w(s,size,font,spacing),txt_h(s,size,font,spacing),angle),rot_h(txt_w(s,size,font,spacing),txt_h(s,size,font,spacing),angle),maxw,maxh,Text_Allow_Enlarge,Text_Max_Scale);
|
||||
function hw(cw=Clearance_Width) = USB_Width + 2*cw;
|
||||
function ht(ct=Clearance_Thickness) = USB_Thickness + 2*ct;
|
||||
function bw(cw=Clearance_Width) = max(Body_Width,hw(cw));
|
||||
function bt(ct=Clearance_Thickness) = max(Body_Thickness,ht(ct));
|
||||
function px(cw=Clearance_Width) = Use_Body_Spacing ? max(hw(cw)+Hole_Wall,bw(cw)+Body_Gap) : hw(cw)+Hole_Wall;
|
||||
function py(ct=Clearance_Thickness) = Use_Body_Spacing ? max(ht(ct)+Hole_Wall,bt(ct)+Body_Gap) : ht(ct)+Hole_Wall;
|
||||
function depth_for(h) = min(Hole_Depth,h-Bottom_Thickness);
|
||||
function text_on() = Show_Text && Text != "";
|
||||
function band_on() = text_on() && Text_Band > 0;
|
||||
function num_band() = Show_Numbers ? Number_Size*1.5+2*Number_Margin : 0;
|
||||
|
||||
module rounded_rect(w,h,r) {
|
||||
rad=rr(w,h,r);
|
||||
if(rad<=0) square([w,h],center=true);
|
||||
else offset(r=rad) square([w-2*rad,h-2*rad],center=true);
|
||||
}
|
||||
module plate(w,h,z,r=Outer_Corner_Radius) {
|
||||
linear_extrude(height=z) rounded_rect(w,h,r);
|
||||
}
|
||||
module usb_cut(depth,cw=Clearance_Width,ct=Clearance_Thickness) {
|
||||
w=hw(cw); h=ht(ct); ch=min(Entry_Chamfer,depth/3);
|
||||
translate([0,0,-eps]) linear_extrude(height=depth+2*eps) rounded_rect(w,h,Hole_Corner_Radius);
|
||||
if(ch>0 && Entry_Clearance>0)
|
||||
translate([0,0,depth-ch]) linear_extrude(height=ch+eps,scale=[(w+2*Entry_Clearance)/w,(h+2*Entry_Clearance)/h]) rounded_rect(w,h,Hole_Corner_Radius);
|
||||
}
|
||||
// Actual ink bounds, not font advance width. Rotation is included before fitting.
|
||||
module fitted_text_2d(s,maxw,maxh,size=Text_Size,font=Text_Font,spacing=Text_Spacing,angle=Text_Rotation) {
|
||||
if(s!="" && maxw>0 && maxh>0) {
|
||||
m=txt_m(s,size,font,spacing);
|
||||
f=Text_Auto_Fit ? txt_fit(s,size,font,spacing,maxw,maxh,angle) : 1;
|
||||
rotate(angle) scale([f,f]) translate([-m.position.x-m.size.x/2,-m.position.y-m.size.y/2])
|
||||
text(s,size=size,font=font,spacing=spacing);
|
||||
}
|
||||
}
|
||||
module text_solid(s,x,y,z,maxw,maxh,angle=Text_Rotation,size=Text_Size,font=Text_Font,spacing=Text_Spacing,depth=Text_Depth) {
|
||||
translate([x+Text_Offset_X,y+Text_Offset_Y,z]) linear_extrude(height=depth+eps)
|
||||
fitted_text_2d(s,maxw,maxh,size,font,spacing,angle);
|
||||
}
|
||||
module top_text(s,x,y,z,maxw,maxh,angle=Text_Rotation,size=Text_Size,font=Text_Font,spacing=Text_Spacing) {
|
||||
text_solid(s,x,y,z+(Text_Mode=="Raised" ? -eps : -Text_Depth),maxw,maxh,angle,size,font,spacing);
|
||||
}
|
||||
module number_text(n,x,y,z,maxw,maxh) {
|
||||
// Number offsets are independent of the main text offsets.
|
||||
translate([x,y,z+(Text_Mode=="Raised" ? -eps : -Text_Depth)]) linear_extrude(height=Text_Depth+eps)
|
||||
fitted_text_2d(str(Number_Start+n),maxw,maxh,Number_Size,Number_Font,1,0);
|
||||
}
|
||||
module connector_cut(x,y,z,angle=0,depth=depth_for(Holder_Height),cw=Clearance_Width,ct=Clearance_Thickness) {
|
||||
translate([x,y,z-depth]) rotate([0,0,angle]) usb_cut(depth,cw,ct);
|
||||
}
|
||||
|
||||
// -------------------- Circular --------------------
|
||||
// Conservative bounding-circle spacing works for every orientation.
|
||||
ce=Use_Body_Spacing ? sqrt(bw()*bw()+bt()*bt())+Body_Gap : sqrt(hw()*hw()+ht()*ht())+Hole_Wall;
|
||||
cr_spacing=Count<=1 ? 0 : ce/(2*sin(180/Count));
|
||||
cr=max(cr_spacing,Center_Clearance/2+max(hw(),ht())/2+Hole_Wall,0);
|
||||
cr_ring=cr+max(hw(),ht())/2+Hole_Wall;
|
||||
number_extra=Show_Numbers ? Number_Size+2*Number_Margin : 0;
|
||||
ring_extra= (text_on() && Circular_Text_Position=="Ring" ? Circular_Ring_Width : 0)+number_extra;
|
||||
cr_outer=max(Circular_Min_Diameter/2,cr_ring+ring_extra+Edge_Margin);
|
||||
cr_center=max(0,cr-sqrt(hw()*hw()+ht()*ht())/2-Hole_Wall);
|
||||
function circ_angle(i) = 360*i/Count;
|
||||
function circ_orientation(a) = Circular_Orientation=="Radial" ? a : Circular_Orientation=="Tangential" ? a+90 : 0;
|
||||
module circular_holes() {
|
||||
for(i=[0:Count-1]) let(a=circ_angle(i)) connector_cut(cr*cos(a),cr*sin(a),Holder_Height,circ_orientation(a));
|
||||
}
|
||||
module circular_numbers() {
|
||||
if(Show_Numbers) for(i=[0:Count-1]) let(a=circ_angle(i),r=cr_ring+Number_Margin+Number_Size/2)
|
||||
number_text(i,r*cos(a),r*sin(a),Holder_Height,max(Number_Size*3,ce-Hole_Wall),Number_Size*1.5);
|
||||
}
|
||||
// True per-glyph circular text, with measured advance widths and ink centering.
|
||||
module arc_text(s,r,z,start=180,dir="CCW") {
|
||||
m=txt_m(s,Text_Size,Text_Font,Text_Spacing);
|
||||
circumference=2*PI*r;
|
||||
avail=max(0.001,circumference-2*Text_Margin);
|
||||
f=Text_Auto_Fit ? min(Text_Allow_Enlarge ? Text_Max_Scale : 1,avail/max(m.advance.x,0.001),max(Circular_Ring_Width-2*Text_Margin,0.001)/max(m.size.y,0.001)) : 1;
|
||||
// textmetrics() per prefix supplies actual advances, including spacing/kerning.
|
||||
for(i=[0:len(s)-1]) let(
|
||||
before=i==0 ? 0 : txt_m(prefix(s,i),Text_Size,Text_Font,Text_Spacing).advance.x,
|
||||
after=txt_m(prefix(s,i+1),Text_Size,Text_Font,Text_Spacing).advance.x,
|
||||
a=start+(dir=="CCW" ? 1 : -1)*((before+after)/2-m.advance.x/2)*f*180/(PI*r),
|
||||
cm=txt_m(s[i],Text_Size,Text_Font,Text_Spacing)
|
||||
)
|
||||
translate([r*cos(a),r*sin(a),z+(Text_Mode=="Raised" ? -eps : -Text_Depth)])
|
||||
rotate([0,0,a+(dir=="CCW" ? -90 : 90)+Text_Rotation])
|
||||
linear_extrude(height=Text_Depth+eps)
|
||||
scale([f,f]) translate([-cm.position.x-cm.size.x/2,-cm.position.y-cm.size.y/2])
|
||||
text(s[i],size=Text_Size,font=Text_Font,spacing=Text_Spacing);
|
||||
}
|
||||
module circular_main_text() {
|
||||
if(text_on()) {
|
||||
if(Circular_Text_Position=="Center") top_text(Text,0,0,Holder_Height,2*max(0,cr_center-Text_Margin),2*max(0,cr_center-Text_Margin));
|
||||
else if(Circular_Text_Position=="Ring") arc_text(Text,cr_ring+number_extra+Circular_Ring_Width/2,Holder_Height,Circular_Text_Start_Angle,Circular_Text_Direction);
|
||||
else if(Circular_Text_Position=="Side") {
|
||||
// Characters follow the cylindrical side, upright in the Z direction.
|
||||
m=txt_m(Text,Text_Size,Text_Font,Text_Spacing);
|
||||
f=Text_Auto_Fit ? min(Text_Allow_Enlarge ? Text_Max_Scale : 1,(2*PI*cr_outer-2*Text_Margin)/max(m.advance.x,0.001),(Holder_Height-2*Text_Margin)/max(m.size.y,0.001)) : 1;
|
||||
for(i=[0:len(Text)-1]) let(
|
||||
before=i==0 ? 0 : txt_m(prefix(Text,i),Text_Size,Text_Font,Text_Spacing).advance.x,
|
||||
after=txt_m(prefix(Text,i+1),Text_Size,Text_Font,Text_Spacing).advance.x,
|
||||
a=Circular_Text_Start_Angle+((before+after)/2-m.advance.x/2)*f*180/(PI*cr_outer),
|
||||
cm=txt_m(Text[i],Text_Size,Text_Font,Text_Spacing)
|
||||
)
|
||||
rotate([0,0,a]) translate([cr_outer-(Text_Mode=="Engraved" ? Text_Depth : 0)-max(0,cr_outer-sqrt(max(0,cr_outer*cr_outer-pow(cm.size.x*f/2,2))))-eps,0,Holder_Height/2]) rotate([90,0,90])
|
||||
linear_extrude(height=Text_Depth+max(0,cr_outer-sqrt(max(0,cr_outer*cr_outer-pow(cm.size.x*f/2,2))))+2*eps)
|
||||
scale([f,f]) translate([-cm.position.x-cm.size.x/2,-cm.position.y-cm.size.y/2])
|
||||
text(Text[i],size=Text_Size,font=Text_Font,spacing=Text_Spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
module circular_holder() {
|
||||
difference() {
|
||||
cylinder(h=Holder_Height,r=cr_outer);
|
||||
circular_holes();
|
||||
if(Text_Mode=="Engraved") { circular_main_text(); circular_numbers(); }
|
||||
}
|
||||
if(Text_Mode=="Raised") { circular_main_text(); circular_numbers(); }
|
||||
}
|
||||
|
||||
// -------------------- Rectangular --------------------
|
||||
rc=ncols(Count,Rows,Columns); rrn=ceil(Count/rc);
|
||||
vertical=Rectangular_Orientation=="Vertical";
|
||||
rhw=vertical ? ht() : hw(); rht=vertical ? hw() : ht();
|
||||
rpx=vertical ? py() : px(); rpy=(vertical ? px() : py())+num_band();
|
||||
rfieldw=(rc-1)*rpx+rhw; rfieldh=(rrn-1)*rpy+rht;
|
||||
rnw=num_band();
|
||||
rtext=band_on() ? Text_Band : 0;
|
||||
rw=rfieldw+2*Edge_Margin;
|
||||
rh=rfieldh+2*Edge_Margin+rnw+rtext;
|
||||
rfieldy=(Text_Position=="Front" ? rtext/2 : -rtext/2)+(Number_Position=="Front" ? rnw/2 : -rnw/2);
|
||||
function rx(i)=(i%rc-(rc-1)/2)*rpx;
|
||||
function ry(i)=(floor(i/rc)-(rrn-1)/2)*rpy+rfieldy;
|
||||
module rectangular_holes() {
|
||||
for(i=[0:Count-1]) connector_cut(rx(i),ry(i),Holder_Height,vertical ? 90 : 0);
|
||||
}
|
||||
module rectangular_numbers() {
|
||||
if(Show_Numbers) for(i=[0:Count-1])
|
||||
number_text(i,rx(i),ry(i)+(Number_Position=="Front" ? -1 : 1)*(rht/2+Number_Margin+Number_Size/2),Holder_Height,rpx-Hole_Wall,Number_Size*1.5);
|
||||
}
|
||||
module rectangular_main_text() {
|
||||
if(band_on()) top_text(Text,0,(Text_Position=="Front" ? -1 : 1)*(rh/2-Text_Band/2),Holder_Height,rw-2*Text_Margin,Text_Band-2*Text_Margin);
|
||||
}
|
||||
module rectangular_holder() {
|
||||
difference() {
|
||||
plate(rw,rh,Holder_Height);
|
||||
rectangular_holes();
|
||||
if(Text_Mode=="Engraved") { rectangular_main_text(); rectangular_numbers(); }
|
||||
}
|
||||
if(Text_Mode=="Raised") { rectangular_main_text(); rectangular_numbers(); }
|
||||
}
|
||||
|
||||
// -------------------- Wall --------------------
|
||||
// Print flat with the back on the build plate. The upper rail contains screws.
|
||||
wc=min(Wall_Columns,Count); wr=ceil(Count/wc);
|
||||
wn=num_band();
|
||||
wpy=py()+wn;
|
||||
wf_w=(wc-1)*px()+hw(); wf_h=(wr-1)*wpy+ht();
|
||||
wt=band_on() ? Text_Band : 0;
|
||||
screw_r=max(Screw_Diameter,Screw_Head_Diameter)/2;
|
||||
ws= Screw_Spacing>0 ? Screw_Spacing : max(wf_w-2*Screw_Edge_Offset,2*screw_r+Hole_Wall);
|
||||
ww=max(wf_w+2*Wall_Margin,ws+2*screw_r+2*Wall_Margin);
|
||||
rail=max(2*screw_r+2*Wall_Margin,12);
|
||||
// Inclined holes sweep in Y. Reserve their full projected envelope.
|
||||
sweep=Hole_Depth*sin(Wall_Angle)+ht()*cos(Wall_Angle)+2*Entry_Clearance;
|
||||
wh=wf_h+max(0,sweep-ht())+2*Wall_Margin+rail+wn+wt;
|
||||
wth=max(Wall_Plate_Thickness,Hole_Depth*cos(Wall_Angle)+ht()*sin(Wall_Angle)/2+Bottom_Thickness);
|
||||
wd=min(Hole_Depth,(wth-Bottom_Thickness-ht()*sin(Wall_Angle)/2)/max(cos(Wall_Angle),0.001));
|
||||
wfieldy= -rail/2+wt/2+(wn>0 ? wn/2 : 0);
|
||||
wscrewy=wh/2-rail/2;
|
||||
function wx(i)=(i%wc-(wc-1)/2)*px();
|
||||
function wy(i)=(floor(i/wc)-(wr-1)/2)*wpy+wfieldy;
|
||||
module wall_holes() {
|
||||
for(i=[0:Count-1]) translate([wx(i),wy(i),wth]) rotate([Wall_Angle,0,0]) translate([0,0,-wd]) usb_cut(wd);
|
||||
}
|
||||
module wall_screws() {
|
||||
for(x=[-ws/2,ws/2]) translate([x,wscrewy,-eps]) {
|
||||
cylinder(h=wth+2*eps,d=Screw_Diameter);
|
||||
if(Screw_Head_Depth>0) translate([0,0,wth-min(Screw_Head_Depth,wth-Bottom_Thickness)])
|
||||
cylinder(h=min(Screw_Head_Depth,wth-Bottom_Thickness)+eps,d1=Screw_Diameter,d2=Screw_Head_Diameter);
|
||||
}
|
||||
}
|
||||
module wall_numbers() {
|
||||
if(Show_Numbers) for(i=[0:Count-1]) number_text(i,wx(i),wy(i)+(Number_Position=="Front" ? -1 : 1)*(ht()/2+Number_Margin+Number_Size/2),wth,px()-Hole_Wall,Number_Size*1.5);
|
||||
}
|
||||
module wall_main_text() {
|
||||
if(band_on()) top_text(Text,0,-wh/2+Text_Band/2,wth,ww-2*Text_Margin,Text_Band-2*Text_Margin);
|
||||
}
|
||||
module wall_holder() {
|
||||
difference() {
|
||||
plate(ww,wh,wth);
|
||||
wall_holes(); wall_screws();
|
||||
if(Text_Mode=="Engraved") { wall_main_text(); wall_numbers(); }
|
||||
}
|
||||
if(Text_Mode=="Raised") { wall_main_text(); wall_numbers(); }
|
||||
}
|
||||
|
||||
// -------------------- Fit test --------------------
|
||||
tc=Test_Count;
|
||||
tmax=Test_Clearance_Start+(tc-1)*Test_Clearance_Step;
|
||||
tw=USB_Width+2*tmax+2*Edge_Margin;
|
||||
tpitch=tw+Hole_Wall;
|
||||
th=USB_Thickness+2*tmax+2*Edge_Margin+10;
|
||||
theight=max(8,min(Holder_Height,Hole_Depth+Bottom_Thickness));
|
||||
module fit_test() {
|
||||
difference() {
|
||||
plate(tc*tpitch-Hole_Wall,th,theight);
|
||||
for(i=[0:tc-1]) let(c=Test_Clearance_Start+i*Test_Clearance_Step)
|
||||
translate([(i-(tc-1)/2)*tpitch,3,theight-min(Hole_Depth,theight-Bottom_Thickness)])
|
||||
usb_cut(min(Hole_Depth,theight-Bottom_Thickness),c,c);
|
||||
if(Test_Label) for(i=[0:tc-1]) let(c=Test_Clearance_Start+i*Test_Clearance_Step)
|
||||
translate([(i-(tc-1)/2)*tpitch,-th/2+5,theight-0.5])
|
||||
linear_extrude(height=0.5+eps)
|
||||
fitted_text_2d(str(c),tw-2,5,4,Text_Font,1,0);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Validation --------------------
|
||||
assert(Count>=1 && Count<=48,"Count must be 1..48");
|
||||
assert(Rows>=1 && Holder_Height>Bottom_Thickness,"Invalid holder height or rows");
|
||||
assert(USB_Width>0 && USB_Thickness>0 && Hole_Wall>0,"Invalid USB dimensions");
|
||||
assert(Hole_Depth>0 && Bottom_Thickness>=1,"Invalid hole depth or bottom thickness");
|
||||
assert(Clearance_Width>=0 && Clearance_Thickness>=0,"Clearances must be nonnegative");
|
||||
assert(Text_Depth>0 && Text_Depth<Holder_Height-Bottom_Thickness,"Text depth is too large");
|
||||
assert(!Show_Numbers || !band_on() || Number_Position!=Text_Position || Text_Band>2*Text_Margin,"Invalid text band");
|
||||
assert(!text_on() || Text_Band==0 || Text_Band>2*Text_Margin,"Text band is too narrow");
|
||||
assert(!Show_Numbers || Number_Size>0,"Invalid numbering size");
|
||||
assert(Screw_Head_Depth>=0 && Screw_Diameter>0 && Screw_Head_Diameter>=Screw_Diameter,"Invalid screw dimensions");
|
||||
assert(Outer_Corner_Radius>=0 && Edge_Margin>=0 && Body_Gap>=0,"Invalid spacing or corner radius");
|
||||
assert(Text_Max_Scale>0 && Text_Size>0 && Text_Margin>=0,"Invalid text parameters");
|
||||
assert(Wall_Angle>=0 && Wall_Angle<=45,"Wall angle must be 0..45 degrees");
|
||||
assert(Test_Count>=1 && Test_Clearance_Start>=0 && Test_Clearance_Step>=0,"Invalid fit test");
|
||||
if(Show_Info) {
|
||||
echo("USB-A Holder Generator v2",Holder_Type);
|
||||
echo("Hole",hw(),ht(),"depth",depth_for(Holder_Height));
|
||||
echo("Body pitch",px(),py(),"quality",$fn);
|
||||
if(Holder_Type=="Circular") echo("Diameter",2*cr_outer);
|
||||
if(Holder_Type=="Rectangular") echo("Dimensions",rw,rh,Holder_Height);
|
||||
if(Holder_Type=="Wall") echo("Dimensions",ww,wh,wth,"screw spacing",ws);
|
||||
if(Holder_Type=="Fit Test") echo("Test clearances",Test_Clearance_Start,Test_Clearance_Step,Test_Count);
|
||||
}
|
||||
if(Holder_Type=="Circular") circular_holder();
|
||||
else if(Holder_Type=="Rectangular") rectangular_holder();
|
||||
else if(Holder_Type=="Wall") wall_holder();
|
||||
else if(Holder_Type=="Fit Test") fit_test();
|
||||