// ============================================================ // 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();