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