Files
parametric-box/wargame_box.scad
T
2026-06-11 17:00:04 +02:00

437 lines
16 KiB
OpenSCAD

// ============================================================
// Wargame Accessory Box & Dice Tray
// ------------------------------------------------------------
// Printable parts:
// * box - open tray, doubles as a dice tray
// * 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
// choose what to render. "assembly" shows an exploded preview,
// "print" lays all parts flat for a quick look at a full set.
// ============================================================
/* [What to render] */
part = "assembly"; // [assembly, box, lid, insert, latch, print]
/* [Outer dimensions (must fit the backpack)] */
// Total outer width (X), including walls
total_x = 220;
// Total outer depth (Y), including walls
total_y = 150;
// Total outer height (Z), INCLUDING the closed lid
total_z = 80;
/* [Shell] */
// Box / lid wall thickness
wall = 2.4;
// Box floor thickness
floor_t = 2.4;
// Outer corner radius (0 = sharp corners)
corner_r = 6;
/* [Dice tray] */
// 45-degree chamfer along the inner floor edges so dice
// don't get stuck in the corners (0 = off)
tray_chamfer = 6;
/* [Lid] */
// Thickness of the flat lid plate
lid_top = 2.4;
// Height of the lip that plugs into the box opening
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
// (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;
// Insert outer wall thickness
insert_wall = 1.6;
// Wall thickness between compartments
divider_t = 1.2;
// Insert floor thickness
insert_floor = 1.6;
// Insert height; 0 = automatic (fills the box up to the lid lip)
insert_h_override = 0;
// Finger notches on the insert's short walls for lifting it out
finger_notches = true;
// Finger notch diameter
notch_d = 22;
/* [Hidden] */
// ------------------------------------------------------------
// Compartment layout
// ------------------------------------------------------------
// One entry per row (front to back). Each entry is
// [row_depth_weight, [column_width_weights]]
// Weights are relative, so the layout always fills the insert
// exactly no matter what the box dimensions are.
//
// Default: a wide slot for measuring tapes across the back,
// and two rows of token compartments in front.
layout = [
[1.0, [1, 1, 1, 1]], // front row: 4 small token bins
[1.0, [2, 1, 1]], // middle row: 1 medium + 2 small
[0.8, [1]] // back row: full-width slot (tapes etc.)
];
$fn = $preview ? 32 : 64;
// ------------------------------------------------------------
// Derived dimensions
// ------------------------------------------------------------
box_h = total_z - lid_top; // box body height
inner_x = total_x - 2 * wall; // cavity size
inner_y = total_y - 2 * wall;
inner_h = box_h - floor_t; // cavity depth
inner_r = max(corner_r - wall, 0.1); // cavity corner radius
ins_x = inner_x - 2 * insert_clearance; // insert outer size
ins_y = inner_y - 2 * insert_clearance;
ins_h = insert_h_override > 0
? insert_h_override
: inner_h - lip_h - 0.6; // leave room for the lid lip
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
// ------------------------------------------------------------
function sum(v, i = 0) = i >= len(v) ? 0 : v[i] + sum(v, i + 1);
// sum of the first n elements
function psum(v, n) = n <= 0 ? 0 : v[n - 1] + psum(v, n - 1);
function row_weights() = [for (r = layout) r[0]];
// rounded-corner rectangle, centered at origin corner (0,0)
module rrect(x, y, r) {
if (r > 0)
translate([r, r])
offset(r) square([x - 2 * r, y - 2 * r]);
else
square([x, y]);
}
module rbox(x, y, z, r) {
linear_extrude(z) rrect(x, y, r);
}
// ------------------------------------------------------------
// Box (dice tray)
// ------------------------------------------------------------
module box() {
// thumb grooves are shallow vertical scoops on the outer
// front/back wall, so the dice tray wall stays closed
groove_depth = min(1.4, wall - 0.8);
difference() {
union() {
difference() {
rbox(total_x, total_y, box_h, corner_r);
translate([wall, wall, floor_t])
rbox(inner_x, inner_y, inner_h + 1, inner_r);
}
if (tray_chamfer > 0) tray_chamfers();
}
if (thumb_grooves && lid_style == "friction")
for (m = [0, 1])
translate([total_x / 2,
m == 0 ? -(14 - groove_depth)
: total_y + (14 - groove_depth),
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() {
c = min(tray_chamfer, inner_h - 1);
// triangular prisms against each inner wall
module prism(length)
rotate([90, 0, 90])
linear_extrude(length)
polygon([[0, 0], [c, 0], [0, c]]);
intersection() {
translate([wall, wall, floor_t])
rbox(inner_x, inner_y, inner_h, inner_r);
translate([wall, wall, floor_t]) {
// along y = 0 wall
prism(inner_x);
// along y = inner_y wall
translate([inner_x, inner_y, 0]) rotate([0, 0, 180]) prism(inner_x);
// along x = 0 wall
translate([0, inner_y, 0]) rotate([0, 0, -90]) prism(inner_y);
// along x = inner_x wall
translate([inner_x, 0, 0]) rotate([0, 0, 90]) prism(inner_y);
}
}
}
// ------------------------------------------------------------
// Lid
// ------------------------------------------------------------
module lid() {
taper = 1.2; // 45-degree lead-in at the lip's top edge
difference() {
union() {
// flat plate, same footprint as the box
rbox(total_x, total_y, lid_top, corner_r);
// friction-fit lip; starts at z = 0 so it overlaps
// the plate and unions into a single manifold solid
translate([wall + lid_clearance, wall + lid_clearance, 0]) {
rbox(lip_x, lip_y, lid_top + lip_h - taper, inner_r);
// tapered tip enters the box first -> easy closing
translate([lip_x / 2, lip_y / 2, lid_top + lip_h - taper])
linear_extrude(taper,
scale = [(lip_x - 2 * taper) / lip_x,
(lip_y - 2 * taper) / lip_y])
translate([-lip_x / 2, -lip_y / 2])
rrect(lip_x, lip_y, inner_r);
}
}
// hollow the lip above the plate
translate([wall + lid_clearance + lip_t,
wall + lid_clearance + lip_t, lid_top])
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();
}
// ------------------------------------------------------------
// Insert
// ------------------------------------------------------------
module insert() {
iw_x = ins_x - 2 * insert_wall; // usable interior
iw_y = ins_y - 2 * insert_wall;
n_rows = len(layout);
rw = row_weights();
avail_y = iw_y - (n_rows - 1) * divider_t;
difference() {
rbox(ins_x, ins_y, ins_h, inner_r);
// compartment cavities
for (ri = [0 : n_rows - 1]) {
row_y0 = insert_wall
+ avail_y * psum(rw, ri) / sum(rw)
+ ri * divider_t;
row_dy = avail_y * rw[ri] / sum(rw);
cols = layout[ri][1];
n_cols = len(cols);
avail_x = iw_x - (n_cols - 1) * divider_t;
for (ci = [0 : n_cols - 1]) {
col_x0 = insert_wall
+ avail_x * psum(cols, ci) / sum(cols)
+ ci * divider_t;
col_dx = avail_x * cols[ci] / sum(cols);
translate([col_x0, row_y0, insert_floor])
cube([col_dx, row_dy, ins_h]);
}
}
// finger notches on the short (left/right) walls
if (finger_notches)
for (x = [0, ins_x])
translate([x, ins_y / 2, ins_h])
rotate([0, 90, 0])
cylinder(h = 2 * (insert_wall + 6),
d = notch_d, center = true, $fn = 48);
}
}
// ------------------------------------------------------------
// Output
// ------------------------------------------------------------
module assembly() {
color("SteelBlue") box();
color("Orange")
translate([wall + insert_clearance,
wall + insert_clearance, floor_t])
insert();
// lid floats above, flipped into closed orientation
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();