Publish curated OpenSCAD generator collection

This commit is contained in:
Christian Johannesen 2026-10-01 23:46:30 +02:00
commit 6c6acb80c0
94 changed files with 7322 additions and 0 deletions

9
LICENSE.md Normal file
View 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
View 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
View file

@ -0,0 +1,47 @@
# Adjustable Cable Clips
![Digital render](images/small.png)
**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).

View 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]);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,47 @@
# CR80 / ID-1 Card Holder
![Digital render](images/default-flex-tabs.png)
**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).

View 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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

View file

@ -0,0 +1,46 @@
# Decorative Insects
![Digital render](images/butterfly-relief.png)
**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).

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Binary file not shown.

47
parametric-box/README.md Normal file
View file

@ -0,0 +1,47 @@
# Parametric Box
![Digital render](images/inner-lip.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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();

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,47 @@
# Parametric Keyring
![Digital render](images/raised-text.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

47
s-hook/README.md Normal file
View file

@ -0,0 +1,47 @@
# Parametric S-Hook
![Digital render](images/round.png)
**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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
s-hook/images/round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

225
s-hook/s-hook.scad Normal file
View 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();

Binary file not shown.

Binary file not shown.

BIN
s-hook/stl-files/round.stl Normal file

Binary file not shown.

47
storage-system/README.md Normal file
View file

@ -0,0 +1,47 @@
# Parametric Storage System
![Digital render](images/standard-box.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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));
}

View file

@ -0,0 +1,47 @@
# Tea-Bag Dispenser
![Digital render](images/front-pull.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View file

@ -0,0 +1,47 @@
# 3D Text Generator
![Digital render](images/standing-text.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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();

View file

@ -0,0 +1,46 @@
# Universal Clock Dial
![Digital render](images/round.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

View 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
View file

@ -0,0 +1,47 @@
# Universal Sign Generator
![Digital render](images/flat-plate.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
# USB Stick Holder
![Digital render](images/rectangular.png)
**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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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();