Add latches to the lid

This commit is contained in:
2026-06-11 17:00:04 +02:00
parent dfed581377
commit cab17d08e9
4 changed files with 218 additions and 16 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
SCAD = wargame_box.scad SCAD = wargame_box.scad
PARTS = box lid insert PARTS = box lid insert latch
STLS = $(addprefix stl/,$(addsuffix .stl,$(PARTS))) STLS = $(addprefix stl/,$(addsuffix .stl,$(PARTS)))
all: $(STLS) all: $(STLS)
+51 -9
View File
@@ -1,12 +1,13 @@
# Wargame Accessory Box & Dice Tray # Wargame Accessory Box & Dice Tray
A parametric OpenSCAD box for carrying tabletop wargaming accessories. A parametric OpenSCAD box for carrying tabletop wargaming accessories.
Three printable parts: Four printable parts:
| Part | Purpose | | Part | Purpose |
|----------|--------------------------------------------------------------------| |----------|--------------------------------------------------------------------|
| `box` | Open tray. With the insert removed it doubles as a dice tray. | | `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. | | `insert` | Removable compartment tray for tokens, measuring tapes, etc. |
![Assembly](docs/assembly.png) ![Assembly](docs/assembly.png)
@@ -19,14 +20,50 @@ the free space in your backpack — they are **outer** dimensions and
measured space exactly. measured space exactly.
Pick what to render with the `part` variable (or the customizer): 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: Or export everything from the command line:
```sh ```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.30.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 ## Configuring the insert layout
Compartments are defined by the `layout` list, one entry per row 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 ## How the parts work together
* The **lid** is a flat plate with a lip that plugs into the box opening. * 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 — With the latch lid the lip just aligns the lid (and keeps tokens in
print a test fit and adjust for your printer/material. Shallow thumb their compartments); with `lid_style = "friction"` it carries the
grooves on the box's front and back walls let you pry the lid off; they whole fit. `lid_clearance` (default 0.2 mm per side) controls how
do **not** pierce the wall, so the dice tray stays fully closed. 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 * 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 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 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 ## Printing notes
* No supports needed for any part. Print all parts flat side down * 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, 23 perimeters, ~10 % infill (the parts are mostly walls). * PLA or PETG, 23 perimeters, ~10 % infill (the parts are mostly walls).
* If the lid is too tight/loose, tune `lid_clearance` in steps of 0.05 mm. * 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 — Same for the insert with `insert_clearance` (looser is fine there —
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+166 -6
View File
@@ -1,9 +1,11 @@
// ============================================================ // ============================================================
// Wargame Accessory Box & Dice Tray // Wargame Accessory Box & Dice Tray
// ------------------------------------------------------------ // ------------------------------------------------------------
// Three printable parts: // Printable parts:
// * box - open tray, doubles as a dice tray // * 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) // * insert - removable compartment tray (configurable layout)
// //
// Set `part` below (or via customizer / -D on the CLI) to // Set `part` below (or via customizer / -D on the CLI) to
@@ -12,7 +14,7 @@
// ============================================================ // ============================================================
/* [What to render] */ /* [What to render] */
part = "assembly"; // [assembly, box, lid, insert, print] part = "assembly"; // [assembly, box, lid, insert, latch, print]
/* [Outer dimensions (must fit the backpack)] */ /* [Outer dimensions (must fit the backpack)] */
// Total outer width (X), including walls // Total outer width (X), including walls
@@ -39,16 +41,39 @@ tray_chamfer = 6;
// Thickness of the flat lid plate // Thickness of the flat lid plate
lid_top = 2.4; lid_top = 2.4;
// Height of the lip that plugs into the box opening // Height of the lip that plugs into the box opening
lip_h = 10; lip_h = 5;
// Lip wall thickness // Lip wall thickness
lip_t = 2.0; lip_t = 2.0;
// Gap per side between lip and box wall. // Gap per side between lip and box wall.
// Tune for your printer: smaller = tighter friction fit. // 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; lid_clearance = 0.20;
// Thumb groove on the box's outer walls to pry the lid off // 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; 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] */ /* [Insert] */
// Gap per side between insert and box wall // Gap per side between insert and box wall
insert_clearance = 0.40; 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_x = inner_x - 2 * lid_clearance; // lid lip outer size
lip_y = inner_y - 2 * lid_clearance; 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 // Helpers
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -141,7 +196,7 @@ module box() {
} }
if (tray_chamfer > 0) tray_chamfers(); if (tray_chamfer > 0) tray_chamfers();
} }
if (thumb_grooves) if (thumb_grooves && lid_style == "friction")
for (m = [0, 1]) for (m = [0, 1])
translate([total_x / 2, translate([total_x / 2,
m == 0 ? -(14 - groove_depth) m == 0 ? -(14 - groove_depth)
@@ -149,6 +204,25 @@ module box() {
box_h - 16]) box_h - 16])
cylinder(h = 20, d = 28, $fn = 48); 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() { module tray_chamfers() {
@@ -203,6 +277,77 @@ module lid() {
rbox(lip_x - 2 * lip_t, lip_y - 2 * lip_t, rbox(lip_x - 2 * lip_t, lip_y - 2 * lip_t,
lip_h + 1, max(inner_r - lip_t, 0.1)); 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) color("LightGreen", 0.7)
translate([0, total_y, box_h + ins_h + 25]) translate([0, total_y, box_h + ins_h + 25])
rotate([180, 0, 0]) translate([0, 0, -lid_top]) lid(); 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(); if (part == "box") box();
else if (part == "lid") lid(); else if (part == "lid") lid();
else if (part == "insert") insert(); else if (part == "insert") insert();
else if (part == "latch") latch();
else if (part == "print") { else if (part == "print") {
box(); box();
translate([total_x + 15, 0, 0]) lid(); translate([total_x + 15, 0, 0]) lid();
translate([0, total_y + 15, 0]) insert(); 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(); else assembly();