commit dfed581377426db2518dd6cee5102303dcf05ac5 Author: luxick Date: Thu Jun 11 16:04:47 2026 +0200 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a9d0a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +stl/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7baea9f --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +SCAD = wargame_box.scad +PARTS = box lid insert +STLS = $(addprefix stl/,$(addsuffix .stl,$(PARTS))) + +all: $(STLS) + +stl/%.stl: $(SCAD) + @mkdir -p stl + openscad -o $@ -D 'part="$*"' $(SCAD) + +docs/assembly.png: $(SCAD) + @mkdir -p docs + openscad -o $@ --render --imgsize=900,800 \ + --camera=110,75,60,60,0,30,700 $(SCAD) + +clean: + rm -rf stl + +.PHONY: all clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..df5ab55 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Wargame Accessory Box & Dice Tray + +A parametric OpenSCAD box for carrying tabletop wargaming accessories. +Three printable parts: + +| Part | Purpose | +|----------|--------------------------------------------------------------------| +| `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). | +| `insert` | Removable compartment tray for tokens, measuring tapes, etc. | + +![Assembly](docs/assembly.png) + +## Quick start + +Open `wargame_box.scad` in OpenSCAD and set the three `total_*` values to +the free space in your backpack — they are **outer** dimensions and +`total_z` already includes the closed lid, so the printed result fits the +measured space exactly. + +Pick what to render with the `part` variable (or the customizer): +`assembly` (exploded preview), `box`, `lid`, `insert`, `print`. + +Or export everything from the command line: + +```sh +make # exports stl/box.stl, stl/lid.stl, stl/insert.stl +``` + +## Configuring the insert layout + +Compartments are defined by the `layout` list, one entry per row +(front to back): + +``` +layout = [ + [1.0, [1, 1, 1, 1]], // front row: 4 equal token bins + [1.0, [2, 1, 1]], // middle row: 1 double-width + 2 small + [0.8, [1]] // back row: full-width slot (tapes etc.) +]; +``` + +* The first number is the **row depth weight**. +* The list is the **column width weights** within that row. + +Weights are relative, so the layout always fills the insert exactly no +matter what box size you choose. A row weight of `2` is twice as deep as +a row of weight `1`; a column weight of `2` is twice as wide as `1`. + +To print a second insert variant (e.g. for a different game system), just +change `layout`, re-export, and swap inserts as needed. Two half-height +stacked inserts are possible too: set `insert_h_override` to about half +of the automatic height and print two with different layouts. + +## How the parts work together + +* 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 — + print a test fit and adjust for your printer/material. 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 + 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 + dice tray. `tray_chamfer` puts a 45° bevel around the floor edges so + dice don't lodge in the corners. + +## Printing notes + +* No supports needed for any part. Print all parts flat side down + (the lid prints plate-down, lip up). +* PLA or PETG, 2–3 perimeters, ~10 % infill (the parts are mostly walls). +* 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 — + it just needs to drop in and lift out easily). +* A piece of felt glued into the box floor makes dice rolling quieter; + if you plan this, add the felt thickness to nothing — the inner depth + is generous — but you may want `tray_chamfer = 0` for a flat floor. diff --git a/docs/assembly.png b/docs/assembly.png new file mode 100644 index 0000000..cdf9966 Binary files /dev/null and b/docs/assembly.png differ diff --git a/wargame_box.scad b/wargame_box.scad new file mode 100644 index 0000000..31674bb --- /dev/null +++ b/wargame_box.scad @@ -0,0 +1,276 @@ +// ============================================================ +// 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();