// ============================================================ // Wargame Accessory Box & Dice Tray // ------------------------------------------------------------ // Three printable parts: // * box - open tray, doubles as a dice tray // * lid - fully detachable friction-fit lid // * 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, 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 = 10; // Lip wall thickness lip_t = 2.0; // Gap per side between lip and box wall. // Tune for your printer: smaller = tighter friction fit. 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) thumb_grooves = true; /* [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; // ------------------------------------------------------------ // 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) 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); } } 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)); } } // ------------------------------------------------------------ // 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(); } if (part == "box") box(); else if (part == "lid") lid(); else if (part == "insert") insert(); else if (part == "print") { box(); translate([total_x + 15, 0, 0]) lid(); translate([0, total_y + 15, 0]) insert(); } else assembly();