← hex_shape · API reference · version 0.1.8
6 file(s), 2988 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
arc_fill fn | 33 | HXS-001 | 42 52 135 137 146 152 … (7 total) |
arc_shell_max fn | 52 | HXS-001 | 73 140 141 590 602 |
arc_is_disk fn | 72 | HXS-009 | 96 142 603 |
arc_recover_centre fn | 92 | HXS-001 | 53 147 154 591 604 |
arc_shells_upto fn | 107 | HXS-001 | 44 127 |
arc_wedge_has fn | 166 | HXS-009 | 185 612 613 |
arc_door_wedge fn | 179 | HXS-009 | 597 |
arc_door_edges fn | 205 | HXS-009 | 608 609 |
BOX_ROT const | 58 | — | 68 77 89 109 |
box_cos fn | 67 | — | 124 |
box_sin fn | 76 | — | 125 |
box_orbit fn | 89 | HXS-006 | 33 34 35 433 437 |
Box struct | 93 | — | 107 108 114 116 118 123 … (7 total) |
box_new fn | 106 | — | 12 26 105 429 465 |
box_hw fn | 114 | — | 145 13 106 |
box_hd fn | 116 | — | 146 |
box_to_local fn | 123 | — | 150 |
box_fill fn | 144 | HXS-006 | 431 467 |
box_ring_out fn | 172 | HXS-007 | 471 |
box_ring_in fn | 196 | HXS-007 | 494 |
flood_outside fn | 223 | HXS-007 | 476 488 |
leak_count fn | 262 | HXS-007 | 473 477 489 490 496 |
line_hexes fn | 283 | HXS-007 | 502 |
set_connected fn | 314 | HXS-007 | 478 484 504 |
WALL_W const | 32 | — | 209 210 |
D24 const | 34 | — | 112 121 131 152 955 1017 |
Wall struct | 38 | — | 138 144 179 184 212 322 … (12 total) |
wall_step_k fn | 111 | — | 139 157 671 683 |
wall_step_m fn | 120 | — | 140 158 672 684 |
wall_is_exact fn | 131 | HXS-002 | 252 178 694 695 |
wall_new fn | 138 | HXS-008 | 19 564 |
wall_angle_nominal fn | 151 | — | 180 |
wall_angle_actual fn | 156 | HXS-002 | 1018 72 195 233 180 |
wall_offset_signed fn | 179 | — | 213 214 |
wall_separates fn | 212 | — | 329 |
hex_corner_x fn | 247 | — | 278 1082 1084 363 364 469 |
hex_corner_y fn | 254 | — | 279 1083 1085 365 366 470 |
cell_x_w fn | 263 | — | 281 329 330 373 57 |
cell_y_w fn | 269 | — | 282 329 330 374 58 |
wall_edge_gap fn | 276 | HXS-004 | 339 |
hex_corner_tri_a fn | 304 | — | 421 423 1148 1150 329 348 … (17 total) |
hex_corner_tri_b fn | 309 | — | 422 424 1149 1151 330 349 … (17 total) |
wall_write fn | 322 | — | 338 388 389 304 314 328 … (10 total) |
wall_along_max fn | 365 | HXS-004 | 306 316 |
wall_chain_ends fn | 459 | HXS-004 | 391 415 329 404 |
wall_chain_branches fn | 475 | HXS-004 | 416 447 448 331 |
WallChain struct | 516 | — | 546 638 |
wall_chain_walk fn | 545 | — | 308 310 339 392 394 401 … (9 total) |
tri_a fn | 670 | — | 697 768 819 935 1195 |
tri_b fn | 682 | — | 698 774 820 936 1196 |
tri_n2 fn | 696 | HXS-002 | 195 |
is_square fn | 704 | — | 714 |
commensurable fn | 714 | HXS-002 | 203 204 205 206 |
tri_class fn | 751 | — | 762 238 531 |
tri_on_hex_scale fn | 755 | — | 762 |
tri_is_vertex fn | 761 | — | 782 783 790 905 128 268 … (9 total) |
wall_end_a fn | 767 | — | 783 832 924 380 382 685 … (9 total) |
wall_end_b fn | 773 | — | 783 833 925 381 383 685 … (9 total) |
wall_run_ok fn | 780 | HXS-003 | 791 800 802 803 959 977 … (18 total) |
wall_min_p fn | 789 | HXS-003 | 259 260 261 368 |
wall_snap_p fn | 798 | HXS-003 | 1046 333 265 266 267 268 … (7 total) |
tri_x fn | 809 | — | 523 632 821 834 836 913 … (14 total) |
tri_y fn | 812 | — | 523 633 822 835 837 914 … (14 total) |
wall_run_len fn | 818 | HXS-003 | 1043 203 205 207 232 251 … (14 total) |
wall_from_run fn | 831 | — | 336 388 389 302 312 326 … (10 total) |
hex_scale_round fn | 875 | HXS-008 | 904 530 |
nearest_vertex fn | 903 | HXS-008 | 127 191 227 274 534 653 |
run_end_dist fn | 922 | — | 960 978 545 679 681 |
snap_run_d24 fn | 952 | HXS-008 | 541 672 |
snap_run_p fn | 971 | HXS-008 | 543 673 |
wall_nearest_d24 fn | 1014 | HXS-010 | 1038 72 85 88 116 |
snap_run_from_heading fn | 1036 | HXS-010 | 136 201 233 269 275 663 |
wall_offset_of fn | 1064 | — | 1090 1091 559 560 |
wall_mesh_stray fn | 1073 | HXS-008 | 554 |
wall_mesh_segments fn | 1105 | HXS-005 | 402 |
wall_read_ends fn | 1138 | HXS-005 | 1192 |
wall_read_run fn | 1190 | HXS-005 | 373 395 405 |
src/hex_shape.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_shape — the shape vocabulary — the line primitive, the box, the arc.
//
// The rest of `𝕄*` beside the turtle form: a WALL as a line primitive with a constant width
// (never a count of lattice rows), the BOX in 12 directions with its two orbits, and the ARC
// whose centre is exact and whose radius quantises to a realisable shell.
//
// Entry point: re-exports `hexwall`, `hexbox`, `hexarc`.
pub use hexwall::*;
pub use hexbox::*;
pub use hexarc::*;
src/hexarc.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hexarc — THE ROUND TOWER, AND WHAT OF AN ARC COMES BACK (rung A6, `OD-10`), plus the DOOR on it
// (rung A7 — the doored tower; the A7 section is at the foot of the file).
//
// `OD-10` asks: from a run of slots marked ROUNDED, are the CENTRE and RADIUS recoverable exactly
// (R1), or is that a fit (R2)? Measured, the answer is split:
//
// CENTRE recoverable EXACTLY — it is the translation, and the field pins it.
// RADIUS NOT recoverable exactly. It is quantised to SHELLS: 161 radii sampled over
// 0.5 .. 4.5 world units collapse to FOUR distinct fields. Every radius inside a
// shell draws the same cells, so only the shell comes back.
//
// SO AN ARC IS R1 ON A QUANTISED PARAMETER GRID, not R1 on a continuous radius — the same shape of
// answer as line endpoints (§10.10) and feature `t` (`X48`). Three independent parameters, one
// rule: **a continuous parameter must be quantised to what the field distinguishes, or it is
// silently snapped rather than refused.**
//
// THE SHELLS ARE EXACT INTEGERS, so none of this needs a float. A cell centre sits at
// `(k·√3/2, m/2)`, so its squared distance from the origin is `(3k² + m²)/4` — and `3k² + m²` is
// `hexform::vec_n2_4`, already in the tree. A shell is therefore an integer `N`, the radius it
// stands for is `√N / 2` world units, and "inside the disk" is the integer test `N' ≤ N`.
use hex_field::*;
use hex_form::*;
// Fill the disk of shell `n24` about (cq, cr): every cell whose 4·squared-distance is at most
// `n24`. Entirely integer — the comparison that decides membership never touches a float.
//
// `n24` is a SHELL, not a radius. Hand it a value no `(k,m)` realises and it draws the
// shell below without a word — `arc_shells_upto` is the grid to snap to first.
//
// Example: @HXS-001
pub fn arc_fill(cq: integer, cr: integer, n24: integer, cells: HexSet) -> integer {
bk = lattice_k(cq, cr);
bm = lattice_m(cq, cr);
n = 0;
for ar in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for aq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if vec_n2_4(lattice_k(aq, ar) - bk, lattice_m(aq, ar) - bm) <= n24 {
hexset_set(cells, aq, ar, true);
n = n + 1;
}
}
}
n
}
// The outermost shell actually occupied, measured about (cq, cr). This is the recoverable radius
// parameter — the largest `N` present, not the continuous radius that was authored.
//
// Example: @HXS-001
pub fn arc_shell_max(cells: HexSet, cq: integer, cr: integer) -> integer {
bk = lattice_k(cq, cr);
bm = lattice_m(cq, cr);
mx = -1;
for sr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for sq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, sq, sr) {
d = vec_n2_4(lattice_k(sq, sr) - bk, lattice_m(sq, sr) - bm);
if d > mx { mx = d; }
}
}
}
mx
}
// Is the set EXACTLY the disk of its own outermost shell about (cq, cr)? A tower must be, and a
// blob will not be — this is what stops an arbitrary round-ish region being read as an arc.
//
// Example: @HXS-001
// Example: @HXS-009
pub fn arc_is_disk(cells: HexSet, cq: integer, cr: integer) -> boolean {
n24 = arc_shell_max(cells, cq, cr);
if n24 < 0 { return false; }
bk = lattice_k(cq, cr);
bm = lattice_m(cq, cr);
for dr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for dq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
inside = vec_n2_4(lattice_k(dq, dr) - bk, lattice_m(dq, dr) - bm) <= n24;
if inside != hexset_get(cells, dq, dr) { return false; }
}
}
true
}
// Recover the centre: the unique cell about which the set IS a disk. Searched over the cells of
// the set itself — a disk's centre is always one of its own cells — so this is O(cells²) and needs
// no guess. Returns `found = false` when the region is not a disk about any of them, which is the
// R2 answer for a shape that is not an arc.
//
// Example: @HXS-001
pub fn arc_recover_centre(cells: HexSet) -> (integer, integer, boolean) {
for cr2 in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for cq2 in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, cq2, cr2) {
if arc_is_disk(cells, cq2, cr2) { return (cq2, cr2, true); }
}
}
}
(0, 0, false)
}
// The shells that actually exist out to `n24`, ascending — the admissible radius grid, i.e. `Sep`
// made concrete. A value of `N` that no `(k,m)` realises is not a shell and cannot be authored.
//
// Example: @HXS-001
pub fn arc_shells_upto(n24: integer) -> vector<integer> {
out: vector<integer> = [];
for sk in -12..13 {
for sm in -24..25 {
// A REALISABLE offset, not merely a lattice point. A cell-to-cell difference has
// `m = 3*dr`, so m must be divisible by THREE, and `k = 2*dq + parity(dr)`, so k must match
// the parity of `m/3`. Taking only `k = m (mod 2)` admits offsets like `(0,2)` that no pair
// of cells realises, which invents shells (N = 4) whose disk is identical to the one below.
if ((sm % 3) + 3) % 3 == 0
&& ((sk - ((sm / 3) ?? 0)) % 2 + 2) % 2 == 0 {
v = vec_n2_4(sk, sm);
if v <= n24 {
seen = false;
for oi in 0..len(out) { if (out[oi] ?? -1) == v { seen = true; } }
if !seen { out += [v]; }
}
}
}
}
for si in 1..len(out) {
ps = si;
while ps > 0 && (out[ps - 1] ?? 0) > (out[ps] ?? 0) {
t = out[ps - 1] ?? 0;
out[ps - 1] = out[ps] ?? 0;
out[ps] = t;
ps = ps - 1;
}
}
out
}
// ── A7 — A DOOR ON THE TOWER WALL ──────────────────────────────────────────
//
// The tower is a filled disk (above); its wall is the boundary edge loop (`draw_walls`). A door
// is a MATERIAL OVERRIDE on a contiguous span of those boundary edges — it re-materials them, it
// NEVER deletes a cell or an edge. This is the whole of A7, and it composes the arc (A6) with the
// feature (A5) at no cost, because:
//
// arc recovery (`arc_recover_centre`, `arc_shell_max`) consumes only the CELL set, and a door
// touches only edge MATERIALS — so the doored tower's cells are byte-identical to the plain
// tower's, and its centre and shell come back unchanged. The doored tower is ONE arc.
//
// The door reads straight back off storage: a boundary edge carrying the door material IS a door
// (ROUNDTRIP §2.4.1), so recovery is R1, not a fit. Deleting the edges instead would fragment the
// wall into k arcs — the doored-tower defect (design/FEATURES.md §3). `arc_door_wedge` cannot
// express that: it only writes a material, so the defect is unreachable through the door API and a
// gate has to reach around it to reproduce the fault.
//
// The door's angular span is the wedge between two integer rays from the centre. A positive-
// diagonal map (k -> k·√3/2, m -> m/2) preserves the CCW cyclic order of directions, so the wedge
// test is an exact integer cross product in lattice coords — `hexarc` stays float-free.
// Cross product of two lattice-coordinate vectors — exact integer; its sign is the turn direction.
fn arc_xprod(ak: integer, am: integer, bk: integer, bm: integer) -> integer { ak * bm - am * bk }
// Is cell (q,r) inside the angular wedge running CCW from ray `lo` up to ray `hi`, about centre
// (cq,cr)? The wedge must span less than 180°, so `lo` and `hi` bound one contiguous arc.
//
// Example: @HXS-009
pub fn arc_wedge_has(cq: integer, cr: integer, q: integer, r: integer,
lok: integer, lom: integer, hik: integer, him: integer) -> boolean {
ok = lattice_k(q, r) - lattice_k(cq, cr);
om = lattice_m(q, r) - lattice_m(cq, cr);
arc_xprod(lok, lom, ok, om) >= 0 && arc_xprod(ok, om, hik, him) > 0
}
// Place a door on the wall: re-material every boundary edge whose inside cell lies in the wedge to
// `mat`. Annotation only — no cell or edge is removed — so the disk, and therefore the recovered
// centre and shell, are untouched. Returns the number of edges marked. The arc analog of
// `place_opening`.
//
// Example: @HXS-009
pub fn arc_door_wedge(cells: HexSet, edges: EdgeSet, cq: integer, cr: integer,
lok: integer, lom: integer, hik: integer, him: integer,
mat: integer) -> integer {
n = 0;
for dr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for dq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, dq, dr) && arc_wedge_has(cq, cr, dq, dr, lok, lom, hik, him) {
for dd in 0..6 {
nq = nb_q(dq, dr, dd);
nr = nb_r(dq, dr, dd);
if !hexset_get(cells, nq, nr) {
edge_set_mat(edges, dq, dr, nq, nr, mat);
n = n + 1;
}
}
}
}
}
n
}
// Recover the door: count the boundary edges carrying `mat`, each exactly once (only the inside
// cell of a boundary edge is in the set, so iterating inside cells visits every boundary edge
// once). This is the R1 read of the feature straight off storage.
//
// Example: @HXS-009
pub fn arc_door_edges(cells: HexSet, edges: EdgeSet, mat: integer) -> integer {
n = 0;
for er in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for eq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, eq, er) {
for ed in 0..6 {
nq = nb_q(eq, er, ed);
nr = nb_r(eq, er, ed);
if !hexset_get(cells, nq, nr) && edge_mat(edges, eq, er, nq, nr) == mat {
n = n + 1;
}
}
}
}
}
n
}
src/hexbox.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hexbox — A BOX IS THE INTERIOR THE EDITOR SELECTS, AND THE WALL IS DERIVED FROM IT.
//
// The editor's gesture is *"select the inside hexes as a rectangle"* — the user picks the
// room, never the wall. Everything a building needs follows from that one set:
//
// box_fill the interior cells (what the user selected)
// draw_walls the THIN wall — housedraw's (an edge between in and out;
// boundary edges, costs no floor) a house, a cottage, a tower)
// box_ring_out the THICK wall — a full ring of (a castle, a town wall: the
// hexes just outside the selection) wall IS ground you stand on)
//
// TWELVE DIRECTIONS, NOT SIX. `housedraw`'s `Plan` rotates in 0..5, sixty degrees a step,
// because those six are the lattice's own rotations. But a stencil side runs in one of the
// TWELVE headings of H12 (`ROUNDTRIP` §2.1), and a rectangle's two sides are a heading and
// the heading three steps on. So `Box` rotates in 0..11, THIRTY degrees a step, and the 12
// split into two orbits that behave completely differently:
//
// EVEN rot — local u on an EDGE heading. The six are lattice rotations of one another,
// so their cell sets are EXACTLY equivariant: same count, related by X1's
// integer map. This is `Plan`'s family.
// ODD rot — local u on a VERTEX heading. Also six, also mutually equivariant — but a
// 30 degree rotation is NOT a lattice symmetry, so this family is NOT related
// to the even one by any exact map. It is a SECOND family of boxes, not a
// rotation of the first.
//
// That is `X24` again — the lattice has 6-fold symmetry, not 12-fold — and it is why the
// two orbits must be measured separately rather than assumed to agree. The rectangle is
// continuous and rasterises at any angle; what differs is the STRIP the wall runs in,
// zigzag on one family and staircase on the other, in ratio 9/8 exactly (`X25`).
// ── Using it ─────────────────────────────────────────────────────────────────
//
// The gesture is "select the room"; the wall is derived. So the sequence starts with a
// Box and asks for whichever wall the building wants:
//
// b = box_new(0, 0, 5, 3, 0, false); // centre cell, width, depth, rot, mirror
// s = hexset_chunk(-8, -8, 17, 17); // somewhere to put the cells
// n = box_fill(b, s); // the INTERIOR — what the user selected
//
// ring = hexset_chunk(-8, -8, 17, 17);
// m = box_ring_out(s, ring); // the THICK wall: hexes just outside it
//
// `box_fill` answers how many cells it wrote, so a caller can reject an empty or an
// oversized selection before doing anything with it. For the THIN wall — an edge between
// in and out, costing no floor — use `hexwall`'s routines on the same interior set.
//
// ⚠ `rot` runs 0..11, thirty degrees a step, and the even and odd halves are two
// different families rather than rotations of one another (see below). A caller that
// treats them alike will find the wall strip changes shape at odd rotations.
//
// Worked examples: `tests/` alongside this file.
use hex_field::*;
use hex_grid::*;
use hex_form::*;
pub const BOX_ROT = 12;
// The exact cosine and sine of `n * 30` degrees. Every value is 0, +-1/2, +-sqrt(3)/2 or
// +-1 — exact in Q(sqrt3), so this is a table and never a trig call (`housedraw` makes the
// same argument for its six).
const SQ3_2B = 0.8660254037844386;
// Cosine of n * 30 degrees, from the exact table rather than a trig call — every value is
// 0, +-1/2, +-sqrt(3)/2 or +-1, so two boxes at the same rotation agree to the bit.
pub fn box_cos(n: integer) -> float {
b = ((n % BOX_ROT) + BOX_ROT) % BOX_ROT;
if b == 0 { 1.0 } else if b == 1 { SQ3_2B } else if b == 2 { 0.5 }
else if b == 3 { 0.0 } else if b == 4 { -0.5 } else if b == 5 { -SQ3_2B }
else if b == 6 { -1.0 } else if b == 7 { -SQ3_2B } else if b == 8 { -0.5 }
else if b == 9 { 0.0 } else if b == 10 { 0.5 } else { SQ3_2B }
}
// Sine of n * 30 degrees, the companion of `box_cos` and exact for the same reason.
pub fn box_sin(n: integer) -> float {
b = ((n % BOX_ROT) + BOX_ROT) % BOX_ROT;
if b == 0 { 0.0 } else if b == 1 { 0.5 } else if b == 2 { SQ3_2B }
else if b == 3 { 1.0 } else if b == 4 { SQ3_2B } else if b == 5 { 0.5 }
else if b == 6 { 0.0 } else if b == 7 { -0.5 } else if b == 8 { -SQ3_2B }
else if b == 9 { -1.0 } else if b == 10 { -SQ3_2B } else { -0.5 }
}
// Which of the two orbits `rot` belongs to: 0 = the EDGE family (`Plan`'s six),
// 1 = the VERTEX family. Members of one orbit are exact lattice rotations of each other;
// members of different orbits are not related by any exact map.
//
// Example: @HXS-006
pub fn box_orbit(rot: integer) -> integer { (((rot % BOX_ROT) + BOX_ROT) % BOX_ROT) % 2 }
// A rectangular footprint of hex cells: an anchor cell, a width and depth in hex steps along
// its local axes, a rotation in thirty-degree steps (0..11) and a mirror flag.
pub struct Box {
pub bx_cq: integer, // anchor cell
pub bx_cr: integer,
bx_wid: integer, // extent along local u, in hex steps (1.5 m each)
bx_dep: integer, // extent along local v
pub bx_rot: integer, // 0..11 — THIRTY degree steps
bx_mir: boolean, // the flip
}
// The selection the editor made: a centre cell, a width and depth in hexes, one of the 12
// rotations and a mirror. Everything else here derives the building from this one value.
// ⚠ `rot` is 0..11 in thirty-degree steps, whose even and odd halves are two families
// rather than rotations of one another — see the note above.
pub fn box_new(cq: integer, cr: integer, wid: integer, dep: integer,
rot: integer, mir: boolean) -> Box {
Box { bx_cq: cq, bx_cr: cr, bx_wid: wid, bx_dep: dep,
bx_rot: ((rot % BOX_ROT) + BOX_ROT) % BOX_ROT, bx_mir: mir }
}
// Half the box's width in world units, for a bound or a hit test without rebuilding the
// frame.
pub fn box_hw(b: Box) -> float { (b.bx_wid as float) * HEX_LEN * 0.5 }
// Half the box's depth in world units, the companion of `box_hw`.
pub fn box_hd(b: Box) -> float { (b.bx_dep as float) * HEX_LEN * 0.5 }
fn box_s(b: Box) -> float { if b.bx_mir { -1.0 } else { 1.0 } }
// World point -> the box's local frame. Same convention as `housedraw::plan_to_local`, so
// an even-rot Box and the Plan with rot/2 describe the SAME rectangle — which the gate
// checks rather than assumes.
pub fn box_to_local(b: Box, px: float, py: float) -> (float, float) {
co = box_cos(b.bx_rot);
si = box_sin(b.bx_rot);
s = box_s(b);
dx = px - cell_x(b.bx_cq, b.bx_cr);
dy = py - cell_y(b.bx_cq, b.bx_cr);
(dx * s * co + dy * s * si, -dx * si + dy * co)
}
// THE SELECTION: every cell whose CENTRE lies in the rectangle.
//
// The comparison is inclusive with an epsilon for exactly the reason `housedraw::draw_floor`
// documents — along an edge heading every cell-to-side distance is an exact multiple of
// sqrt(3)/2, which is the default halfwidth, so a strict test sits on the last bit of a
// float for a whole column. On the VERTEX headings the same tie appears at a different
// spacing, which is why the epsilon is kept here too rather than assumed unnecessary.
const BOX_EPS = 0.000000001;
fn bfabs(v: float) -> float { if v < 0.0 { -v } else { v } }
// Example: @HXS-006
pub fn box_fill(b: Box, cells: HexSet) -> integer {
hw = box_hw(b);
hd = box_hd(b);
n = 0;
for br in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for bq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
(lu, lv) = box_to_local(b, cell_x(bq, br), cell_y(bq, br));
if bfabs(lu) <= hw + BOX_EPS && bfabs(lv) <= hd + BOX_EPS {
hexset_set(cells, bq, br, true);
n = n + 1;
}
}
}
n
}
// ── THE THICK WALL — a castle, not a cottage ───────────────────────────────
//
// A cottage wall is an EDGE: it costs no floor, and 27 cells of house stay 27 cells of
// house. A castle wall is different in kind — it is GROUND, wide enough to stand and walk
// on, with an inside and an outside. So it is a ring of whole CELLS, and the only question
// is which side of the selection it takes.
//
// `box_ring_out` puts it OUTSIDE: the user's selection is the courtyard, untouched, and the
// wall is the ring of cells around it. This is the one that matches the editor's gesture —
// what you select is what you get to keep.
//
// Example: @HXS-007
pub fn box_ring_out(cells: HexSet, ring: HexSet) -> integer {
n = 0;
for rr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for rq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, rq, rr) {
for rd in 0..6 {
nq = nb_q(rq, rr, rd);
nr = nb_r(rq, rr, rd);
if !hexset_get(cells, nq, nr) && !hexset_get(ring, nq, nr) {
hexset_set(ring, nq, nr, true);
n = n + 1;
}
}
}
}
}
n
}
// `box_ring_in` takes it from INSIDE instead: the outermost layer of the selection becomes
// wall and the courtyard shrinks. Same ring, opposite side — a town wall drawn to a
// surveyed outer boundary wants this one.
//
// Example: @HXS-007
pub fn box_ring_in(cells: HexSet, ring: HexSet) -> integer {
n = 0;
for ir in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for iq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, iq, ir) {
edge = false;
for id in 0..6 {
if !hexset_get(cells, nb_q(iq, ir, id), nb_r(iq, ir, id)) { edge = true; }
}
if edge { hexset_set(ring, iq, ir, true); n = n + 1; }
}
}
}
n
}
// ── DOES THE WALL HOLD? ────────────────────────────────────────────────────
//
// A ring that looks closed and is not is the exact defect `SPEC` I3's control names — "2
// components, 0 enclosed, yet still 12/12 equivariant". For a thick wall the honest test is
// not to count cells but to try to WALK IN: flood the outside and see whether it reaches the
// courtyard. One hole and it does.
//
// Relaxation rather than a queue: the region is a box, so sweeping until nothing changes
// terminates in at most its perimeter in passes, and it needs no auxiliary storage.
//
// Example: @HXS-007
pub fn flood_outside(wall: HexSet, out: HexSet) {
q0 = hexset_q0(out);
r0 = hexset_r0(out);
w = hexset_w(out);
h = hexset_h(out);
// seed: the whole border of the window, minus any wall on it
for sq in q0..q0 + w {
if !hexset_get(wall, sq, r0) { hexset_set(out, sq, r0, true); }
if !hexset_get(wall, sq, r0 + h - 1) { hexset_set(out, sq, r0 + h - 1, true); }
}
for sr in r0..r0 + h {
if !hexset_get(wall, q0, sr) { hexset_set(out, q0, sr, true); }
if !hexset_get(wall, q0 + w - 1, sr) { hexset_set(out, q0 + w - 1, sr, true); }
}
changed = true;
while changed {
changed = false;
for fr in r0..r0 + h {
for fq in q0..q0 + w {
if hexset_get(out, fq, fr) {
for fd in 0..6 {
nq = nb_q(fq, fr, fd);
nr = nb_r(fq, fr, fd);
if nq >= q0 && nq < q0 + w && nr >= r0 && nr < r0 + h {
if !hexset_get(wall, nq, nr) && !hexset_get(out, nq, nr) {
hexset_set(out, nq, nr, true);
changed = true;
}
}
}
}
}
}
}
}
// How many cells of `inner` the outside flood reached. Zero means the wall holds.
//
// Example: @HXS-007
pub fn leak_count(inner: HexSet, out: HexSet) -> integer {
n = 0;
for lr in hexset_r0(inner)..hexset_r0(inner) + hexset_h(inner) {
for lq in hexset_q0(inner)..hexset_q0(inner) + hexset_w(inner) {
if hexset_get(inner, lq, lr) && hexset_get(out, lq, lr) { n = n + 1; }
}
}
n
}
// ── A THICK LINE — the town wall that is not a box ─────────────────────────
//
// Same wall, no enclosure: a run of cells one hex wide, for a curtain wall or a rampart.
// The cells are those the segment passes THROUGH — a hex is on the line iff the segment
// enters it — so the chain is connected by construction and exactly one cell wide.
//
// Stepping is by a third of a cell, which cannot skip one: consecutive samples are 0.577 wu
// apart at most while a hex is 1.732 wu across, so every cell the segment crosses is
// sampled at least twice.
//
// Example: @HXS-007
pub fn line_hexes(x0: float, y0: float, x1: float, y1: float, cells: HexSet) -> integer {
dx = x1 - x0;
dy = y1 - y0;
ln = sqrt(dx * dx + dy * dy) ?? 0.0;
steps = ((ln * 3.0) as integer) + 1;
// `/` is FALLIBLE in loft, so `t` below types as `float?`. `steps` is `(..) + 1` on a
// non-negative `sqrt`, so it is at least 1 and the divide cannot fail — but a null `t` taken as
// `0.0` would put EVERY sample on the start point, laying a one-cell stub where a wall was asked
// for and returning a plausible count for it. Refuse instead: 0 cells is visible to the caller,
// a collapsed wall is not. The `?? 0.0` below is unreachable past this guard.
if steps < 1 { return 0; }
n = 0;
for si in 0..steps + 1 {
t = (si as float) / (steps as float) ?? 0.0;
(hq, hr) = px_to_hex(x0 + dx * t, y0 + dy * t);
if !hexset_get(cells, hq, hr) {
hexset_set(cells, hq, hr, true);
n = n + 1;
}
}
n
}
// Is every cell of the set reachable from every other, stepping only within the set?
// A wall with a gap is not a wall; this is what makes "one hex wide" mean a CHAIN.
//
// CONNECTED IS NOT CLOSED. A ring with one cell taken out is still one connected chain, so
// this answers true on a wall you can walk straight through. For an ENCLOSURE the verdict is
// `flood_outside` + `leak_count`; this one is the contract for an open run.
//
// Example: @HXS-007
pub fn set_connected(cells: HexSet) -> boolean {
seedq = 0;
seedr = 0;
found = false;
total = 0;
for cr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for cq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(cells, cq, cr) {
total = total + 1;
if !found { seedq = cq; seedr = cr; found = true; }
}
}
}
if !found { return false; }
reach = hexset_chunk(hexset_q0(cells), hexset_r0(cells), hexset_w(cells), hexset_h(cells));
hexset_set(reach, seedq, seedr, true);
changed = true;
while changed {
changed = false;
for rr in hexset_r0(cells)..hexset_r0(cells) + hexset_h(cells) {
for rq in hexset_q0(cells)..hexset_q0(cells) + hexset_w(cells) {
if hexset_get(reach, rq, rr) {
for rd in 0..6 {
nq = nb_q(rq, rr, rd);
nr = nb_r(rq, rr, rd);
if hexset_get(cells, nq, nr) && !hexset_get(reach, nq, nr) {
hexset_set(reach, nq, nr, true);
changed = true;
}
}
}
}
}
}
hexset_count(reach) == total
}
src/hexwall.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hexwall — A WALL IS A STRAIGHT LINE, AND IT WRITES ITSELF INTO THE FOXEL.
//
// The wall is the primitive, not the house. It does not care which side is inside, or
// whether there is a house at all — the same line is a house wall, a party wall or a fence.
// A house is four of them; a fence is one.
//
// THE 24 DIRECTIONS, AND WHY ONLY 12 ARE FOR HOUSES. A direction is `d24`, in
// twenty-fourths of a revolution (24 of them, 15 degrees apart):
//
// EVEN d24 — the 12 headings of H12, each an EXACT lattice step (hexform). These are the
// only directions a STENCIL may use: a house is built from them.
// ODD d24 — the 12 in-between directions. THEY ARE NOT EXACT: no lattice vector points
// at an odd multiple of 15 degrees, so each uses the vector `(7, 3)` turned, and
// lands 1.1021 degrees off the nominal. We use them anyway, and that is
// precisely why HOUSES ARE NEVER DRAWN WITH AN IN-BETWEEN ANGLE: they belong to
// world linework (roads, fences, cliffs — SPEC domain B), where the offset does
// not have to close a shape or meet a corner.
//
// THE STORAGE IS THE FOXEL AND NOTHING ELSE: three wall slots per hex. `EdgeSet` already is
// that model — an edge is stored once, by the cell that owns it — so `wall_write` marks the
// chain of hex edges lying ALONG the wall (the boundary between the half-planes it divides,
// `wall_separates`) and stores nothing else. No direction, no offset, no triangles: recovering
// the line from the marks is `rebuild`'s job, not storage's.
use hex_field::*;
use hex_grid::*;
use hex_form::*;
// The wall is one triangle thick. Triangle side = 1/3 hex edge, so the height — and the
// wall's width — is sqrt(3)/6 world units = 1/4 m exactly. (DESIGN.md §10.7.)
pub const WALL_W = 0.28867513459481287;
pub const D24 = 24;
// A straight wall run in world units: its centre, unit direction and half-length, and the
// material it writes into every slot it crosses. `wall_new` and `wall_from_run` make one.
pub struct Wall {
pub wa_ox: float, // centre of the run, world units
pub wa_oy: float,
pub wa_dx: float, // unit direction
pub wa_dy: float,
wa_half: float, // half the run length
wa_mat: integer, // the material written into every slot it crosses
}
// The direction of `d24` as a doubled-coordinate vector, before normalising.
//
// Even `d24`: the heading itself, exact. Odd: the in-between vector below, which is inexact —
// no lattice vector points at an odd multiple of 15° at all (`X31`).
//
// ── THE IN-BETWEEN VECTOR (odd `d24`) — `N = 39`, the vector `(7,−2)` ──────
//
// `N = 39` puts every in-between direction **1.1021°** off its nominal angle, against 4.1066° for
// the sum of the two adjacent headings (`N = 21`), and is one of only two vectors in the whole
// neighbourhood that keeps **`δ = 0`**. Changing it would change which edges an in-between wall
// marks, so stored linework would no longer read back: it is fixed (law `A₂`).
//
// **`δ = (tri_a − tri_b) mod 3` is the axis that decides LINKING**, and it is why `N = 39` and not
// the finer-looking `N = 13`. A run of `p` periods from a vertex of class `c` ends on class
// `(c + p·δ) mod 3`, and class 0 is a hex CENTRE, which `wall_run_ok` refuses:
//
// δ = 0 → the vertex class is PRESERVED: every multiple is admissible from every vertex (9/9)
// δ ≠ 0 → the class CYCLES: one run in three lands on a centre and is refused (6/9), and the
// shortest legal run depends on which class you started from
//
// A house wall can leave you on either class — the `N = 1` family (30/90/150°) changes it — so a
// `δ ≠ 0` in-between direction links to the house angles *conditionally*: from half of all house
// corners its shortest legal run doubles. `N = 13` is finer (3.122 m against 5.408 m) and equally
// accurate, but it spends exactly that property. `N = 39` buys the whole angle improvement and
// leaves linework-meets-house behaving as it does today. `X31`'s ladder compared only angle and
// period, so it could not see this axis at all.
//
// The vectors are **vertex**-lattice vectors, not cell-to-cell ones (`m` need not be a multiple of
// 3). That is fine: `wall_step_*` feeds only `wall_new` (which normalises to a unit direction),
// `wall_angle_actual` (angle only) and `tri_a`/`tri_b` (which convert to triangle coordinates and
// reduce) — nothing steps cell by cell along it. The one requirement is `m ≡ k (mod 2)`, so
// `tri_b`'s `(m − k)/2` stays integral; every entry satisfies it.
//
// Entries 6..11 are the negation of 0..5, because `d` and `d + 12` are opposite directions. The
// first six come from rotating `d = 1` by the exact 60° lattice map, and `d = 3` from reflecting
// then rotating — so the table is one vector, turned, not twelve independent choices.
fn between_k(i: integer) -> integer {
j = i;
s = 1;
if i >= 6 { j = i - 6; s = 0 - 1; }
kk = 7;
if j == 1 { kk = 5; }
if j == 2 { kk = 2; }
if j == 3 { kk = 0 - 2; }
if j == 4 { kk = 0 - 5; }
if j == 5 { kk = 0 - 7; }
s * kk
}
fn between_m(i: integer) -> integer {
j = i;
s = 1;
if i >= 6 { j = i - 6; s = 0 - 1; }
mm = 3;
if j == 1 { mm = 9; }
if j == 2 { mm = 12; }
if j == 3 { mm = 12; }
if j == 4 { mm = 9; }
if j == 5 { mm = 3; }
s * mm
}
// The `k` of one step along wall direction `d24`. The wall runs on a finer set of
// directions than the cells do, which is why it has its own step table.
pub fn wall_step_k(d24: integer) -> integer {
n = ((d24 % D24) + D24) % D24;
h = n / 2;
a = head_step(h);
if n % 2 == 0 { return a.0; }
between_k((n - 1) / 2)
}
// The `m` of that same step — walk both to move along a wall.
pub fn wall_step_m(d24: integer) -> integer {
n = ((d24 % D24) + D24) % D24;
h = n / 2;
a = head_step(h);
if n % 2 == 0 { return a.1; }
between_m((n - 1) / 2)
}
// An even d24 is one of the 12 exact headings; an odd one is an in-between.
//
// Example: @HXS-002
pub fn wall_is_exact(d24: integer) -> boolean { (((d24 % D24) + D24) % D24) % 2 == 0 }
// World direction of a doubled-coordinate vector: x = k*sqrt(3)/2, y = m/2.
fn dir_x(k: integer, _m: integer) -> float { (k as float) * 0.8660254037844386 }
fn dir_y(_k: integer, m: integer) -> float { (m as float) * 0.5 }
// Example: @HXS-008
pub fn wall_new(d24: integer, ox: float, oy: float, half: float, mat: integer) -> Wall {
sk = wall_step_k(d24);
sm = wall_step_m(d24);
vx = dir_x(sk, sm);
vy = dir_y(sk, sm);
len = sqrt(vx * vx + vy * vy);
Wall { wa_ox: ox, wa_oy: oy, wa_dx: (vx / len) ?? 0.0, wa_dy: (vy / len) ?? 0.0,
wa_half: half, wa_mat: mat }
}
// The nominal angle of `d24` in degrees, and the angle its step actually points at. The two
// are EQUAL on every even d24 and differ by 1.1021 degrees on every odd one — the gate prints
// both rather than hiding it.
pub fn wall_angle_nominal(d24: integer) -> float {
((((d24 % D24) + D24) % D24) as float) * 15.0
}
// Example: @HXS-002
pub fn wall_angle_actual(d24: integer) -> float {
sk = wall_step_k(d24);
sm = wall_step_m(d24);
a = atan2(dir_y(sk, sm), dir_x(sk, sm)) * 57.29577951308232;
if a < 0.0 { a + 360.0 } else { a }
}
// ── writing into the foxel — ALONG the wall, not ACROSS it (OD-12) ─────────
//
// NOT THE EDGES A THIN BAND OVERLAPS. Those lie ACROSS the wall, roughly perpendicular: a
// due-east wall would come out as a comb of vertical pickets (`X28`), because a hex has edges on
// only three lines (30/90/150) and none runs east.
//
// THE WALL IS A BOUNDARY, NOT A BAND. A straight line divides the plane into two
// half-planes. The hex edges that lie ALONG the line are exactly the ones that SEPARATE a
// cell on one side from a cell on the other — the boundary between the two half-planes, which
// for any direction is a single connected chain, straight in the three edge directions and an
// alternating wobble otherwise. This is `housedraw::draw_walls`' own rule (an edge between an
// inside cell and an outside one), applied to a half-plane instead of a filled region — so it
// inherits `I3`'s "closed by construction, one wide" for free.
//
// Signed perpendicular offset of a world point from the wall's centre line. Positive on the
// side the normal (-dy, dx) points to; the sign is all that matters for separation.
pub fn wall_offset_signed(w: Wall, px: float, py: float) -> float {
(px - w.wa_ox) * (0.0 - w.wa_dy) + (py - w.wa_oy) * w.wa_dx
}
// Signed distance along the run from its centre — in [-wa_half, +wa_half] over the segment.
fn wall_along(w: Wall, px: float, py: float) -> float {
(px - w.wa_ox) * w.wa_dx + (py - w.wa_oy) * w.wa_dy
}
// Does the wall's centre line SEPARATE the two cell centres — and does it cross between them
// WITHIN the run? A hex edge is marked iff its two owning cells answer yes.
//
// The tie-break (offset == 0 counts as the positive side) makes the test total and
// deterministic (`I9`). A wall centred on a hex CENTRE (the bare `wall_new(d, 0, 0, …)` form) can
// put a whole row of centres on the line, and then the tie-break places the boundary just to one
// side of them rather than through them.
//
// ⚠ THE COMPARISON MUST TREAT A MATHEMATICAL ZERO AS ZERO — that is what `WALL_EPS` is doing here.
// A VERTEX-anchored wall can put a cell centre on the line too: the `N = 1` family (30/90/150…)
// at `p = 3` does — e.g. `d = 2` from vertex `(3,0)` puts cell `(1,1)` exactly on the line. Its offset is mathematically 0, but in FLOAT it comes out as `−1.39e-16` in one
// orientation and as a clean `0` in the rotation of the same wall. A bare `oc >= 0.0` then sorts
// that one cell onto opposite sides in the two orientations, and the rasterisation stops being
// rotation-covariant — 18 cases, exactly the ones with a centre on the line.
//
// Comparing against `−WALL_EPS` instead makes a zero read as zero in EVERY orientation, so the
// documented tie-break behaves as documented. This is not a tolerance in a fit (`P4`): the
// quantity is *exactly* zero and the epsilon only removes rounding noise from a SIGN test. It
// cannot misclassify a genuinely off-line cell either — a cell centre that is not on the line sits
// an algebraic distance away, orders of magnitude above `1e-9`.
const WALL_EPS = 0.000000001; // a float SIGN-test guard, not a P4 tolerance
// Does this wall lie BETWEEN two points? The question a mover asks before stepping: if
// the wall separates where you are from where you are going, the step is blocked.
pub fn wall_separates(w: Wall, cx: float, cy: float, nx: float, ny: float) -> boolean {
oc = wall_offset_signed(w, cx, cy);
on = wall_offset_signed(w, nx, ny);
pc = oc > 0.0 - WALL_EPS;
pn = on > 0.0 - WALL_EPS;
if pc == pn { return false; }
// where the segment C->N crosses the line, and whether that point is within the run
t = (oc / (oc - on)) ?? 0.5;
ax = cx + t * (nx - cx);
ay = cy + t * (ny - cy);
al = wall_along(w, ax, ay);
al >= (0.0 - w.wa_half) - WALL_EPS && al <= w.wa_half + WALL_EPS
}
// The endpoint edges sit exactly at along = +-wa_half; include them so the chain reaches its
// vertices. A rasterisation tie-break, not a geometric fudge — the same role `draw_floor`'s
// EPS plays, and it never moves a mark off its edge.
// ── THE EDGE TABLE IS hex_grid's, NOT OURS ─────────────────────────────────
//
// `hex_grid` already owns the moros edge-wall model — it says so in the file: "walls live
// on hex edges, stored on 3 canonical edges per hex (dirs 0,1,2); dirs 3,4,5 belong to the
// neighbour". So `hex_edge_corners(dir)` IS the map from a neighbour direction to the two
// corners bounding that edge, and `moros_render::emit_hex_walls` emits exactly those corner
// pairs (N = 5->0, NE = 0->1, SE = 1->2) for the three stored slots.
//
// A PRIVATE CORNER CONVENTION IS A SILENT WRONG ANSWER. This file first carried its own
// table (corner 0 at 30 degrees) and paired edge `c` with `nb_q/nb_r(q, r, c)` — but
// hex_field's direction order is [E, W, SE, SW, NE, NW] and hex_grid's is
// [E, SE, SW, W, NW, NE], so five of the six edges were stored against the WRONG
// neighbour. Every gate stayed green: a consistently wrong edge is still written once,
// still idempotent, still non-empty. Only measuring the marks back against the geometry
// caught it, which is why `wall_edge_gap` below is a gate and not a comment.
//
// Corner `c` of the hex at (q,r), in world units — hex_grid's table, hex_grid's indices.
pub fn hex_corner_x(q: integer, r: integer, c: integer) -> float {
(cx, cy) = hex_to_px(q, r);
(ox, oy) = hex_corner_offset(((c % 6) + 6) % 6);
cx + ox
}
// Corner c of cell (q,r) in world y — the companion of `hex_corner_x`.
pub fn hex_corner_y(q: integer, r: integer, c: integer) -> float {
(cx, cy) = hex_to_px(q, r);
(ox, oy) = hex_corner_offset(((c % 6) + 6) % 6);
cy + oy
}
// Cell centre in world units. hex_field's exact lattice and hex_grid's float world agree
// term for term — k*sqrt(3)/2 = sqrt(3)*(q + (r&1)/2) and m/2 = 1.5r — so this is the
// integer spelling of `hex_to_px`, not a second convention.
pub fn cell_x_w(q: integer, r: integer) -> float {
(lattice_k(q, r) as float) * 0.8660254037844386
}
// A cell centre's world y in WALL units, which are half the cell units: the wall lattice
// is finer, and mixing the two is the mistake this separate name exists to prevent.
pub fn cell_y_w(q: integer, r: integer) -> float { (lattice_m(q, r) as float) * 0.5 }
// THE CHECK THAT WOULD HAVE CAUGHT IT: how far the midpoint of edge `dir` sits from the
// midpoint between the two cell centres it is supposed to separate. Zero for every
// direction, or the edge table and the neighbour table disagree.
//
// Example: @HXS-004
pub fn wall_edge_gap(q: integer, r: integer, dir: integer) -> float {
(e1, e2) = hex_edge_corners(dir);
mx = (hex_corner_x(q, r, e1) + hex_corner_x(q, r, e2)) / 2.0;
my = (hex_corner_y(q, r, e1) + hex_corner_y(q, r, e2)) / 2.0;
(nq, nr) = hex_neighbor(q, r, dir);
hx = (cell_x_w(q, r) + cell_x_w(nq, nr)) / 2.0;
hy = (cell_y_w(q, r) + cell_y_w(nq, nr)) / 2.0;
sqrt((mx - hx) * (mx - hx) + (my - hy) * (my - hy)) ?? 0.0
}
// Triangle-lattice coordinates of a hex corner — EXACT integers, because every hex corner is
// a vertex of the triangle lattice. The centre of (q,r) is (3k, 3(m-k)/2) with k = lattice_k,
// m = lattice_m; the six corner deltas are constant. Used to key the edge chain by its shared
// vertices when checking connectivity (`tests/wall.loft` §6b).
fn corner_da(ci: integer) -> integer {
c = ((ci % 6) + 6) % 6;
if c == 0 { 0 } else if c == 1 { -3 } else if c == 2 { -3 }
else if c == 3 { 0 } else if c == 4 { 3 } else { 3 }
}
fn corner_db(ci: integer) -> integer {
c = ((ci % 6) + 6) % 6;
if c == 0 { 3 } else if c == 1 { 3 } else if c == 2 { 0 }
else if c == 3 { -3 } else if c == 4 { -3 } else { 0 }
}
// A hex corner's position on the triangular wall lattice, `a` axis. Corners are shared
// between cells, so naming one exactly is what lets two cells agree about their join.
pub fn hex_corner_tri_a(q: integer, r: integer, ci: integer) -> integer {
3 * lattice_k(q, r) + corner_da(ci)
}
// The same corner's `b` axis — the pair is the corner's exact triangular-lattice address.
pub fn hex_corner_tri_b(q: integer, r: integer, ci: integer) -> integer {
((3 * (lattice_m(q, r) - lattice_k(q, r)) / 2) ?? 0) + corner_db(ci)
}
// `wall_from_run` — the principled vertex-anchored constructor — is defined further down, beside
// the endpoint machinery it depends on (`tri_x`, `wall_end_a`), which is where it belongs anyway.
// It was once forced there by a loft defect (LOFT-HANDOFF H5, fixed and verified on 2026.7.2).
// THE ROUTINE: draw the wall into the foxel's three wall slots per hex.
//
// An edge is marked iff the wall's centre line SEPARATES its two owning cells, within the run.
// `edge_set_mat` stores an edge once, under whichever cell owns it, so writing from both sides
// is idempotent — which is what makes the three-slots-per-hex model hold.
pub fn wall_write(w: Wall, edges: EdgeSet, q0: integer, r0: integer,
wq: integer, hq: integer) -> integer {
n = 0;
for wr in r0..r0 + hq {
for wqq in q0..q0 + wq {
for wd in 0..6 {
(nq, nr) = hex_neighbor(wqq, wr, wd);
if wall_separates(w, cell_x_w(wqq, wr), cell_y_w(wqq, wr),
cell_x_w(nq, nr), cell_y_w(nq, nr)) {
if edge_mat(edges, wqq, wr, nq, nr) == 0 {
edge_set_mat(edges, wqq, wr, nq, nr, w.wa_mat);
n = n + 1;
}
}
}
}
}
n
}
// ── READING THE CHAIN BACK: is it ALONG the wall, and is it ONE chain? ─────
//
// These are the two properties that distinguish the fixed marking from the old picket comb,
// and neither is visible in the perpendicular STRAY (a comb and a zigzag both swing ~1 hex off
// the line). They are what `tests/wall.loft` §6 asserts.
//
// ALONG-NESS. For a marked edge between cells C and N, the vector C->N is PERPENDICULAR to the
// edge. An edge lying ALONG the wall therefore has C->N pointing ACROSS the wall — a small
// `|C->N . wall_dir|`. The old picket marked edges lying ACROSS the wall, whose C->N pointed
// ALONG it — `|C->N . wall_dir| ~ 1`.
//
// ⚠ THAT MAKES THE WORST `|dot|` A DISCRIMINATOR ON HALF THE DIRECTIONS ONLY. On the twelve
// EXACT headings it tops out at 0.5 (N = 3) or 0.866 (N = 1), and nothing reaches 0.9. On the
// twelve IN-BETWEEN ones it reads **0.97** on completely correct output: a shallow line has to
// cross the vertical edge between two cells of the SAME row wherever it steps up, and that
// edge's dot is `cos(13.9 deg)`. Measured, 3 of a 25-edge run are above 0.9. So this number
// describes the WOBBLE; it does not deliver a verdict, and a gate that thresholds it flags a
// correct in-between wall.
//
// The verdict is the CHAIN property below — two degree-1 vertices, no branches — which is exact
// and holds for all 24 directions.
//
// Example: @HXS-004
pub fn wall_along_max(w: Wall, edges: EdgeSet, q0: integer, r0: integer,
wq: integer, hq: integer) -> float {
worst = 0.0;
for wr in r0..r0 + hq {
for wqq in q0..q0 + wq {
for wd in 0..3 {
(nq, nr) = hex_neighbor(wqq, wr, wd);
if edge_mat(edges, wqq, wr, nq, nr) != 0 {
dx = cell_x_w(nq, nr) - cell_x_w(wqq, wr);
dy = cell_y_w(nq, nr) - cell_y_w(wqq, wr);
dl = sqrt(dx * dx + dy * dy) ?? 1.0;
dot = ((dx * w.wa_dx + dy * w.wa_dy) / dl) ?? 0.0;
ad = if dot < 0.0 { 0.0 - dot } else { dot };
if ad > worst { worst = ad; }
}
}
}
}
worst
}
// ── THE MARKING, KEYED BY ITS SHARED VERTICES ──────────────────────────────
//
// Every question below — how many ends, how many branches, and what ORDER the chain runs in
// — is asked of the same two tables: the marked edges in the window's own scan order, and
// their endpoints as exact triangle-lattice integers, two per mark. They are built once
// here so the three answers cannot disagree about which vertex is which.
// The marked edges of the window, in scan order: the cell that owns each and its direction.
fn chain_marks(edges: EdgeSet, q0: integer, r0: integer, wq: integer, hq: integer)
-> (vector<integer>, vector<integer>, vector<integer>) {
mq: vector<integer> = [];
mr: vector<integer> = [];
md: vector<integer> = [];
for wr in r0..r0 + hq {
for wqq in q0..q0 + wq {
for wd in 0..3 {
(nq, nr) = hex_neighbor(wqq, wr, wd);
if edge_mat(edges, wqq, wr, nq, nr) == 0 { continue; }
mq += [wqq];
mr += [wr];
md += [wd];
}
}
}
(mq, mr, md)
}
// …and their endpoints, two per mark and in the same order, so endpoint `j` belongs to
// mark `j / 2` and its partner is `j ^ 1`.
fn chain_verts(mq: vector<integer>, mr: vector<integer>, md: vector<integer>)
-> (vector<integer>, vector<integer>) {
va: vector<integer> = [];
vb: vector<integer> = [];
for i in 0..len(mq) {
(e1, e2) = hex_edge_corners(md[i] ?? 0);
va += [hex_corner_tri_a(mq[i] ?? 0, mr[i] ?? 0, e1)];
vb += [hex_corner_tri_b(mq[i] ?? 0, mr[i] ?? 0, e1)];
va += [hex_corner_tri_a(mq[i] ?? 0, mr[i] ?? 0, e2)];
vb += [hex_corner_tri_b(mq[i] ?? 0, mr[i] ?? 0, e2)];
}
(va, vb)
}
// How many marks meet at this vertex.
fn vert_degree(va: vector<integer>, vb: vector<integer>, ka: integer,
kb: integer) -> integer {
deg = 0;
for j in 0..len(va) {
if (va[j] ?? 0) == ka && (vb[j] ?? 0) == kb { deg = deg + 1; }
}
deg
}
// Is endpoint `i` this vertex's first occurrence? Counting degrees at every occurrence
// would multiply each vertex by its own degree.
fn vert_first(va: vector<integer>, vb: vector<integer>, i: integer) -> boolean {
for k in 0..i {
if (va[k] ?? 0) == (va[i] ?? 0) && (vb[k] ?? 0) == (vb[i] ?? 0) { return false; }
}
true
}
// ONE CHAIN. The marked edges form a connected path iff, keyed by their shared hex vertices
// (exact triangle-lattice integers), exactly two vertices have degree 1 (the ends) and none has
// degree > 2. A picket comb of k disjoint edges has 2k degree-1 vertices — so this separates a
// chain from a comb with no tolerance. Returns the number of degree-1 vertices (2 for a single
// chain, 0 for empty, 4 after a break, 2k for a comb).
//
// ⚠ TWO WALLS MEETING AT A CORNER USUALLY DO NOT FUSE, so this is 4 and not 2 for an L — each
// wall keeps its own two ends. That is a fact about the marking and not a defect: a consumer
// reading it as *the marking is broken* will refuse a shape somebody drew on purpose.
//
// Example: @HXS-004
pub fn wall_chain_ends(edges: EdgeSet, q0: integer, r0: integer,
wq: integer, hq: integer) -> integer {
(mq, mr, md) = chain_marks(edges, q0, r0, wq, hq);
(va, vb) = chain_verts(mq, mr, md);
ends = 0;
for i in 0..len(va) {
if !vert_first(va, vb, i) { continue; }
if vert_degree(va, vb, va[i] ?? 0, vb[i] ?? 0) == 1 { ends = ends + 1; }
}
ends
}
// Any vertex where three or more marked edges meet — a branch, which a straight wall's boundary
// never has. Non-zero means the marking is not a simple path.
//
// Example: @HXS-004
pub fn wall_chain_branches(edges: EdgeSet, q0: integer, r0: integer,
wq: integer, hq: integer) -> integer {
(mq, mr, md) = chain_marks(edges, q0, r0, wq, hq);
(va, vb) = chain_verts(mq, mr, md);
branches = 0;
for i in 0..len(va) {
if !vert_first(va, vb, i) { continue; }
if vert_degree(va, vb, va[i] ?? 0, vb[i] ?? 0) > 2 { branches = branches + 1; }
}
branches
}
// ── THE CHAIN IN READING ORDER ─────────────────────────────────────────────
//
// The two counters above answer *how many ends* and *how many branches* and throw the walk
// itself away, so a consumer holding a marking has the numbers and not the chain. This
// returns it: an unordered set of marked edges in, the vertices in the order a walk meets
// them out — which is what `wall_read_run` and `surface_of` already consume, and what a scan
// of the store can never give, because a scan has no order at all.
//
// A CONSUMER WANTS THIS TO CUT A MARKING INTO RUNS. `wall_read_run` reads a chain's two
// ends and answers for one run; a marking with a CORNER in it has no such pair, and a closed
// room has no free end at all. With the chain ordered, the question becomes *where along it
// does one run stop and the next begin*, which is a decision about spans rather than a search
// over the plane.
//
// ⚠ ONE CALL IS ONE CHAIN, AND A MARKING IS OFTEN SEVERAL. Two walls meeting at a corner do
// not necessarily fuse — an L has FOUR degree-1 vertices, so each wall keeps its own two ends
// and the marking is two chains. `wc_n` is how many there are and `nth` picks one; a consumer
// that walks one and stops describes half an L and calls the rest a residual.
//
// ⚠ A CLOSED LOOP HAS NO CANONICAL START. With no degree-1 vertex the seed is the first
// endpoint in scan order, which lands wherever the scan lands — so a room of four walls comes
// back as one chain that begins and ends in the middle of the same wall. `wc_loop` says so;
// what to do about it belongs to the consumer, because only the consumer knows whether the
// first and last spans it cut are one run.
//
// ⚠ AND AT A BRANCH THE ORDER IS ONE OF SEVERAL. Where three or more marks meet, the walk
// continues along the first unused one in scan order — deterministic, and *a* traversal rather
// than *the* traversal. `wc_branch` counts the vertices this chain passes through where that
// choice was made, so a consumer needing a simple path can refuse rather than believe.
pub struct WallChain {
pub wc_ok: boolean, // there is an `nth` chain
pub wc_n: integer, // how many chains the marking falls into
pub wc_branch: integer, // vertices on this chain where three or more marks met
pub wc_loop: boolean, // the walk came back to the vertex it started from
pub wc_va: vector<integer>, // vertex `i`, in exact triangle-lattice coordinates
pub wc_vb: vector<integer>,
pub wc_x: vector<float>, // …and in world units, from `tri_x` / `tri_y`
pub wc_y: vector<float>,
pub wc_q: vector<integer>, // the mark walked between vertex `i` and `i + 1`: its cell
pub wc_r: vector<integer>,
pub wc_d: vector<integer>, // …and the direction out of that cell
}
// How many marks meet at this vertex, counting only marks the walk has not taken yet. A
// vertex in mid-chain has one left — the way ahead — however many it started with, which is
// why the branch count below is asked of the FULL degree instead.
fn deg_left(va: vector<integer>, vb: vector<integer>, used: vector<boolean>,
ka: integer, kb: integer) -> integer {
deg = 0;
for j in 0..len(va) {
if used[(j / 2) ?? 0] ?? false { continue; }
if (va[j] ?? 0) == ka && (vb[j] ?? 0) == kb { deg = deg + 1; }
}
deg
}
// The `nth` chain of this marking, in the order a walk meets its vertices. `nth = 0` is the
// first; `wc_n` is how many there are, and is filled in whether or not `nth` names one.
pub fn wall_chain_walk(edges: EdgeSet, q0: integer, r0: integer, wq: integer,
hq: integer, nth: integer) -> WallChain {
(mq, mr, md) = chain_marks(edges, q0, r0, wq, hq);
(va, vb) = chain_verts(mq, mr, md);
used: vector<boolean> = [];
for _i in 0..len(mq) { used += [false]; }
ok = false;
closed = false;
branch = 0;
pv_a: vector<integer> = [];
pv_b: vector<integer> = [];
pw_x: vector<float> = [];
pw_y: vector<float> = [];
pm_q: vector<integer> = [];
pm_r: vector<integer> = [];
pm_d: vector<integer> = [];
found = 0;
for _pass in 0..len(mq) + 1 {
// ⚠ A FREE END FIRST, so a chain that HAS one is walked from it and comes back whole;
// seeding a path in its middle would return two half-chains.
sa = 0;
sb = 0;
have = false;
for j in 0..len(va) {
if used[(j / 2) ?? 0] ?? false { continue; }
if deg_left(va, vb, used, va[j] ?? 0, vb[j] ?? 0) != 1 { continue; }
sa = va[j] ?? 0;
sb = vb[j] ?? 0;
have = true;
break;
}
if !have {
for j in 0..len(va) {
if used[(j / 2) ?? 0] ?? false { continue; }
sa = va[j] ?? 0;
sb = vb[j] ?? 0;
have = true;
break;
}
}
if !have { break; }
ca = sa;
cb = sb;
cv_a: vector<integer> = [sa];
cv_b: vector<integer> = [sb];
cm_q: vector<integer> = [];
cm_r: vector<integer> = [];
cm_d: vector<integer> = [];
cbr = 0;
for _step in 0..len(mq) {
if vert_degree(va, vb, ca, cb) > 2 { cbr = cbr + 1; }
pick = 0 - 1;
na = 0;
nb = 0;
for j in 0..len(va) {
e = (j / 2) ?? 0;
if used[e] ?? false { continue; }
if (va[j] ?? 0) != ca || (vb[j] ?? 0) != cb { continue; }
pick = e;
o = if ((j % 2) ?? 0) == 0 { j + 1 } else { j - 1 };
na = va[o] ?? 0;
nb = vb[o] ?? 0;
break;
}
if pick < 0 { break; }
used[pick] = true;
cm_q += [mq[pick] ?? 0];
cm_r += [mr[pick] ?? 0];
cm_d += [md[pick] ?? 0];
cv_a += [na];
cv_b += [nb];
ca = na;
cb = nb;
}
if found == nth {
ok = true;
branch = cbr;
closed = ca == sa && cb == sb && len(cm_q) > 0;
pv_a = cv_a;
pv_b = cv_b;
pm_q = cm_q;
pm_r = cm_r;
pm_d = cm_d;
// ⚠ THE WORLD POINT COMES FROM THE KEY, NEVER FROM THE CELL AND CORNER IT WAS FOUND
// AT. One vertex is reached from two different cells, and reading its position off
// whichever cell the scan happened to be in would make the same point two points.
for v in 0..len(cv_a) {
pw_x += [tri_x(cv_a[v] ?? 0, cv_b[v] ?? 0)];
pw_y += [tri_y(cv_a[v] ?? 0, cv_b[v] ?? 0)];
}
}
found = found + 1;
}
WallChain { wc_ok: ok, wc_n: found, wc_branch: branch, wc_loop: closed,
wc_va: pv_a, wc_vb: pv_b, wc_x: pw_x, wc_y: pw_y,
wc_q: pm_q, wc_r: pm_r, wc_d: pm_d }
}
// ── THE TRIANGLE LATTICE, AND WHY WIDTH CANNOT COME FROM IT ────────────────
//
// The wall band lives in the TRIANGLE lattice: equilateral, side = 1/3 of a hex edge, basis
// U at 30 degrees and V at 90 degrees (DESIGN.md §10.6). A doubled-coordinate step (k, m)
// is the triangle-lattice vector (3k, 3(m-k)/2) — integral because every lattice point has
// k = m (mod 2) — reduced here to its primitive form.
//
// THE POINT OF THIS BLOCK is one question: can a wall be made EXACTLY the same width in all
// 24 directions by counting lattice rows? It cannot, and the reason is an integer fact.
// For a primitive lattice vector `v`, the parallel lattice lines in its direction are spaced
//
// spacing = covolume / |v| = S^2*sqrt(3)/2 / (S*sqrt(N)) = S*sqrt(3) / (2*sqrt(N))
//
// where N = |v|^2 / S^2 = a^2 + ab + b^2, an INTEGER. So two directions' spacings are in
// ratio sqrt(N2/N1), and that is rational — the only way integer row-counts could ever make
// the two widths equal — exactly when N1*N2 is a perfect square. Among the 24 directions
// only three values of N occur, and no pair of them multiplies to a square.
fn igcd(x: integer, y: integer) -> integer {
a = if x < 0 { 0 - x } else { x };
b = if y < 0 { 0 - y } else { y };
// `b != 0` guards the modulo, but loft types `%` as nullable regardless (DN1), so the
// fallback is discharged here rather than left to leak into `b`.
while b != 0 { t = (a % b) ?? 0; a = b; b = t; }
a
}
// The primitive triangle-lattice vector of direction `d24`.
pub fn tri_a(d24: integer) -> integer {
k = wall_step_k(d24);
m = wall_step_m(d24);
ta = 3 * k;
tb = 3 * (m - k) / 2;
g = igcd(ta, tb);
if g == 0 { return ta; }
(ta / g) ?? 0
}
// The `b` component of a unit step in wall direction `d24`, for walking the triangular
// lattice the way `wall_step_k`/`_m` walk the doubled one.
pub fn tri_b(d24: integer) -> integer {
k = wall_step_k(d24);
m = wall_step_m(d24);
ta = 3 * k;
tb = 3 * (m - k) / 2;
g = igcd(ta, tb);
if g == 0 { return tb; }
(tb / g) ?? 0
}
// N = |v|^2 / S^2 for direction `d24` — an exact integer, never a float comparison.
// The direction's `N`. Only THREE values occur over the 24 — measured, not assumed.
//
// Example: @HXS-002
pub fn tri_n2(d24: integer) -> integer {
a = tri_a(d24);
b = tri_b(d24);
a * a + a * b + b * b
}
// Is n a perfect square? Integer-only, so a length test stays exact where a sqrt would
// introduce the first float in an otherwise exact chain.
pub fn is_square(n: integer) -> boolean {
if n < 0 { return false; }
r = 0;
while r * r < n { r = r + 1; }
r * r == n
}
// Are the two directions' lattice-line spacings COMMENSURABLE — i.e. can integer counts of
// rows in each ever be exactly equal? Only if sqrt(N2/N1) is rational, i.e. N1*N2 square.
// Example: @HXS-002
pub fn commensurable(n1: integer, n2: integer) -> boolean { is_square(n1 * n2) }
// ── WHERE A LINE MAY START AND END — the editor's doorstep (SPEC K-FIT) ────
//
// The editor can draw only straight lines, and the stored form is hex-edge marks. moros'
// emitter stands a quad on each marked edge, so THE MESH'S EXTREMITIES ARE HEX VERTICES —
// an endpoint anywhere else simply cannot be represented, and the rebuilt wall would stop
// at the nearest vertex instead. So `fits?` for a line is exactly:
//
// both endpoints are HEX VERTICES, and B - A is a whole number of the direction's
// vertex-to-vertex period.
//
// THE ARITHMETIC. In triangle coordinates, hex vertices AND centres are precisely the
// points with a = b = 0 (mod 3). The class
//
// c = ((a - b) / 3) mod 3
//
// separates them: c = 0 is a CENTRE (never an endpoint), c = 1 and c = 2 are the two vertex
// sublattices of the honeycomb. A primitive direction vector `v` never has both coordinates
// divisible by 3, so a displacement `n*v` reaches a hex-scale point only when 3 | n — write
// n = 3p, and the class then shifts by `p*(a_v - b_v) mod 3`. Two cases follow, and they
// are the whole answer:
//
// (a_v - b_v) = 0 (mod 3) the shift is always 0: EVERY p works, from EVERY vertex.
// This is the N=3 class (0/60/120/...) and the N=39 in-between.
// otherwise the shift cycles: exactly TWO OF EVERY THREE p work, and
// WHICH two depends on the start vertex's class. This is the
// N=1 class (30/90/150/...), where a line runs one hex edge,
// another, and then meets a hex CENTRE — so p = 2 (mod 3) has
// no endpoint to land on.
fn imod3(v: integer) -> integer {
r = (v % 3) ?? 0;
if r < 0 { r + 3 } else { r }
}
// The class of a triangle-lattice point: 0 = hex centre, 1 and 2 = the two vertex classes.
// Only meaningful once `tri_on_hex_scale` holds.
pub fn tri_class(a: integer, b: integer) -> integer { imod3(((a - b) / 3) ?? 0) }
// Does this triangular-lattice point fall on the coarser HEX scale? The wall lattice is
// three times finer, so this is the test for "is this also a cell-level position".
pub fn tri_on_hex_scale(a: integer, b: integer) -> boolean {
imod3(a) == 0 && imod3(b) == 0
}
// Is this point a hex CORNER rather than a cell centre? Both sit on the hex scale, and
// only the class tells them apart — which is what a wall needs to know before it turns.
pub fn tri_is_vertex(a: integer, b: integer) -> boolean {
tri_on_hex_scale(a, b) && tri_class(a, b) != 0
}
// The endpoint of a run of `p` periods of direction `d24` from (a0, b0). One period is
// THREE primitive steps — the smallest displacement that can reach a hex-scale point at all.
pub fn wall_end_a(d24: integer, a0: integer, p: integer) -> integer {
a0 + 3 * p * tri_a(d24)
}
// Where a wall run of `p` steps from `b0` ends, on the `b` axis. Compute the end without
// walking the run, so a length or a bound costs nothing.
pub fn wall_end_b(d24: integer, b0: integer, p: integer) -> integer {
b0 + 3 * p * tri_b(d24)
}
// THE DOORSTEP ITSELF. `p` periods of `d24` from (a0,b0): admissible or not.
//
// Example: @HXS-003
pub fn wall_run_ok(d24: integer, a0: integer, b0: integer, p: integer) -> boolean {
if p <= 0 { return false; }
if !tri_is_vertex(a0, b0) { return false; }
tri_is_vertex(wall_end_a(d24, a0, p), wall_end_b(d24, b0, p))
}
// The smallest admissible run from this vertex in this direction — the editor's snap unit.
// -1 if the start is not a vertex at all.
// Example: @HXS-003
pub fn wall_min_p(d24: integer, a0: integer, b0: integer) -> integer {
if !tri_is_vertex(a0, b0) { return -1; }
for mp in 1..4 { if wall_run_ok(d24, a0, b0, mp) { return mp; } }
-1
}
// A REFUSAL NAMES ITS RESTRICTION (K-FIT): given a wanted length in periods, the nearest
// admissible one, searching outward so ties go to the shorter run. 0 if none exists.
// Example: @HXS-003
pub fn wall_snap_p(d24: integer, a0: integer, b0: integer, want: integer) -> integer {
if want < 1 { return 0; }
if wall_run_ok(d24, a0, b0, want) { return want; }
for sd in 1..4 {
if wall_run_ok(d24, a0, b0, want - sd) { return want - sd; }
if wall_run_ok(d24, a0, b0, want + sd) { return want + sd; }
}
0
}
// World position of a triangle-lattice point, in world units.
pub fn tri_x(a: integer, _b: integer) -> float { (a as float) * 0.28867513459481287 }
// A triangular-lattice point in world y. The last step before rendering, and the only
// place these exact coordinates become floats.
pub fn tri_y(a: integer, b: integer) -> float {
(a as float) / 6.0 + (b as float) / 3.0
}
// The world length of `p` periods of `d24`, in world units.
// Example: @HXS-003
pub fn wall_run_len(d24: integer, p: integer) -> float {
ra = 3 * p * tri_a(d24);
rb = 3 * p * tri_b(d24);
wx = tri_x(ra, rb);
wy = tri_y(ra, rb);
sqrt(wx * wx + wy * wy) ?? 0.0
}
// Build a wall from an ADMISSIBLE run: anchor vertex (a0,b0) in triangle coords, `p` periods of
// direction `d24`. The principled constructor — endpoints are hex vertices and the run is legal
// (`wall_run_ok`), so the boundary chain terminates cleanly at both ends. Defined here, below
// `tri_x`/`wall_end_a`, so their float returns are known at the call site (see the note by
// `wall_write`).
pub fn wall_from_run(d24: integer, a0: integer, b0: integer, p: integer, mat: integer) -> Wall {
fea = wall_end_a(d24, a0, p);
feb = wall_end_b(d24, b0, p);
fax = tri_x(a0, b0);
fay = tri_y(a0, b0);
fbx = tri_x(fea, feb);
fby = tri_y(fea, feb);
fox = (fax + fbx) * 0.5;
foy = (fay + fby) * 0.5;
fvx = fbx - fax;
fvy = fby - fay;
fln = sqrt(fvx * fvx + fvy * fvy) ?? 0.0;
Wall { wa_ox: fox, wa_oy: foy, wa_dx: (fvx / fln) ?? 1.0, wa_dy: (fvy / fln) ?? 0.0,
wa_half: fln * 0.5, wa_mat: mat }
}
// ── FROM AN ARBITRARY POINT IN THE PLANE TO A LEGAL LINE ───────────────────
//
// The editor's real input is a mouse: two arbitrary float points, neither of them on
// anything. Two snaps turn that into a legal wall, and both are exact once the doorstep
// above is known.
//
// SNAP 1 — the anchor. Round to the nearest HEX-SCALE point (vertices and centres
// together form a triangular lattice of spacing 1 wu, so this is one cube-rounding), then,
// if that landed on a centre, take the nearest of its six neighbours — which are all
// vertices. That second step is complete, not a heuristic: the query point lies in the
// rounded point's Voronoi cell (circumradius 1/sqrt(3) = 0.577), so every vertex outside
// its six neighbours is at least sqrt(3) - 0.577 = 1.155 away while a neighbour is at most
// 1 + 0.577 — the nearest vertex is always among the six.
//
// SNAP 2 — the far end. For each of the 24 directions, project the target onto that
// direction to get a real-valued period count, then test the few integers around it (the
// N=1 directions refuse one in three, so the nearest legal `p` is not always the nearest
// integer). Keep the globally closest admissible endpoint over all 24. That is exactly
// "move towards any point that gives a correct line, in any of the 24 directions".
//
// `hex_round` is hex_grid's — the lattice rounding is the library's job, not ours (L11).
const INV_SQ3 = 0.5773502691896258;
// Nearest HEX-SCALE lattice point (vertex OR centre) to a world point, in triangle
// coordinates. The hex-scale lattice is spanned by e1 = (sqrt3/2, 1/2) and e2 = (0, 1),
// which is (a,b) = (3i, 3j) — a triangular lattice of spacing 1 wu.
//
// Example: @HXS-008
pub fn hex_scale_round(x: float, y: float) -> (integer, integer) {
al = x * 2.0 * INV_SQ3;
be = y - x * INV_SQ3;
(ri, rj) = hex_round(al, be);
(3 * ri, 3 * rj)
}
// The six hex-scale neighbours, as triangle-coordinate deltas.
fn hs_nb_a(n: integer) -> integer {
if n == 0 { return 3; }
if n == 1 { return 0; }
if n == 2 { return -3; }
if n == 3 { return -3; }
if n == 4 { return 0; }
3
}
fn hs_nb_b(n: integer) -> integer {
if n == 0 { return 0; }
if n == 1 { return 3; }
if n == 2 { return 3; }
if n == 3 { return 0; }
if n == 4 { return -3; }
-3
}
// SNAP 1: the nearest HEX VERTEX to an arbitrary world point, in triangle coordinates.
// Example: @HXS-008
pub fn nearest_vertex(x: float, y: float) -> (integer, integer) {
(ca, cb) = hex_scale_round(x, y);
if tri_is_vertex(ca, cb) { return (ca, cb); }
// it rounded onto a centre — the nearest vertex is one of the six neighbours
ba = ca;
bb = cb;
best = 1.0e30;
for nv in 0..6 {
na = ca + hs_nb_a(nv);
nb2 = cb + hs_nb_b(nv);
dx = tri_x(na, nb2) - x;
dy = tri_y(na, nb2) - y;
d2 = dx * dx + dy * dy;
if d2 < best { best = d2; ba = na; bb = nb2; }
}
(ba, bb)
}
// How far the endpoint of `p` periods of `d24` from (a0,b0) lands from a target point.
pub fn run_end_dist(d24: integer, a0: integer, b0: integer, p: integer,
tx: float, ty: float) -> float {
ea = wall_end_a(d24, a0, p);
eb = wall_end_b(d24, b0, p);
dx = tri_x(ea, eb) - tx;
dy = tri_y(ea, eb) - ty;
sqrt(dx * dx + dy * dy) ?? 0.0
}
// The period count that projects nearest to the target, for direction `d24` — real-valued,
// rounded. This is only a STARTING GUESS: the N=1 directions refuse one p in three, so
// `snap_run_*` searches outward from here.
fn project_p(d24: integer, a0: integer, b0: integer, tx: float, ty: float) -> integer {
pa = 3 * tri_a(d24);
pb = 3 * tri_b(d24);
px = tri_x(pa, pb);
py = tri_y(pa, pb);
vx = tx - tri_x(a0, b0);
vy = ty - tri_y(a0, b0);
den = px * px + py * py;
if den <= 0.0 { return 1; }
(round(((vx * px + vy * py) / den) ?? 0.0)) as integer
}
// How far either side of the projection to look for a legal p. Three is enough: the only
// refusals are the N=1 classes' one-in-three, so a legal p is never more than 2 away.
const SNAP_SPAN = 3;
// SNAP 2, direction part: the direction whose best legal endpoint lands nearest the target.
// Example: @HXS-008
pub fn snap_run_d24(a0: integer, b0: integer, tx: float, ty: float) -> integer {
bestd = -1;
best = 1.0e30;
for sd in 0..D24 {
p0 = project_p(sd, a0, b0, tx, ty);
for sp in 0..2 * SNAP_SPAN + 1 {
pp = p0 - SNAP_SPAN + sp;
if wall_run_ok(sd, a0, b0, pp) {
dd = run_end_dist(sd, a0, b0, pp, tx, ty);
if dd < best { best = dd; bestd = sd; }
}
}
}
bestd
}
// SNAP 2, length part: the legal period count in that direction nearest the target.
// 0 when nothing legal was found, which the caller must treat as a refusal.
// Example: @HXS-008
pub fn snap_run_p(d24: integer, a0: integer, b0: integer, tx: float, ty: float) -> integer {
bestp = 0;
best = 1.0e30;
p0 = project_p(d24, a0, b0, tx, ty);
for sq in 0..2 * SNAP_SPAN + 1 {
pq = p0 - SNAP_SPAN + sq;
if wall_run_ok(d24, a0, b0, pq) {
dq = run_end_dist(d24, a0, b0, pq, tx, ty);
if dq < best { best = dq; bestp = pq; }
}
}
bestp
}
// ── SNAP 3: THE AIM IDIOM — A HEADING AND A LENGTH, NOT A TARGET POINT ──────
//
// `snap_run_d24`/`snap_run_p` above take an ENDPOINT. That is what a mouse drag has and
// what a walked trace has, and for years it was the only shape an author's input came in.
//
// A CONTROLLER HAS NEITHER. It has a facing and a push: an angle and a magnitude, with no
// point anywhere in it. Constructing a target point from those two and calling the pair
// above is the obvious implementation and it is the wrong one, for a reason worth stating
// because it is invisible from the call site:
//
// ⚠ THE TWO SNAPS MINIMISE DIFFERENT THINGS. `snap_run_d24` picks the direction whose best
// legal ENDPOINT lands nearest the target — a single 2-D distance, in which a direction
// error and a length error are freely traded against each other. An author who aims is
// making two separate statements, "this way" and "this far", and expects the first to be
// honoured as a direction. Round-tripping through a point silently lets a length error buy
// a direction change, which on the 1.1021-degree in-between classes (@HXS-002) is exactly
// where it hurts.
//
// So this quantises the two AXES SEPARATELY: the angle against `D`, the length against the
// period. It is a peer of the endpoint snap, not a wrapper on it, and whether the two agree
// is therefore a real question rather than a tautology.
// The direction in `D` nearest a world angle in degrees. The inverse of
// `wall_angle_actual`, and it must be asked of the ACTUAL angles rather than of the nominal
// 15-degree multiples: half of `D` sits 1.1021 degrees off its nominal (@HXS-002), so the
// nearest-nominal answer and the nearest-actual answer differ on a band around every
// in-between. Monotone and complete over a full turn — 24 boundaries, each +1.
//
// Example: @HXS-010
pub fn wall_nearest_d24(deg: float) -> integer {
bd = -1;
bb = 1.0e30;
for nd in 0..D24 {
x = deg - wall_angle_actual(nd);
while x > 180.0 { x = x - 360.0; }
while x < 0.0 - 180.0 { x = x + 360.0; }
if x < 0.0 { x = 0.0 - x; }
if x < bb { bb = x; bd = nd; }
}
bd
}
// The run an author reaches by AIMING: from vertex (a0,b0), facing `deg`, wanting `dist`
// world units. Returns `(d24, p)` — the same pair the endpoint snap returns, so both
// idioms hand the rest of the library one shape of answer.
//
// `p = 0` is a REFUSAL and the caller must treat it as one: it means no legal run exists
// from that anchor in that direction near that length, which happens when (a0,b0) is not a
// vertex at all, or when `dist` is shorter than the smallest admissible run.
//
// Example: @HXS-010
pub fn snap_run_from_heading(a0: integer, b0: integer, deg: float,
dist: float) -> (integer, integer) {
hd = wall_nearest_d24(deg);
if hd < 0 { return (-1, 0); }
// One period's world length. Linear in `p`, so the wanted count is a division — and
// `wall_snap_p` is the library's own K-fit for turning a wanted count into a legal one,
// ties to the shorter run. Searching for it here would be a second rule.
per = wall_run_len(hd, 1);
if per <= 0.0 { return (hd, 0); }
hw = (round((dist / per) ?? 0.0)) as integer;
(hd, wall_snap_p(hd, a0, b0, hw))
}
// ── EVALUATING THE MARKS BACK TO THE MESH (crawler / moros' own rule) ───────
//
// This is the second half of the round trip, and it is NOT ours to invent: moros already
// evaluates the three wall slots to geometry in `moros_render::emit_hex_walls`, and the
// rule is one line — a marked edge becomes the quad standing on the segment between its
// two corners. `hex_edge_corners` is that segment, so the mesh a marked field produces is
// fully determined here with no renderer, no `Mesh`, no graphics import.
//
// WHAT THAT MESH IS. It is the SAWTOOTH — the chain of hex edges the band crossed — not
// the straight line we drew. That is the honest state of the round trip: the foxel stores
// where the wall went, and recovering the straight line from it is `rebuild`'s job. So
// the measurement below is not "does the mesh equal the wall" (it cannot) but "how far
// does the mesh stray from it", which is the quantity the recovery has to undo.
//
// Perpendicular distance from (px,py) to the wall's infinite centre line.
pub fn wall_offset_of(w: Wall, px: float, py: float) -> float {
d = (px - w.wa_ox) * (0.0 - w.wa_dy) + (py - w.wa_oy) * w.wa_dx;
if d < 0.0 { 0.0 - d } else { d }
}
// The worst perpendicular stray of any emitted segment endpoint, over the marked field.
// Only edges whose midpoint lies within the run are measured — past the ends there is no
// line to be near.
// Example: @HXS-008
pub fn wall_mesh_stray(w: Wall, edges: EdgeSet, q0: integer, r0: integer,
wq: integer, hq: integer) -> float {
worst = 0.0;
for wr in r0..r0 + hq {
for wqq in q0..q0 + wq {
for wd in 0..6 {
(nq, nr) = hex_neighbor(wqq, wr, wd);
if edge_mat(edges, wqq, wr, nq, nr) != 0 {
(e1, e2) = hex_edge_corners(wd);
ax = hex_corner_x(wqq, wr, e1);
ay = hex_corner_y(wqq, wr, e1);
bx = hex_corner_x(wqq, wr, e2);
by = hex_corner_y(wqq, wr, e2);
alo = ((ax + bx) / 2.0 - w.wa_ox) * w.wa_dx
+ ((ay + by) / 2.0 - w.wa_oy) * w.wa_dy;
if alo < 0.0 { alo = 0.0 - alo; }
if alo <= w.wa_half {
da = wall_offset_of(w, ax, ay);
db = wall_offset_of(w, bx, by);
if da > worst { worst = da; }
if db > worst { worst = db; }
}
}
}
}
}
worst
}
// How many distinct segments the mesh emitter would produce for the marked field — the
// vertex count of moros' wall mesh divided by the per-edge quad, i.e. the segment count.
// Example: @HXS-005
pub fn wall_mesh_segments(edges: EdgeSet, q0: integer, r0: integer,
wq: integer, hq: integer) -> integer {
n = 0;
for wr in r0..r0 + hq {
for wqq in q0..q0 + wq {
for wd in 0..3 {
(nq, nr) = hex_neighbor(wqq, wr, wd);
if edge_mat(edges, wqq, wr, nq, nr) != 0 { n = n + 1; }
}
}
}
n
}
// ── READING A RUN BACK OUT OF THE FIELD ────────────────────────────────────
//
// The reader `OD-13` needs, and it is exact: **a run's two chain ENDS are its endpoints.** A
// marked run is one connected chain with exactly two degree-1 vertices (`X32`), those vertices are
// hex VERTICES on the triangle lattice, and the doorstep put the authored endpoints on exactly
// those points (§10.10). So recovery is a degree count and one integer division — no averaging, no
// tolerance, nothing to fit.
//
// ⚠ **USE `hex_neighbor` WITH `hex_edge_corners`, NEVER `nb_q`/`nb_r`.** Both enumerate the same
// six neighbours but in a DIFFERENT ORDER, so pairing `hex_edge_corners(d)` — indexed by hex_grid's
// order — with `nb_q(q,r,d)` reads the corners of a different edge. Three separate reader
// instruments were wrong for exactly this reason before it was found, and it is `SPEC` **L11**'s
// named hazard and `X26`'s failure mode: the counts stay plausible while the geometry is nonsense.
// The two degree-1 vertices of the marked chain, in triangle coordinates. `ok` is false unless
// there are exactly two — a fragmented or branching marking is REFUSED, never averaged into an
// answer.
//
// Example: @HXS-005
pub fn wall_read_ends(edges: EdgeSet, q0: integer, r0: integer, wq: integer, hq: integer)
-> (integer, integer, integer, integer, boolean) {
va: vector<integer> = [];
vb: vector<integer> = [];
for rr in r0..r0 + hq {
for qq in q0..q0 + wq {
for dd in 0..3 {
(nq, nr) = hex_neighbor(qq, rr, dd);
if edge_mat(edges, qq, rr, nq, nr) != 0 {
(k1, k2) = hex_edge_corners(dd);
va += [hex_corner_tri_a(qq, rr, k1)];
vb += [hex_corner_tri_b(qq, rr, k1)];
va += [hex_corner_tri_a(qq, rr, k2)];
vb += [hex_corner_tri_b(qq, rr, k2)];
}
}
}
}
n = 0;
a1 = 0; b1 = 0; a2 = 0; b2 = 0;
for i in 0..len(va) {
first = true;
for k in 0..i {
if (va[k] ?? 0) == (va[i] ?? 0) && (vb[k] ?? 0) == (vb[i] ?? 0) { first = false; }
}
if first {
deg = 0;
for j in 0..len(va) {
if (va[j] ?? 0) == (va[i] ?? 0) && (vb[j] ?? 0) == (vb[i] ?? 0) { deg = deg + 1; }
}
if deg == 1 {
n = n + 1;
if n == 1 { a1 = va[i] ?? 0; b1 = vb[i] ?? 0; }
if n == 2 { a2 = va[i] ?? 0; b2 = vb[i] ?? 0; }
}
}
}
(a1, b1, a2, b2, n == 2)
}
// Recover the whole run: direction, anchor and period count, all exact integers. One period of
// `d24` displaces a run by `3·tri(d24)`, so the endpoint displacement pins both `d24` and `p`; the
// answer is canonicalised to the end that gives `p > 0`. `ok` is false when the marking is not a
// single chain, or when no direction divides the displacement — an unreadable field is REFUSED, not
// approximated (law **P4**).
//
// WHAT IT CANNOT RECOVER IS THE ORIENTATION, because the field does not store one: A-to-B and
// B-to-A mark the identical edges. The answer is canonicalised to `p > 0`, so the direction
// comes back as `d24` OR as `d24 + 12` with the other endpoint as the anchor — both naming the
// same wall. `read.d == d` is therefore the wrong round-trip assertion; compare the ENDPOINTS.
//
// Example: @HXS-005
pub fn wall_read_run(edges: EdgeSet, q0: integer, r0: integer, wq: integer, hq: integer)
-> (integer, integer, integer, integer, boolean) {
(a1, b1, a2, b2, okends) = wall_read_ends(edges, q0, r0, wq, hq);
if !okends { return (0, 0, 0, 0, false); }
for d in 0..24 {
ta = tri_a(d);
tb = tri_b(d);
da = a2 - a1;
db = b2 - b1;
// parallel? exact integer cross product
if da * tb - db * ta == 0 {
p = 0;
if ta != 0 { p = (da / (3 * ta)) ?? 0; } else { p = (db / (3 * tb)) ?? 0; }
if p > 0 && a1 + 3 * p * ta == a2 && b1 + 3 * p * tb == b2 {
return (d, a1, b1, p, true);
}
}
}
(0, 0, 0, 0, false)
}
tests/01-hex-shape.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_shape — the conformance tests: the box in 12 directions, and the arc whose radius is a shell.
//
// Contract only; the exhaustive measurement is hexbody's `tests/box.loft` and `tests/arc.loft`.
use hex_shape::*;
use hex_form::*;
use hex_field::*;
use hex_grid::*;
fn main() {
b = box_new(0, 0, 5, 4, 0, false);
assert(box_hw(b) > 0.0, "a box has a positive half-width");
}
// A consumer places a wall by its centre (hexbody's wall gate reads it back): the centre is
// readable outside this library.
fn test_a_wall_keeps_the_centre_it_was_made_with() {
w = wall_new(0, 2.5, -1.5, 3.0, 4);
assert(w.wa_ox == 2.5 && w.wa_oy == -1.5, "the centre is (2.5, -1.5), got ({w.wa_ox}, {w.wa_oy})");
}
// A consumer places a footprint by its anchor cell and rotation (moros's editor gestures do):
// both are readable outside this library.
fn test_a_box_keeps_its_anchor_and_rotation() {
b = box_new(3, -2, 4, 2, 5, false);
assert(b.bx_cq == 3 && b.bx_cr == -2 && b.bx_rot == 5, "anchor (3, -2) rot 5, got ({b.bx_cq}, {b.bx_cr}) {b.bx_rot}");
}
fn test_the_two_orbits_are_distinct() {
// 60-degree steps are a lattice symmetry (orbit 0); 30-degree steps are not (orbit 1). A
// consumer choosing a box angle must know which family it is in, so the split is asserted.
assert(box_orbit(0) == 0, "rot 0 is the edge family");
assert(box_orbit(1) == 1, "rot 1 is the other family");
assert(box_orbit(2) == 0, "rot 2 is back to the edge family");
}
fn test_an_arc_radius_quantises_to_a_shell() {
// `X49`: an arc's radius is not continuous -- it realises only the shells 3k^2+m^2. A disk of
// "radius 6" fills to the nearest shell, and its recovered shell is one of the realisable set.
cells = hexset_chunk(-20, -20, 41, 41);
n = arc_fill(0, 0, 36, cells); // n24 = 36 is a shell (radius 6)
assert(n > 0, "the disk filled {n} cells");
shells = arc_shells_upto(64);
found = false;
for si in 0..len(shells) { if (shells[si] ?? -1) == 36 { found = true; } }
assert(found, "36 is one of the realisable shells");
}
fn test_an_arcs_centre_recovers_exactly() {
cells = hexset_chunk(-20, -20, 41, 41);
arc_fill(3, 2, 12, cells);
(cq, cr, ok) = arc_recover_centre(cells);
assert(ok, "the centre was recovered");
assert(cq == 3 && cr == 2, "the centre comes back exactly, got ({cq}, {cr})");
}
// ── SNAP 3: THE AIM IDIOM ───────────────────────────────────────────────────
//
// ⚠ WHAT IS DELIBERATELY NOT ASSERTED HERE: that aiming and tracing agree. They
// minimise different things on purpose (see `snap_run_from_heading`'s note), so
// whether they coincide is a MEASUREMENT and not a contract — a test asserting it
// would either be vacuous or would freeze an answer nobody has looked at yet. The
// consumer measures it; these tests pin the properties the function must have for
// that measurement to mean anything.
fn test_aiming_at_a_directions_own_angle_returns_it() {
// The weakest of these, and it is here as a FIXTURE GUARD rather than as a finding:
// a table checked against the table cannot be surprised. It exists so that the three
// tests below, which can be surprised, are known to be running against a sane map.
for d in 0..24 {
got = wall_nearest_d24(wall_angle_actual(d));
assert(got == d, "aiming at d24 {d}'s own angle returns {got}");
}
}
fn test_the_direction_map_is_monotone_and_complete() {
// ⚠ THE PROPERTY AN AUTHOR ACTUALLY FEELS. Sweeping the facing must cross exactly 24
// boundaries, each advancing by one: if turning further could hand back an EARLIER
// direction, no amount of practice would let anyone steer to a target, and a live
// preview of "the direction you would get" would be a lie at the folds.
changes = 0;
back = 0;
jump = 0;
pd = wall_nearest_d24(0.0);
deg = 0.0;
while deg < 360.0 {
cur = wall_nearest_d24(deg);
if cur != pd {
changes = changes + 1;
step = ((cur - pd) % 24 + 24) % 24;
if step != 1 {
if step == 23 { back = back + 1; } else { jump = jump + 1; }
}
pd = cur;
}
deg = deg + 0.02;
}
assert(back == 0, "{back} boundaries handed back an earlier direction");
assert(jump == 0, "{jump} boundaries skipped a direction");
assert(changes == 24, "one full turn crosses 24 boundaries, counted {changes}");
}
fn test_nearest_actual_is_not_nearest_nominal() {
// ⚠ THE WHOLE REASON THIS FUNCTION EXISTS. Half of `D` sits 1.1021 degrees off its
// nominal 15-degree multiple (@HXS-002), so quantising against the NOMINAL grid picks a
// different direction on a band around every in-between. If this test ever goes green
// the other way, `wall_nearest_d24` is an expensive way to divide by 15.
diff = 0;
deg = 0.0;
while deg < 360.0 {
k = deg / 15.0 ?? 0.0;
ki = 0;
if k >= 0.0 { ki = (k + 0.5) as integer; } else { ki = (k - 0.5) as integer; }
nom = ((ki % 24) + 24) % 24;
if wall_nearest_d24(deg) != nom { diff = diff + 1; }
deg = deg + 0.05;
}
assert(diff > 0, "nearest-in-D never differs from the nominal 15-degree grid");
}
fn test_an_aimed_run_is_always_admissible() {
// ⚠ THE SAFETY PROPERTY, AND THE ONE A WRAPPER WOULD HAVE GOT FOR FREE WITHOUT MEANING
// IT. This function reaches `p` by dividing a length, not by searching endpoints, so
// nothing in its arithmetic knows about `wall_run_ok`'s one-in-three refusals — the
// guarantee comes from `wall_snap_p` and has to be checked rather than assumed.
(va, vb) = nearest_vertex(0.0, 0.0);
assert(tri_is_vertex(va, vb), "the fixture anchor is a vertex");
tried = 0;
legal = 0;
refused = 0;
for i in 0..72 {
ang = (i as float) * 5.0;
for j in 1..9 {
dist = (j as float) * 1.37; // deliberately not a multiple of any period
(d, p) = snap_run_from_heading(va, vb, ang, dist);
tried = tried + 1;
if p == 0 { refused = refused + 1; continue; }
assert(d >= 0 && d < 24, "a returned direction is in D, got {d}");
assert(wall_run_ok(d, va, vb, p),
"aiming {ang} deg for {dist} gave d24 {d} p {p}, which is NOT admissible");
legal = legal + 1;
}
}
assert(tried == 576, "the sweep ran, {tried} aims");
assert(legal > 0, "at least one aim produced a run — {legal} did");
}
// The WIDEST gap between consecutive legal run lengths in one direction from one anchor,
// in periods — the granularity an author aiming a length actually meets.
//
// ⚠ TWO WRONG MODELS DIED HERE, AND BOTH LOOKED RIGHT. First `wall_min_p`, which says
// where the legal runs START and nothing about their spacing. Then "the first gap", which
// assumed an arithmetic progression. Measured, the legal sets are:
//
// 18 of 24 directions every p is legal gap 1
// 6 of 24 directions two p in three, e.g. {1,3,4,6,7,9,...} gaps alternate 2,1
//
// So it is neither a start nor a period: it is the complement of a progression, and only
// the MAXIMUM gap bounds the error. `project_p`'s own note is the explanation — the N = 1
// classes "refuse one p in three".
fn legal_gap_max(d: integer, a0: integer, b0: integer) -> integer {
prev = 0;
worst = 0;
for q in 1..16 {
if wall_run_ok(d, a0, b0, q) {
if prev > 0 && q - prev > worst { worst = q - prev; }
prev = q;
}
}
worst
}
fn second_legal_p(d: integer, a0: integer, b0: integer) -> integer {
seen = 0;
for q in 1..16 {
if wall_run_ok(d, a0, b0, q) {
seen = seen + 1;
if seen == 2 { return q; }
}
}
0
}
fn test_an_aimed_run_lands_near_the_length_asked_for() {
// ⚠ THE GRANULARITY IS NOT ONE PERIOD, AND THE FIRST VERSION OF THIS TEST ASSERTED THAT
// IT WAS. It went red on `aimed 5.48, got 4.0 — off by 1.48, more than one period 1.0 of
// d24 2`, which was the library telling the truth. An author aiming between two
// reachable lengths is moved to the SHORTER, because `wall_snap_p` breaks ties that way,
// so the miss can be a full gap rather than half of one.
(va, vb) = nearest_vertex(0.0, 0.0);
worst_ratio = 0.0;
widest = 0;
for i in 0..24 {
ang = wall_angle_actual(i);
gp = legal_gap_max(i, va, vb);
assert(gp > 0, "d24 {i} has at least two legal run lengths from this anchor");
if gp > widest { widest = gp; }
for j in 2..10 {
dist = (j as float) * 1.37;
(d, p) = snap_run_from_heading(va, vb, ang, dist);
if p == 0 { continue; }
err = wall_run_len(d, p) - dist;
if err < 0.0 { err = 0.0 - err; }
step = wall_run_len(d, 1) * (gp as float);
assert(err <= step + 0.000001,
"aimed {dist} in d24 {d}, got {wall_run_len(d, p)} — off by {err}, more "
+ "than its {gp}-period granularity {step}");
r = err / step ?? 0.0;
if r > worst_ratio { worst_ratio = r; }
}
}
// ⚠ AND THE FIXTURE MUST BE ABLE TO MISS. Every distance is a multiple of 1.37, chosen
// not to be a multiple of any period — if the sweep only ever asked for lengths that
// were already legal, the bound above could not fail and this test would assert nothing.
assert(worst_ratio > 0.0, "no aim missed its length at all — the fixture asks only for "
+ "already-legal lengths, so the bound cannot fail");
// The two-in-three family is the finding, so it is pinned: if every direction became 1,
// the bound above would have quietly tightened by 2x and nothing would have said so.
assert(widest == 2, "some direction refuses one run in three, widest gap is {widest}");
}
fn test_an_exactly_legal_length_is_hit_exactly() {
// The other side of the test above: when the length asked for IS reachable, the aim must
// land on it with no error at all. Without this, a function that always returned the
// smallest legal run would satisfy the bound above.
(va, vb) = nearest_vertex(0.0, 0.0);
hit = 0;
for i in 0..24 {
want_p = second_legal_p(i, va, vb);
assert(want_p > 0, "d24 {i} has a second legal run from this anchor");
want_len = wall_run_len(i, want_p);
(d, p) = snap_run_from_heading(va, vb, wall_angle_actual(i), want_len);
assert(d == i, "aiming along d24 {i}'s own angle chose d24 {d}");
assert(p == want_p, "asking for exactly {want_len} in d24 {i} should give p {want_p}, "
+ "got {p}");
hit = hit + 1;
}
assert(hit == 24, "every direction was checked, {hit} of 24");
}
fn test_the_in_between_directions_are_four_times_coarser_in_length() {
// ⚠ FOUND WHILE FIXING THE BOUND ABOVE, AND IT IS THE LARGEST AUTHORING CONSEQUENCE IN
// THIS FILE. One period is NOT one size: the twelve exact directions have periods of
// 1.0 or 1.732 world units, while all twelve in-betweens have a period of 6.245. So an
// author aiming along an in-between can only make walls of 6.245, 12.49, 18.73 — the
// length axis is roughly FOUR TIMES coarser there, and no input precision changes it.
short = 1.0e30;
long = 0.0;
for i in 0..24 {
per = wall_run_len(i, 1);
if wall_is_exact(i) {
if per > long { long = per; }
} else {
if per < short { short = per; }
}
}
assert(long > 0.0 && short < 1.0e30, "both families were sampled");
assert(short > long * 3.0,
"the coarsest exact period is {long} and the finest in-between is {short} — "
+ "they are no longer separated, so this test is no longer describing the lattice");
}
fn test_a_non_vertex_anchor_is_refused() {
// ⚠ A REFUSAL MUST BE A REFUSAL, NOT A ZERO-LENGTH RUN THAT LOOKS LEGAL. `p = 0` is the
// caller's signal, and a hex CENTRE is the anchor a caller most easily arrives at by
// mistake — `nearest_vertex` exists precisely because a raw point is usually not one.
assert(!tri_is_vertex(0, 0), "the origin is a centre, not a vertex");
(d, p) = snap_run_from_heading(0, 0, 30.0, 5.0);
assert(p == 0, "a centre anchor must refuse, got d24 {d} p {p}");
}
fn test_a_zero_length_aim_is_refused() {
(va, vb) = nearest_vertex(0.0, 0.0);
(d, p) = snap_run_from_heading(va, vb, 90.0, 0.0);
assert(p == 0, "aiming zero distance must refuse, got d24 {d} p {p}");
}
// ── THE CHAIN IN READING ORDER ──────────────────────────────────────────────
//
// `wall_chain_ends` and `wall_chain_branches` have always answered *how many* and thrown
// the walk away. `wall_chain_walk` returns it, and what these pin is the one property a
// scan of the store can never have: the vertices come back in the order a walk meets them.
const CQ0 = -14;
const CR0 = -14;
const CW = 29;
const CH = 29;
// How many edges this marking holds — the number every "nothing was dropped" claim below
// is measured against.
fn marked_edges(e: EdgeSet) -> integer {
n = 0;
for r in CR0..CR0 + CH {
for q in CQ0..CQ0 + CW {
for d in 0..3 {
(nq, nr) = hex_neighbor(q, r, d);
if edge_mat(e, q, r, nq, nr) != 0 { n = n + 1; }
}
}
}
n
}
// The marks the walk actually took, over every chain of this marking.
fn walked_marks(e: EdgeSet) -> integer {
n = 0;
first = wall_chain_walk(e, CQ0, CR0, CW, CH, 0);
for i in 0..first.wc_n {
n = n + len(wall_chain_walk(e, CQ0, CR0, CW, CH, i).wc_q);
}
n
}
// The six edges around one hex — a closed loop with no free end, built directly rather
// than stamped, because what it has to BE is a cycle and not a wall.
fn ring_at(e: EdgeSet, q: integer, r: integer) {
for d in 0..6 {
(nq, nr) = hex_neighbor(q, r, d);
edge_set_mat(e, q, r, nq, nr, 1);
}
}
fn test_a_walked_wall_comes_back_in_order() {
// ⚠ THE CONSECUTIVE TEST IS THE WHOLE POINT, and it is what separates this from the two
// counters: mark `i` must join vertex `i` to vertex `i + 1`. A walk seeded at the first
// edge in SCAN order satisfies every count above and fails this on the first bend, because
// it heads off at that edge's arbitrary second vertex.
a0 = hex_corner_tri_a(0, 0, 0);
b0 = hex_corner_tri_b(0, 0, 0);
tried = 0;
for d in 0..24 {
p = wall_snap_p(d, a0, b0, 3);
if p <= 0 { continue; }
tried = tried + 1;
w = wall_from_run(d, a0, b0, p, 7);
e = edgeset_new(CQ0, CR0, CW, CH);
n = wall_write(w, e, CQ0, CR0, CW, CH);
ch = wall_chain_walk(e, CQ0, CR0, CW, CH, 0);
assert(ch.wc_ok && ch.wc_n == 1, "d={d}: one wall is {ch.wc_n} chains");
assert(!ch.wc_loop, "d={d}: a wall with two free ends is not a loop");
assert(ch.wc_branch == 0, "d={d}: a wall has no branch");
assert(len(ch.wc_q) == n, "d={d}: the walk took {len(ch.wc_q)} of {n} marks");
assert(len(ch.wc_va) == n + 1,
"d={d}: {n} marks need {n + 1} vertices, got {len(ch.wc_va)}");
for i in 0..len(ch.wc_q) {
(f1, f2) = hex_edge_corners(ch.wc_d[i] ?? 0);
xa = hex_corner_tri_a(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f1);
xb = hex_corner_tri_b(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f1);
ya = hex_corner_tri_a(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f2);
yb = hex_corner_tri_b(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f2);
ua = ch.wc_va[i] ?? 0; ub = ch.wc_vb[i] ?? 0;
va = ch.wc_va[i + 1] ?? 0; vb = ch.wc_vb[i + 1] ?? 0;
fwd = xa == ua && xb == ub && ya == va && yb == vb;
bwd = ya == ua && yb == ub && xa == va && xb == vb;
assert(fwd || bwd,
"d={d}: mark {i} does not join vertex {i} to vertex {i + 1} — the walk is "
+ "not in reading order");
// ⛔ AND THE WORLD POINT IS TIED TO THE CELL AND CORNER IT CAME FROM. `wc_x` is
// computed from the KEY, so asserting it equals `tri_x` of that key would be a
// table checked against itself; this asks the other side — one of this mark's two
// hex corners must be standing exactly where the chain says its vertex is.
hx = if fwd { hex_corner_x(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f1) }
else { hex_corner_x(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f2) };
hy = if fwd { hex_corner_y(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f1) }
else { hex_corner_y(ch.wc_q[i] ?? 0, ch.wc_r[i] ?? 0, f2) };
ex = (ch.wc_x[i] ?? 0.0) - hx;
ey = (ch.wc_y[i] ?? 0.0) - hy;
if ex < 0.0 { ex = -ex; }
if ey < 0.0 { ey = -ey; }
assert(ex < 0.000000001 && ey < 0.000000001,
"d={d}: vertex {i} is at ({ch.wc_x[i]},{ch.wc_y[i]}) and its own corner is "
+ "at ({hx},{hy})");
}
}
assert(tried >= 12, "only {tried} directions were exercised");
}
fn test_the_walk_reaches_a_second_chain() {
// ⛔ TWO WALLS ARE TWO CHAINS AND A CONSUMER MUST BE ABLE TO REACH BOTH. A walk that
// ordered one chain and stopped would pass every assertion above and describe half the
// marking.
a0 = hex_corner_tri_a(0, 0, 0);
b0 = hex_corner_tri_b(0, 0, 0);
c0 = hex_corner_tri_a(0, 8, 0);
d0 = hex_corner_tri_b(0, 8, 0);
e = edgeset_new(CQ0, CR0, CW, CH);
n1 = wall_write(wall_from_run(0, a0, b0, 3, 7), e, CQ0, CR0, CW, CH);
n2 = wall_write(wall_from_run(0, c0, d0, 2, 7), e, CQ0, CR0, CW, CH);
assert(n1 > 0 && n2 > 0, "the fixture wrote {n1} and {n2} marks");
assert(wall_chain_ends(e, CQ0, CR0, CW, CH) == 4, "two separate walls have four ends");
first = wall_chain_walk(e, CQ0, CR0, CW, CH, 0);
assert(first.wc_n == 2, "two walls are {first.wc_n} chains");
second = wall_chain_walk(e, CQ0, CR0, CW, CH, 1);
assert(second.wc_ok, "the second chain was not returned");
assert(len(first.wc_q) + len(second.wc_q) == marked_edges(e),
"the two chains hold {len(first.wc_q)} + {len(second.wc_q)} of "
+ "{marked_edges(e)} marks");
// ⚠ AND A THIRD IS REFUSED WITHOUT LYING ABOUT THE COUNT, so a caller looping on `wc_n`
// cannot walk off the end and cannot be told there is nothing there.
past = wall_chain_walk(e, CQ0, CR0, CW, CH, 2);
assert(!past.wc_ok, "a third chain was offered");
assert(past.wc_n == 2, "the refusal reports {past.wc_n} chains, not 2");
assert(len(past.wc_q) == 0, "the refusal carries marks");
}
fn test_a_closed_ring_is_one_chain_that_says_so() {
// ⛔ A LOOP HAS NO FREE END, which is exactly the case `wall_read_run` cannot answer for
// and `wall_chain_ends` reports as zero. The walk must still return it, and must say that
// where it stopped is where it started — a consumer cutting the chain into runs needs to
// know its first and last span may be one run.
e = edgeset_new(CQ0, CR0, CW, CH);
ring_at(e, 0, 0);
assert(marked_edges(e) == 6, "a hex ring is 6 marks, got {marked_edges(e)}");
assert(wall_chain_ends(e, CQ0, CR0, CW, CH) == 0, "a ring has no free end");
assert(wall_chain_branches(e, CQ0, CR0, CW, CH) == 0, "and no branch");
ch = wall_chain_walk(e, CQ0, CR0, CW, CH, 0);
assert(ch.wc_ok && ch.wc_n == 1, "a ring is {ch.wc_n} chains");
assert(ch.wc_loop, "a ring is not reported as a loop");
assert(len(ch.wc_q) == 6, "the walk took {len(ch.wc_q)} of the ring's 6 marks");
assert(len(ch.wc_va) == 7, "6 marks round a loop need 7 vertices");
assert((ch.wc_va[0] ?? 0) == (ch.wc_va[6] ?? 1)
&& (ch.wc_vb[0] ?? 0) == (ch.wc_vb[6] ?? 1),
"the loop does not close on the vertex it started from");
}
fn test_a_branch_is_reported_and_nothing_is_dropped() {
// ⚠ WHERE THREE MARKS MEET THE ORDER IS ONE OF SEVERAL, so the walk says how often it
// had to choose. What it must NOT do is lose the marks it did not take first.
e = edgeset_new(CQ0, CR0, CW, CH);
ring_at(e, 0, 0);
(e1, e2) = hex_edge_corners(0);
ka = hex_corner_tri_a(0, 0, e1);
kb = hex_corner_tri_b(0, 0, e1);
(n0q, n0r) = hex_neighbor(0, 0, 0);
put = false;
for d in 0..6 {
(mq, mr) = hex_neighbor(n0q, n0r, d);
if mq == 0 && mr == 0 { continue; }
(f1, f2) = hex_edge_corners(d);
hit = (hex_corner_tri_a(n0q, n0r, f1) == ka && hex_corner_tri_b(n0q, n0r, f1) == kb)
|| (hex_corner_tri_a(n0q, n0r, f2) == ka && hex_corner_tri_b(n0q, n0r, f2) == kb);
if hit && !put { edge_set_mat(e, n0q, n0r, mq, mr, 1); put = true; }
}
assert(put, "no edge of the neighbour shares the ring vertex — the fixture is not a T");
assert(marked_edges(e) == 7, "the ring plus a stub is 7 marks, got {marked_edges(e)}");
assert(wall_chain_branches(e, CQ0, CR0, CW, CH) == 1,
"the fixture has {wall_chain_branches(e, CQ0, CR0, CW, CH)} branches, not one");
assert(walked_marks(e) == 7,
"the walk took {walked_marks(e)} of 7 marks — a branch is losing what it did "
+ "not take first");
seen = 0;
first = wall_chain_walk(e, CQ0, CR0, CW, CH, 0);
for i in 0..first.wc_n {
seen = seen + wall_chain_walk(e, CQ0, CR0, CW, CH, i).wc_branch;
}
assert(seen > 0, "the walk passed a degree-3 vertex and reported no branch");
}
fn test_a_vertexs_world_point_is_its_keys() {
// ⛔ ONE VERTEX IS REACHED FROM TWO CELLS. Reading its position off whichever cell the
// scan was in would make one point two, and a run between them would be a run between
// two places. `wc_x` comes from the KEY; this is the bridge that makes that legal.
for r in -3..4 {
for q in -3..4 {
for c in 0..6 {
ta = hex_corner_tri_a(q, r, c);
tb = hex_corner_tri_b(q, r, c);
dx = tri_x(ta, tb) - hex_corner_x(q, r, c);
dy = tri_y(ta, tb) - hex_corner_y(q, r, c);
if dx < 0.0 { dx = -dx; }
if dy < 0.0 { dy = -dy; }
assert(dx < 0.000000001 && dy < 0.000000001,
"corner {c} of ({q},{r}) is ({dx},{dy}) away from its own key's point");
}
}
}
}
// ⚠ **WHAT THE SABOTAGE SWEEP ASKED FOR, AND WHAT IT FOUND MISSING.** Six sabotages of
// `wall_chain_walk`, each with the package asserted to still BUILD before its row was read,
// and two controls — a comment beside the subject and a renamed local — which must stay
// green, because that is what separates *these tests detect THIS* from *they detect any
// change at all*:
//
// seed anywhere, not at a free end ⛔ one wall is 2 chains
// no used-mark guard when stepping ⛔ one wall is 7 chains, a ring is 7
// world point from the swapped key ⛔ vertex 0 is at (0.866,0.5), its corner at (0,1)
// the chain count is always one ⛔ two walls are 1 chains
// a loop is never reported as one ⛔ a ring is not reported as a loop
// a branch is never counted ⛔ passed a degree-3 vertex and reported none
// CONTROL a comment beside it ✅ 27 passed
// CONTROL a local renamed ✅ 27 passed
//
// ⛔ **AND ROW 3 WAS INVISIBLE UNTIL THE WORLD-POINT ASSERTION WAS WRITTEN.** Measured, not
// assumed: the same sabotage against the suite as it stood one commit earlier gives **27
// passed**. Nothing else here reads `wc_x` or `wc_y`, so a chain whose vertices were in the
// right order at the wrong places was a green suite.
tests/02-worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for `hex_shape` (@PLN141). Each `test_*` below is a REAL,
// run-every-CI demonstration of a contract the signature cannot state; the
// function documents itself by CITING the tag with `// Example: @HXS-00N`, and
// `check_doc_drift.sh examples` fails if a citation goes dangling. These are
// teaching examples, distinct from the conformance suite in `01-hex-shape.loft`.
//
// THE ONE IDEA. Three primitives — the wall, the box, the arc — and every one
// of them takes a CONTINUOUS parameter that the lattice cannot represent
// continuously. A radius, a run length, an angle, a width. So each has an
// admissible GRID, and the package's whole job is to say what that grid is
// instead of silently snapping to it.
//
// WHICH MEANS EVERY ANSWER HERE IS SPLIT. An arc's centre comes back exactly
// and its radius does not. A run's line comes back exactly and which end you
// called the start does not. A direction's angle is exact on twelve headings
// and 1.1 degrees off on the other twelve. A split answer is precisely what a
// signature cannot carry, which is why it is worked here.
//
// AND WHERE THE MEASUREMENTS THEMSELVES MISLEAD. Two of the examples below are
// about an instrument rather than a shape: a stated discriminator that reads the
// same on good output as on the failure it was built to catch (`@HXS-004`), and
// a connectivity check that passes on a wall you can walk straight through
// (`@HXS-007`). Both are the package's own checks, and both needed a second
// number beside them.
use hex_shape::*;
use hex_field::*;
use hex_form::*;
use hex_grid::*;
const Q0 = -12;
const R0 = -12;
const W = 25;
const H = 25;
// The wall window: smaller, because the chain readers are O(edges squared).
const WQ0 = -10;
const WR0 = -10;
const WW = 21;
const WH = 21;
fn abs_f(v: float) -> float { if v < 0.0 { return 0.0 - v; } v }
fn chunk() -> HexSet { hexset_chunk(Q0, R0, W, H) }
// How many marked edges have their cell-to-cell vector pointing ALONG the wall
// rather than across it — the picket signature, counted rather than maximised.
fn edges_pointing_along(w: Wall, e: EdgeSet) -> integer {
n = 0;
for wr in WR0..WR0 + WH {
for wq in WQ0..WQ0 + WW {
for wd in 0..3 {
(nq, nr) = hex_neighbor(wq, wr, wd);
if edge_mat(e, wq, wr, nq, nr) != 0 {
dx = cell_x_w(nq, nr) - cell_x_w(wq, wr);
dy = cell_y_w(nq, nr) - cell_y_w(wq, wr);
dl = sqrt(dx * dx + dy * dy) ?? 1.0;
if abs_f(((dx * w.wa_dx + dy * w.wa_dy) / dl) ?? 0.0) > 0.9 { n = n + 1; }
}
}
}
}
n
}
// Clear the `k`-th marked edge, counting in scan order — used to break a chain in
// the middle, where a reader must refuse rather than answer.
fn clear_nth_edge(e: EdgeSet, k: integer) {
seen = 0;
for wr in WR0..WR0 + WH {
for wq in WQ0..WQ0 + WW {
for wd in 0..3 {
(nq, nr) = hex_neighbor(wq, wr, wd);
if edge_mat(e, wq, wr, nq, nr) != 0 {
if seen == k { edge_set_mat(e, wq, wr, nq, nr, 0); }
seen = seen + 1;
}
}
}
}
}
fn copy_set(src: HexSet) -> HexSet {
out = chunk();
for r in R0..R0 + H {
for q in Q0..Q0 + W {
if hexset_get(src, q, r) { hexset_set(out, q, r, true); }
}
}
out
}
fn drop_first_cell(s: HexSet) {
done = false;
for r in R0..R0 + H {
for q in Q0..Q0 + W {
if !done && hexset_get(s, q, r) { hexset_set(s, q, r, false); done = true; }
}
}
}
fn main() {
b = box_new(0, 0, 5, 4, 0, false);
assert(box_hw(b) > 0.0, "a box has a positive half-width");
}
// @HXS-001 — THE ARC'S ANSWER IS SPLIT: THE CENTRE IS EXACT, THE RADIUS IS NOT.
//
// This is the question the module was written to answer, and the answer is two
// answers. The centre is a translation and the field pins it, so it comes back
// exactly. The radius does not: a disk realises only the SHELLS `3k² + m²`, and
// every radius between two shells draws the same cells.
//
// So `arc_fill` takes a shell, not a radius — and if you hand it something that
// is not a shell it draws the shell BELOW and never says so. Ask for 16 and you
// get 12: same cells, and `arc_shell_max` reports 12, because 16 was never stored
// anywhere. Nothing in the field remembers what you asked for.
//
// The cure is not a tolerance, it is the grid: `arc_shells_upto` is the set of
// radii that exist, and a caller snaps to it BEFORE authoring rather than being
// snapped afterwards. Over the whole span 0.5..4.5 world units there are only
// FOUR of them.
fn test_an_arcs_centre_is_exact_and_its_radius_is_a_grid() {
// The admissible radii out to 4.5 world units (shell 81) — there are four.
shells = arc_shells_upto(81);
assert(len(shells) == 4, "0.5..4.5 world units offers four radii, not a continuum");
assert((shells[0] ?? -1) == 0 && (shells[1] ?? -1) == 12
&& (shells[2] ?? -1) == 36 && (shells[3] ?? -1) == 48,
"and they are 0, 12, 36, 48");
// 16 is not one of them. Ask for it anyway.
asked = chunk();
arc_fill(0, 0, 16, asked);
got = chunk();
arc_fill(0, 0, 12, got);
assert(hexset_count(asked) == 7 && hexset_count(got) == 7,
"a 16 disk and a 12 disk are the same seven cells");
assert(arc_shell_max(asked, 0, 0) == 12,
"and the field reports 12 — the 16 you asked for is nowhere, got {arc_shell_max(asked, 0, 0)}");
assert(arc_is_disk(asked, 0, 0), "it is still a perfectly good disk");
// The centre half of the answer, which really is exact, anywhere.
off = chunk();
arc_fill(3, 2, 12, off);
(cq, cr, ok) = arc_recover_centre(off);
assert(ok && cq == 3 && cr == 2, "the centre comes back exactly, got ({cq}, {cr})");
// A refusal, not a fit: a region that is not a disk has no centre to recover.
blob = chunk();
arc_fill(0, 0, 12, blob);
hexset_set(blob, 5, 5, true);
(bq, br, bok) = arc_recover_centre(blob);
assert(!bok, "a blob is refused rather than fitted, got ({bq}, {br})");
}
// @HXS-002 — TWELVE DIRECTIONS ARE EXACT AND TWELVE ARE 1.1 DEGREES OFF, AND
// THAT IS WHY THE WALL'S WIDTH IS A CONSTANT.
//
// `d24` counts in 15-degree steps, which reads like 24 equal directions. It is
// not: no lattice vector points at an odd multiple of 15 degrees at all, so the
// twelve in-between headings are approximated, and `wall_angle_actual` is 1.1021
// degrees from `wall_angle_nominal` on every one of them. `wall_is_exact` is the
// function that tells them apart, and a stencil may use only the exact twelve.
//
// The width follows from the same arithmetic, and it is why `WALL_W` is a
// constant rather than a count of lattice rows. A direction's parallel lattice
// lines are spaced `sqrt(3) / (2*sqrt(N))`, so two directions' spacings are in a
// rational ratio — the only way integer row counts could ever make two walls
// equally wide — exactly when `N1*N2` is a perfect square. Over all 24
// directions only THREE values of N occur, and no pair of them qualifies. So
// there is no row count that is the same width twice, and the width has to be
// stated in world units instead.
fn test_only_twelve_directions_are_exact_and_no_two_widths_commensurate() {
// Exactly the even d24 are exact, and they hit their nominal angle.
for d in 0..24 {
ex = wall_is_exact(d);
assert(ex == (((d % 2) ?? 0) == 0), "d={d}: the even headings are the exact ones");
err = abs_f(wall_angle_actual(d) - wall_angle_nominal(d));
if ex {
assert(err < 0.000000001, "d={d} is exact, so its angle error is 0, got {err}");
} else {
assert(err > 1.10 && err < 1.11,
"d={d} is in-between, so it is 1.1021 degrees off, got {err}");
}
}
// Three values of N over 24 directions — 3 and 1 on the exact headings, 39 on
// the in-between ones.
n3 = 0;
n1 = 0;
n39 = 0;
for d in 0..24 {
v = tri_n2(d);
if v == 3 { n3 = n3 + 1; } else if v == 1 { n1 = n1 + 1; }
else if v == 39 { n39 = n39 + 1; }
}
assert(n3 + n1 + n39 == 24, "every direction is one of three classes");
assert(n3 == 6 && n1 == 6 && n39 == 12, "six, six and twelve, got {n3}/{n1}/{n39}");
// And no two distinct classes are commensurable, which is the whole argument.
assert(!commensurable(3, 1), "3 and 1 cannot be matched by integer row counts");
assert(!commensurable(3, 39), "nor 3 and 39");
assert(!commensurable(1, 39), "nor 1 and 39");
assert(commensurable(3, 3), "a class is trivially commensurable with itself");
// So the width is a world-unit constant: one triangle high, sqrt(3)/6.
assert(abs_f(WALL_W - 0.2886751345948129) < 0.000000001,
"WALL_W is sqrt(3)/6 world units, got {WALL_W}");
}
// @HXS-003 — NOT EVERY RUN LENGTH IS LEGAL, AND WHICH ONES ARE DEPENDS ON WHICH
// CORNER YOU STARTED FROM.
//
// A wall must begin and end on a hex VERTEX — the mesh has no way to represent an
// endpoint anywhere else — so a run is a whole number of periods, and the
// question `wall_run_ok` answers is whether that number lands on a vertex again.
// A period displaces the class by `delta = (tri_a - tri_b) mod 3`, and class 0 is
// a hex CENTRE, which is not a vertex:
//
// delta = 0 every p works, from every vertex. The N=3 and N=39 directions.
// delta != 0 two p in every three work, and WHICH two moves with the start
// vertex's class. The N=1 family — 30, 90, 150 degrees.
//
// The consequence a caller feels: `wall_min_p` for the SAME direction is 1 from
// one corner of a hex and 2 from the next. The editor's snap unit is not a
// property of the direction, and a wall that fitted before you moved the anchor
// one vertex does not fit after. `wall_snap_p` is the refusal that names its
// restriction, searching outward so a tie goes to the SHORTER run.
fn test_a_legal_run_length_depends_on_the_starting_vertex() {
// Two vertices of the same hex, one in each class.
a1 = hex_corner_tri_a(0, 0, 0);
b1 = hex_corner_tri_b(0, 0, 0);
a2 = hex_corner_tri_a(0, 0, 1);
b2 = hex_corner_tri_b(0, 0, 1);
assert(tri_is_vertex(a1, b1) && tri_is_vertex(a2, b2), "both are vertices");
assert(tri_class(a1, b1) == 2 && tri_class(a2, b2) == 1, "and they are different classes");
// A delta = 0 direction: everything is legal from everywhere.
for p in 1..7 {
assert(wall_run_ok(0, a1, b1, p), "d=0 admits p={p} from the first vertex");
assert(wall_run_ok(0, a2, b2, p), "and from the second");
assert(wall_run_ok(1, a1, b1, p), "the in-between d=1 admits p={p} too");
}
// A delta != 0 direction refuses one in three, and not the same one.
refused1 = 0;
refused2 = 0;
for p in 1..7 {
if !wall_run_ok(2, a1, b1, p) { refused1 = refused1 + 1; }
if !wall_run_ok(2, a2, b2, p) { refused2 = refused2 + 1; }
}
assert(refused1 == 2 && refused2 == 2, "two of six refused from each vertex");
assert(!wall_run_ok(2, a1, b1, 1), "p=1 is refused from the first vertex");
assert(wall_run_ok(2, a2, b2, 1), "and admitted from the second — same direction");
// Which is the thing a caller actually trips over.
assert(wall_min_p(2, a1, b1) == 2, "the shortest legal d=2 wall here is 2 periods");
assert(wall_min_p(2, a2, b2) == 1, "and one vertex away it is 1 — twice as short");
assert(wall_min_p(0, a1, b1) == 1 && wall_min_p(0, a2, b2) == 1,
"an exact-lattice direction has no such surprise");
// A refusal that names its restriction, ties to the shorter run.
assert(wall_snap_p(2, a1, b1, 2) == 2, "a legal want comes back unchanged");
assert(wall_snap_p(2, a1, b1, 4) == 3, "4 is refused and snaps DOWN to 3");
assert(wall_snap_p(6, a1, b1, 2) == 1, "2 sits between 1 and 3, and the tie goes short");
assert(wall_snap_p(2, a1, b1, 0) == 0, "a nonsense want is refused outright");
// The period length itself differs per class, which is why the snap unit is
// worth reading rather than assuming.
assert(abs_f(wall_run_len(0, 1) - 1.7320508075688772) < 0.000001, "N=3 period is sqrt(3) wu");
assert(abs_f(wall_run_len(6, 1) - 1.0) < 0.000001, "N=1 period is 1 wu");
assert(abs_f(wall_run_len(1, 1) - 6.244997998398398) < 0.000001,
"an in-between period is 6.24 wu — six times coarser");
}
// @HXS-004 — THE DISCRIMINATOR THAT STOPS DISCRIMINATING.
//
// `wall_write` marks the edges lying ALONG the wall, and an earlier version
// marked the ones lying ACROSS it — a comb of pickets instead of a chain. The
// measurement built to tell those apart is `wall_along_max`: for an edge lying
// along the wall, the vector between its two cells points across, so the dot with
// the wall direction is small; for a picket it is near 1.
//
// On the twelve EXACT headings that works: the worst dot is 0.5 or 0.866, and no
// edge is above 0.9. On the twelve IN-BETWEEN headings it does not. A shallow
// line genuinely has to cross the vertical edge between two cells of the same
// row wherever it steps up, and that edge's dot is `cos(13.9 deg) = 0.97` — the
// picket's own value, on completely correct output.
//
// So `wall_along_max` is a discriminator on half its domain and a false alarm on
// the other half. What separates a chain from a comb everywhere, with no
// tolerance at all, is the CHAIN property: exactly two degree-1 vertices and no
// branches. A comb of k edges has 2k ends. Read the max as a description of
// the wobble, and read the chain count as the verdict.
fn test_the_along_test_is_not_a_verdict_but_the_chain_count_is() {
a0 = hex_corner_tri_a(0, 0, 0);
b0 = hex_corner_tri_b(0, 0, 0);
// An exact heading: the along test behaves as documented.
we = wall_from_run(0, a0, b0, 4, 7);
ee = edgeset_new(WQ0, WR0, WW, WH);
ne = wall_write(we, ee, WQ0, WR0, WW, WH);
assert(ne == 8, "the exact run marks 8 edges, got {ne}");
assert(wall_along_max(we, ee, WQ0, WR0, WW, WH) < 0.51,
"and none of them points along the wall");
assert(edges_pointing_along(we, ee) == 0, "not one edge is above 0.9");
// An in-between heading: the same measurement, on correct output, reads like
// the failure it was built to catch.
wb = wall_from_run(1, a0, b0, 4, 7);
eb = edgeset_new(WQ0, WR0, WW, WH);
nb = wall_write(wb, eb, WQ0, WR0, WW, WH);
assert(nb == 25, "the in-between run marks 25 edges, got {nb}");
am = wall_along_max(wb, eb, WQ0, WR0, WW, WH);
assert(am > 0.96 && am < 0.98,
"and its worst dot is cos(13.9 deg) = 0.97 — the picket value, got {am}");
assert(edges_pointing_along(wb, eb) == 3,
"though only 3 edges of 25 are there, not all of them");
// The verdict that holds for both, and for every direction.
for d in 0..24 {
p = wall_snap_p(d, a0, b0, 3);
if p > 0 {
w = wall_from_run(d, a0, b0, p, 7);
e = edgeset_new(WQ0, WR0, WW, WH);
wall_write(w, e, WQ0, WR0, WW, WH);
assert(wall_chain_ends(e, WQ0, WR0, WW, WH) == 2,
"d={d}: one chain has exactly two ends");
assert(wall_chain_branches(e, WQ0, WR0, WW, WH) == 0,
"d={d}: and no branches");
}
}
// The geometry check that catches a mismatched corner/neighbour table — the
// defect that stayed green through every count-based gate.
for dir in 0..6 {
assert(wall_edge_gap(2, 1, dir) < 0.000000001,
"edge {dir}'s midpoint sits between the cells it separates");
}
}
// @HXS-005 — A RUN READS BACK EXACTLY, BUT NOT WHICH END YOU CALLED THE START.
//
// Recovery here is arithmetic, not fitting: the marked chain's two degree-1
// vertices ARE the endpoints, so `wall_read_run` hands back direction, anchor and
// period count as exact integers with nothing to tolerance.
//
// What it cannot hand back is the ORIENTATION, because the field does not store
// it. A run from A to B and the same run from B to A mark the identical edges,
// so the reader returns whichever end its scan met first — the direction may come
// back as `d` or as `d + 12`, with the other endpoint as the anchor. Both name
// the same wall. So the obvious round-trip assertion, `read.d == d`, is wrong,
// and it is wrong on the very first direction: writing `d = 0` reads back `12`.
//
// And a marking that is not one chain is REFUSED rather than approximated — but
// only a marking that is not one chain. Break a run in the MIDDLE and the reader
// says no; shorten it at the END and it reads back a shorter wall, correctly,
// because a shorter wall is exactly what the field now contains.
fn test_a_run_reads_back_exactly_except_which_end_is_the_start() {
a0 = hex_corner_tri_a(0, 0, 0);
b0 = hex_corner_tri_b(0, 0, 0);
// Every direction round-trips to the same LINE.
flipped = 0;
for d in 0..24 {
p = wall_min_p(d, a0, b0);
if p > 0 {
w = wall_from_run(d, a0, b0, p, 7);
e = edgeset_new(WQ0, WR0, WW, WH);
wall_write(w, e, WQ0, WR0, WW, WH);
(rd, ra, rb, rp, ok) = wall_read_run(e, WQ0, WR0, WW, WH);
assert(ok, "d={d}: the run reads back");
assert(rp == p, "d={d}: and with the same period count, got {rp}");
assert(rd == d || rd == ((d + 12) % 24 ?? 0),
"d={d}: the direction is d or its opposite, got {rd}");
if rd != d { flipped = flipped + 1; }
// Whichever end it chose, the endpoints are the authored pair.
ea = wall_end_a(rd, ra, rp);
eb2 = wall_end_b(rd, rb, rp);
oa = wall_end_a(d, a0, p);
ob = wall_end_b(d, b0, p);
same = (ra == a0 && rb == b0 && ea == oa && eb2 == ob);
rev = (ra == oa && rb == ob && ea == a0 && eb2 == b0);
assert(same || rev, "d={d}: the endpoints are the ones that were authored");
}
}
assert(flipped > 0, "and some of them really do come back reversed — {flipped} of 24");
// The concrete case, so nobody writes `read.d == d` again.
w0 = wall_from_run(0, a0, b0, 1, 7);
e0 = edgeset_new(WQ0, WR0, WW, WH);
wall_write(w0, e0, WQ0, WR0, WW, WH);
(d0, xa, xb, pp, ok0) = wall_read_run(e0, WQ0, WR0, WW, WH);
assert(ok0 && d0 == 12, "writing d=0 reads back d=12 — the same wall, other way up");
// A fragmented marking is refused, not fitted.
wl = wall_from_run(0, a0, b0, 4, 7);
el = edgeset_new(WQ0, WR0, WW, WH);
n = wall_write(wl, el, WQ0, WR0, WW, WH);
assert(wall_mesh_segments(el, WQ0, WR0, WW, WH) == n, "the mesh is one segment per marked edge");
clear_nth_edge(el, (n / 2) ?? 0);
assert(wall_chain_ends(el, WQ0, WR0, WW, WH) == 4, "a break makes four ends");
(bd, ba, bb, bp, bok) = wall_read_run(el, WQ0, WR0, WW, WH);
assert(!bok, "and the reader refuses rather than answering, got ({bd}, {bp})");
}
// @HXS-006 — TWELVE BOX ANGLES ARE TWO FAMILIES OF SIX, NOT ONE FAMILY OF TWELVE.
//
// `Box` rotates in 30-degree steps because a stencil side runs in one of twelve
// headings. But the lattice has 6-fold symmetry, not 12-fold, so a 30-degree
// rotation is not a symmetry of it. The twelve split into two ORBITS that behave
// completely differently:
//
// EVEN rot — sides on edge headings. The six are exact lattice rotations of
// one another, so their cell sets are equivariant: same count.
// ODD rot — sides on vertex headings. Also six, also mutually equivariant,
// and NOT related to the even six by any exact map.
//
// The same rectangle, turned 30 degrees, is a different number of cells — and no
// integer map takes one to the other. `box_orbit` is what says which family a
// rot is in, and a consumer that measures one orbit and assumes the other has
// measured half its cases.
fn test_the_twelve_box_angles_are_two_families_of_six() {
even_n = -1;
odd_n = -1;
for rot in 0..12 {
b = box_new(0, 0, 6, 4, rot, false);
cells = chunk();
n = box_fill(b, cells);
if ((rot % 2) ?? 0) == 0 {
assert(box_orbit(rot) == 0, "rot {rot} is the edge family");
if even_n < 0 { even_n = n; }
assert(n == even_n, "every edge-family box has {even_n} cells, rot {rot} had {n}");
} else {
assert(box_orbit(rot) == 1, "rot {rot} is the vertex family");
if odd_n < 0 { odd_n = n; }
assert(n == odd_n, "every vertex-family box has {odd_n} cells, rot {rot} had {n}");
}
}
assert(even_n == 33, "the edge family fills 33 cells, got {even_n}");
assert(odd_n == 31, "the vertex family fills 31, got {odd_n}");
assert(even_n != odd_n,
"so a 30-degree turn is NOT a rotation of the same shape — {even_n} against {odd_n}");
}
// @HXS-007 — CONNECTED IS NOT CLOSED, AND ONLY WALKING IN CAN TELL.
//
// A thick wall is a ring of cells, and the property that matters is that nothing
// gets from outside to the courtyard. Every cheap check of a ring passes on a
// ring with a hole in it: the cell count barely moves, and `set_connected` still
// says true — because a ring missing one cell is still one connected chain of
// cells. It just is not a loop any more.
//
// So the honest test is `flood_outside` plus `leak_count`: seed the window's
// border and relax inward. Intact, nothing reaches the courtyard. Take ONE cell
// out of twenty-two and the outside reaches all twenty-seven.
//
// The other half of the example is which side the ring comes from.
// `box_ring_out` puts it outside the selection, so the courtyard is exactly what
// the user picked; `box_ring_in` takes it from the selection, so the same call on
// the same box leaves a courtyard of eleven cells instead of twenty-seven.
fn test_a_ring_can_be_connected_and_still_leak() {
b = box_new(0, 0, 5, 4, 0, false);
inner = chunk();
ni = box_fill(b, inner);
assert(ni == 27, "the user selected 27 cells, got {ni}");
ring = chunk();
nr = box_ring_out(inner, ring);
assert(nr == 22, "the wall outside it is 22 cells, got {nr}");
assert(leak_count(inner, ring) == 0, "and it takes none of the courtyard");
out = chunk();
flood_outside(ring, out);
assert(leak_count(inner, out) == 0, "the intact wall holds — nothing walks in");
assert(set_connected(ring), "and it is connected, as a ring should be");
// One cell out. Every cheap check still passes.
holed = copy_set(ring);
drop_first_cell(holed);
assert(hexset_count(holed) == 21, "21 cells instead of 22 — a 5% change");
assert(set_connected(holed), "STILL connected: a broken ring is one chain, not two");
// The check that is not fooled.
out2 = chunk();
flood_outside(holed, out2);
assert(leak_count(inner, out2) == 27,
"and the outside reaches the whole courtyard, got {leak_count(inner, out2)}");
// The other side: the same ring taken from the inside.
ring_i = chunk();
n2 = box_ring_in(inner, ring_i);
assert(n2 == 16, "ring_in is 16 cells, got {n2}");
assert(leak_count(inner, ring_i) == 16,
"and all 16 are cells the user had selected — the courtyard drops to 11");
// A curtain wall encloses nothing, so `leak_count` has nothing to say about it
// and connectivity is the whole contract: one hex wide, and a chain.
curtain = chunk();
nc = line_hexes(-8.0, -5.0, 8.0, 5.0, curtain);
assert(nc == 13, "an 18.9 wu run is 13 cells, got {nc}");
assert(set_connected(curtain), "and it is a chain — sampling every third of a cell cannot skip one");
}
// @HXS-008 — THE EDITOR'S INPUT IS A MOUSE, AND THREE SEPARATE ERRORS SIT
// BETWEEN WHERE IT CLICKED AND WHAT GETS STORED.
//
// A wall cannot start where you clicked, cannot end where you dragged to, and is
// not stored as the line between them. All three are by construction, all three
// are recoverable, and none of them is visible at the call site — so the numbers
// are here.
//
// SNAP 1 `nearest_vertex` moves the anchor to a hex vertex. Note it is two
// steps: rounding lands on the hex-scale lattice, which includes hex
// CENTRES, and a centre is never a legal endpoint — so it steps to the
// nearest of six neighbours. The click below does exactly that.
// SNAP 2 `snap_run_d24` + `snap_run_p` pick the direction and period count
// whose legal endpoint lands nearest the target. `run_end_dist` is
// how far that is, and it is not small: 1.43 world units here.
// STORAGE `wall_write` marks a chain of hex edges, which is a sawtooth about
// the line, not the line. `wall_mesh_stray` is its worst excursion.
//
// The target is not even ON the wall the editor built from it — `wall_offset_of`
// puts it 0.61 world units to one side. An editor that draws the user's line
// rather than the snapped one is drawing something it will not be able to store.
fn test_the_mouse_is_three_snaps_away_from_what_gets_stored() {
// A click at an arbitrary point, which rounds onto a hex CENTRE.
(ha, hb) = hex_scale_round(2.37, 1.11);
assert(tri_class(ha, hb) == 0, "the nearest hex-scale point is a centre, not a vertex");
assert(!tri_is_vertex(ha, hb), "so it cannot anchor a wall");
(va, vb) = nearest_vertex(2.37, 1.11);
assert(tri_is_vertex(va, vb), "the second step lands on a real vertex");
moved = sqrt((tri_x(va, vb) - 2.37) * (tri_x(va, vb) - 2.37)
+ (tri_y(va, vb) - 1.11) * (tri_y(va, vb) - 1.11)) ?? 0.0;
assert(moved > 0.6 && moved < 0.7, "and the anchor moved {moved} wu to get there");
// The far end: the nearest legal endpoint over all 24 directions.
d = snap_run_d24(va, vb, 8.9, 3.4);
assert(d >= 0, "a direction was found");
p = snap_run_p(d, va, vb, 8.9, 3.4);
assert(p > 0 && wall_run_ok(d, va, vb, p), "and a legal period count in it");
miss = run_end_dist(d, va, vb, p, 8.9, 3.4);
assert(miss > 1.4 && miss < 1.5,
"the endpoint lands {miss} wu from where the user dragged to");
// The stored form is a sawtooth about that line, not the line.
w = wall_from_run(d, va, vb, p, 5);
e = edgeset_new(-14, -14, 29, 29);
n = wall_write(w, e, -14, -14, 29, 29);
assert(n > 0, "the wall marks {n} edges");
stray = wall_mesh_stray(w, e, -14, -14, 29, 29);
assert(stray > 0.8 && stray < 0.9,
"and they swing {stray} wu off the line they store");
// Which is why the user's own target is not on the result.
assert(wall_offset_of(w, 8.9, 3.4) > 0.5,
"the target sits {wall_offset_of(w, 8.9, 3.4)} wu off the wall built from it");
// `wall_new` builds the same direction from the heading alone — the unanchored
// form, for when there is no run to fit.
w2 = wall_new(d, 0.0, 0.0, 3.0, 5);
assert(abs_f(w2.wa_dx - w.wa_dx) < 0.000000001
&& abs_f(w2.wa_dy - w.wa_dy) < 0.000000001,
"and it agrees with the run-anchored one on direction");
}
// @HXS-009 — A DOOR IS AN ANNOTATION, WHICH IS WHY THE TOWER SURVIVES IT.
//
// The obvious way to put a door in a wall is to take the wall away. Do that here
// and the tower stops being a tower: deleting boundary edges fragments the wall
// into several arcs, and the recovery that answered "one disk, centre here,
// shell this" has nothing left to answer with.
//
// `arc_door_wedge` cannot express that. It writes a MATERIAL onto the boundary
// edges inside an angular wedge and touches nothing else — so the cell set is
// byte-identical before and after, and the centre and the shell come back
// unchanged. The doored tower is ONE arc, and the door reads straight back off
// storage with `arc_door_edges` rather than being inferred.
//
// The wedge test is exact integers: a positive-diagonal map preserves the cyclic
// order of directions, so "is this cell between these two rays" is a cross
// product in lattice coordinates and never a trig call.
fn test_a_door_is_an_annotation_so_the_tower_is_still_one_arc() {
cells = chunk();
arc_fill(0, 0, 48, cells);
before = hexset_count(cells);
shell_before = arc_shell_max(cells, 0, 0);
(cq0, cr0, ok0) = arc_recover_centre(cells);
assert(before == 19 && shell_before == 48 && ok0 && cq0 == 0 && cr0 == 0,
"a 19-cell tower on shell 48, centred at the origin");
// A door across the first sixty degrees: from ray (2,0) round to ray (1,3).
edges = edgeset_new(Q0, R0, W, H);
n = arc_door_wedge(cells, edges, 0, 0, 2, 0, 1, 3, 9);
assert(n == 5, "the door covers 5 boundary edges, got {n}");
// Nothing about the arc moved.
assert(hexset_count(cells) == before, "not one cell was added or removed");
assert(arc_shell_max(cells, 0, 0) == shell_before, "the shell is unchanged");
assert(arc_is_disk(cells, 0, 0), "it is still exactly a disk");
(cq1, cr1, ok1) = arc_recover_centre(cells);
assert(ok1 && cq1 == cq0 && cr1 == cr0, "and the centre still recovers to the same cell");
// The door reads back off storage — a count, not a fit.
assert(arc_door_edges(cells, edges, 9) == n, "and the door reads back as {n} edges");
assert(arc_door_edges(cells, edges, 3) == 0, "while a material nobody wrote reads back empty");
// The wedge itself is an exact integer test.
assert(arc_wedge_has(0, 0, 1, 0, 2, 0, 1, 3), "the cell due east is in the wedge");
assert(!arc_wedge_has(0, 0, -1, 0, 2, 0, 1, 3), "the one due west is not");
}
// @HXS-010 — A CONTROLLER HAS NO POINT AT ALL, AND AIMING A RUN IS NOT THE
// SAME MAP AS DRAGGING ONE.
//
// `@HXS-008` walks the three snaps between a MOUSE CLICK and what gets stored. A
// controller does not have a click: it has a facing and a push — an angle and a
// magnitude, with no point anywhere in it. `snap_run_from_heading` is that entry
// point, and the temptation is to implement it by building a target point from the
// two and calling `snap_run_d24`/`snap_run_p`.
//
// ⚠ THAT WOULD BE A DIFFERENT ANSWER, NOT A TIDIER ROUTE TO THE SAME ONE, because
// the two snaps minimise different quantities:
//
// DRAG `snap_run_d24` picks the direction whose best legal ENDPOINT lands nearest
// the target — ONE 2-D distance, in which a direction error and a length
// error trade freely against each other.
// AIM `snap_run_from_heading` quantises the two axes SEPARATELY: the angle
// against `D`, the length against the legal runs in the chosen direction.
//
// An author who aims is making two statements, "this way" and "this far", and expects
// the first to be honoured as a direction. An author who drags is making one, "to
// here". Those are different intents and they produce different walls.
//
// THE CASE BELOW IS THE CLEANEST ONE: aim 9 degrees, push 3.93 world units.
//
// aim -> d24 1 (13.898 deg), length 6.245 — the ANGLE is honoured to 4.9 deg,
// and the wall comes out 59% LONGER than the push asked for
// drag -> d24 0 (0 deg), length 3.464 — the POINT is honoured to 0.74 wu, and
// the direction is off by the whole 9 degrees
//
// and the two ends are 3.0 world units apart. ⚠ THE CAUSE IS THE PERIOD, NOT THE
// ANGLE: d24 1's smallest run is 6.245 wu while d24 0's is 1.732, so a SHORT run in an
// in-between direction does not exist at all. The drag notices and leaves the
// direction; the aim cannot, and must round the length up instead.
//
// A consumer must therefore pick ONE idiom per gesture and show what it will produce.
// Mixing them — start by aiming, finish by dragging — yields a wall neither would.
fn test_aiming_a_run_is_not_the_same_map_as_dragging_one() {
(va, vb) = nearest_vertex(0.0, 0.0);
ax = tri_x(va, vb);
ay = tri_y(va, vb);
dist = 3.93;
rad = 9.0 * 0.017453292519943295;
tx = ax + cos(rad) * dist;
ty = ay + sin(rad) * dist;
// AIM: the angle is quantised against `D`'s ACTUAL angles, not the nominal
// 15-degree multiples — 9 degrees is nearer d24 1 (13.898) than d24 0.
(da, pa) = snap_run_from_heading(va, vb, 9.0, dist);
assert(da == 1, "aiming 9 degrees chooses d24 1, got {da}");
assert(pa == 1, "and one period of it, got {pa}");
assert(wall_run_ok(da, va, vb, pa), "which is an admissible run");
alen = wall_run_len(da, pa);
assert(alen > 6.2 && alen < 6.3,
"the aimed wall is {alen} wu long — the push asked for {dist}");
// DRAG: the same intent expressed as a point.
dt = snap_run_d24(va, vb, tx, ty);
pt = snap_run_p(dt, va, vb, tx, ty);
assert(dt == 0 && pt == 2, "dragging to the same point gives d24 {dt} p {pt}");
tlen = wall_run_len(dt, pt);
assert(tlen > 3.4 && tlen < 3.5, "a {tlen} wu wall, much nearer the {dist} asked");
// ⚠ EACH HONOURS WHAT ITS OWN AUTHOR SAID, AND NEITHER IS WRONG.
assert(run_end_dist(dt, va, vb, pt, tx, ty) < 0.8,
"the drag lands within 0.8 wu of the point");
assert(run_end_dist(da, va, vb, pa, tx, ty) > 2.3,
"the aim lands over 2.3 wu from it — because it kept the angle instead");
// The two walls are genuinely different objects.
ea_x = tri_x(wall_end_a(da, va, pa), wall_end_b(da, vb, pa));
ea_y = tri_y(wall_end_a(da, va, pa), wall_end_b(da, vb, pa));
et_x = tri_x(wall_end_a(dt, va, pt), wall_end_b(dt, vb, pt));
et_y = tri_y(wall_end_a(dt, va, pt), wall_end_b(dt, vb, pt));
gap = sqrt((ea_x - et_x) * (ea_x - et_x) + (ea_y - et_y) * (ea_y - et_y)) ?? 0.0;
assert(gap > 2.9 && gap < 3.1, "their far ends are {gap} wu apart");
// ⚠ AND THE REASON IS THE PERIOD. The twelve in-between directions have a period
// roughly 3.6x the exact ones, so they cannot make a short wall at all.
assert(!wall_is_exact(da), "d24 {da} is an in-between direction");
assert(wall_is_exact(dt), "d24 {dt} is an exact one");
assert(wall_run_len(da, 1) > wall_run_len(dt, 1) * 3.0,
"the in-between's period {wall_run_len(da, 1)} dwarfs the exact one's "
+ "{wall_run_len(dt, 1)} — which is why the aim had to overshoot");
}