259 lines
9.5 KiB
OpenSCAD
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();
|
|
}
|