commit 6c6acb80c0643d0764601a0d24e563029449b835 Author: Christian Johannesen Date: Thu Oct 1 23:46:30 2026 +0200 Publish curated OpenSCAD generator collection diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4a5a62a --- /dev/null +++ b/LICENSE.md @@ -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/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..49fa9c8 --- /dev/null +++ b/README.md @@ -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 + + + + + + + + + + + + + + + + + + + + + + +
CR80 / ID-1 Card Holder render
CR80 / ID-1 Card Holder
Adjustable Cable Clips render
Adjustable Cable Clips
Decorative Insects render
Decorative Insects
Parametric Box render
Parametric Box
Parametric Keyring render
Parametric Keyring
Parametric S-Hook render
Parametric S-Hook
Parametric Storage System render
Parametric Storage System
Tea-Bag Dispenser render
Tea-Bag Dispenser
3D Text Generator render
3D Text Generator
Universal Clock Dial render
Universal Clock Dial
Universal Sign Generator render
Universal Sign Generator
USB Stick Holder render
USB Stick Holder
+ +## 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). diff --git a/cable-clips/README.md b/cable-clips/README.md new file mode 100644 index 0000000..e8e6ac8 --- /dev/null +++ b/cable-clips/README.md @@ -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). diff --git a/cable-clips/cable-clips.scad b/cable-clips/cable-clips.scad new file mode 100644 index 0000000..39685d4 --- /dev/null +++ b/cable-clips/cable-clips.scad @@ -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]); +} diff --git a/cable-clips/images/large.png b/cable-clips/images/large.png new file mode 100644 index 0000000..b3e0a18 Binary files /dev/null and b/cable-clips/images/large.png differ diff --git a/cable-clips/images/small.png b/cable-clips/images/small.png new file mode 100644 index 0000000..76ec8b2 Binary files /dev/null and b/cable-clips/images/small.png differ diff --git a/cable-clips/images/standard.png b/cable-clips/images/standard.png new file mode 100644 index 0000000..8ed2eb5 Binary files /dev/null and b/cable-clips/images/standard.png differ diff --git a/cable-clips/stl-files/large.stl b/cable-clips/stl-files/large.stl new file mode 100644 index 0000000..4c492b8 Binary files /dev/null and b/cable-clips/stl-files/large.stl differ diff --git a/cable-clips/stl-files/small.stl b/cable-clips/stl-files/small.stl new file mode 100644 index 0000000..2a8e69b Binary files /dev/null and b/cable-clips/stl-files/small.stl differ diff --git a/cable-clips/stl-files/standard.stl b/cable-clips/stl-files/standard.stl new file mode 100644 index 0000000..6c48dc3 Binary files /dev/null and b/cable-clips/stl-files/standard.stl differ diff --git a/cr80-card-holder/README.md b/cr80-card-holder/README.md new file mode 100644 index 0000000..9d8ffd4 --- /dev/null +++ b/cr80-card-holder/README.md @@ -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). diff --git a/cr80-card-holder/cr80-card-holder.scad b/cr80-card-holder/cr80-card-holder.scad new file mode 100644 index 0000000..e9c57b4 --- /dev/null +++ b/cr80-card-holder/cr80-card-holder.scad @@ -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(); diff --git a/cr80-card-holder/images/default-flex-tabs.png b/cr80-card-holder/images/default-flex-tabs.png new file mode 100644 index 0000000..42e6dba Binary files /dev/null and b/cr80-card-holder/images/default-flex-tabs.png differ diff --git a/cr80-card-holder/images/full-bezel-round-attachment.png b/cr80-card-holder/images/full-bezel-round-attachment.png new file mode 100644 index 0000000..c9a056e Binary files /dev/null and b/cr80-card-holder/images/full-bezel-round-attachment.png differ diff --git a/cr80-card-holder/images/landscape-security-rails.png b/cr80-card-holder/images/landscape-security-rails.png new file mode 100644 index 0000000..5ee7531 Binary files /dev/null and b/cr80-card-holder/images/landscape-security-rails.png differ diff --git a/cr80-card-holder/stl-files/default-flex-tabs.stl b/cr80-card-holder/stl-files/default-flex-tabs.stl new file mode 100644 index 0000000..376dcf0 Binary files /dev/null and b/cr80-card-holder/stl-files/default-flex-tabs.stl differ diff --git a/cr80-card-holder/stl-files/full-bezel-round-attachment.stl b/cr80-card-holder/stl-files/full-bezel-round-attachment.stl new file mode 100644 index 0000000..0f9e861 Binary files /dev/null and b/cr80-card-holder/stl-files/full-bezel-round-attachment.stl differ diff --git a/cr80-card-holder/stl-files/landscape-security-rails.stl b/cr80-card-holder/stl-files/landscape-security-rails.stl new file mode 100644 index 0000000..b349411 Binary files /dev/null and b/cr80-card-holder/stl-files/landscape-security-rails.stl differ diff --git a/decorative-insects/README.md b/decorative-insects/README.md new file mode 100644 index 0000000..ac57b19 --- /dev/null +++ b/decorative-insects/README.md @@ -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). diff --git a/decorative-insects/decorative-insects.scad b/decorative-insects/decorative-insects.scad new file mode 100644 index 0000000..6b30a3a --- /dev/null +++ b/decorative-insects/decorative-insects.scad @@ -0,0 +1,1051 @@ +/* + PARAMETRISK DEKORATIV INSEKT-GENERATOR v3 + ----------------------------------------- + For OpenSCAD (gerne Nightly, men uden eksterne biblioteker). + + Formaal: + - 3D-print (STL / 3MF) + - laserskaering (SVG / DXF) + - folieskaering / vinyl + - gravering / relief + + Nyheder i v3: + - Flere arts-/morfologi-presets inkl. Admiral, Paafugleoje, Blue_Morpho, + Kejserguldsmed og vandnymfe-varianter. + - Stencil/bridge-mode til laser og folie, saa indre oeer kan forbindes. + - Multi-layer output for separate krop/vinger/dekoration/pletter. + - Panel-/grid-generator til batch-produktion. + - Ornamental / Art Nouveau mode med ekstra dekorative kurver. + - Fortsat produktionsprofiler og robust 2D/3D-output. +*/ + +// ============================== // +// [00 - Art / output] // +// ============================== // + +Artspreset = "Fri"; // [Fri,Monark,Admiral,Paafugleoje,Morpho,Blue_Morpho,Svalehale,Luna,Atlas,Guldsmed_Stor,Kejserguldsmed,Vandnymfe,Vandnymfe_Smal,Cikade,Mariehoene,Skarabee] +Bruger_Type = "Sommerfugl"; // [Sommerfugl,Natsvaermer,Guldsmed,Cikade,Bille,Fantasi] +Output = "3D_RELIEF"; // [2D_SILHOUET,2D_DEKORATION,2D_COMBINED,3D_FLAT,3D_RELIEF] + +// Samlet vingefang. For guldsmed/cikade er det bredeste vingesaet. +Vingefang = 100; // [20:1:320] +// Samlet laengde fra hale til antennespids, omtrentligt. +Kropslaengde = 52; // [10:1:220] +// Global skalering i laengderetningen; 1.0 = naturligt preset-forhold. +Laengde_Skala = 1.00; // [0.50:0.01:2.20] + +// ============================== // +// [01 - Produktionsprofil] // +// ============================== // + +Produktionsprofil = "Brugerdefineret"; // [Brugerdefineret,3D_Print_Robust,Laser_Fin,Folie_Simpel,Gravering_Relief] +Automatisk_Produktionsoptimering = true; + +// ============================== // +// [02 - Symmetri / stil] // +// ============================== // + +// 0 = fuldstaendig spejlsymmetrisk. Smaa vaerdier giver organisk variation. +Asymmetri = 0.00; // [0.00:0.01:0.15] +// Giver blodere former med offset-runding. +Bloedgoering = 0.35; // [0:0.05:2] +// Minimum bredde paa tynde dekorative elementer. +Min_Feature = 0.80; // [0.20:0.05:5] +// Polygon-/kurveoploesning. Hoejere er glattere men langsommere. +Oploesning = 56; // [16:4:180] + +// ============================== // +// [03 - Vinger generelt] // +// ============================== // + +Vinge_Laengde = 1.00; // [0.60:0.01:1.60] +Vinge_Bredde = 1.00; // [0.50:0.01:1.80] +// Flytter vingespids frem (+) eller tilbage (-). +Vinge_Sweep = 0.00; // [-0.50:0.01:0.50] +// 0 = buttet/oval, 1 = spids. +Vingespids = 0.45; // [0.00:0.01:1.00] +// 0 = jaevn bredde, 1 = kraftigt smallere mod spidsen. +Vinge_Taper = 0.35; // [0.00:0.01:0.90] +// Rotation af hele vingeparret. +Vinge_Vinkel = 0; // [-35:1:35] +// Indtraek mellem krop og vingens geometriske start. +Vinge_Rod_Overlap = 1.5; // [0:0.25:10] +// Indhak i forvingens bagkant naer roden. +Forvinge_Indhak = 0.00; // [0.00:0.01:1.00] +// Bagvingens boelgede/scalloped bagkant. +Bagvinge_Skalop = 0.00; // [0.00:0.01:1.00] + +// ============================== // +// [04 - Sommerfugl / natsvaermer]// +// ============================== // + +Forvinge_Skala = 1.00; // [0.60:0.01:1.60] +Bagvinge_Skala = 0.88; // [0.50:0.01:1.60] +Bagvinge_Forskydning = -7; // [-30:0.5:20] +Bagvinge_Runding = 0.65; // [0.00:0.01:1.00] +// 0 = ingen hale; hoejere = svalehale-/luna-lignende hale. +Vinge_Hale = 0.00; // [0.00:0.01:1.00] + +// ============================== // +// [05 - Guldsmed / cikade] // +// ============================== // + +Langvinge_For_Skala = 1.00; // [0.60:0.01:1.30] +Langvinge_Bag_Skala = 0.92; // [0.60:0.01:1.30] +Langvinge_Separation = 8.0; // [2:0.5:30] +Langvinge_Rod_Bredde = 0.42; // [0.15:0.01:0.85] +Langvinge_Spids_Bredde = 0.55; // [0.15:0.01:1.00] +// Lille farvet / graferbar pterostigma-lignende markering naer spidsen. +Pterostigma = true; +Pterostigma_Skala = 1.00; // [0.40:0.05:2.50] + +// ============================== // +// [06 - Krop] // +// ============================== // + +Krop_Bredde = 7.0; // [1:0.25:30] +Bryst_Skala = 1.15; // [0.60:0.01:2.00] +Hoved_Skala = 0.90; // [0.50:0.01:2.00] +Bagkrop_Taper = 0.55; // [0.10:0.01:1.00] +Bagkrop_Segmenter = 6; // [0:1:18] +Talje = 0.15; // [0.00:0.01:0.80] + +// ============================== // +// [07 - Antenner / ben] // +// ============================== // + +Antenner = true; +Antenne_Laengde = 12; // [0:0.5:50] +Antenne_Spredning = 28; // [0:1:80] +Antenne_Krumning = 0.35; // [-1.00:0.05:1.00] +Antenne_Knop = 0.0; // [0:0.1:4] +// Simpel pseudo-fjering paa antenner, nyttig til natsvaermere. +Antenne_Fjering = 0.00; // [0.00:0.05:2.00] + +Ben = false; +Ben_Laengde = 10; // [2:0.5:30] +Ben_Vinkel = 35; // [5:1:80] + +// ============================== // +// [08 - Vingedekoration / aarer] // +// ============================== // + +Vinge_Aarer = true; +Aare_Tykkelse = 0.65; // [0.20:0.05:3] +Aare_Taethed = 5; // [1:1:14] +Tvaer_Aarer = true; +Aare_Organisk = 0.55; // [0.00:0.01:1.00] + +Vinge_Pletter = true; +Plet_Antal = 2; // [0:1:6] +Plet_Skala = 1.0; // [0.3:0.05:3] +Plet_Ring = true; + +// Elytra-/dækningsdekor til biller / mariehoener. +Daekvinge_Soem = true; +Daekvinge_Pletter = true; +Daekvinge_Plet_Antal = 3; // [0:1:8] + +// ============================== // +// [09 - Kant / udskaeringer] // +// ============================== // + +Ydre_Kant = 0.0; // [0:0.1:5] +Udskaering = 0.0; // [0.00:0.05:1.00] +Udskaering_Margin = 3.0; // [0.5:0.25:10] + +// ============================== // +// [10 - Ophaeng / montage] // +// ============================== // + +Ophaengshul = false; +Ophaengshul_Diameter = 3.2; // [1:0.1:10] +Ophaengshul_Y = 0; // [-20:0.5:20] + +// ============================== // +// [11 - 3D] // +// ============================== // + +Tykkelse = 2.4; // [0.4:0.1:12] +Relief_Hoejde = 0.70; // [0.1:0.05:5] +Relief_Haevede_Detaljer = true; + +// ============================== // +// [12 - Stencil / bridges] // +// ============================== // + +Stencil_Mode = false; +// Brobredde til at forbinde indre oeer med yderkonturen. +Stencil_Bro_Bredde = 1.20; // [0.30:0.05:6] +// Antal broer pr. side, placeret radialt mod vingerne. +Stencil_Bro_Antal = 2; // [1:1:6] +// Hvor langt broer ma gaa ind i silhuetten. +Stencil_Bro_Laengde = 18; // [4:0.5:50] +// Tilfoej bro til ophaengshul / centrale huller. +Stencil_Centerbro = true; + +// ============================== // +// [13 - Multi-layer output] // +// ============================== // + +Layer_Output = "Alle"; // [Alle,Vinger,Krop,Aarer,Pletter,Stencilbroer] +// Lidt overlap mellem separate lag kan vaere nyttigt til vinyl/laserregistrering. +Layer_Overlap = 0.20; // [0:0.05:2] + +// ============================== // +// [14 - Panel / batch] // +// ============================== // + +Panel_Mode = false; +Panel_Kolonner = 3; // [1:1:10] +Panel_Raekker = 2; // [1:1:10] +Panel_Afstand_X = 125; // [20:1:400] +Panel_Afstand_Y = 95; // [20:1:400] +Panel_Skiftevis_Spejling = false; +Panel_Skiftevis_Rotation = 0; // [-180:5:180] + +// ============================== // +// [15 - Ornamental / Art Nouveau]// +// ============================== // + +Ornamental_Mode = false; +Ornamental_Styrke = 0.55; // [0.00:0.05:1.50] +Ornamental_Tykkelse = 0.70; // [0.20:0.05:4] +Ornamental_Slyng = 3; // [1:1:8] +Ornamental_Ringe = true; + +// ============================== // +// [16 - Preview] // +// ============================== // + +Vis_Centerlinje = false; + +$fn = Oploesning; +EPS = 0.01; + +// ------------------------------------------------------------ +// Hjaelpefunktioner +// ------------------------------------------------------------ +function clamp(v,a,b) = min(max(v,a),b); +function lerp(a,b,t) = a + (b-a)*t; +function v2lerp(a,b,t) = [lerp(a[0],b[0],t), lerp(a[1],b[1],t)]; +function bz3(p0,p1,p2,p3,t) = + let(u=1-t) + [u*u*u*p0[0] + 3*u*u*t*p1[0] + 3*u*t*t*p2[0] + t*t*t*p3[0], + u*u*u*p0[1] + 3*u*u*t*p1[1] + 3*u*t*t*p2[1] + t*t*t*p3[1]]; +function bez3pts(p0,p1,p2,p3,n=16) = [for(i=[0:n]) bz3(p0,p1,p2,p3,i/n)]; + +function profile_min_floor() = + Produktionsprofil=="3D_Print_Robust" ? 0.80 : + Produktionsprofil=="Laser_Fin" ? 0.35 : + Produktionsprofil=="Folie_Simpel" ? 0.60 : + Produktionsprofil=="Gravering_Relief" ? 0.45 : 0; + +function eff_min_feature() = max(Min_Feature, Automatisk_Produktionsoptimering ? profile_min_floor() : 0); +function eff_vein_thickness() = max(Aare_Tykkelse, eff_min_feature()*0.60); +function eff_outline() = Ydre_Kant + (Automatisk_Produktionsoptimering && Produktionsprofil=="3D_Print_Robust" ? eff_min_feature()*0.20 : 0); +function eff_cutout() = Automatisk_Produktionsoptimering && (Produktionsprofil=="3D_Print_Robust" || Produktionsprofil=="Folie_Simpel") ? min(Udskaering,0.35) : Udskaering; +function eff_veins_enabled() = Automatisk_Produktionsoptimering && Produktionsprofil=="Folie_Simpel" ? false : Vinge_Aarer; +function eff_spots_enabled() = Automatisk_Produktionsoptimering && Produktionsprofil=="Folie_Simpel" ? false : Vinge_Pletter; +function eff_crossveins() = Automatisk_Produktionsoptimering && Produktionsprofil=="Folie_Simpel" ? false : Tvaer_Aarer; +function eff_deck_spots() = Automatisk_Produktionsoptimering && Produktionsprofil=="Folie_Simpel" ? false : Daekvinge_Pletter; + +function chosen_family() = + Artspreset=="Fri" ? Bruger_Type : + Artspreset=="Monark" ? "Sommerfugl" : + Artspreset=="Admiral" ? "Sommerfugl" : + Artspreset=="Paafugleoje" ? "Sommerfugl" : + Artspreset=="Morpho" ? "Sommerfugl" : + Artspreset=="Blue_Morpho" ? "Sommerfugl" : + Artspreset=="Svalehale" ? "Sommerfugl" : + Artspreset=="Luna" ? "Natsvaermer" : + Artspreset=="Atlas" ? "Natsvaermer" : + Artspreset=="Guldsmed_Stor" ? "Guldsmed" : + Artspreset=="Kejserguldsmed" ? "Guldsmed" : + Artspreset=="Vandnymfe" ? "Guldsmed" : + Artspreset=="Vandnymfe_Smal" ? "Guldsmed" : + Artspreset=="Cikade" ? "Cikade" : + Artspreset=="Mariehoene" ? "Bille" : + Artspreset=="Skarabee" ? "Bille" : Bruger_Type; + +function is_butterfly() = chosen_family()=="Sommerfugl" || chosen_family()=="Natsvaermer" || chosen_family()=="Fantasi"; +function is_longwing() = chosen_family()=="Guldsmed" || chosen_family()=="Cikade"; +function is_beetle() = chosen_family()=="Bille"; + +function art_adj_forwing() = + Artspreset=="Monark" ? 1.00 : + Artspreset=="Admiral" ? 0.98 : + Artspreset=="Paafugleoje" ? 1.03 : + Artspreset=="Morpho" ? 1.08 : + Artspreset=="Blue_Morpho" ? 1.10 : + Artspreset=="Svalehale" ? 1.02 : + Artspreset=="Luna" ? 0.98 : + Artspreset=="Atlas" ? 1.12 : + 1.00; +function art_adj_hindwing() = + Artspreset=="Monark" ? 0.92 : + Artspreset=="Admiral" ? 0.88 : + Artspreset=="Paafugleoje" ? 0.96 : + Artspreset=="Morpho" ? 1.00 : + Artspreset=="Blue_Morpho" ? 1.02 : + Artspreset=="Svalehale" ? 0.82 : + Artspreset=="Luna" ? 0.96 : + Artspreset=="Atlas" ? 1.05 : + 0.88; +function art_adj_rounding() = + Artspreset=="Morpho" ? 0.82 : + Artspreset=="Blue_Morpho" ? 0.86 : + Artspreset=="Paafugleoje" ? 0.78 : + Artspreset=="Admiral" ? 0.60 : + Artspreset=="Monark" ? 0.62 : + Artspreset=="Svalehale" ? 0.54 : + Artspreset=="Luna" ? 0.86 : + Artspreset=="Atlas" ? 0.76 : + Bagvinge_Runding; +function art_adj_tail() = + Artspreset=="Svalehale" ? 0.52 : + Artspreset=="Luna" ? 0.86 : + Artspreset=="Atlas" ? 0.18 : + Vinge_Hale; +function art_adj_tip() = + Artspreset=="Monark" ? 0.46 : + Artspreset=="Admiral" ? 0.55 : + Artspreset=="Paafugleoje" ? 0.40 : + Artspreset=="Morpho" ? 0.30 : + Artspreset=="Blue_Morpho" ? 0.26 : + Artspreset=="Svalehale" ? 0.64 : + Artspreset=="Luna" ? 0.42 : + Artspreset=="Atlas" ? 0.38 : + Vingespids; +function art_adj_taper() = + Artspreset=="Monark" ? 0.36 : + Artspreset=="Admiral" ? 0.42 : + Artspreset=="Paafugleoje" ? 0.30 : + Artspreset=="Morpho" ? 0.22 : + Artspreset=="Blue_Morpho" ? 0.18 : + Artspreset=="Svalehale" ? 0.48 : + Artspreset=="Luna" ? 0.30 : + Artspreset=="Atlas" ? 0.24 : + Vinge_Taper; +function art_adj_sweep() = + Artspreset=="Monark" ? 0.04 : + Artspreset=="Morpho" ? 0.00 : + Artspreset=="Svalehale" ? 0.06 : + Artspreset=="Luna" ? -0.08 : + Artspreset=="Atlas" ? -0.04 : + Artspreset=="Guldsmed_Stor" ? 0.02 : + Artspreset=="Kejserguldsmed" ? 0.05 : + Artspreset=="Vandnymfe" ? -0.06 : + Artspreset=="Vandnymfe_Smal" ? -0.10 : + Artspreset=="Cikade" ? 0.00 : + Vinge_Sweep; +function art_adj_fore_notch() = + Artspreset=="Svalehale" ? 0.42 : + Artspreset=="Atlas" ? 0.25 : + Forvinge_Indhak; +function art_adj_scallop() = + Artspreset=="Morpho" ? 0.12 : + Artspreset=="Svalehale" ? 0.34 : + Artspreset=="Luna" ? 0.20 : + Bagvinge_Skalop; +function art_adj_long_sep() = + Artspreset=="Guldsmed_Stor" ? 11 : + Artspreset=="Kejserguldsmed" ? 12 : + Artspreset=="Vandnymfe" ? 7 : + Artspreset=="Vandnymfe_Smal" ? 5.5 : + Artspreset=="Cikade" ? 8 : + Langvinge_Separation; +function art_adj_root_width() = + Artspreset=="Guldsmed_Stor" ? 0.34 : + Artspreset=="Kejserguldsmed" ? 0.38 : + Artspreset=="Vandnymfe" ? 0.26 : + Artspreset=="Vandnymfe_Smal" ? 0.20 : + Artspreset=="Cikade" ? 0.48 : + Langvinge_Rod_Bredde; +function art_adj_tip_width() = + Artspreset=="Guldsmed_Stor" ? 0.46 : + Artspreset=="Kejserguldsmed" ? 0.50 : + Artspreset=="Vandnymfe" ? 0.34 : + Artspreset=="Vandnymfe_Smal" ? 0.28 : + Artspreset=="Cikade" ? 0.66 : + Langvinge_Spids_Bredde; +function art_adj_thorax() = + Artspreset=="Guldsmed_Stor" ? 1.25 : + Artspreset=="Vandnymfe" ? 1.05 : + Artspreset=="Cikade" ? 1.15 : + Artspreset=="Mariehoene" ? 1.35 : + Artspreset=="Skarabee" ? 1.22 : + Bryst_Skala; +function art_adj_head() = + Artspreset=="Guldsmed_Stor" ? 1.18 : + Artspreset=="Vandnymfe" ? 1.06 : + Artspreset=="Cikade" ? 1.20 : + Artspreset=="Mariehoene" ? 0.88 : + Artspreset=="Skarabee" ? 0.92 : + Hoved_Skala; +function art_adj_ab_taper() = + Artspreset=="Guldsmed_Stor" ? 0.38 : + Artspreset=="Vandnymfe" ? 0.32 : + Artspreset=="Cikade" ? 0.72 : + Artspreset=="Mariehoene" ? 0.90 : + Artspreset=="Skarabee" ? 0.74 : + Bagkrop_Taper; +function art_adj_segments() = + Artspreset=="Guldsmed_Stor" ? 9 : + Artspreset=="Vandnymfe" ? 8 : + Artspreset=="Cikade" ? 7 : + Artspreset=="Mariehoene" ? 4 : + Artspreset=="Skarabee" ? 5 : + Bagkrop_Segmenter; +function art_body_w_factor() = + Artspreset=="Mariehoene" ? 1.55 : + Artspreset=="Skarabee" ? 1.35 : + Artspreset=="Guldsmed_Stor" ? 0.72 : + Artspreset=="Vandnymfe" ? 0.58 : + Artspreset=="Cikade" ? 1.22 : + 1.00; +function art_body_len_factor() = + Artspreset=="Guldsmed_Stor" ? 1.22 : + Artspreset=="Vandnymfe" ? 1.14 : + Artspreset=="Mariehoene" ? 0.70 : + Artspreset=="Skarabee" ? 0.82 : + 1.00; +function art_ant_len() = + Artspreset=="Luna" ? 16 : + Artspreset=="Atlas" ? 15 : + Artspreset=="Guldsmed_Stor" ? 5 : + Artspreset=="Vandnymfe" ? 5 : + Artspreset=="Cikade" ? 6 : + Antenne_Laengde; +function art_ant_spread() = + Artspreset=="Luna" ? 34 : + Artspreset=="Atlas" ? 28 : + Artspreset=="Guldsmed_Stor" ? 18 : + Artspreset=="Vandnymfe" ? 14 : + Artspreset=="Cikade" ? 18 : + Antenne_Spredning; +function art_ant_fan() = + Artspreset=="Luna" ? 1.25 : + Artspreset=="Atlas" ? 1.00 : + Antenne_Fjering; +function art_vein_density() = + Artspreset=="Monark" ? 6 : + Artspreset=="Admiral" ? 5 : + Artspreset=="Paafugleoje" ? 5 : + Artspreset=="Morpho" ? 5 : + Artspreset=="Blue_Morpho" ? 5 : + Artspreset=="Svalehale" ? 5 : + Artspreset=="Luna" ? 4 : + Artspreset=="Atlas" ? 4 : + Artspreset=="Guldsmed_Stor" ? 7 : + Artspreset=="Kejserguldsmed" ? 8 : + Artspreset=="Vandnymfe" ? 6 : + Artspreset=="Vandnymfe_Smal" ? 6 : + Artspreset=="Cikade" ? 8 : + Aare_Taethed; +function art_spot_count() = + Artspreset=="Monark" ? 1 : + Artspreset=="Admiral" ? 2 : + Artspreset=="Paafugleoje" ? 3 : + Artspreset=="Morpho" ? 0 : + Artspreset=="Blue_Morpho" ? 0 : + Artspreset=="Svalehale" ? 2 : + Artspreset=="Luna" ? 2 : + Artspreset=="Atlas" ? 1 : + Plet_Antal; +function art_spots_default() = + Artspreset=="Morpho" ? false : + Artspreset=="Blue_Morpho" ? false : + Artspreset=="Guldsmed_Stor" ? false : + Artspreset=="Kejserguldsmed" ? false : + Artspreset=="Vandnymfe_Smal" ? false : + Artspreset=="Vandnymfe" ? false : + Artspreset=="Cikade" ? false : + is_butterfly(); + +function eff_body_w() = Krop_Bredde * art_body_w_factor() * (chosen_family()=="Guldsmed" ? 0.65 : chosen_family()=="Cikade" ? 1.15 : chosen_family()=="Bille" ? 1.35 : 1); +function eff_body_len() = Kropslaengde * Laengde_Skala * art_body_len_factor() * (chosen_family()=="Guldsmed" ? 1.15 : chosen_family()=="Bille" ? 0.80 : 1); +function eff_forwing_scale() = Forvinge_Skala * art_adj_forwing(); +function eff_hindwing_scale() = Bagvinge_Skala * art_adj_hindwing(); +function eff_hindwing_round() = clamp(Artspreset=="Fri" ? Bagvinge_Runding : art_adj_rounding(),0,1); +function eff_wing_tail() = clamp(Artspreset=="Fri" ? Vinge_Hale : art_adj_tail(),0,1); +function eff_tip() = clamp(Artspreset=="Fri" ? Vingespids : art_adj_tip(),0,1); +function eff_taper() = clamp(Artspreset=="Fri" ? Vinge_Taper : art_adj_taper(),0,0.95); +function eff_sweep() = Artspreset=="Fri" ? Vinge_Sweep : art_adj_sweep(); +function eff_notch() = clamp(Artspreset=="Fri" ? Forvinge_Indhak : art_adj_fore_notch(),0,1); +function eff_scallop() = clamp(Artspreset=="Fri" ? Bagvinge_Skalop : art_adj_scallop(),0,1); +function eff_long_sep() = Artspreset=="Fri" ? Langvinge_Separation : art_adj_long_sep(); +function eff_long_rootw() = Artspreset=="Fri" ? Langvinge_Rod_Bredde : art_adj_root_width(); +function eff_long_tipw() = Artspreset=="Fri" ? Langvinge_Spids_Bredde : art_adj_tip_width(); +function eff_thorax_scale() = Artspreset=="Fri" ? Bryst_Skala : art_adj_thorax(); +function eff_head_scale() = Artspreset=="Fri" ? Hoved_Skala : art_adj_head(); +function eff_ab_taper() = Artspreset=="Fri" ? Bagkrop_Taper : art_adj_ab_taper(); +function eff_segments() = Artspreset=="Fri" ? Bagkrop_Segmenter : art_adj_segments(); +function eff_ant_len() = Artspreset=="Fri" ? Antenne_Laengde : art_ant_len(); +function eff_ant_spread() = Artspreset=="Fri" ? Antenne_Spredning : art_ant_spread(); +function eff_ant_fan() = Artspreset=="Fri" ? Antenne_Fjering : art_ant_fan(); +function eff_vein_density_count() = Artspreset=="Fri" ? Aare_Taethed : art_vein_density(); +function eff_spot_count() = Artspreset=="Fri" ? Plet_Antal : art_spot_count(); +function eff_spot_state() = eff_spots_enabled() && (Artspreset=="Fri" ? Vinge_Pletter : art_spots_default()); + +module stroke_polyline(pts, w=1) { + ww=max(w,eff_min_feature()); + for(i=[0:len(pts)-2]) hull() { + translate(pts[i]) circle(d=ww); + translate(pts[i+1]) circle(d=ww); + } +} + +module stroke_bezier(p0,p1,p2,p3,w=1,n=14) { + stroke_polyline(bez3pts(p0,p1,p2,p3,n),w); +} + +module soft2d(r=Bloedgoering) { + if(r>0) offset(r=r) offset(delta=-r) children(); + else children(); +} + +module mirrored_pair() { + children(); + mirror([1,0,0]) children(); +} + +// ------------------------------------------------------------ +// Vingegeometri +// ------------------------------------------------------------ +module butterfly_forewing_half(scale_fac=1) { + span = Vingefang/2 * 0.96 * Vinge_Laengde * eff_forwing_scale() * scale_fac; + chord = Vingefang * 0.29 * Vinge_Bredde * scale_fac * (chosen_family()=="Natsvaermer" ? 0.78 : 1); + rootx = eff_body_w()/2 - Vinge_Rod_Overlap; + rooty = eff_body_len()*0.08; + sweep = eff_sweep() * chord; + tipy = rooty + chord*(0.45+sweep/chord); + tip_round = 1-eff_tip(); + taper=eff_taper(); + notch=eff_notch(); + + p0=[rootx, rooty-chord*0.28]; + p1=[rootx+span*0.22, rooty-chord*0.48]; + p2=[rootx+span*0.72, tipy-chord*(0.42-0.15*tip_round)]; + p3=[rootx+span, tipy]; + p4=[rootx+span*(0.82+0.10*tip_round), tipy+chord*(0.16+0.10*tip_round)]; + p5=[rootx+span*(0.45+0.08*taper), rooty+chord*(0.46-0.12*taper)]; + pn=[rootx+span*(0.16+0.10*notch), rooty+chord*(0.34-0.18*notch)]; + p6=[rootx, rooty+chord*0.28]; + + rotate(Vinge_Vinkel) + soft2d() + polygon(concat( + bez3pts(p0,p1,p2,p3,10), + bez3pts(p3,p4,p4,p5,8), + bez3pts(p5,pn,pn,p6,8) + )); +} + +module butterfly_hindwing_half(scale_fac=1) { + span = Vingefang/2 * 0.72 * Vinge_Laengde * eff_hindwing_scale() * scale_fac; + chord = Vingefang * 0.28 * Vinge_Bredde * scale_fac; + rootx=eff_body_w()/2 - Vinge_Rod_Overlap; + rooty=-eff_body_len()*0.04 + Bagvinge_Forskydning; + round=eff_hindwing_round(); + tail=eff_wing_tail(); + scal=eff_scallop(); + + p0=[rootx,rooty+chord*0.35]; + p1=[rootx+span*0.22,rooty+chord*0.46]; + p2=[rootx+span*0.78,rooty+chord*0.40]; + p3=[rootx+span,rooty+chord*(0.05+0.10*eff_sweep())]; + p4=[rootx+span*(0.96-0.12*round),rooty-chord*(0.28+0.20*round)]; + pd=[rootx+span*(0.68-0.08*scal),rooty-chord*(0.42+0.22*scal)]; + tailp=[rootx+span*(0.62+0.10*tail),rooty-chord*(0.45+0.50*tail)]; + p5=[rootx+span*(0.34+0.12*round),rooty-chord*(0.40+0.18*round)]; + p6=[rootx,rooty-chord*0.22]; + + rotate(Vinge_Vinkel*0.45) + soft2d() + polygon(concat( + bez3pts(p0,p1,p2,p3,9), + tail>0.02 ? concat(bez3pts(p3,p4,p4,tailp,6), bez3pts(tailp,pd,p5,p6,8)) + : concat(bez3pts(p3,p4,pd,p5,8), bez3pts(p5,p5,p6,p6,3)) + )); +} + +module longwing_half(yshift=0, scale_fac=1, rear=false) { + span=Vingefang/2 * 0.98 * Vinge_Laengde * scale_fac; + width=Vingefang * (chosen_family()=="Cikade" ? 0.16 : 0.105) * Vinge_Bredde * (rear ? 1.06 : 1); + rootx=eff_body_w()/2 - Vinge_Rod_Overlap; + rooty=yshift; + rootw=width*eff_long_rootw(); + tipw=width*eff_long_tipw(); + sweep=eff_sweep()*width*1.4; + + p0=[rootx,rooty-rootw/2]; + p1=[rootx+span*0.28,rooty-width*0.56]; + p2=[rootx+span*0.77,rooty+sweep-width*0.46]; + p3=[rootx+span,rooty+sweep-tipw*0.08]; + p4=[rootx+span,rooty+sweep+tipw*0.18]; + p5=[rootx+span*0.70,rooty+sweep+width*0.48]; + p6=[rootx+span*0.22,rooty+width*0.48]; + p7=[rootx,rooty+rootw/2]; + + rotate(Vinge_Vinkel) + soft2d() + polygon(concat(bez3pts(p0,p1,p2,p3,10),bez3pts(p3,p4,p5,p6,8),bez3pts(p6,p6,p7,p7,3))); +} + +module beetle_elytron_half() { + span=Vingefang/2*(Artspreset=="Mariehoene" ? 0.46 : 0.53)*Vinge_Bredde; + len=eff_body_len()*(Artspreset=="Mariehoene" ? 0.56 : 0.62); + rootx=eff_body_w()*0.10; + y0=-eff_body_len()*0.12; + soft2d() + polygon(concat( + bez3pts([rootx,y0+len*0.48],[span*0.65,y0+len*0.55],[span,y0+len*0.20],[span,y0-len*0.08],10), + bez3pts([span,y0-len*0.08],[span*0.95,y0-len*0.48],[span*0.42,y0-len*0.58],[rootx,y0-len*0.52],10) + )); +} + +module wings_2d() { + if(is_butterfly()) { + mirrored_pair() { + butterfly_forewing_half(1 + Asymmetri); + butterfly_hindwing_half(1 + Asymmetri*0.5); + } + } + else if(is_longwing()) { + sep=eff_long_sep(); + mirrored_pair() { + longwing_half(+sep/2, Langvinge_For_Skala*(1+Asymmetri), false); + longwing_half(-sep/2, Langvinge_Bag_Skala*(1+Asymmetri*0.5), true); + } + } + else if(is_beetle()) { + mirrored_pair() beetle_elytron_half(); + mirrored_pair() rotate(-8) scale([1.12,0.72]) longwing_half(-3,0.72,false); + } +} + +// ------------------------------------------------------------ +// Krop, hoved, antenner, ben +// ------------------------------------------------------------ +module body_core_2d() { + L=eff_body_len(); + W=eff_body_w(); + head_y=L*0.32; + thor_y=L*0.12; + abdomen_y=-L*0.13; + abdomen_len=L*0.53; + + union() { + hull() { + translate([0,abdomen_y+abdomen_len*0.35]) circle(d=W*(0.82-0.28*Talje)); + translate([0,abdomen_y-abdomen_len*0.48]) circle(d=W*eff_ab_taper()); + } + translate([0,thor_y]) scale([eff_thorax_scale(),1.18]) circle(d=W); + hull() { + translate([0,thor_y+W*0.30]) circle(d=W*0.48); + translate([0,head_y]) scale([eff_head_scale(),0.90*eff_head_scale()]) circle(d=W); + } + } +} + +module antenna_half(side=1) { + L=eff_body_len(); W=eff_body_w(); + y=L*0.37; + antL=eff_ant_len(); + fan=eff_ant_fan(); + p0=[side*W*0.20,y]; + p1=[side*(W*0.35 + antL*0.18), y+antL*0.30]; + p2=[side*(W*0.35 + antL*(0.48+0.12*Antenne_Krumning)), y+antL*0.68]; + p3=[side*(W*0.30 + antL*sin(eff_ant_spread())), y+antL]; + stroke_bezier(p0,p1,p2,p3,eff_min_feature(),14); + + if(fan>0.02) + for(t=[0.18:0.12:0.82]) { + pt=bz3(p0,p1,p2,p3,t); + dir=[side*fan*antL*(0.08+0.08*t), fan*antL*(0.01+0.01*t)]; + stroke_polyline([pt, pt+[dir[0],dir[1]]], eff_min_feature()*0.65); + stroke_polyline([pt, pt+[dir[0],-dir[1]]], eff_min_feature()*0.65); + } + + if(Antenne_Knop>0) translate(p3) circle(d=max(Antenne_Knop,eff_min_feature())); +} + +module legs_2d() { + if(Ben) { + L=eff_body_len(); W=eff_body_w(); + for(yy=[-0.03,0.10,0.20]) + for(s=[-1,1]) { + root=[s*W*0.30,L*yy]; + knee=[s*(W*0.65+Ben_Laengde*0.38),L*yy-Ben_Laengde*0.12]; + tip=[s*(W*0.65+Ben_Laengde*cos(Ben_Vinkel)),L*yy-Ben_Laengde*sin(Ben_Vinkel)]; + stroke_polyline([root,knee,tip],eff_min_feature()); + } + } +} + +module body_2d() { + union() { + soft2d(Bloedgoering*0.5) body_core_2d(); + if(Antenner) { antenna_half(-1); antenna_half(1); } + legs_2d(); + } +} + +// ------------------------------------------------------------ +// Dekoration: aarer, pletter, kropssegmenter +// ------------------------------------------------------------ +module butterfly_veins_half(fore=true) { + span=Vingefang/2 * (fore ? 0.90*eff_forwing_scale() : 0.66*eff_hindwing_scale()) * Vinge_Laengde; + chord=Vingefang * (fore ? 0.24 : 0.23) * Vinge_Bredde; + root=[eff_body_w()/2, fore ? eff_body_len()*0.08 : -eff_body_len()*0.04+Bagvinge_Forskydning]; + baseang=fore ? Vinge_Vinkel : Vinge_Vinkel*0.45; + + rotate(baseang) { + stroke_bezier(root, + [root[0]+span*0.25,root[1]+chord*0.02], + [root[0]+span*0.58,root[1]+chord*(fore?0.15:-0.08)], + [root[0]+span*0.90,root[1]+chord*(fore?0.28:-0.12)], eff_vein_thickness(),14); + + for(i=[1:eff_vein_density_count()]) { + t=i/(eff_vein_density_count()+1); + start=[root[0]+span*(0.07+0.50*t), root[1]+chord*(fore?0.04:-0.02)]; + ey = fore ? lerp(-0.31,0.48,t) : lerp(-0.38,0.30,t); + end=[root[0]+span*lerp(0.50,0.95,t), root[1]+chord*ey]; + bend=Aare_Organisk*chord*0.10*sin(i*37); + stroke_bezier(start, + v2lerp(start,end,0.35)+[0,bend], + v2lerp(start,end,0.72)+[0,-bend*0.4],end, + eff_vein_thickness(),9); + } + + if(eff_crossveins()) + for(i=[1:max(1,eff_vein_density_count()-1)]) { + t=i/eff_vein_density_count(); + x=root[0]+span*(0.34+0.45*t); + stroke_bezier([x,root[1]-chord*0.18],[x+span*0.03,root[1]-chord*0.05], + [x-span*0.02,root[1]+chord*0.15],[x,root[1]+chord*0.30], + eff_vein_thickness()*0.75,7); + } + } +} + +module longwing_veins_half(yshift=0,scale_fac=1) { + span=Vingefang/2*0.91*Vinge_Laengde*scale_fac; + width=Vingefang*(chosen_family()=="Cikade"?0.14:0.09)*Vinge_Bredde; + root=[eff_body_w()/2,yshift]; + rotate(Vinge_Vinkel) { + for(j=[-1,0,1]) + stroke_bezier(root, + [root[0]+span*0.28,root[1]+j*width*0.22], + [root[0]+span*0.66,root[1]+j*width*0.26], + [root[0]+span*0.92,root[1]+j*width*0.16],eff_vein_thickness(),12); + if(eff_crossveins()) + for(i=[1:eff_vein_density_count()]) { + t=i/(eff_vein_density_count()+1); + x=root[0]+span*(0.15+0.70*t); + w=width*(0.35+0.55*sin(t*180)); + stroke_bezier([x,root[1]-w],[x+span*0.025,root[1]-w*0.3], + [x-span*0.015,root[1]+w*0.3],[x,root[1]+w],eff_vein_thickness()*0.70,6); + } + } +} + +module pterostigma_half(yshift=0,scale_fac=1) { + if(Pterostigma && (chosen_family()=="Guldsmed" || chosen_family()=="Cikade")) { + span=Vingefang/2*0.91*Vinge_Laengde*scale_fac; + width=Vingefang*(chosen_family()=="Cikade"?0.14:0.09)*Vinge_Bredde; + root=[eff_body_w()/2,yshift]; + rotate(Vinge_Vinkel) + translate([root[0]+span*0.82,root[1]+width*0.18]) + scale([1.8,0.75]) circle(d=max(width*0.22*Pterostigma_Skala, eff_min_feature()*1.4)); + } +} + +module wing_spots_half() { + if(eff_spot_state() && eff_spot_count()>0 && is_butterfly()) { + span=Vingefang/2*0.90*Vinge_Laengde*eff_forwing_scale(); + chord=Vingefang*0.24*Vinge_Bredde; + root=[eff_body_w()/2,eff_body_len()*0.08]; + rotate(Vinge_Vinkel) + for(i=[0:eff_spot_count()-1]) { + t=(i+1)/(eff_spot_count()+1); + c=[root[0]+span*lerp(0.52,0.78,t),root[1]+chord*lerp(-0.08,0.25,t)]; + d=Vingefang*0.045*Plet_Skala*(1-0.15*i); + if(Plet_Ring) { + difference() { + translate(c) circle(d=d); + translate(c) circle(d=max(d-2*max(eff_vein_thickness(),eff_min_feature()),eff_min_feature())); + } + } else translate(c) circle(d=d); + } + } +} + +module beetle_spots_half() { + if(eff_deck_spots() && is_beetle()) { + span=Vingefang/2*(Artspreset=="Mariehoene" ? 0.42 : 0.45); + y0=-eff_body_len()*0.12; + for(i=[0:Daekvinge_Plet_Antal-1]) { + t=(i+1)/(Daekvinge_Plet_Antal+1); + px=eff_body_w()*0.18 + span*lerp(0.22,0.82,t); + py=y0 + eff_body_len()*lerp(0.10,-0.28,t) + (i%2==0 ? 2.0 : -1.5); + translate([px,py]) scale([1.15,0.95]) circle(d=Vingefang*0.045*(Artspreset=="Mariehoene" ? 1.0 : 0.75)); + } + } +} + +module body_decoration_2d() { + L=eff_body_len(); W=eff_body_w(); + if(eff_segments()>0) + for(i=[0:eff_segments()-1]) { + t=(i+0.75)/(eff_segments()+1); + y=lerp(-L*0.38,L*0.05,t); + ww=lerp(W*eff_ab_taper(),W*0.75,t); + stroke_polyline([[-ww*0.34,y],[ww*0.34,y]],max(eff_vein_thickness(),eff_min_feature())); + } + stroke_polyline([[0,L*0.03],[0,L*0.22]],max(eff_vein_thickness(),eff_min_feature())); + + if(Daekvinge_Soem && is_beetle()) + stroke_polyline([[0,-L*0.36],[0,L*0.18]],max(eff_vein_thickness(),eff_min_feature())); + + if(chosen_family()=="Cikade") { + translate([ W*0.28, L*0.30]) circle(d=max(W*0.18, eff_min_feature())); + translate([-W*0.28, L*0.30]) circle(d=max(W*0.18, eff_min_feature())); + } +} + +module decorations_raw_2d() { + intersection() { + union() { + if(eff_veins_enabled()) { + if(is_butterfly()) mirrored_pair() { + butterfly_veins_half(true); + butterfly_veins_half(false); + } + else if(is_longwing()) mirrored_pair() { + longwing_veins_half(+eff_long_sep()/2,Langvinge_For_Skala); + longwing_veins_half(-eff_long_sep()/2,Langvinge_Bag_Skala); + pterostigma_half(+eff_long_sep()/2,Langvinge_For_Skala); + pterostigma_half(-eff_long_sep()/2,Langvinge_Bag_Skala); + } + else if(is_beetle()) mirrored_pair() { + stroke_bezier([eff_body_w()*0.15,eff_body_len()*0.18], + [Vingefang*0.13,eff_body_len()*0.12], + [Vingefang*0.20,-eff_body_len()*0.15], + [Vingefang*0.17,-eff_body_len()*0.35],eff_vein_thickness(),12); + } + } + wing_spots_half(); + mirror([1,0,0]) wing_spots_half(); + beetle_spots_half(); + mirror([1,0,0]) beetle_spots_half(); + } + wings_2d(); + } + body_decoration_2d(); + ornamental_2d(); +} + +// ------------------------------------------------------------ +// Ornamental / Art Nouveau +// ------------------------------------------------------------ +module ornamental_half() { + if(Ornamental_Mode && Ornamental_Styrke>0) { + W=Vingefang; + L=eff_body_len(); + w=max(Ornamental_Tykkelse,eff_min_feature()*0.65); + for(i=[0:Ornamental_Slyng-1]) { + t=(i+1)/(Ornamental_Slyng+1); + p0=[eff_body_w()*0.40, lerp(-L*0.22,L*0.24,t)]; + p1=[W*lerp(0.10,0.17,t), lerp(-L*0.34,L*0.38,t)]; + p2=[W*lerp(0.25,0.36,t), lerp(L*0.18,-L*0.12,t)]; + p3=[W*lerp(0.34,0.46,t), lerp(-L*0.05,L*0.30,t)]; + stroke_bezier(p0,p1,p2,p3,w*Ornamental_Styrke,16); + if(Ornamental_Ringe) + translate(p3) difference() { + circle(d=W*(0.018+0.008*t)*Ornamental_Styrke+2*w); + circle(d=max(W*(0.018+0.008*t)*Ornamental_Styrke,w)); + } + } + } +} + +module ornamental_2d() { + intersection() { + mirrored_pair() ornamental_half(); + wings_2d(); + } +} + +// ------------------------------------------------------------ +// Stencil bridges +// ------------------------------------------------------------ +module stencil_bridges_half() { + if(Stencil_Mode) { + bw=max(Stencil_Bro_Bredde,eff_min_feature()); + for(i=[0:Stencil_Bro_Antal-1]) { + t=(i+1)/(Stencil_Bro_Antal+1); + y=lerp(-eff_body_len()*0.18,eff_body_len()*0.20,t); + x0=eff_body_w()*0.25; + x1=x0+Stencil_Bro_Laengde*(0.72+0.28*t); + stroke_polyline([[x0,y],[x1,y+(t-0.5)*Stencil_Bro_Laengde*0.18]],bw); + } + } +} + +module stencil_bridges_2d() { + union() { + mirrored_pair() stencil_bridges_half(); + if(Stencil_Mode && Stencil_Centerbro && Ophaengshul) { + bw=max(Stencil_Bro_Bredde,eff_min_feature()); + y=eff_body_len()*0.32+Ophaengshul_Y; + translate([-bw/2,y-Ophaengshul_Diameter/2-0.2]) square([bw,Ophaengshul_Diameter+0.4]); + } + } +} + +// ------------------------------------------------------------ +// Silhuet, udskaeringer, montage +// ------------------------------------------------------------ +module cutout_pattern_half() { + if(eff_cutout()>0) { + span=Vingefang/2*0.78; + for(i=[1:3]) { + yy=lerp(-Vingefang*0.12,Vingefang*0.14,i/4); + translate([eff_body_w()/2+span*(0.25+0.16*i),yy]) + scale([1.5,1]) circle(d=Vingefang*0.05*eff_cutout()); + } + } +} + +module silhouette_raw_2d() { + union() { wings_2d(); body_2d(); } +} + +module silhouette_2d() { + union() { + difference() { + if(eff_outline()>0) offset(r=eff_outline()) offset(r=0.06) offset(delta=-0.06) silhouette_raw_2d(); + else offset(r=0.06) offset(delta=-0.06) silhouette_raw_2d(); + + if(eff_cutout()>0) + intersection() { + offset(delta=-Udskaering_Margin) wings_2d(); + union() { cutout_pattern_half(); mirror([1,0,0]) cutout_pattern_half(); } + } + + if(Ophaengshul) + translate([0,eff_body_len()*0.32+Ophaengshul_Y]) circle(d=Ophaengshul_Diameter); + } + if(Stencil_Mode) + intersection() { stencil_bridges_2d(); silhouette_raw_2d(); } + } +} + +module decoration_2d() { + decorations_raw_2d(); +} + +// ------------------------------------------------------------ +// Multi-layer helpers +// ------------------------------------------------------------ +module wing_layer_2d() { + if(Layer_Overlap>0) offset(delta=Layer_Overlap) wings_2d(); else wings_2d(); +} +module body_layer_2d() { + if(Layer_Overlap>0) offset(delta=Layer_Overlap) body_2d(); else body_2d(); +} +module spot_layer_2d() { + intersection() { + union() { + wing_spots_half(); mirror([1,0,0]) wing_spots_half(); + beetle_spots_half(); mirror([1,0,0]) beetle_spots_half(); + if(is_longwing()) mirrored_pair() { + pterostigma_half(+eff_long_sep()/2,Langvinge_For_Skala); + pterostigma_half(-eff_long_sep()/2,Langvinge_Bag_Skala); + } + } + wings_2d(); + } +} +module vein_layer_2d() { + difference() { decoration_2d(); spot_layer_2d(); } +} +module selected_layer_2d() { + if(Layer_Output=="Vinger") wing_layer_2d(); + else if(Layer_Output=="Krop") body_layer_2d(); + else if(Layer_Output=="Aarer") vein_layer_2d(); + else if(Layer_Output=="Pletter") spot_layer_2d(); + else if(Layer_Output=="Stencilbroer") stencil_bridges_2d(); + else union() { silhouette_2d(); decoration_2d(); } +} + +// ------------------------------------------------------------ +// Panel / batch layout +// ------------------------------------------------------------ +module panelize() { + for(r=[0:Panel_Raekker-1]) + for(c=[0:Panel_Kolonner-1]) { + idx=r*Panel_Kolonner+c; + tx=(c-(Panel_Kolonner-1)/2)*Panel_Afstand_X; + ty=((Panel_Raekker-1)/2-r)*Panel_Afstand_Y; + translate([tx,ty,0]) + rotate(idx%2==1 ? Panel_Skiftevis_Rotation : 0) + if(Panel_Skiftevis_Spejling && idx%2==1) mirror([1,0,0]) children(); + else children(); + } +} + +// ------------------------------------------------------------ +// 3D +// ------------------------------------------------------------ +module extrude_soft(height=2) { + linear_extrude(height=height) children(); +} + +module model_3d_flat() { + extrude_soft(Tykkelse) silhouette_2d(); +} + +module model_3d_relief() { + if(Relief_Haevede_Detaljer) { + union() { + extrude_soft(Tykkelse) silhouette_2d(); + translate([0,0,Tykkelse-EPS]) + linear_extrude(height=Relief_Hoejde+EPS) + intersection() { decoration_2d(); silhouette_2d(); } + } + } else { + difference() { + extrude_soft(Tykkelse) silhouette_2d(); + translate([0,0,max(0,Tykkelse-Relief_Hoejde)]) + linear_extrude(height=Relief_Hoejde+EPS*2) + intersection() { decoration_2d(); silhouette_2d(); } + } + } +} + +// ------------------------------------------------------------ +// Top-level output +// ------------------------------------------------------------ +module single_output() { + if(Output=="2D_SILHOUET") + if(Layer_Output=="Alle") silhouette_2d(); else selected_layer_2d(); + else if(Output=="2D_DEKORATION") + if(Layer_Output=="Alle") decoration_2d(); else selected_layer_2d(); + else if(Output=="2D_COMBINED") + if(Layer_Output=="Alle") union() { silhouette_2d(); decoration_2d(); } else selected_layer_2d(); + else if(Output=="3D_FLAT") + model_3d_flat(); + else + model_3d_relief(); +} + +if(Panel_Mode) + panelize() single_output(); +else + single_output(); + +if(Vis_Centerlinje && $preview && !Panel_Mode) + color("red") translate([-0.15,-eff_body_len()/2,0]) square([0.3,eff_body_len()]); \ No newline at end of file diff --git a/decorative-insects/images/butterfly-relief.png b/decorative-insects/images/butterfly-relief.png new file mode 100644 index 0000000..1721965 Binary files /dev/null and b/decorative-insects/images/butterfly-relief.png differ diff --git a/decorative-insects/images/dragonfly-flat.png b/decorative-insects/images/dragonfly-flat.png new file mode 100644 index 0000000..38bc711 Binary files /dev/null and b/decorative-insects/images/dragonfly-flat.png differ diff --git a/decorative-insects/stl-files/butterfly-relief.stl b/decorative-insects/stl-files/butterfly-relief.stl new file mode 100644 index 0000000..d9cdda7 Binary files /dev/null and b/decorative-insects/stl-files/butterfly-relief.stl differ diff --git a/decorative-insects/stl-files/dragonfly-flat.stl b/decorative-insects/stl-files/dragonfly-flat.stl new file mode 100644 index 0000000..214b0ec Binary files /dev/null and b/decorative-insects/stl-files/dragonfly-flat.stl differ diff --git a/parametric-box/README.md b/parametric-box/README.md new file mode 100644 index 0000000..d8cdbac --- /dev/null +++ b/parametric-box/README.md @@ -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). diff --git a/parametric-box/images/hinged-snap.png b/parametric-box/images/hinged-snap.png new file mode 100644 index 0000000..0c8db66 Binary files /dev/null and b/parametric-box/images/hinged-snap.png differ diff --git a/parametric-box/images/inner-lip.png b/parametric-box/images/inner-lip.png new file mode 100644 index 0000000..aae0686 Binary files /dev/null and b/parametric-box/images/inner-lip.png differ diff --git a/parametric-box/images/sliding-lid.png b/parametric-box/images/sliding-lid.png new file mode 100644 index 0000000..57b5e33 Binary files /dev/null and b/parametric-box/images/sliding-lid.png differ diff --git a/parametric-box/parametric-box.scad b/parametric-box/parametric-box.scad new file mode 100644 index 0000000..742f9c2 --- /dev/null +++ b/parametric-box/parametric-box.scad @@ -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 0; + +function body_diameter_if_circle() = min(body_width, body_height); + +function nominal_w() = + shape == "Circle" ? body_diameter_if_circle() : body_width; + +function nominal_h() = + shape == "Circle" ? body_diameter_if_circle() : body_height; + +// Conservative rectangles fully contained well inside each shape. +// These are intentionally cautious; strict clipping remains the final guard. +function shape_safe_fx() = + shape == "Rounded Rectangle" ? 0.92 : + shape == "Capsule" ? 0.80 : + shape == "Circle" ? 0.70 : + shape == "Oval" ? 0.70 : + shape == "Hexagon" ? 0.72 : + shape == "Dog Tag" ? 0.70 : + shape == "Shield" ? 0.58 : + shape == "Heart" ? 0.52 : + 0.70; + +function shape_safe_fy() = + shape == "Rounded Rectangle" ? 0.88 : + shape == "Capsule" ? 0.68 : + shape == "Circle" ? 0.70 : + shape == "Oval" ? 0.70 : + shape == "Hexagon" ? 0.72 : + shape == "Dog Tag" ? 0.68 : + shape == "Shield" ? 0.58 : + shape == "Heart" ? 0.42 : + 0.65; + +function safe_rect_w_raw() = + max(minimum_safe_size, nominal_w()*shape_safe_fx() - 2*text_edge_clearance); + +function safe_rect_h_raw() = + max(minimum_safe_size, nominal_h()*shape_safe_fy() - 2*text_edge_clearance); + +// Hole size for internal holes is limited by the conservative safe rectangle. +// Side-lug holes do not need this clamp because the lug itself can grow. +function inside_hole_max_d() = + max(1, + min( + nominal_w()*shape_safe_fx(), + nominal_h()*shape_safe_fy() + ) - 2*minimum_wall); + +function effective_hole_d() = + attachment == "Inside Hole" + ? min(ring_hole_diameter, inside_hole_max_d()) + : ring_hole_diameter; + +function effective_lug_outer_d() = + max(lug_outer_diameter, effective_hole_d() + 2*minimum_wall); + +function effective_lug_overlap() = + lug_overlap > 0 + ? min(lug_overlap, effective_lug_outer_d()*0.48) + : max(minimum_wall, effective_lug_outer_d()*0.24); + +function lug_preset_x() = + lug_position_mode == "Right" ? nominal_w()/2 + effective_lug_outer_d()/2 - effective_lug_overlap() : + lug_position_mode == "Left" ? -nominal_w()/2 - effective_lug_outer_d()/2 + effective_lug_overlap() : + 0; + +function lug_preset_y() = + lug_position_mode == "Top" ? nominal_h()/2 + effective_lug_outer_d()/2 - effective_lug_overlap() : + lug_position_mode == "Bottom" ? -nominal_h()/2 - effective_lug_outer_d()/2 + effective_lug_overlap() : + 0; + +function lug_center_x() = + (lug_position_mode == "Custom XY" ? lug_x : lug_preset_x()) + lug_offset_x; + +function lug_center_y() = + (lug_position_mode == "Custom XY" ? lug_y : lug_preset_y()) + lug_offset_y; + +// Lug 2 uses its own hole diameter, OD, overlap, bridge and position. +function effective_lug2_hole_d() = + max(1, lug2_hole_diameter); + +function effective_lug2_outer_d() = + max(lug2_outer_diameter, effective_lug2_hole_d() + 2*minimum_wall); + +function effective_lug2_overlap() = + lug2_overlap > 0 + ? min(lug2_overlap, effective_lug2_outer_d()*0.48) + : max(minimum_wall, effective_lug2_outer_d()*0.24); + +function lug2_preset_x() = + lug2_position_mode == "Right" ? nominal_w()/2 + effective_lug2_outer_d()/2 - effective_lug2_overlap() : + lug2_position_mode == "Left" ? -nominal_w()/2 - effective_lug2_outer_d()/2 + effective_lug2_overlap() : + 0; + +function lug2_preset_y() = + lug2_position_mode == "Top" ? nominal_h()/2 + effective_lug2_outer_d()/2 - effective_lug2_overlap() : + lug2_position_mode == "Bottom" ? -nominal_h()/2 - effective_lug2_outer_d()/2 + effective_lug2_overlap() : + 0; + +function lug2_center_x() = + (lug2_position_mode == "Custom XY" ? lug2_x : lug2_preset_x()) + lug2_offset_x; + +function lug2_center_y() = + (lug2_position_mode == "Custom XY" ? lug2_y : lug2_preset_y()) + lug2_offset_y; + +function inside_hole_x_limit() = + max(0, nominal_w()*shape_safe_fx()/2 - minimum_wall - inside_hole_radius()); + +function inside_hole_y_limit() = + max(0, nominal_h()*shape_safe_fy()/2 - minimum_wall - inside_hole_radius()); + +function inside_hole_preset_x() = + inside_hole_position_mode == "Left" ? -inside_hole_x_limit() : + inside_hole_position_mode == "Right" ? inside_hole_x_limit() : 0; + +function inside_hole_preset_y() = + inside_hole_position_mode == "Top" ? inside_hole_y_limit() : + inside_hole_position_mode == "Bottom" ? -inside_hole_y_limit() : 0; + +function inside_hole_x() = + clamp((inside_hole_position_mode == "Custom XY" ? inside_hole_custom_x : inside_hole_preset_x()) + inside_hole_offset_x, -inside_hole_x_limit(), inside_hole_x_limit()); + +function inside_hole_y() = + clamp((inside_hole_position_mode == "Custom XY" ? inside_hole_custom_y : inside_hole_preset_y()) + inside_hole_offset_y, -inside_hole_y_limit(), inside_hole_y_limit()); + +function hole_keepout_r() = + inside_hole_radius() + text_hole_clearance; + +// Text box accounts for internal hole by reserving everything left of the +// keepout circle. Fine user X adjustment is then clamped to keep text safe. +function text_box_left() = + attachment == "Inside Hole" + ? max( + -safe_rect_w_raw()/2, + inside_hole_x() + hole_keepout_r() + ) + : -safe_rect_w_raw()/2; + +function text_box_right() = safe_rect_w_raw()/2; + +function text_box_w() = + max(minimum_safe_size, text_box_right() - text_box_left()); + +function text_box_h() = safe_rect_h_raw(); + +function text_box_cx() = (text_box_left() + text_box_right())/2; + +function text_x_adjust_clamped() = + clamp( + text_x_adjust, + -text_box_w()*0.18, + text_box_w()*0.18 + ); + +function text_y_adjust_clamped() = + clamp( + text_y_adjust, + -text_box_h()*0.18, + text_box_h()*0.18 + ); + + +// textmetrics() wrapper matching the settings used by text(). +function tm(s, size, fnt, spacing=1) = + textmetrics( + text=s, + size=size, + font=fnt, + halign="left", + valign="baseline", + spacing=spacing + ); + +function fit_text_size(s, wanted_size, max_w, max_h, fnt, spacing=1, min_size=1) = + !nonempty(s) ? wanted_size : + let( + m = tm(s, wanted_size, fnt, spacing), + sx = max_w / nz(m.size.x), + sy = max_h / nz(m.size.y), + k = min(1, sx, sy) + ) + max(min_size, wanted_size*k); + +// Offset which places actual glyph ink bounds around origin, not merely the +// font's nominal advance box. +function ink_center_offset(s, size, fnt, spacing=1) = + let(m = tm(s, size, fnt, spacing)) + [ + -m.position.x - m.size.x/2, + -m.position.y - m.size.y/2 + ]; + +function line_count() = + (nonempty(text_line_1) ? 1 : 0) + + (nonempty(text_line_2) ? 1 : 0); + +function per_line_box_h() = + line_count() <= 1 + ? text_box_h() + : max(minimum_safe_size, (text_box_h()-line_gap)/2); + +function line1_size() = + fit_text_size( + text_line_1, font_size, + text_box_w(), per_line_box_h(), + font, text_spacing, minimum_font_size + ); + +function line2_size() = + fit_text_size( + text_line_2, font_size, + text_box_w(), per_line_box_h(), + font, text_spacing, minimum_font_size + ); + + +// ============================================================================ +// Color functions +// ============================================================================ + +// Palette base color. +function palette_base_col() = + color_scheme == "Blue White" ? [0.08,0.32,0.78,1] : + color_scheme == "Black Yellow" ? [0.04,0.04,0.05,1] : + color_scheme == "Red White" ? [0.72,0.05,0.08,1] : + color_scheme == "Green White" ? [0.05,0.48,0.24,1] : + color_scheme == "Purple Gold" ? [0.32,0.10,0.52,1] : + color_scheme == "Orange Black" ? [0.95,0.30,0.03,1] : + custom_base_color; + +// Palette secondary color, used by Two Colors mode for *everything except base*. +function palette_secondary_col() = + color_scheme == "Blue White" ? [0.96,0.97,1.00,1] : + color_scheme == "Black Yellow" ? [1.00,0.80,0.02,1] : + color_scheme == "Red White" ? [1.00,0.97,0.96,1] : + color_scheme == "Green White" ? [0.96,1.00,0.96,1] : + color_scheme == "Purple Gold" ? [0.95,0.68,0.12,1] : + color_scheme == "Orange Black" ? [0.03,0.03,0.03,1] : + custom_secondary_color; + +// Palette text color, used only by Three Colors mode. +function palette_text_col() = + color_scheme == "Blue White" ? [0.96,0.97,1.00,1] : + color_scheme == "Black Yellow" ? [1.00,0.80,0.02,1] : + color_scheme == "Red White" ? [1.00,0.97,0.96,1] : + color_scheme == "Green White" ? [0.96,1.00,0.96,1] : + color_scheme == "Purple Gold" ? [0.95,0.68,0.12,1] : + color_scheme == "Orange Black" ? [0.03,0.03,0.03,1] : + custom_text_color; + +// Palette decoration/accent color, used only by Three Colors mode. +function palette_decoration_col() = + color_scheme == "Blue White" ? [0.15,0.72,1.00,1] : + color_scheme == "Black Yellow" ? [1.00,0.52,0.00,1] : + color_scheme == "Red White" ? [1.00,0.58,0.12,1] : + color_scheme == "Green White" ? [0.20,0.85,0.52,1] : + color_scheme == "Purple Gold" ? [1.00,0.73,0.18,1] : + color_scheme == "Orange Black" ? [1.00,0.76,0.20,1] : + custom_decoration_color; + +// Final preview colors according to the requested display mode. +function base_col() = + palette_base_col(); + +function text_col() = + color_display_mode == "Single Color" ? palette_base_col() : + color_display_mode == "Two Colors" ? palette_secondary_col() : + palette_text_col(); + +function decoration_col() = + color_display_mode == "Single Color" ? palette_base_col() : + color_display_mode == "Two Colors" ? palette_secondary_col() : + palette_decoration_col(); + + +// ============================================================================ +// ============================================================================ +// 2D body geometry +// ============================================================================ + +module rounded_rectangle_2d(w, h, r) { + rr = clamp(r, 0, min(w,h)/2); + if (rr <= epsilon) + square([w,h], center=true); + else + offset(r=rr) + square( + [max(epsilon,w-2*rr), max(epsilon,h-2*rr)], + center=true + ); +} + +module capsule_2d(w,h) { + if (w >= h) { + r = h/2; + hull() { + translate([-w/2+r,0]) circle(r=r); + translate([ w/2-r,0]) circle(r=r); + } + } else { + r = w/2; + hull() { + translate([0,-h/2+r]) circle(r=r); + translate([0, h/2-r]) circle(r=r); + } + } +} + +module hexagon_2d(w,h) { + polygon([ + [-w/2,0],[-w/4,h/2],[w/4,h/2], + [w/2,0],[w/4,-h/2],[-w/4,-h/2] + ]); +} + +module dog_tag_2d(w,h,r) { + rr = clamp(r,0.8,min(w,h)/5); + hull() { + translate([-w/2+rr,-h/2+rr]) circle(r=rr); + translate([ w/2-rr,-h/2+rr]) circle(r=rr); + translate([-w/2+rr, h/2-rr*1.7]) circle(r=rr); + translate([ w/2-rr, h/2-rr*1.7]) circle(r=rr); + translate([0,h/2-rr*0.30]) + square([max(epsilon,w-4*rr),rr*0.55],center=true); + } +} + +module shield_2d(w,h) { + polygon([ + [-w/2,h*0.40], + [-w*0.36,h/2], + [ w*0.36,h/2], + [ w/2,h*0.40], + [ w*0.43,-h*0.08], + [ w*0.24,-h*0.32], + [ 0,-h/2], + [-w*0.24,-h*0.32], + [-w*0.43,-h*0.08] + ]); +} + +module heart_2d(w,h) { + // Symmetric, solid printable heart centered around the origin. + scale([w/40,h/38]) + union() { + hull() { + translate([-10,7]) circle(r=10); + translate([0,-15]) circle(r=1.0); + } + hull() { + translate([10,7]) circle(r=10); + translate([0,-15]) circle(r=1.0); + } + } +} + +module core_body_2d() { + w = nominal_w(); + h = nominal_h(); + + if (shape == "Rounded Rectangle") + rounded_rectangle_2d(w,h,corner_radius); + else if (shape == "Capsule") + capsule_2d(w,h); + else if (shape == "Circle") + circle(d=body_diameter_if_circle()); + else if (shape == "Oval") + scale([w/2,h/2]) circle(r=1); + else if (shape == "Hexagon") + hexagon_2d(w,h); + else if (shape == "Dog Tag") + dog_tag_2d(w,h,corner_radius); + else if (shape == "Shield") + shield_2d(w,h); + else if (shape == "Heart") + heart_2d(w,h); + else + rounded_rectangle_2d(w,h,corner_radius); +} + + +// ============================================================================ +// Attachment geometry +// ============================================================================ + +module one_lug_positive_2d(position_mode, cx, cy, d, overlap, bridge) { + bridge_d = max(bridge, minimum_wall*2); + + ax = + position_mode == "Right" ? nominal_w()/2-overlap : + position_mode == "Left" ? -nominal_w()/2+overlap : + clamp(cx, -nominal_w()/2+overlap, nominal_w()/2-overlap); + + ay = + position_mode == "Top" ? nominal_h()/2-overlap : + position_mode == "Bottom" ? -nominal_h()/2+overlap : + clamp(cy, -nominal_h()/2+overlap, nominal_h()/2-overlap); + + // Main neck. hull() guarantees a connected printable join. + hull() { + translate([ax,ay]) circle(d=bridge_d); + translate([cx,cy]) circle(d=d); + } + + // Reinforced Lug adds two gussets perpendicular to the bridge direction. + if (attachment == "Reinforced Lug") { + vx = cx-ax; + vy = cy-ay; + vl = max(epsilon, sqrt(vx*vx + vy*vy)); + px = -vy/vl; + py = vx/vl; + + for (sgn=[-1,1]) + hull() { + translate([ + ax + px*sgn*bridge_d*0.36, + ay + py*sgn*bridge_d*0.36 + ]) + circle(d=max(minimum_wall*1.6,bridge_d*0.55)); + + translate([ + cx - vx/vl*d*0.18 + px*sgn*d*0.18, + cy - vy/vl*d*0.18 + py*sgn*d*0.18 + ]) + circle(d=max(minimum_wall*1.6,bridge_d*0.50)); + } + } +} + +module side_lug_1_positive_2d() { + one_lug_positive_2d( + lug_position_mode, + lug_center_x(), + lug_center_y(), + effective_lug_outer_d(), + effective_lug_overlap(), + lug_bridge + ); +} + +module side_lug_2_positive_2d() { + one_lug_positive_2d( + lug2_position_mode, + lug2_center_x(), + lug2_center_y(), + effective_lug2_outer_d(), + effective_lug2_overlap(), + lug2_bridge + ); +} + +module attachment_positive_2d() { + if (attachment == "Side Lug" || attachment == "Reinforced Lug") { + side_lug_1_positive_2d(); + + if (lug_count >= 2) + side_lug_2_positive_2d(); + } +} + +module ring_hole_2d(extra=0) { + if (attachment == "Side Lug" || attachment == "Reinforced Lug") { + translate([lug_center_x(),lug_center_y()]) + circle(d=max(epsilon,effective_hole_d()+2*extra)); + + if (lug_count >= 2) + translate([lug2_center_x(),lug2_center_y()]) + circle(d=max(epsilon,effective_lug2_hole_d()+2*extra)); + } + + else if (attachment == "Inside Hole") + translate([inside_hole_x(),inside_hole_y()]) + circle(d=max(epsilon,effective_hole_d()+2*extra)); +} + +module full_profile_2d() { + difference() { + union() { + core_body_2d(); + attachment_positive_2d(); + } + ring_hole_2d(); + } +} + + +// ============================================================================ +// Safe masks +// ============================================================================ + +module inset_core_mask_2d(clearance) { + // offset(delta<0) follows the ACTUAL contour, so circles, hearts, + // shields, etc. receive a true geometry-based inset. + offset(delta=-max(0,clearance)) + core_body_2d(); +} + +module text_safe_mask_2d() { + difference() { + inset_core_mask_2d(text_edge_clearance); + + // Reserve a wider safety circle around an internal ring hole. + if (attachment == "Inside Hole") + ring_hole_2d(text_hole_clearance); + } +} + +module accent_safe_mask_2d() { + difference() { + inset_core_mask_2d(max(0.5,text_edge_clearance*0.5)); + if (attachment == "Inside Hole") + ring_hole_2d(max(0.5,text_hole_clearance*0.5)); + } +} + + +// ============================================================================ +// Text geometry +// ============================================================================ + +module tight_text_2d(s,size,fnt,spacing=1) { + if (nonempty(s)) { + o = ink_center_offset(s,size,fnt,spacing); + translate(o) + text( + text=s, + size=size, + font=fnt, + halign="left", + valign="baseline", + spacing=spacing + ); + } +} + +module raw_fitted_text_2d() { + n = line_count(); + cx = text_box_cx() + text_x_adjust_clamped(); + cy = text_y_adjust_clamped(); + + if (n == 1) { + s = nonempty(text_line_1) ? text_line_1 : text_line_2; + fs = nonempty(text_line_1) ? line1_size() : line2_size(); + + translate([cx,cy]) + tight_text_2d(s,fs,font,text_spacing); + } + + if (n == 2) { + fs1 = line1_size(); + fs2 = line2_size(); + + m1 = tm(text_line_1,fs1,font,text_spacing); + m2 = tm(text_line_2,fs2,font,text_spacing); + + total_h = m1.size.y + line_gap + m2.size.y; + + y1 = cy + total_h/2 - m1.size.y/2; + y2 = cy - total_h/2 + m2.size.y/2; + + translate([cx,y1]) + tight_text_2d(text_line_1,fs1,font,text_spacing); + + translate([cx,y2]) + tight_text_2d(text_line_2,fs2,font,text_spacing); + } +} + +module fitted_text_2d() { + if (strict_text_clipping) + intersection() { + raw_fitted_text_2d(); + text_safe_mask_2d(); + } + else + raw_fitted_text_2d(); +} + +module symbol_raw_2d() { + if (nonempty(symbol_text)) + translate([symbol_x,symbol_y]) + tight_text_2d(symbol_text,symbol_size,symbol_font,1); +} + +module symbol_safe_2d() { + if (strict_text_clipping) + intersection() { + symbol_raw_2d(); + text_safe_mask_2d(); + } + else + symbol_raw_2d(); +} + + +// ============================================================================ +// Border geometry +// ============================================================================ + +module border_ring_2d() { + // Inset border follows the exact body silhouette. + difference() { + offset(delta=-border_inset) + core_body_2d(); + offset(delta=-(border_inset+border_width)) + core_body_2d(); + } +} + + +// ============================================================================ +// Feature classification helpers +// ============================================================================ + +function text_is_subtractive() = + text_mode == "Engraved" + || text_mode == "Cut Through" + || text_mode == "Inlay"; + +function symbol_is_subtractive() = + symbol_mode == "Engraved" + || symbol_mode == "Cut Through" + || symbol_mode == "Inlay"; + +function border_is_subtractive() = + border && ( + border_mode == "Engraved" + || border_mode == "Inlay" + ); + + +// ============================================================================ +// Base part +// ============================================================================ + +module base_solid_3d() { + difference() { + linear_extrude(height=body_thickness) + full_profile_2d(); + + // Main text cavities/cuts. + if (text_mode == "Engraved" && line_count()>0) + translate([0,0,max(0,body_thickness-engrave_depth)]) + linear_extrude(height=min(body_thickness,engrave_depth)+2*epsilon) + fitted_text_2d(); + + if (text_mode == "Cut Through" && line_count()>0) + translate([0,0,-epsilon]) + linear_extrude(height=body_thickness+2*epsilon) + fitted_text_2d(); + + if (text_mode == "Inlay" && line_count()>0) + translate([0,0,max(0,body_thickness-inlay_depth)]) + linear_extrude(height=min(body_thickness,inlay_depth)+2*epsilon) + fitted_text_2d(); + + // Symbol cavities/cuts. + if (symbol_mode == "Engraved" && nonempty(symbol_text)) + translate([0,0,max(0,body_thickness-symbol_engrave_depth)]) + linear_extrude(height=min(body_thickness,symbol_engrave_depth)+2*epsilon) + symbol_safe_2d(); + + if (symbol_mode == "Cut Through" && nonempty(symbol_text)) + translate([0,0,-epsilon]) + linear_extrude(height=body_thickness+2*epsilon) + symbol_safe_2d(); + + if (symbol_mode == "Inlay" && nonempty(symbol_text)) + translate([0,0,max(0,body_thickness-symbol_inlay_depth)]) + linear_extrude(height=min(body_thickness,symbol_inlay_depth)+2*epsilon) + symbol_safe_2d(); + + // Border cavity. + if (border && border_mode == "Engraved") + translate([0,0,max(0,body_thickness-border_engrave_depth)]) + linear_extrude(height=min(body_thickness,border_engrave_depth)+2*epsilon) + border_ring_2d(); + + if (border && border_mode == "Inlay") + translate([0,0,max(0,body_thickness-border_inlay_depth)]) + linear_extrude(height=min(body_thickness,border_inlay_depth)+2*epsilon) + border_ring_2d(); + } +} + + +// ============================================================================ +// Text / inlay part +// ============================================================================ + +module text_part_3d() { + // Raised main text. + if (text_mode == "Raised" && line_count()>0) + translate([0,0,body_thickness-epsilon]) + linear_extrude(height=text_relief+epsilon) + fitted_text_2d(); + + // Inlay text. Negative offset creates assembly clearance. + if (text_mode == "Inlay" && line_count()>0) + translate([0,0,body_thickness-inlay_depth]) + linear_extrude(height=inlay_depth) + offset(delta=-inlay_clearance) + fitted_text_2d(); + + // Raised symbol shares the text color. + if (symbol_mode == "Raised" && nonempty(symbol_text)) + translate([0,0,body_thickness-epsilon]) + linear_extrude(height=symbol_relief+epsilon) + symbol_safe_2d(); + + if (symbol_mode == "Inlay" && nonempty(symbol_text)) + translate([0,0,body_thickness-symbol_inlay_depth]) + linear_extrude(height=symbol_inlay_depth) + offset(delta=-inlay_clearance) + symbol_safe_2d(); +} + + +// ============================================================================ +// Accent part +// ============================================================================ + +module accent_part_3d() { + if (border && border_mode == "Raised") + translate([0,0,body_thickness-epsilon]) + linear_extrude(height=border_relief+epsilon) + border_ring_2d(); + + if (border && border_mode == "Inlay") + translate([0,0,body_thickness-border_inlay_depth]) + linear_extrude(height=border_inlay_depth) + offset(delta=-min(inlay_clearance,border_width*0.25)) + border_ring_2d(); +} + + +// ============================================================================ +// 2D output +// ============================================================================ + +module base_part_2d() { + difference() { + full_profile_2d(); + + if (text_mode == "Cut Through") + fitted_text_2d(); + + if (symbol_mode == "Cut Through") + symbol_safe_2d(); + } +} + +module text_part_2d() { + if ( + text_mode == "Raised" + || text_mode == "Engraved" + || text_mode == "Inlay" + ) + fitted_text_2d(); + + if ( + symbol_mode == "Raised" + || symbol_mode == "Engraved" + || symbol_mode == "Inlay" + ) + symbol_safe_2d(); +} + +module accent_part_2d() { + if (border) + border_ring_2d(); +} + + +// ============================================================================ +// Colored assembly / selective export +// ============================================================================ + +module colored_3d() { + if (output_part == "All" || output_part == "Base Only") + color(base_col()) + base_solid_3d(); + + if (output_part == "All" || output_part == "Text Only") + color(text_col()) + text_part_3d(); + + if (output_part == "All" || output_part == "Accent Only") + color(decoration_col()) + accent_part_3d(); +} + +module colored_2d() { + if (output_part == "All" || output_part == "Base Only") + color(base_col()) + base_part_2d(); + + if (output_part == "All" || output_part == "Text Only") + color(text_col()) + text_part_2d(); + + if (output_part == "All" || output_part == "Accent Only") + color(decoration_col()) + accent_part_2d(); +} + + +// ============================================================================ +// Diagnostics +// ============================================================================ + +module diagnostics() { + if (debug_info) { + echo("=== KEYRING GENERATOR v5 ==="); + echo("Shape",shape); + echo("Body", [nominal_w(),nominal_h(),body_thickness]); + echo("Attachment",attachment); + echo("Requested ring hole",ring_hole_diameter); + echo("Effective ring hole",effective_hole_d()); + echo("Lug count",lug_count); + echo("Lug 1 position mode",lug_position_mode); + echo("Lug 1 center",[lug_center_x(),lug_center_y()]); + if (lug_count >= 2) { + echo("Lug 2 position mode",lug2_position_mode); + echo("Lug 2 center",[lug2_center_x(),lug2_center_y()]); + echo("Lug 2 hole diameter",effective_lug2_hole_d()); + echo("Lug 2 outer diameter",effective_lug2_outer_d()); + echo("Lug 2 radial wall",(effective_lug2_outer_d()-effective_lug2_hole_d())/2); + } + echo("Inside hole position mode",inside_hole_position_mode); + + if (attachment == "Side Lug" || attachment == "Reinforced Lug") { + echo("Requested lug OD",lug_outer_diameter); + echo("Effective lug OD",effective_lug_outer_d()); + echo( + "Lug radial wall", + (effective_lug_outer_d()-effective_hole_d())/2 + ); + } + + if (attachment == "Inside Hole") { + echo("Inside hole center",[inside_hole_x(),inside_hole_y()]); + echo("Inside hole max diameter",inside_hole_max_d()); + + if (ring_hole_diameter > inside_hole_max_d()) + echo( + "NOTICE: Inside hole was automatically reduced to preserve minimum_wall." + ); + } + + echo("Safe text rectangle",[text_box_w(),text_box_h()]); + echo("Safe text center",[text_box_cx(),0]); + echo("Text line 1 fitted size",line1_size()); + echo("Text line 2 fitted size",line2_size()); + echo("Strict text clipping",strict_text_clipping); + echo("Color display mode",color_display_mode); + + if (text_mode == "Engraved" && engrave_depth >= body_thickness) + echo( + "NOTICE: Engraving depth reaches body thickness; consider Cut Through." + ); + + if (text_mode == "Inlay" && inlay_depth >= body_thickness) + echo( + "WARNING: Inlay depth consumes full base thickness." + ); + + if (border && border_inset+border_width >= min(nominal_w(),nominal_h())/2) + echo( + "WARNING: Border inset/width may collapse on this body size." + ); + } +} + + +// ============================================================================ +// Build +// ============================================================================ + +diagnostics(); + +if (output_mode == "2D") + colored_2d(); +else + colored_3d(); diff --git a/parametric-keyring/stl-files/heart-two-lugs.stl b/parametric-keyring/stl-files/heart-two-lugs.stl new file mode 100644 index 0000000..a7c24fa Binary files /dev/null and b/parametric-keyring/stl-files/heart-two-lugs.stl differ diff --git a/parametric-keyring/stl-files/inlay-insert.stl b/parametric-keyring/stl-files/inlay-insert.stl new file mode 100644 index 0000000..9b44af7 Binary files /dev/null and b/parametric-keyring/stl-files/inlay-insert.stl differ diff --git a/parametric-keyring/stl-files/raised-text.stl b/parametric-keyring/stl-files/raised-text.stl new file mode 100644 index 0000000..f4e3014 Binary files /dev/null and b/parametric-keyring/stl-files/raised-text.stl differ diff --git a/s-hook/README.md b/s-hook/README.md new file mode 100644 index 0000000..28b1d16 --- /dev/null +++ b/s-hook/README.md @@ -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). diff --git a/s-hook/images/flat-bulb.png b/s-hook/images/flat-bulb.png new file mode 100644 index 0000000..bf87c89 Binary files /dev/null and b/s-hook/images/flat-bulb.png differ diff --git a/s-hook/images/reinforced-oval.png b/s-hook/images/reinforced-oval.png new file mode 100644 index 0000000..4ef5719 Binary files /dev/null and b/s-hook/images/reinforced-oval.png differ diff --git a/s-hook/images/round.png b/s-hook/images/round.png new file mode 100644 index 0000000..fe10378 Binary files /dev/null and b/s-hook/images/round.png differ diff --git a/s-hook/s-hook.scad b/s-hook/s-hook.scad new file mode 100644 index 0000000..45487b3 --- /dev/null +++ b/s-hook/s-hook.scad @@ -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(); diff --git a/s-hook/stl-files/flat-bulb.stl b/s-hook/stl-files/flat-bulb.stl new file mode 100644 index 0000000..c6129ad Binary files /dev/null and b/s-hook/stl-files/flat-bulb.stl differ diff --git a/s-hook/stl-files/reinforced-oval.stl b/s-hook/stl-files/reinforced-oval.stl new file mode 100644 index 0000000..a04c6c7 Binary files /dev/null and b/s-hook/stl-files/reinforced-oval.stl differ diff --git a/s-hook/stl-files/round.stl b/s-hook/stl-files/round.stl new file mode 100644 index 0000000..30cb9b4 Binary files /dev/null and b/s-hook/stl-files/round.stl differ diff --git a/storage-system/README.md b/storage-system/README.md new file mode 100644 index 0000000..df42e3d --- /dev/null +++ b/storage-system/README.md @@ -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). diff --git a/storage-system/images/standard-box.png b/storage-system/images/standard-box.png new file mode 100644 index 0000000..f4b9efa Binary files /dev/null and b/storage-system/images/standard-box.png differ diff --git a/storage-system/images/standard-divider.png b/storage-system/images/standard-divider.png new file mode 100644 index 0000000..c8397c0 Binary files /dev/null and b/storage-system/images/standard-divider.png differ diff --git a/storage-system/images/standard-lid.png b/storage-system/images/standard-lid.png new file mode 100644 index 0000000..9b7082f Binary files /dev/null and b/storage-system/images/standard-lid.png differ diff --git a/storage-system/stl-files/standard-box.stl b/storage-system/stl-files/standard-box.stl new file mode 100644 index 0000000..d5ef4b9 Binary files /dev/null and b/storage-system/stl-files/standard-box.stl differ diff --git a/storage-system/stl-files/standard-divider.stl b/storage-system/stl-files/standard-divider.stl new file mode 100644 index 0000000..e8ea1a3 Binary files /dev/null and b/storage-system/stl-files/standard-divider.stl differ diff --git a/storage-system/stl-files/standard-lid.stl b/storage-system/stl-files/standard-lid.stl new file mode 100644 index 0000000..645dc7e Binary files /dev/null and b/storage-system/stl-files/standard-lid.stl differ diff --git a/storage-system/storage-system.scad b/storage-system/storage-system.scad new file mode 100644 index 0000000..eb86258 --- /dev/null +++ b/storage-system/storage-system.scad @@ -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)); +} diff --git a/tea-bag-dispenser/README.md b/tea-bag-dispenser/README.md new file mode 100644 index 0000000..37cbd94 --- /dev/null +++ b/tea-bag-dispenser/README.md @@ -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). diff --git a/tea-bag-dispenser/images/angled-tray.png b/tea-bag-dispenser/images/angled-tray.png new file mode 100644 index 0000000..f64ccfc Binary files /dev/null and b/tea-bag-dispenser/images/angled-tray.png differ diff --git a/tea-bag-dispenser/images/front-pull.png b/tea-bag-dispenser/images/front-pull.png new file mode 100644 index 0000000..2b225e4 Binary files /dev/null and b/tea-bag-dispenser/images/front-pull.png differ diff --git a/tea-bag-dispenser/images/open-organizer.png b/tea-bag-dispenser/images/open-organizer.png new file mode 100644 index 0000000..e3fbdcf Binary files /dev/null and b/tea-bag-dispenser/images/open-organizer.png differ diff --git a/tea-bag-dispenser/stl-files/angled-tray.stl b/tea-bag-dispenser/stl-files/angled-tray.stl new file mode 100644 index 0000000..cafe869 Binary files /dev/null and b/tea-bag-dispenser/stl-files/angled-tray.stl differ diff --git a/tea-bag-dispenser/stl-files/front-pull.stl b/tea-bag-dispenser/stl-files/front-pull.stl new file mode 100644 index 0000000..280506f Binary files /dev/null and b/tea-bag-dispenser/stl-files/front-pull.stl differ diff --git a/tea-bag-dispenser/stl-files/open-organizer.stl b/tea-bag-dispenser/stl-files/open-organizer.stl new file mode 100644 index 0000000..8e90d87 Binary files /dev/null and b/tea-bag-dispenser/stl-files/open-organizer.stl differ diff --git a/tea-bag-dispenser/tea-bag-dispenser.scad b/tea-bag-dispenser/tea-bag-dispenser.scad new file mode 100644 index 0000000..fe58db2 --- /dev/null +++ b/tea-bag-dispenser/tea-bag-dispenser.scad @@ -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(); } diff --git a/text-generator/README.md b/text-generator/README.md new file mode 100644 index 0000000..f2cca05 --- /dev/null +++ b/text-generator/README.md @@ -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). diff --git a/text-generator/images/engraved-plaque.png b/text-generator/images/engraved-plaque.png new file mode 100644 index 0000000..0871885 Binary files /dev/null and b/text-generator/images/engraved-plaque.png differ diff --git a/text-generator/images/multiline-stand.png b/text-generator/images/multiline-stand.png new file mode 100644 index 0000000..7c1b15f Binary files /dev/null and b/text-generator/images/multiline-stand.png differ diff --git a/text-generator/images/standing-text.png b/text-generator/images/standing-text.png new file mode 100644 index 0000000..b16d3c5 Binary files /dev/null and b/text-generator/images/standing-text.png differ diff --git a/text-generator/stl-files/engraved-plaque.stl b/text-generator/stl-files/engraved-plaque.stl new file mode 100644 index 0000000..c298099 Binary files /dev/null and b/text-generator/stl-files/engraved-plaque.stl differ diff --git a/text-generator/stl-files/multiline-stand.stl b/text-generator/stl-files/multiline-stand.stl new file mode 100644 index 0000000..a9937ed Binary files /dev/null and b/text-generator/stl-files/multiline-stand.stl differ diff --git a/text-generator/stl-files/standing-text.stl b/text-generator/stl-files/standing-text.stl new file mode 100644 index 0000000..21373bb Binary files /dev/null and b/text-generator/stl-files/standing-text.stl differ diff --git a/text-generator/text-generator.scad b/text-generator/text-generator.scad new file mode 100644 index 0000000..9a1006b --- /dev/null +++ b/text-generator/text-generator.scad @@ -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(); diff --git a/universal-clock-dial/README.md b/universal-clock-dial/README.md new file mode 100644 index 0000000..bad455e --- /dev/null +++ b/universal-clock-dial/README.md @@ -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). diff --git a/universal-clock-dial/images/round.png b/universal-clock-dial/images/round.png new file mode 100644 index 0000000..84bdfa5 Binary files /dev/null and b/universal-clock-dial/images/round.png differ diff --git a/universal-clock-dial/images/star-subdial.png b/universal-clock-dial/images/star-subdial.png new file mode 100644 index 0000000..5c67652 Binary files /dev/null and b/universal-clock-dial/images/star-subdial.png differ diff --git a/universal-clock-dial/stl-files/round.stl b/universal-clock-dial/stl-files/round.stl new file mode 100644 index 0000000..d6617ce Binary files /dev/null and b/universal-clock-dial/stl-files/round.stl differ diff --git a/universal-clock-dial/stl-files/star-subdial.stl b/universal-clock-dial/stl-files/star-subdial.stl new file mode 100644 index 0000000..1d6dcf2 Binary files /dev/null and b/universal-clock-dial/stl-files/star-subdial.stl differ diff --git a/universal-clock-dial/universal-clock-dial.scad b/universal-clock-dial/universal-clock-dial.scad new file mode 100644 index 0000000..ce2cf60 --- /dev/null +++ b/universal-clock-dial/universal-clock-dial.scad @@ -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(); \ No newline at end of file diff --git a/universal-sign/README.md b/universal-sign/README.md new file mode 100644 index 0000000..b983c49 --- /dev/null +++ b/universal-sign/README.md @@ -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). diff --git a/universal-sign/images/desk-stand.png b/universal-sign/images/desk-stand.png new file mode 100644 index 0000000..9d5cbc1 Binary files /dev/null and b/universal-sign/images/desk-stand.png differ diff --git a/universal-sign/images/flat-plate.png b/universal-sign/images/flat-plate.png new file mode 100644 index 0000000..866418b Binary files /dev/null and b/universal-sign/images/flat-plate.png differ diff --git a/universal-sign/images/inlay-insert.png b/universal-sign/images/inlay-insert.png new file mode 100644 index 0000000..34fe293 Binary files /dev/null and b/universal-sign/images/inlay-insert.png differ diff --git a/universal-sign/stl-files/desk-stand.stl b/universal-sign/stl-files/desk-stand.stl new file mode 100644 index 0000000..73f7af5 Binary files /dev/null and b/universal-sign/stl-files/desk-stand.stl differ diff --git a/universal-sign/stl-files/flat-plate.stl b/universal-sign/stl-files/flat-plate.stl new file mode 100644 index 0000000..76aa71f Binary files /dev/null and b/universal-sign/stl-files/flat-plate.stl differ diff --git a/universal-sign/stl-files/inlay-insert.stl b/universal-sign/stl-files/inlay-insert.stl new file mode 100644 index 0000000..8444138 Binary files /dev/null and b/universal-sign/stl-files/inlay-insert.stl differ diff --git a/universal-sign/universal-sign.scad b/universal-sign/universal-sign.scad new file mode 100644 index 0000000..d9306e8 --- /dev/null +++ b/universal-sign/universal-sign.scad @@ -0,0 +1,1358 @@ +/* +=============================================================================== + UNIVERSAL SIGN GENERATOR - OpenSCAD Nightly + Version 1.0 - 2026-09 +------------------------------------------------------------------------------- + Selvstændig parametrisk skiltegenerator til: + - 3D-print + - CNC-fræsning / 2.5D-gravering + - Laserskæring / lasergravering + + Designmål: + * Flere tekstlinjer med individuel font, størrelse, spacing og finjustering. + * Nightly textmetrics() + fontmetrics() til reel fontmåling og auto-sizing. + * Moderne em-baseret fontstørrelse kan vælges. + * Mange pladeformer, konstruktioner og monteringsformer kan kombineres. + * Raised / engraved / cut-through / inlay tekst. + * Raised / engraved / cut-through kant. + * 2D outputs til SVG/DXF og 3D outputs til STL/3MF. + * Ingen eksterne libraries. + + NIGHTLY: + textmetrics() / fontmetrics() er fortsat en udviklingsfunktion i aktuelle + snapshots. I GUI: Preferences -> Features -> textmetrics. + CLI: openscad-nightly --enable=textmetrics ... + + FALLBACK: + Sæt Use_Nightly_Metrics=false hvis du vil åbne filen i en ældre OpenSCAD. + Auto-sizing bliver da en tilnærmelse og em-mode bør ikke anvendes. + + FONT-FILER: + Projektspecifikke .ttf/.otf kan registreres med en literal use<>-linje, + eksempel: + use + Derefter anvendes fontens logiske navn i Line_*_Font. + + OBS: + OpenSCAD arbejder uden fysiske enheder; værdierne er tænkt som millimeter. +=============================================================================== +*/ + + +/* [Output] */ + +// Primært output +Output_Mode = "3D"; // ["3D","2D Cut Outline","2D Engrave Text","2D Text/Inlay","2D Border","2D Fold Score"] + +// Grundkonstruktion +Construction = "Flat Plate"; // ["Flat Plate","Desk L-Stand","Desk Wedge","Desk Slot Stand","A-Frame Tent","Foldable Sheet Blank","Stake Sign"] + +// Hvilken del vises/eksporteres i 3D +Part_Selection = "Assembly"; // ["Assembly","Face only","Support only","Text insert only"] + +// Brug præcis Nightly fontmåling +Use_Nightly_Metrics = true; + + +/* [Skiltform] */ + +Plate_Shape = "Rounded Rectangle"; // ["Rectangle","Square","Rounded Rectangle","Capsule","Circle","Oval","Triangle","Diamond","Hexagon","Octagon","Shield","Arch","Tag","Arrow Right","Pennant"] + +Auto_Size_Plate = true; + +// Bruges hvis Auto_Size_Plate=false +Manual_Width = 160; // [20:1:600] +Manual_Height = 70; // [20:1:400] + +// Minimum ved auto-size +Minimum_Width = 70; // [10:1:600] +Minimum_Height = 35; // [10:1:400] + +// Luft omkring tekstblokken +Padding_X = 14; // [0:0.5:80] +Padding_Y = 10; // [0:0.5:80] + +// Ekstra konservativ sikkerhed for former med smalle hjørner +Shape_Text_Safety = 1.00; // [0.5:0.01:1.2] + +// Hjørner / formdetaljer +Corner_Radius = 8; // [0:0.5:60] +Tag_Cut = 10; // [1:0.5:60] +Arrow_Head_Fraction = 0.30; // [0.15:0.01:0.50] +Arch_Radius_Fraction = 0.40; // [0.20:0.01:0.50] + +// Pladetykkelse +Plate_Thickness = 3.0; // [0.4:0.1:30] + +// Kantprofil +Edge_Profile = "Straight"; // ["Straight","Chamfered"] +Bevel_Size = 0.8; // [0:0.1:5] + + +/* [Tekst - generelt] */ + +Font_Sizing_Mode = "size"; // ["size","em"] + +Text_Mode = "Raised"; // ["Raised","Engraved","Cut-through","Inlay","None"] + +Text_HAlign = "center"; // ["left","center","right"] +Text_Direction = "ltr"; // ["ltr","rtl","ttb","btt"] +Text_Language = "da"; +Text_Script = "latin"; + +// Afstand mellem line-boxes ud over fontens interline +Line_Gap = 2.0; // [-5:0.25:30] +Leading_Scale = 1.00; // [0.5:0.01:2.0] + +// Global flytning af tekstblokken +Text_Offset_X = 0; // [-100:0.5:100] +Text_Offset_Y = 0; // [-100:0.5:100] + +// Ved fast pladestørrelse: nedskaler automatisk hvis teksten ikke kan være der +Scale_Text_Down_To_Fit = true; +Text_Global_Scale = 1.0; // [0.1:0.01:3] + +// Relief / gravering +Text_Raised_Height = 1.0; // [0.1:0.1:10] +Text_Engrave_Depth = 0.8; // [0.1:0.1:20] + +// Inlay +Inlay_Depth = 1.2; // [0.2:0.1:20] +Inlay_XY_Clearance = 0.15; // [0:0.01:1] +Inlay_Z_Clearance = 0.10; // [0:0.01:1] + + +/* [Tekstlinje 1] */ +Line_1_Text = "MAKERSPACE"; +Line_1_Size = 18; // [1:0.5:150] +Line_1_Font = "Liberation Sans:style=Bold"; +Line_1_Spacing = 1.0; // [0.5:0.01:2] +Line_1_X = 0; // [-100:0.5:100] +Line_1_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 2] */ +Line_2_Text = "NÆSTVED"; +Line_2_Size = 11; // [1:0.5:150] +Line_2_Font = "Liberation Sans"; +Line_2_Spacing = 1.0; // [0.5:0.01:2] +Line_2_X = 0; // [-100:0.5:100] +Line_2_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 3] */ +Line_3_Text = ""; +Line_3_Size = 9; // [1:0.5:150] +Line_3_Font = "Liberation Sans"; +Line_3_Spacing = 1.0; // [0.5:0.01:2] +Line_3_X = 0; // [-100:0.5:100] +Line_3_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 4] */ +Line_4_Text = ""; +Line_4_Size = 8; // [1:0.5:150] +Line_4_Font = "Liberation Sans"; +Line_4_Spacing = 1.0; // [0.5:0.01:2] +Line_4_X = 0; // [-100:0.5:100] +Line_4_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 5] */ +Line_5_Text = ""; +Line_5_Size = 8; // [1:0.5:150] +Line_5_Font = "Liberation Sans"; +Line_5_Spacing = 1.0; // [0.5:0.01:2] +Line_5_X = 0; // [-100:0.5:100] +Line_5_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 6] */ +Line_6_Text = ""; +Line_6_Size = 8; // [1:0.5:150] +Line_6_Font = "Liberation Sans"; +Line_6_Spacing = 1.0; // [0.5:0.01:2] +Line_6_X = 0; // [-100:0.5:100] +Line_6_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 7] */ +Line_7_Text = ""; +Line_7_Size = 8; // [1:0.5:150] +Line_7_Font = "Liberation Sans"; +Line_7_Spacing = 1.0; // [0.5:0.01:2] +Line_7_X = 0; // [-100:0.5:100] +Line_7_Y = 0; // [-100:0.5:100] + +/* [Tekstlinje 8] */ +Line_8_Text = ""; +Line_8_Size = 8; // [1:0.5:150] +Line_8_Font = "Liberation Sans"; +Line_8_Spacing = 1.0; // [0.5:0.01:2] +Line_8_X = 0; // [-100:0.5:100] +Line_8_Y = 0; // [-100:0.5:100] + + +/* [Kant / border] */ + +Border_Style = "None"; // ["None","Raised","Engraved","Cut-through"] +Border_Width = 2.0; // [0.3:0.1:20] +Border_Inset = 3.0; // [0:0.1:30] +Border_Raised_Height = 0.8; // [0.1:0.1:10] +Border_Engrave_Depth = 0.6; // [0.1:0.1:20] + + +/* [Montering] */ + +Mounting_Style = "None"; // ["None","2 Hanging Holes","2 Side Holes","4 Corner Holes","2 Countersunk","4 Countersunk","Lanyard Hole","Zip-tie Slots","Keyhole Pair","Magnet Pair","Magnet Quad"] + +Mount_Edge = 10; // [2:0.5:80] +Hole_Diameter = 4.0; // [1:0.1:30] + +Countersink_Diameter = 8.0; // [2:0.1:40] +Countersink_Depth = 1.8; // [0.2:0.1:10] + +// Magnetlommer skæres fra bagsiden +Magnet_Diameter = 10.2; // [2:0.1:40] +Magnet_Depth = 2.2; // [0.2:0.1:20] +Magnet_Spacing = 70; // [5:1:400] + +// Nøglehulslommer skæres fra bagsiden +Keyhole_Head_Diameter = 8.5; // [2:0.1:30] +Keyhole_Neck_Width = 4.0; // [1:0.1:20] +Keyhole_Length = 14; // [4:0.5:50] +Keyhole_Depth = 2.0; // [0.2:0.1:20] + +// Kabelbinder-slots +Zip_Slot_Length = 12; // [3:0.5:50] +Zip_Slot_Width = 3.5; // [1:0.1:20] + + +/* [Bordskilte / standere] */ + +Desk_Angle = 72; // [45:1:90] + +Base_Thickness = 4; // [1:0.5:30] +Base_Depth = 45; // [10:1:150] +Base_Width_Extra = 16; // [0:1:100] +Base_Front_Lip = 8; // [0:1:50] + +// Giver fysisk overlap ved monolitiske standere +Base_Embed = 1.5; // [0.2:0.1:10] + +// Slotstander +Slot_Clearance = 0.30; // [0:0.05:2] +Slot_Depth = 2.5; // [0.5:0.1:20] + +// Wedge +Wedge_Depth = 35; // [5:1:150] +Wedge_Height_Fraction = 0.65; // [0.2:0.01:0.95] +Wedge_Width_Fraction = 0.70; // [0.2:0.01:1] + +// A-frame +Tent_Angle = 70; // [45:1:85] +Tent_Top_Bar = true; +Tent_Top_Bar_Diameter = 5; // [1:0.5:20] + + +/* [Foldbart plademateriale] */ + +// Foldable Sheet Blank er lavet til fx karton, PP, tynd plast, finer mv. +Fold_Hinge_Band = 4; // [0:0.5:30] +Fold_Score_Width = 0.35; // [0.1:0.05:3] +Fold_Corner_Radius = 4; // [0:0.5:30] +Fold_Sheet_Thickness = 1.0; // [0.2:0.1:10] +Fold_Text_On_Both_Panels = true; + + +/* [Stake sign] */ + +Stake_Width = 18; // [5:1:80] +Stake_Length = 90; // [20:1:400] +Stake_Tip_Length = 25; // [5:1:100] + + +/* [Kvalitet] */ + +Curve_FN = 96; // [12:4:360] +Text_FN = 32; // [8:2:128] +Arch_Segments = 48; // [12:2:180] + +EPS = 0.02; + + +/* --------------------------------------------------------------------------- + DATA + FONT METRICS +--------------------------------------------------------------------------- */ + +raw_lines = [ + [Line_1_Text, Line_1_Size, Line_1_Font, Line_1_Spacing, Line_1_X, Line_1_Y], + [Line_2_Text, Line_2_Size, Line_2_Font, Line_2_Spacing, Line_2_X, Line_2_Y], + [Line_3_Text, Line_3_Size, Line_3_Font, Line_3_Spacing, Line_3_X, Line_3_Y], + [Line_4_Text, Line_4_Size, Line_4_Font, Line_4_Spacing, Line_4_X, Line_4_Y], + [Line_5_Text, Line_5_Size, Line_5_Font, Line_5_Spacing, Line_5_X, Line_5_Y], + [Line_6_Text, Line_6_Size, Line_6_Font, Line_6_Spacing, Line_6_X, Line_6_Y], + [Line_7_Text, Line_7_Size, Line_7_Font, Line_7_Spacing, Line_7_X, Line_7_Y], + [Line_8_Text, Line_8_Size, Line_8_Font, Line_8_Spacing, Line_8_X, Line_8_Y] +]; + +active_lines = [for (l = raw_lines) if (len(l[0]) > 0) l]; + +function sum_vec(v, i=0) = + i >= len(v) ? 0 : v[i] + sum_vec(v, i+1); + +function sum_first(v, count, i=0) = + i >= count ? 0 : v[i] + sum_first(v, count, i+1); + +function line_tm(l) = + Font_Sizing_Mode == "em" + ? textmetrics( + text=l[0], em=l[1], font=l[2], + direction=Text_Direction, language=Text_Language, script=Text_Script, + halign="center", valign="center", spacing=l[3]) + : textmetrics( + text=l[0], size=l[1], font=l[2], + direction=Text_Direction, language=Text_Language, script=Text_Script, + halign="center", valign="center", spacing=l[3]); + +function line_fm(l) = + Font_Sizing_Mode == "em" + ? fontmetrics(em=l[1], font=l[2]) + : fontmetrics(size=l[1], font=l[2]); + +function fallback_width(l) = + max([0.01, len(l[0]) * l[1] * 0.62 * l[3]]); + +function fallback_height(l) = + max([0.01, l[1]]); + +function line_width(l) = + Use_Nightly_Metrics ? max([0.01, line_tm(l).size[0]]) : fallback_width(l); + +function line_height(l) = + Use_Nightly_Metrics ? max([0.01, line_tm(l).size[1]]) : fallback_height(l); + +function line_interline(l) = + Use_Nightly_Metrics + ? max([0.01, line_fm(l).interline]) + : l[1] * 1.22; + +line_boxes = [ + for (l = active_lines) + max([line_height(l), line_interline(l) * Leading_Scale]) +]; + +text_block_width = + len(active_lines) > 0 + ? max([for (l = active_lines) line_width(l)]) + : 0.01; + +text_block_height = + len(active_lines) > 0 + ? sum_vec(line_boxes) + max([0, len(active_lines)-1]) * Line_Gap + : 0.01; + +function line_center_y(i) = + text_block_height/2 + - sum_first(line_boxes, i) + - i * Line_Gap + - line_boxes[i]/2; + +function text_anchor_x() = + Text_HAlign == "left" ? -text_block_width/2 : + Text_HAlign == "right" ? text_block_width/2 : + 0; + + +/* --------------------------------------------------------------------------- + SHAPE MATH +--------------------------------------------------------------------------- */ + +// Faktor for den nogenlunde rektangulære "safe text area" inde i formen. +function native_shape_safe(shape) = + shape == "Circle" ? [0.80, 0.80] : + shape == "Oval" ? [0.84, 0.78] : + shape == "Triangle" ? [0.62, 0.54] : + shape == "Diamond" ? [0.65, 0.65] : + shape == "Hexagon" ? [0.82, 0.82] : + shape == "Octagon" ? [0.88, 0.88] : + shape == "Shield" ? [0.76, 0.74] : + shape == "Arch" ? [0.88, 0.82] : + shape == "Arrow Right" ? [0.66, 0.82] : + shape == "Pennant" ? [0.70, 0.82] : + [1.0, 1.0]; + +safe_factor = native_shape_safe(Plate_Shape) * Shape_Text_Safety; + +mount_feature_d = + Mounting_Style == "Magnet Pair" || Mounting_Style == "Magnet Quad" + ? Magnet_Diameter + : Mounting_Style == "Keyhole Pair" + ? Keyhole_Head_Diameter + : max([Hole_Diameter, Countersink_Diameter]); + +mount_min_w = + Mounting_Style == "None" || Mounting_Style == "Lanyard Hole" + ? 0 + : 2 * (Mount_Edge + mount_feature_d/2 + 2); + +mount_min_h = + Mounting_Style == "None" + ? 0 + : 2 * (Mount_Edge + mount_feature_d/2 + 2); + +auto_width_raw = + max([ + Minimum_Width, + (text_block_width + 2*Padding_X) / max([0.1, safe_factor[0]]), + mount_min_w + ]); + +auto_height_raw = + max([ + Minimum_Height, + (text_block_height + 2*Padding_Y) / max([0.1, safe_factor[1]]), + mount_min_h + ]); + +pre_w = Auto_Size_Plate ? auto_width_raw : Manual_Width; +pre_h = Auto_Size_Plate ? auto_height_raw : Manual_Height; + +square_side = max([pre_w, pre_h]); + +plate_w = + Plate_Shape == "Square" || Plate_Shape == "Circle" + ? square_side + : pre_w; + +plate_h = + Plate_Shape == "Square" || Plate_Shape == "Circle" + ? square_side + : pre_h; + +usable_text_w = + max([0.1, (plate_w - 2*Padding_X) * safe_factor[0]]); + +usable_text_h = + max([0.1, (plate_h - 2*Padding_Y) * safe_factor[1]]); + +auto_fit_scale = + Scale_Text_Down_To_Fit + ? min([ + 1, + usable_text_w / max([0.01, text_block_width]), + usable_text_h / max([0.01, text_block_height]) + ]) + : 1; + +final_text_scale = auto_fit_scale * Text_Global_Scale; + + +/* --------------------------------------------------------------------------- + VALIDATION + DIAGNOSTICS +--------------------------------------------------------------------------- */ + +module validate_parameters() { + assert(plate_w > 0 && plate_h > 0, "Plate dimensions must be positive."); + assert(Plate_Thickness > 0, "Plate_Thickness must be > 0."); + assert(Corner_Radius >= 0, "Corner_Radius cannot be negative."); + assert(Bevel_Size >= 0, "Bevel_Size cannot be negative."); + assert(final_text_scale > 0, "Text scale must be > 0."); + + if (Text_Mode == "Engraved") + assert(Text_Engrave_Depth <= Plate_Thickness + EPS, + "Text_Engrave_Depth is larger than Plate_Thickness."); + + if (Text_Mode == "Inlay") { + assert(Inlay_Depth <= Plate_Thickness + EPS, + "Inlay_Depth is larger than Plate_Thickness."); + assert(Inlay_Depth > Inlay_Z_Clearance, + "Inlay_Z_Clearance must be smaller than Inlay_Depth."); + } + + if (Border_Style == "Engraved") + assert(Border_Engrave_Depth <= Plate_Thickness + EPS, + "Border_Engrave_Depth is larger than Plate_Thickness."); + + if (Mounting_Style == "Magnet Pair" || Mounting_Style == "Magnet Quad") + assert(Magnet_Depth < Plate_Thickness, + "Magnet_Depth must be smaller than Plate_Thickness."); + + if (Mounting_Style == "Keyhole Pair") + assert(Keyhole_Depth < Plate_Thickness, + "Keyhole_Depth must be smaller than Plate_Thickness."); + + if (Construction == "Desk Slot Stand") + assert(Slot_Depth < Base_Thickness, + "Slot_Depth must be smaller than Base_Thickness."); + + children(); +} + +echo("=== Universal Sign Generator / Nightly ==="); +echo("OpenSCAD version", version()); +echo("Use_Nightly_Metrics", Use_Nightly_Metrics); +echo("Plate shape", Plate_Shape); +echo("Construction", Construction); +echo("Plate size", [plate_w, plate_h, Plate_Thickness]); +echo("Text block raw", [text_block_width, text_block_height]); +echo("Final text scale", final_text_scale); +echo("Mounting", Mounting_Style); + +if (!Use_Nightly_Metrics) + echo("WARNING: Nightly metrics disabled: text auto-sizing is approximate."); + +if (final_text_scale < 0.65) + echo("WARNING: Text has been scaled below 65%. Consider a larger sign or smaller text."); + +if (Text_Mode == "Raised" && Construction == "Foldable Sheet Blank") + echo("NOTE: Foldable Sheet Blank is primarily intended for 2D cut/engrave workflows."); + + +/* --------------------------------------------------------------------------- + 2D SHAPES +--------------------------------------------------------------------------- */ + +module rounded_rect_2d(w, h, r) { + rr = min([max([0, r]), w/2, h/2]); + + if (rr <= EPS) + square([w, h], center=true); + else + offset(r=rr, $fn=Curve_FN) + square([max([EPS, w-2*rr]), max([EPS, h-2*rr])], center=true); +} + +module capsule_2d(w, h) { + if (w >= h) { + r = h/2; + hull() { + translate([-(w-h)/2, 0]) circle(r=r, $fn=Curve_FN); + translate([ +(w-h)/2, 0]) circle(r=r, $fn=Curve_FN); + } + } else { + r = w/2; + hull() { + translate([0, -(h-w)/2]) circle(r=r, $fn=Curve_FN); + translate([0, +(h-w)/2]) circle(r=r, $fn=Curve_FN); + } + } +} + +module ellipse_2d(w, h) { + scale([w/2, h/2]) + circle(r=1, $fn=Curve_FN); +} + +module regular_polygon_2d(w, h, n, rot=90) { + polygon(points=[ + for (i=[0:n-1]) + [w/2*cos(rot + i*360/n), h/2*sin(rot + i*360/n)] + ]); +} + +module arch_2d(w, h) { + r = min([w/2, h * Arch_Radius_Fraction]); + cy = h/2 - r; + + pts = concat( + [[-w/2, -h/2], [w/2, -h/2], [w/2, cy]], + [for (i=[0:Arch_Segments]) + let(a = i * 180 / Arch_Segments) + [r*cos(a), cy + r*sin(a)] + ] + ); + + polygon(points=pts); +} + +module shield_2d(w, h) { + polygon(points=[ + [-w/2, h*0.34], + [-w*0.34, h/2], + [ w*0.34, h/2], + [ w/2, h*0.34], + [ w*0.43, -h*0.18], + [ 0, -h/2], + [-w*0.43, -h*0.18] + ]); +} + +module tag_2d(w, h) { + c = min([Tag_Cut, w/3, h/2]); + polygon(points=[ + [-w/2+c, -h/2], + [ w/2-c, -h/2], + [ w/2, -h/2+c], + [ w/2, h/2-c], + [ w/2-c, h/2], + [-w/2+c, h/2], + [-w/2, h/2-c], + [-w/2, -h/2+c] + ]); +} + +module arrow_right_2d(w, h) { + ah = w * Arrow_Head_Fraction; + polygon(points=[ + [-w/2, -h/2], + [ w/2-ah, -h/2], + [ w/2-ah, -h/2], + [ w/2, 0], + [ w/2-ah, h/2], + [-w/2, h/2] + ]); +} + +module pennant_2d(w, h) { + polygon(points=[ + [-w/2, -h/2], + [ w*0.20, -h/2], + [ w/2, 0], + [ w*0.20, h/2], + [-w/2, h/2] + ]); +} + +module face_outline_2d() { + if (Plate_Shape == "Rectangle") + square([plate_w, plate_h], center=true); + + else if (Plate_Shape == "Square") + square([plate_w, plate_h], center=true); + + else if (Plate_Shape == "Rounded Rectangle") + rounded_rect_2d(plate_w, plate_h, Corner_Radius); + + else if (Plate_Shape == "Capsule") + capsule_2d(plate_w, plate_h); + + else if (Plate_Shape == "Circle") + circle(d=plate_w, $fn=Curve_FN); + + else if (Plate_Shape == "Oval") + ellipse_2d(plate_w, plate_h); + + else if (Plate_Shape == "Triangle") + polygon(points=[ + [-plate_w/2, -plate_h/2], + [ plate_w/2, -plate_h/2], + [0, plate_h/2] + ]); + + else if (Plate_Shape == "Diamond") + polygon(points=[ + [0, plate_h/2], + [plate_w/2, 0], + [0, -plate_h/2], + [-plate_w/2, 0] + ]); + + else if (Plate_Shape == "Hexagon") + regular_polygon_2d(plate_w, plate_h, 6, 0); + + else if (Plate_Shape == "Octagon") + regular_polygon_2d(plate_w, plate_h, 8, 22.5); + + else if (Plate_Shape == "Shield") + shield_2d(plate_w, plate_h); + + else if (Plate_Shape == "Arch") + arch_2d(plate_w, plate_h); + + else if (Plate_Shape == "Tag") + tag_2d(plate_w, plate_h); + + else if (Plate_Shape == "Arrow Right") + arrow_right_2d(plate_w, plate_h); + + else if (Plate_Shape == "Pennant") + pennant_2d(plate_w, plate_h); + + else + rounded_rect_2d(plate_w, plate_h, Corner_Radius); +} + +module inset_outline_2d(d) { + offset(delta=-d, $fn=Curve_FN) + face_outline_2d(); +} + + +/* --------------------------------------------------------------------------- + TEXT GEOMETRY +--------------------------------------------------------------------------- */ + +module one_line_text_2d(l) { + if (Font_Sizing_Mode == "em") + text( + text=l[0], + em=l[1], + font=l[2], + direction=Text_Direction, + language=Text_Language, + script=Text_Script, + halign=Text_HAlign, + valign="center", + spacing=l[3], + $fn=Text_FN + ); + else + text( + text=l[0], + size=l[1], + font=l[2], + direction=Text_Direction, + language=Text_Language, + script=Text_Script, + halign=Text_HAlign, + valign="center", + spacing=l[3], + $fn=Text_FN + ); +} + +module text_block_2d() { + translate([Text_Offset_X, Text_Offset_Y]) + scale([final_text_scale, final_text_scale]) + if (len(active_lines) > 0) + for (i=[0:len(active_lines)-1]) { + l = active_lines[i]; + + translate([ + text_anchor_x() + l[4], + line_center_y(i) + l[5] + ]) + one_line_text_2d(l); + } +} + +module text_recess_shape_2d() { + if (Text_Mode == "Inlay") + offset(delta=Inlay_XY_Clearance, $fn=Curve_FN) + text_block_2d(); + else + text_block_2d(); +} + + +/* --------------------------------------------------------------------------- + BORDER +--------------------------------------------------------------------------- */ + +module border_ring_2d() { + if (Border_Width > EPS) + difference() { + inset_outline_2d(Border_Inset); + inset_outline_2d(Border_Inset + Border_Width); + } +} + + +/* --------------------------------------------------------------------------- + MOUNTING - 2D CUT SHAPES +--------------------------------------------------------------------------- */ + +function x_edge_pos() = max([0, plate_w/2 - Mount_Edge]); +function y_edge_pos() = max([0, plate_h/2 - Mount_Edge]); + +module round_hole_2d(x, y, d=Hole_Diameter) { + translate([x,y]) + circle(d=d, $fn=Curve_FN); +} + +module slot_2d(x, y, length, width, vertical=true) { + translate([x,y]) + hull() { + if (vertical) { + translate([0, -(length-width)/2]) circle(d=width, $fn=Curve_FN); + translate([0, +(length-width)/2]) circle(d=width, $fn=Curve_FN); + } else { + translate([-(length-width)/2, 0]) circle(d=width, $fn=Curve_FN); + translate([+(length-width)/2, 0]) circle(d=width, $fn=Curve_FN); + } + } +} + +module through_mounts_2d() { + xe = x_edge_pos(); + ye = y_edge_pos(); + + if (Mounting_Style == "2 Hanging Holes") { + round_hole_2d(-xe, ye); + round_hole_2d( xe, ye); + } + + else if (Mounting_Style == "2 Side Holes") { + round_hole_2d(-xe, 0); + round_hole_2d( xe, 0); + } + + else if (Mounting_Style == "4 Corner Holes" || + Mounting_Style == "4 Countersunk") { + for (x=[-xe,xe], y=[-ye,ye]) + round_hole_2d(x,y); + } + + else if (Mounting_Style == "2 Countersunk") { + round_hole_2d(-xe, ye); + round_hole_2d( xe, ye); + } + + else if (Mounting_Style == "Lanyard Hole") { + round_hole_2d(0, ye); + } + + else if (Mounting_Style == "Zip-tie Slots") { + slot_2d(-xe, 0, Zip_Slot_Length, Zip_Slot_Width, true); + slot_2d( xe, 0, Zip_Slot_Length, Zip_Slot_Width, true); + } +} + +module keyhole_2d() { + // Stort indføringshul nederst, smal kanal opad. + union() { + translate([0, -Keyhole_Length/2 + Keyhole_Head_Diameter/2]) + circle(d=Keyhole_Head_Diameter, $fn=Curve_FN); + + translate([0, Keyhole_Length/4]) + square([Keyhole_Neck_Width, Keyhole_Length], center=true); + } +} + + +/* --------------------------------------------------------------------------- + 3D BASE PLATE +--------------------------------------------------------------------------- */ + +module plate_solid_3d() { + b = min([ + max([0, Bevel_Size]), + max([0, Plate_Thickness/2 - EPS]) + ]); + + if (Edge_Profile == "Straight" || b <= EPS) { + linear_extrude(height=Plate_Thickness) + face_outline_2d(); + } else { + // Chamfer begge sider uden Minkowski: hurtigere og robust på mange former. + union() { + hull() { + translate([0,0,0]) + linear_extrude(height=EPS) + inset_outline_2d(b); + + translate([0,0,b]) + linear_extrude(height=EPS) + face_outline_2d(); + } + + if (Plate_Thickness > 2*b) + translate([0,0,b]) + linear_extrude(height=Plate_Thickness-2*b) + face_outline_2d(); + + hull() { + translate([0,0,Plate_Thickness-b]) + linear_extrude(height=EPS) + face_outline_2d(); + + translate([0,0,Plate_Thickness-EPS]) + linear_extrude(height=EPS) + inset_outline_2d(b); + } + } + } +} + +module through_mounts_3d() { + translate([0,0,-EPS]) + linear_extrude(height=Plate_Thickness + 2*EPS) + through_mounts_2d(); +} + +module countersinks_3d() { + xe = x_edge_pos(); + ye = y_edge_pos(); + + positions = + Mounting_Style == "2 Countersunk" + ? [[-xe,ye],[xe,ye]] + : Mounting_Style == "4 Countersunk" + ? [[-xe,-ye],[-xe,ye],[xe,-ye],[xe,ye]] + : []; + + if (len(positions) > 0) + for (p = positions) + translate([p[0], p[1], Plate_Thickness-Countersink_Depth]) + cylinder( + h=Countersink_Depth + EPS, + d1=Hole_Diameter, + d2=Countersink_Diameter, + $fn=Curve_FN + ); +} + +module backside_mount_recesses_3d() { + xe = x_edge_pos(); + ye = y_edge_pos(); + + if (Mounting_Style == "Magnet Pair") { + mx = min([Magnet_Spacing/2, xe]); + + for (x=[-mx,mx]) + translate([x,0,-EPS]) + cylinder(h=Magnet_Depth+EPS, d=Magnet_Diameter, $fn=Curve_FN); + } + + else if (Mounting_Style == "Magnet Quad") { + for (x=[-xe,xe], y=[-ye,ye]) + translate([x,y,-EPS]) + cylinder(h=Magnet_Depth+EPS, d=Magnet_Diameter, $fn=Curve_FN); + } + + else if (Mounting_Style == "Keyhole Pair") { + for (x=[-xe,xe]) + translate([x, ye - Keyhole_Length/2, -EPS]) + linear_extrude(height=Keyhole_Depth+EPS) + keyhole_2d(); + } +} + + +/* --------------------------------------------------------------------------- + FACE: BASE + SUBTRACTIONS + RELIEF +--------------------------------------------------------------------------- */ + +module face_core_3d() { + difference() { + plate_solid_3d(); + + // Gennemgående monteringshuller / slots + through_mounts_3d(); + + // Forsænkning på forsiden + countersinks_3d(); + + // Magneter / keyholes fra bagsiden + backside_mount_recesses_3d(); + + // Tekst + if (Text_Mode == "Engraved") + translate([0,0,Plate_Thickness-Text_Engrave_Depth]) + linear_extrude(height=Text_Engrave_Depth+EPS) + text_block_2d(); + + else if (Text_Mode == "Cut-through") + translate([0,0,-EPS]) + linear_extrude(height=Plate_Thickness+2*EPS) + text_block_2d(); + + else if (Text_Mode == "Inlay") + translate([0,0,Plate_Thickness-Inlay_Depth]) + linear_extrude(height=Inlay_Depth+EPS) + text_recess_shape_2d(); + + // Border + if (Border_Style == "Engraved") + translate([0,0,Plate_Thickness-Border_Engrave_Depth]) + linear_extrude(height=Border_Engrave_Depth+EPS) + border_ring_2d(); + + else if (Border_Style == "Cut-through") + translate([0,0,-EPS]) + linear_extrude(height=Plate_Thickness+2*EPS) + border_ring_2d(); + } +} + +module face_relief_3d() { + if (Text_Mode == "Raised") + translate([0,0,Plate_Thickness-EPS]) + linear_extrude(height=Text_Raised_Height+EPS) + text_block_2d(); + + if (Border_Style == "Raised") + translate([0,0,Plate_Thickness-EPS]) + linear_extrude(height=Border_Raised_Height+EPS) + border_ring_2d(); +} + +module sign_face_3d() { + union() { + face_core_3d(); + face_relief_3d(); + } +} + +module inlay_insert_3d(on_face=false) { + insert_h = max([EPS, Inlay_Depth-Inlay_Z_Clearance]); + + z0 = + on_face + ? Plate_Thickness-Inlay_Depth+Inlay_Z_Clearance + : 0; + + translate([0,0,z0]) + linear_extrude(height=insert_h) + text_block_2d(); +} + + +/* --------------------------------------------------------------------------- + DESK / SUPPORT GEOMETRY +--------------------------------------------------------------------------- */ + +module desk_base_3d(with_slot=false) { +bw = plate_w + Base_Width_Extra; + + difference() { + translate([0, (Base_Depth-Base_Front_Lip)/2, Base_Thickness/2]) + cube([bw, Base_Depth+Base_Front_Lip, Base_Thickness], center=true); + + if (with_slot) + translate([ + 0, + 0, + Base_Thickness - Slot_Depth/2 + EPS + ]) + cube([ + plate_w + 2*Slot_Clearance, + Plate_Thickness + 2*Slot_Clearance, + Slot_Depth + 2*EPS + ], center=true); + } +} + +// Sign_face er modelleret i XY. Denne transform drejer bundkanten ned på bordet. +module posed_face(angle=90, y0=0, z0=0) { + translate([0,y0,z0]) + rotate([angle,0,0]) + translate([0,plate_h/2,0]) + children(); +} + +// Ekstrudér en polygon i YZ langs X. +module prism_x(width, yz_points) { + rotate([0,90,0]) + linear_extrude(height=width, center=true, convexity=10) + polygon(points=[ + for (p=yz_points) [-p[1], p[0]] + ]); +} + +module l_stand_support_3d() { + desk_base_3d(false); +} + +module desk_l_stand_3d(include_face=true, include_support=true) { + if (include_support) + l_stand_support_3d(); + + if (include_face) + posed_face(90, 0, Base_Thickness-Base_Embed) + sign_face_3d(); +} + +module wedge_support_3d() { + support_w = plate_w * Wedge_Width_Fraction; + rise_len = plate_h * Wedge_Height_Fraction; + tip_y = rise_len * cos(Desk_Angle); + tip_z = Base_Thickness + rise_len * sin(Desk_Angle) - Base_Embed; + + desk_base_3d(false); + + prism_x( + support_w, + [ + [0, Base_Thickness-EPS], + [Wedge_Depth, Base_Thickness-EPS], + [tip_y, tip_z] + ] + ); +} + +module desk_wedge_3d(include_face=true, include_support=true) { + if (include_support) + wedge_support_3d(); + + if (include_face) + posed_face(Desk_Angle, 0, Base_Thickness-Base_Embed) + sign_face_3d(); +} + +module slot_support_3d() { + desk_base_3d(true); +} + +module desk_slot_stand_3d(include_face=true, include_support=true) { + if (include_support) + slot_support_3d(); + + if (include_face) + posed_face( + 90, + 0, + Base_Thickness-Slot_Depth + ) + sign_face_3d(); +} + +module tent_face_pair_3d() { + a = Tent_Angle; + half_spread = plate_h * cos(a); + top_z = plate_h * sin(a); + + // Forside + posed_face(a, -half_spread, 0) + sign_face_3d(); + + // Bagside: spejling over Y giver læsbar tekst fra modsatte side. + mirror([0,1,0]) + posed_face(a, -half_spread, 0) + sign_face_3d(); + + if (Tent_Top_Bar) + translate([0,0,top_z]) + rotate([0,90,0]) + cylinder( + h=plate_w, + d=Tent_Top_Bar_Diameter, + center=true, + $fn=Curve_FN + ); +} + + +/* --------------------------------------------------------------------------- + FOLDABLE SHEET BLANK +--------------------------------------------------------------------------- */ + +fold_blank_h = 2*plate_h + Fold_Hinge_Band; + +module fold_blank_outline_2d() { + rounded_rect_2d( + plate_w, + fold_blank_h, + min([Fold_Corner_Radius, plate_w/2, fold_blank_h/2]) + ); +} + +module fold_score_2d() { + // To fold-linjer skaber en lille hinge-band. Ved band=0 bliver de samme linje. + y1 = -Fold_Hinge_Band/2; + y2 = Fold_Hinge_Band/2; + + translate([0,y1]) + square([plate_w-2*Fold_Corner_Radius, Fold_Score_Width], center=true); + + if (Fold_Hinge_Band > EPS) + translate([0,y2]) + square([plate_w-2*Fold_Corner_Radius, Fold_Score_Width], center=true); +} + +module fold_text_2d() { + panel_offset = (plate_h + Fold_Hinge_Band)/2; + + translate([0, panel_offset]) + text_block_2d(); + + if (Fold_Text_On_Both_Panels) + translate([0,-panel_offset]) + rotate(180) + text_block_2d(); +} + +module fold_blank_3d() { + linear_extrude(height=Fold_Sheet_Thickness) + difference() { + fold_blank_outline_2d(); + + if (Text_Mode == "Cut-through") + fold_text_2d(); + } +} + + +/* --------------------------------------------------------------------------- + STAKE SIGN +--------------------------------------------------------------------------- */ + +module stake_2d() { + y_top = -plate_h/2 + EPS; + y_shoulder = -plate_h/2 - (Stake_Length-Stake_Tip_Length); + y_tip = -plate_h/2 - Stake_Length; + + polygon(points=[ + [-Stake_Width/2, y_top], + [ Stake_Width/2, y_top], + [ Stake_Width/2, y_shoulder], + [0, y_tip], + [-Stake_Width/2, y_shoulder] + ]); +} + +module stake_body_3d() { + union() { + sign_face_3d(); + + linear_extrude(height=Plate_Thickness) + stake_2d(); + } +} + + +/* --------------------------------------------------------------------------- + 2D OUTPUTS +--------------------------------------------------------------------------- */ + +module face_cut_outline_2d() { + difference() { + face_outline_2d(); + + // Kun features der faktisk går igennem pladen. + through_mounts_2d(); + + if (Text_Mode == "Cut-through") + text_block_2d(); + + if (Border_Style == "Cut-through") + border_ring_2d(); + } +} + +module stake_cut_outline_2d() { + difference() { + union() { + face_outline_2d(); + stake_2d(); + } + + through_mounts_2d(); + + if (Text_Mode == "Cut-through") + text_block_2d(); + + if (Border_Style == "Cut-through") + border_ring_2d(); + } +} + +module output_2d_cut() { + if (Construction == "Foldable Sheet Blank") + difference() { + fold_blank_outline_2d(); + if (Text_Mode == "Cut-through") + fold_text_2d(); + } + + else if (Construction == "Stake Sign") + stake_cut_outline_2d(); + + else + face_cut_outline_2d(); +} + +module output_2d_text() { + if (Construction == "Foldable Sheet Blank") + fold_text_2d(); + else + text_block_2d(); +} + + +/* --------------------------------------------------------------------------- + 3D CONSTRUCTION DISPATCH +--------------------------------------------------------------------------- */ + +module output_3d() { + want_face = + Part_Selection == "Assembly" || + Part_Selection == "Face only"; + + want_support = + Part_Selection == "Assembly" || + Part_Selection == "Support only"; + + if (Part_Selection == "Text insert only") { + inlay_insert_3d(false); + } + + else if (Construction == "Flat Plate") { + if (want_face) + if (Text_Mode == "Inlay" && Part_Selection == "Assembly") { + sign_face_3d(); + inlay_insert_3d(true); + } else { + sign_face_3d(); + } + } + + else if (Construction == "Desk L-Stand") { + desk_l_stand_3d(want_face, want_support); + } + + else if (Construction == "Desk Wedge") { + desk_wedge_3d(want_face, want_support); + } + + else if (Construction == "Desk Slot Stand") { + desk_slot_stand_3d(want_face, want_support); + } + + else if (Construction == "A-Frame Tent") { + if (Part_Selection == "Support only") { + if (Tent_Top_Bar) { + top_z = plate_h * sin(Tent_Angle); + + translate([0,0,top_z]) + rotate([0,90,0]) + cylinder( + h=plate_w, + d=Tent_Top_Bar_Diameter, + center=true, + $fn=Curve_FN + ); + } + } else { + tent_face_pair_3d(); + } + } + + else if (Construction == "Foldable Sheet Blank") { + if (want_face) + fold_blank_3d(); + } + + else if (Construction == "Stake Sign") { + if (want_face) + stake_body_3d(); + } + + else { + sign_face_3d(); + } +} + + +/* --------------------------------------------------------------------------- + MAIN +--------------------------------------------------------------------------- */ + +validate_parameters() { + if (Output_Mode == "3D") + output_3d(); + + else if (Output_Mode == "2D Cut Outline") + output_2d_cut(); + + else if (Output_Mode == "2D Engrave Text") + output_2d_text(); + + else if (Output_Mode == "2D Text/Inlay") + output_2d_text(); + + else if (Output_Mode == "2D Border") + border_ring_2d(); + + else if (Output_Mode == "2D Fold Score") + if (Construction == "Foldable Sheet Blank") + fold_score_2d(); + + else + output_3d(); +} \ No newline at end of file diff --git a/usb-stick-holder/README.md b/usb-stick-holder/README.md new file mode 100644 index 0000000..30935f4 --- /dev/null +++ b/usb-stick-holder/README.md @@ -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). diff --git a/usb-stick-holder/images/circular.png b/usb-stick-holder/images/circular.png new file mode 100644 index 0000000..1a26ca9 Binary files /dev/null and b/usb-stick-holder/images/circular.png differ diff --git a/usb-stick-holder/images/fit-test.png b/usb-stick-holder/images/fit-test.png new file mode 100644 index 0000000..135c810 Binary files /dev/null and b/usb-stick-holder/images/fit-test.png differ diff --git a/usb-stick-holder/images/rectangular.png b/usb-stick-holder/images/rectangular.png new file mode 100644 index 0000000..1a020d0 Binary files /dev/null and b/usb-stick-holder/images/rectangular.png differ diff --git a/usb-stick-holder/stl-files/circular.stl b/usb-stick-holder/stl-files/circular.stl new file mode 100644 index 0000000..998af95 Binary files /dev/null and b/usb-stick-holder/stl-files/circular.stl differ diff --git a/usb-stick-holder/stl-files/fit-test.stl b/usb-stick-holder/stl-files/fit-test.stl new file mode 100644 index 0000000..831d6c0 Binary files /dev/null and b/usb-stick-holder/stl-files/fit-test.stl differ diff --git a/usb-stick-holder/stl-files/rectangular.stl b/usb-stick-holder/stl-files/rectangular.stl new file mode 100644 index 0000000..c29e9e1 Binary files /dev/null and b/usb-stick-holder/stl-files/rectangular.stl differ diff --git a/usb-stick-holder/usb-stick-holder.scad b/usb-stick-holder/usb-stick-holder.scad new file mode 100644 index 0000000..0b5251f --- /dev/null +++ b/usb-stick-holder/usb-stick-holder.scad @@ -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_Depth2*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();