diff --git a/Makefile b/Makefile index 7baea9f..1dba9bf 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SCAD = wargame_box.scad -PARTS = box lid insert +PARTS = box lid insert latch STLS = $(addprefix stl/,$(addsuffix .stl,$(PARTS))) all: $(STLS) diff --git a/README.md b/README.md index df5ab55..6ca4eeb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Wargame Accessory Box & Dice Tray A parametric OpenSCAD box for carrying tabletop wargaming accessories. -Three printable parts: +Four printable parts: | Part | Purpose | |----------|--------------------------------------------------------------------| | `box` | Open tray. With the insert removed it doubles as a dice tray. | -| `lid` | Fully detachable friction-fit lid (no hinge to get in the way). | +| `lid` | Fully detachable lid, held shut by snap latches hinged on filament pins (or a classic friction fit). | +| `latch` | One-piece snap latch — print two per latch position. | | `insert` | Removable compartment tray for tokens, measuring tapes, etc. | ![Assembly](docs/assembly.png) @@ -19,14 +20,50 @@ the free space in your backpack — they are **outer** dimensions and measured space exactly. Pick what to render with the `part` variable (or the customizer): -`assembly` (exploded preview), `box`, `lid`, `insert`, `print`. +`assembly` (exploded preview), `box`, `lid`, `insert`, `latch`, `print`. Or export everything from the command line: ```sh -make # exports stl/box.stl, stl/lid.stl, stl/insert.stl +make # exports stl/box.stl, stl/lid.stl, stl/insert.stl, stl/latch.stl ``` +## The latch lid + +By default (`lid_style = "latch"`) the lid is held shut by snap +latches that hinge on short pieces of filament: + +* The **lid** carries pairs of lugs along its front and back edges. +* Each **latch** is a single printed part with a pivot barrel that + fits between a lug pair. Its window snaps over a chamfered catch + bar on the box wall; pull the flared tip outward to open. +* To assemble one latch: cut a ~23 mm piece of 1.75 mm filament, + slide it through lug → latch barrel → lug, and fix it with a drop + of glue **on the outer lug holes only** — the lug holes are sized + tight and the barrel hole loose, so the latch keeps pivoting + freely. Trim the pin flush. + +Tuning knobs (all per side unless noted): + +* `latches_per_side` — how many latches on each long wall + (default 2, evenly spread). +* `catch_proud` — how far the catch bar sticks out = how hard the + snap is (default 1.4 mm). +* `latch_play` — vertical play between latch and catch bar + (default 0.1 mm). Use a small negative value to preload the lid + shut. +* `filament_d` — hinge pin diameter, in case you want to use + 2.85 mm filament or a piece of wire instead. + +Note: lugs and latches stick out about 7 mm beyond the box on the +front and back, so allow `total_y + 14` of space in the backpack. +With latches doing the holding, a looser lip (`lid_clearance` around +0.3–0.4) makes the lid pleasant to take off. + +Prefer the original hinge-free push-fit lid? Set +`lid_style = "friction"` and you get the old behaviour, including +the thumb grooves for prying the lid off. + ## Configuring the insert layout Compartments are defined by the `layout` list, one entry per row @@ -55,10 +92,13 @@ of the automatic height and print two with different layouts. ## How the parts work together * The **lid** is a flat plate with a lip that plugs into the box opening. - `lid_clearance` (default 0.2 mm per side) controls the friction fit — - print a test fit and adjust for your printer/material. Shallow thumb - grooves on the box's front and back walls let you pry the lid off; they - do **not** pierce the wall, so the dice tray stays fully closed. + With the latch lid the lip just aligns the lid (and keeps tokens in + their compartments); with `lid_style = "friction"` it carries the + whole fit. `lid_clearance` (default 0.2 mm per side) controls how + snug it is — print a test fit and adjust for your printer/material. + In friction mode, shallow thumb grooves on the box's front and back + walls let you pry the lid off; they do **not** pierce the wall, so + the dice tray stays fully closed. * The **insert** sits below the lid's lip, so the lip also keeps loose tokens from jumping compartments in the bag. Finger notches on its short walls let you lift it straight out; then the empty box is your @@ -68,7 +108,9 @@ of the automatic height and print two with different layouts. ## Printing notes * No supports needed for any part. Print all parts flat side down - (the lid prints plate-down, lip up). + (the lid prints plate-down, lip and lugs up). The latch prints + standing on its flat side — that gives a clean pivot hole and puts + the layer lines along the strip, where the snap stress is. * PLA or PETG, 2–3 perimeters, ~10 % infill (the parts are mostly walls). * If the lid is too tight/loose, tune `lid_clearance` in steps of 0.05 mm. Same for the insert with `insert_clearance` (looser is fine there — diff --git a/docs/assembly.png b/docs/assembly.png index cdf9966..6ba8e21 100644 Binary files a/docs/assembly.png and b/docs/assembly.png differ diff --git a/wargame_box.scad b/wargame_box.scad index 31674bb..20d0b50 100644 --- a/wargame_box.scad +++ b/wargame_box.scad @@ -1,9 +1,11 @@ // ============================================================ // Wargame Accessory Box & Dice Tray // ------------------------------------------------------------ -// Three printable parts: +// Printable parts: // * box - open tray, doubles as a dice tray -// * lid - fully detachable friction-fit lid +// * lid - fully detachable lid, held shut by snap latches +// hinged on filament pins (or classic friction fit) +// * latch - one-piece snap latch (print 2 per latch position) // * insert - removable compartment tray (configurable layout) // // Set `part` below (or via customizer / -D on the CLI) to @@ -12,7 +14,7 @@ // ============================================================ /* [What to render] */ -part = "assembly"; // [assembly, box, lid, insert, print] +part = "assembly"; // [assembly, box, lid, insert, latch, print] /* [Outer dimensions (must fit the backpack)] */ // Total outer width (X), including walls @@ -39,16 +41,39 @@ tray_chamfer = 6; // Thickness of the flat lid plate lid_top = 2.4; // Height of the lip that plugs into the box opening -lip_h = 10; +lip_h = 5; // Lip wall thickness lip_t = 2.0; // Gap per side between lip and box wall. // Tune for your printer: smaller = tighter friction fit. +// With lid_style = "latch" the latches do the holding, so a +// looser 0.3 - 0.4 makes the lid easy to take off. lid_clearance = 0.20; // Thumb groove on the box's outer walls to pry the lid off -// (does NOT pierce the wall, dice tray stays closed) +// (friction lid only; latches replace it) thumb_grooves = true; +/* [Latches] */ +// How the lid is held shut. "latch": snap latches hinged on +// short pieces of filament (see README). "friction": original +// push fit. +lid_style = "latch"; // [latch, friction] +// Latches per long (front/back) wall, spread out evenly +latches_per_side = 2; +// Diameter of the filament used as hinge pin +filament_d = 1.75; +// Latch strip width +latch_w = 12; +// Latch strip thickness (thinner = softer snap) +latch_t = 2.0; +// Catch bar distance below the box rim +catch_drop = 14; +// Catch bar protrusion from the wall = snap engagement +catch_proud = 1.4; +// Vertical play between latch window and catch bar. +// Negative values preload the lid shut. +latch_play = 0.1; + /* [Insert] */ // Gap per side between insert and box wall insert_clearance = 0.40; @@ -103,6 +128,36 @@ ins_h = insert_h_override > 0 lip_x = inner_x - 2 * lid_clearance; // lid lip outer size lip_y = inner_y - 2 * lid_clearance; +// --- latch hinge internals --- +lug_w = 5; // lid lug width (each side) +hinge_gap = 0.4; // side play lug <-> latch +pin_boss_r = filament_d / 2 + 2.3; // lug / barrel outer radius +pin_standoff = pin_boss_r + 0.4; // pin axis to wall face +lug_hole_d = filament_d + 0.15; // tight: pin glued into lugs +barrel_hole_d = filament_d + 0.35; // loose: latch pivots freely +latch_gap = 0.4; // latch strip to wall face +bar_h = 2.8; // catch bar height +bar_rise = 1.2; // straight part below chamfer +window_rail = 3; // strip rails beside window +window_w = latch_w - 2 * window_rail; +bar_len = window_w - 0.8; +grip_l = 6; // thumb flare below window + +z_pin = box_h + lid_top - pin_boss_r; // hinge axis, lid closed +z_bar_top = box_h - catch_drop; +// latch window, measured down the strip from the pin axis +win_t1 = z_pin - z_bar_top - 0.4; +win_t2 = z_pin - z_bar_top + bar_h + latch_play; +latch_len = win_t2 + 4 + grip_l; // pin axis to latch tip + +function latch_pos_x() = + latches_per_side <= 0 ? [] + : [for (i = [1 : latches_per_side]) + total_x * i / (latches_per_side + 1)]; + +assert(lid_style != "latch" || z_pin - latch_len > 2, + "box too shallow for the latch: reduce catch_drop"); + // ------------------------------------------------------------ // Helpers // ------------------------------------------------------------ @@ -141,7 +196,7 @@ module box() { } if (tray_chamfer > 0) tray_chamfers(); } - if (thumb_grooves) + if (thumb_grooves && lid_style == "friction") for (m = [0, 1]) translate([total_x / 2, m == 0 ? -(14 - groove_depth) @@ -149,6 +204,25 @@ module box() { box_h - 16]) cylinder(h = 20, d = 28, $fn = 48); } + if (lid_style == "latch") + for (x0 = latch_pos_x()) { + translate([x0, 0, z_bar_top]) catch_bar(); + translate([x0, total_y, z_bar_top]) + mirror([0, 1, 0]) catch_bar(); + } +} + +// catch bar on the outer wall: origin on the wall face with the +// bar top at z = 0, protruding in -y. Chamfered on top so the +// latch rides over it, flat below so the latch can't cam out. +module catch_bar() { + translate([-bar_len / 2, 0, 0]) + rotate([90, 0, 90]) + linear_extrude(bar_len) + polygon([[0, 0], + [-catch_proud, -bar_h + bar_rise], + [-catch_proud, -bar_h], + [0, -bar_h]]); } module tray_chamfers() { @@ -203,6 +277,77 @@ module lid() { rbox(lip_x - 2 * lip_t, lip_y - 2 * lip_t, lip_h + 1, max(inner_r - lip_t, 0.1)); } + if (lid_style == "latch") + for (x0 = latch_pos_x()) { + translate([x0, 0, 0]) lug_pair(); + translate([x0, total_y, 0]) mirror([0, 1, 0]) lug_pair(); + } +} + +// ------------------------------------------------------------ +// Latch hardware +// ------------------------------------------------------------ +// One-piece snap latch hinged on a short piece of filament: the +// lid carries two lugs, the latch barrel sits between them, and +// the filament pin is glued into the (tighter) lug holes only, +// so the latch pivots freely. The window in the strip snaps +// over the catch bar on the box wall. + +// one lid lug, centered on x = 0 at the lid edge y = 0, hanging +// outward in -y. Modeled in lid print orientation, so the pin +// boss rests flat on the bed. +module lid_lug() { + difference() { + hull() { + translate([-lug_w / 2, -pin_standoff, pin_boss_r]) + rotate([0, 90, 0]) cylinder(r = pin_boss_r, h = lug_w); + translate([-lug_w / 2, 0, 0]) cube([lug_w, 2, lid_top]); + } + translate([-lug_w / 2 - 1, -pin_standoff, pin_boss_r]) + rotate([0, 90, 0]) cylinder(d = lug_hole_d, h = lug_w + 2); + } +} + +module lug_pair() { + off = latch_w / 2 + hinge_gap + lug_w / 2; + for (sx = [-1, 1]) translate([sx * off, 0, 0]) lid_lug(); +} + +// modeled flat: x = down the strip from the pin axis, y = away +// from the box wall, z = width. Prints standing on that flat +// face -> clean pivot hole and layer lines along the strip. +module latch() { + u0 = latch_gap; // inner face (toward the wall) + u1 = latch_gap + latch_t; // outer face + flare = 2.5; + difference() { + linear_extrude(latch_w) { + // pivot barrel + translate([0, pin_standoff]) circle(r = pin_boss_r); + polygon([ + [0, u0], + [latch_len - 2, u0], + [latch_len, u0 + 1.4], // tip chamfer, rides + [latch_len, u1 + flare], // over the catch bar + [latch_len - 2, u1 + flare], // thumb flare to open + [win_t2 + 4, u1], + [0, u1] + ]); + } + // pivot hole + translate([0, pin_standoff, -1]) + cylinder(d = barrel_hole_d, h = latch_w + 2); + // window that snaps over the catch bar + translate([win_t1, -1, window_rail]) + cube([win_t2 - win_t1, u1 + flare + 2, window_w]); + } +} + +// latch in closed/engaged position on the front wall (y = 0) +module placed_latch(x0) { + translate([x0, 0, z_pin]) + rotate([0, 0, 180]) rotate([0, 90, 0]) + translate([0, 0, -latch_w / 2]) latch(); } // ------------------------------------------------------------ @@ -263,14 +408,29 @@ module assembly() { color("LightGreen", 0.7) translate([0, total_y, box_h + ins_h + 25]) rotate([180, 0, 0]) translate([0, 0, -lid_top]) lid(); + // latches hang from the floating lid's lugs + if (lid_style == "latch") + color("Tomato") + translate([0, 0, ins_h + 25]) + for (x0 = latch_pos_x()) { + placed_latch(x0); + translate([0, total_y, 0]) + mirror([0, 1, 0]) placed_latch(x0); + } } if (part == "box") box(); else if (part == "lid") lid(); else if (part == "insert") insert(); +else if (part == "latch") latch(); else if (part == "print") { box(); translate([total_x + 15, 0, 0]) lid(); translate([0, total_y + 15, 0]) insert(); + if (lid_style == "latch" && latches_per_side > 0) + for (i = [0 : 2 * latches_per_side - 1]) + translate([total_x + 15 + pin_boss_r, + total_y + 15 + i * (2 * pin_boss_r + 4), 0]) + latch(); } else assembly();