449 lines
16 KiB
OpenSCAD
449 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 = "friction"; // [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
|
|
// ------------------------------------------------------------
|
|
// The layout is a recursive grid. Each cell is one of
|
|
// * a number -> a compartment, sized by that weight
|
|
// * [weight, [cells]] -> a cell split perpendicular to its
|
|
// parent into the given sub-cells
|
|
// * [cells] -> shorthand for [1, [cells]]
|
|
// The top level splits the insert front-to-back into rows, each
|
|
// row splits left-to-right into columns, and so on, alternating
|
|
// direction at every nesting level. Weights are relative, so the
|
|
// layout always fills the insert exactly.
|
|
// Caveat: a two-element list whose second element is a list is
|
|
// always read as [weight, [cells]]; use the explicit weight form
|
|
// if that's not what you mean.
|
|
//
|
|
// Default: one full-depth bay on the left (weight 2, ~67 mm wide
|
|
// here - fits a deck of standard cards lying flat), two medium
|
|
// bays, and a right column split into three equal token bins.
|
|
layout = [
|
|
[1.0, [2, 1, 1, [1, 1, 1]]],
|
|
];
|
|
|
|
|
|
$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);
|
|
|
|
// layout cell accessors (see "Compartment layout" above)
|
|
function cell_weight(c) = is_num(c) ? c
|
|
: len(c) == 2 && is_list(c[1]) ? c[0] : 1;
|
|
function cell_children(c) = is_num(c) ? undef
|
|
: len(c) == 2 && is_list(c[1]) ? c[1] : c;
|
|
function cell_weights(cells) = [for (c = cells) cell_weight(c)];
|
|
|
|
// 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;
|
|
|
|
difference() {
|
|
rbox(ins_x, ins_y, ins_h, inner_r);
|
|
|
|
// compartment cavities
|
|
carve(layout, insert_wall, insert_wall, iw_x, iw_y, false);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// carve the cells into the region [x0, y0]..[x0+dx, y0+dy],
|
|
// split along x or y; nested cells recurse on the other axis
|
|
module carve(cells, x0, y0, dx, dy, split_x) {
|
|
n = len(cells);
|
|
w = cell_weights(cells);
|
|
avail = (split_x ? dx : dy) - (n - 1) * divider_t;
|
|
for (i = [0 : n - 1]) {
|
|
off = avail * psum(w, i) / sum(w) + i * divider_t;
|
|
sz = avail * w[i] / sum(w);
|
|
cx = split_x ? x0 + off : x0;
|
|
cy = split_x ? y0 : y0 + off;
|
|
cdx = split_x ? sz : dx;
|
|
cdy = split_x ? dy : sz;
|
|
sub = cell_children(cells[i]);
|
|
if (is_undef(sub))
|
|
translate([cx, cy, insert_floor]) cube([cdx, cdy, ins_h]);
|
|
else
|
|
carve(sub, cx, cy, cdx, cdy, !split_x);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// 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();
|