3d-printing/hako-dice/hako-dice.scad
2026-06-10 10:23:33 +02:00

259 lines
9.5 KiB
OpenSCAD

// ============================================
// Hako Dice
// ============================================
// A set of dice for the two-player dice game Hako
// (https://wiki.xxiivv.com/site/hako.html).
//
// Each Hako die is a six-sided cube whose faces are each split by their
// two diagonals into four triangles, painted in two contrasting colors.
// A full die carries all six distinct triangle patterns (counting how
// many of the four triangles are "painted"):
//
// day 0 painted (the bright face)
// dusk 1 painted
// half (split) 2 painted, adjacent pair
// half (cross) 2 painted, opposite pair
// dawn 3 painted
// night 4 painted (the dark face)
//
// A player owns four such dice, all identical, traditionally strung
// together through the day and night faces by a cord called the Sonozai.
// Since the dice are identical this file renders a single die by default;
// raise dice_count to batch-print a set. It exports cleanly for two-color
// / multi-material printing.
//
// ---- Balance / fair rolling --------------------------------------------
// A die only rolls fairly if its center of mass sits at its geometric
// center. Recessing (or embossing) triangles removes (or adds) material
// unevenly between faces, which biases the roll. Two things keep these
// dice fair:
//
// 1. Opposite faces are paired so the painted-triangle counts on each
// axis are as close as possible: the two 2-triangle faces sit
// opposite each other (0 difference), day(0) opposes dusk(1), and
// dawn(3) opposes night(4). No axis differs by more than a single
// triangle of shallow material.
//
// 2. The triangle markings are emitted as a separate colored part seated
// in the face pockets. In plain mode they sit flush, restoring the
// removed mass when printed in a second material of similar density.
// In recess mode they sit below the surface, so prefer a shallow
// recess_depth (or plain mode) for the fairest single-material dice.
//
// When the Sonozai cord hole is enabled, day and night must sit opposite
// each other so the cord can pass straight through; the centered hole is
// symmetric and does not itself unbalance the die.
//
// ---- Printing for two colors -------------------------------------------
// render_part = "preview" both parts, colored, for the customizer
// render_part = "bodies" just the die bodies -> primary filament
// render_part = "markings" just the triangle fill -> accent filament
// Export "bodies" and "markings" as separate STLs and assign each a
// filament, or print "bodies" alone and paint the recesses by hand.
// ============================================
// CUSTOMIZABLE PARAMETERS
// ============================================
/* [Dice] */
// Edge length of each die (mm)
die_size = 16; // [8:0.5:30]
// Corner/edge rounding radius (mm)
corner_radius = 1.6; // [0:0.1:5]
// Number of dice to lay out (all identical; bump up to batch-print a set)
dice_count = 1; // [1:1:8]
// Dice per row when laying out more than one
columns = 4; // [1:1:8]
/* [Triangles] */
// Recessed (engraved) triangles, or plain flat faces with flush color polygons
recessed_triangles = true;
// How far below the surface the colored triangle sits (mm) — used when recessed
recess_depth = 0.4; // [0.4:0.1:3]
// Depth of the flush color polygons (mm) — used when not recessed
marking_thickness = 0.5; // [0.2:0.1:2]
// Inset of the triangle pattern from the face edge (mm)
triangle_margin = 1.0; // [0:0.1:4]
// Gap between triangles and around the pattern (mm)
triangle_gap = 0.7; // [0:0.1:3]
/* [Sonozai cord] */
// Bore a centered hole through the day & night faces for the binding cord
sonozai_hole = false;
// Cord hole diameter (mm)
sonozai_diameter = 3; // [1:0.1:8]
/* [Render] */
// What to emit
render_part = "preview"; // [preview, bodies, markings]
/* [Quality] */
// Facets for rounded geometry
$fn = 48; // [16:8:128]
// ============================================
// DERIVED CONSTANTS
// ============================================
EPS = 0.02;
// Half edge / distance from center to each flat face.
face_half = die_size / 2;
// Where the colored triangle sits, in local face coordinates (z = 0 is the
// die surface, negative goes into the material):
// plain -> flush: top at the surface, a thin colored inlay.
// recess -> sunk: the colored triangle's top sits recess_depth below the
// surface, leaving a visible/tactile recess above it.
mark_top = recessed_triangles ? -recess_depth : 0;
mark_bot = mark_top - marking_thickness;
// Total depth of the pocket cut into the body to hold the marking.
pocket_depth = -mark_bot;
// Half-size of the square the four base triangles span (inset from edge).
tri_half = face_half - triangle_margin;
// Spacing between dice in the layout grid.
grid_pitch = die_size + max(die_size * 0.5, 6);
// --- Triangle index convention (per face) -------------------------------
// 0 = N (top), 1 = E (right), 2 = S (bottom), 3 = W (left).
// Each is the triangle between one edge of the face and the face center.
// The six unique faces, as the set of painted triangle indices.
FACE_DAY = []; // 0 painted
FACE_DUSK = [0]; // 1 painted
FACE_SPLIT = [0, 1]; // 2 painted, adjacent
FACE_CROSS = [0, 2]; // 2 painted, opposite
FACE_DAWN = [0, 1, 2]; // 3 painted
FACE_NIGHT = [0, 1, 2, 3]; // 4 painted
// Face assigned to each cube side, indexed by the place_on_face() id:
// 0=+Z, 1=-Z, 2=+X, 3=-X, 4=+Y, 5=-Y
//
// Balanced layout (default): closest possible painted counts per axis.
// Z: 2 vs 2 X: 0 vs 1 Y: 3 vs 4
FACES_BALANCED = [FACE_SPLIT, FACE_CROSS, FACE_DAY, FACE_DUSK, FACE_DAWN, FACE_NIGHT];
// Cord layout: day & night opposite (on Z) so the Sonozai threads through.
// Z: 0 vs 4 (cord) X: 1 vs 3 Y: 2 vs 2
FACES_CORD = [FACE_DAY, FACE_NIGHT, FACE_DUSK, FACE_DAWN, FACE_SPLIT, FACE_CROSS];
face_layout = sonozai_hole ? FACES_CORD : FACES_BALANCED;
// ============================================
// 2D PATTERN
// ============================================
// The three corner points of base triangle `i` on a square of half-size s,
// apex at the face center (0,0).
function base_tri(i, s) =
i == 0 ? [[-s, s], [ s, s], [0, 0]] : // N
i == 1 ? [[ s, s], [ s, -s], [0, 0]] : // E
i == 2 ? [[ s, -s], [-s, -s], [0, 0]] : // S
[[-s, -s], [-s, s], [0, 0]]; // W
// The painted triangles of one face as a 2D region, shrunk by the gap so
// the triangles read as separate inlays with a border.
module face_pattern_2d(idxs) {
for (i = idxs)
offset(delta = -triangle_gap / 2)
polygon(points = base_tri(i, tri_half));
}
// The painted triangles of one face as a solid spanning local z = [z0, z1].
module face_marks(idxs, z0, z1) {
translate([0, 0, z0])
linear_extrude(height = z1 - z0)
face_pattern_2d(idxs);
}
// ============================================
// PLACEMENT
// ============================================
// Bring +Z-facing children onto cube side `n`, flush with that face.
// 0=+Z, 1=-Z, 2=+X, 3=-X, 4=+Y, 5=-Y
module place_on_face(n) {
rot = [[0, 0, 0], [180, 0, 0], [0, 90, 0], [0, -90, 0], [-90, 0, 0], [90, 0, 0]];
rotate(rot[n])
translate([0, 0, face_half])
children();
}
// All six faces' marks for the current layout, spanning local z = [z0, z1].
module all_face_marks(z0, z1) {
for (n = [0 : 5])
place_on_face(n)
face_marks(face_layout[n], z0, z1);
}
// ============================================
// GEOMETRY
// ============================================
// A cube of edge `die_size` with rounded edges and corners.
module rounded_cube() {
o = face_half - corner_radius;
if (corner_radius <= 0)
cube(die_size, center = true);
else
hull()
for (x = [-o, o], y = [-o, o], z = [-o, o])
translate([x, y, z]) sphere(r = corner_radius);
}
// Centered through-hole along Z for the Sonozai cord.
module sonozai_cut() {
cylinder(h = die_size + 2 * EPS, d = sonozai_diameter, center = true);
}
// One die body: rounded cube with a triangular pocket cut for each painted
// triangle (and the cord hole bored, if enabled). In recess mode the pocket
// is deeper than the marking, leaving the visible recess.
module die_body() {
difference() {
rounded_cube();
all_face_marks(-pocket_depth, EPS);
if (sonozai_hole) sonozai_cut();
}
}
// One die's markings: the colored triangles seated in the pockets — flush in
// plain mode, sunk below the surface in recess mode (kept clear of the cord).
module die_markings() {
difference() {
all_face_marks(mark_bot, mark_top);
if (sonozai_hole) sonozai_cut();
}
}
// ============================================
// LAYOUT + OUTPUT
// ============================================
// Place `children()` once per die in a grid, centered on the origin.
module for_each_die() {
rows = ceil(dice_count / columns);
for (i = [0 : dice_count - 1]) {
col = i % columns;
row = floor(i / columns);
translate([
(col - (columns - 1) / 2) * grid_pitch,
((rows - 1) / 2 - row) * grid_pitch,
face_half
]) children();
}
}
module bodies() { for_each_die() die_body(); }
module markings() { for_each_die() die_markings(); }
if (render_part == "bodies") {
bodies();
} else if (render_part == "markings") {
markings();
} else { // preview
color("WhiteSmoke") bodies();
color("MidnightBlue") markings();
}