From dfed581377426db2518dd6cee5102303dcf05ac5 Mon Sep 17 00:00:00 2001 From: luxick Date: Thu, 11 Jun 2026 16:04:47 +0200 Subject: [PATCH] Initial --- .gitignore | 1 + Makefile | 19 ++++ README.md | 78 +++++++++++++ docs/assembly.png | Bin 0 -> 35148 bytes wargame_box.scad | 276 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 374 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/assembly.png create mode 100644 wargame_box.scad 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 0000000000000000000000000000000000000000..cdf99669f645082fb7833a0d16c427bb8caabb42 GIT binary patch literal 35148 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 literal 0 HcmV?d00001 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();