← hex_way · API reference · version 0.1.6
3 file(s), 1032 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 |
|---|---|---|---|
WAY_STRAIGHT const | 25 | — | 46 80 99 118 140 181 … (9 total) |
WAY_ARC const | 26 | — | 55 160 |
Track struct | 30 | — | 38 41 45 54 61 79 … (23 total) |
track_new fn | 38 | — | 179 14 19 27 37 47 … (22 total) |
track_straight fn | 45 | — | 185 20 28 48 138 143 … (13 total) |
track_arc fn | 54 | — | 192 39 139 144 263 289 … (12 total) |
seg_len fn | 79 | — | 93 100 119 229 427 23 … (17 total) |
track_len fn | 91 | — | 15 21 40 115 281 |
seg_point fn | 98 | — | 228 229 29 45 46 153 … (13 total) |
seg_tangent fn | 117 | — | 182 261 32 |
seg_curvature fn | 139 | HXY-001 | 182 183 184 185 186 |
offset_legal fn | 156 | HXY-003 | 265 266 267 268 291 292 |
track_offset fn | 178 | HXY-001 | 152 161 163 165 167 174 … (12 total) |
seg_distance fn | 200 | — | 241 279 425 490 491 |
track_distance fn | 238 | — | 302 349 49 176 274 284 … (7 total) |
way_surfaces fn | 257 | HXY-004 | 317 318 |
nearest_seg fn | 275 | — | 360 |
way_mark fn | 297 | HXY-004 | 55 336 337 418 420 508 … (7 total) |
cut_arb fn | 312 | HXY-004 | 338 |
way_stamp fn | 343 | HXY-004 | 326 327 331 332 |
seg_param fn | 374 | HXY-005 | 430 377 381 386 |
way_param fn | 419 | HXY-005 | 453 |
way_steps fn | 447 | HXY-006 | 426 428 436 442 |
src/hex_way.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_way — WAYS as exact curves, and the one pass that rasterises them.
//
// A way is authored as ONE exact centreline;
// every other line — rails, ruts, lane markings, bed edges — is an OFFSET of it. That is
// what escapes the hex quantisation floor: rasterising a band bottoms out, while an offset
// is equidistant by construction.
//
// The floor is not one number. It is the spacing of the cell centres ACROSS the way, so it
// depends on the heading: 0.866 (half a hex width) down a column, 1.5 down a row — and
// between two such rings every requested width gives the identical footprint. 0.5 hex
// widths is the best case, not the case.
//
// The load-bearing function is `way_stamp`: it walks the centreline once and writes the
// cells, the blocked edges AND the surface ids in a single pass. The surface id is attached where it is known — at derivation time — which
// is why no separate index is ever needed to answer "which wall segment did I hit?".
//
// Segment record: 6 floats, uniform for both kinds (flat arrays, no vector<vector<>>).
// straight: [x0, y0, x1, y1, 0, 0]
// arc: [cx, cy, R, a0, a1, 0]
use hex_field::*;
use hex_edge::*;
pub const WAY_STRAIGHT = 1;
pub const WAY_ARC = 2;
// A track: straights and arcs (`WAY_STRAIGHT`, `WAY_ARC`) in order, six numbers per segment.
// `track_new` makes an empty one.
pub struct Track {
pub tk_kind: vector<integer>,
pub tk_p: vector<float>, // 6 per segment
pub tk_n: integer,
}
// An empty track. Append straights and arcs in order; the track IS the sequence, and
// segment index is the only identity a caller needs.
pub fn track_new() -> Track {
k: vector<integer> = [];
p: vector<float> = [];
Track { tk_kind: k, tk_p: p, tk_n: 0 }
}
// Append a straight from one point to another.
pub fn track_straight(t: Track, x0: float, y0: float, x1: float, y1: float) {
t.tk_kind += [WAY_STRAIGHT];
t.tk_p += [x0]; t.tk_p += [y0]; t.tk_p += [x1]; t.tk_p += [y1];
t.tk_p += [0.0]; t.tk_p += [0.0];
t.tk_n = t.tk_n + 1;
}
// Append an arc: centre, radius and the angles it runs between. Arcs and straights are
// measured and sampled through the same routines, so a mixed track needs no special case.
pub fn track_arc(t: Track, cx: float, cy: float, r: float, a0: float, a1: float) {
t.tk_kind += [WAY_ARC];
t.tk_p += [cx]; t.tk_p += [cy]; t.tk_p += [r]; t.tk_p += [a0];
t.tk_p += [a1]; t.tk_p += [0.0];
t.tk_n = t.tk_n + 1;
}
fn pv(t: Track, i: integer, j: integer) -> float { t.tk_p[i * 6 + j] ?? 0.0 }
// An angle difference brought into [0, 2pi).
//
// Example: @HXY-007
//
// ⚠ **BOTH DIRECTIONS.** A wrap that only adds 2pi leaves an arc authored below -2pi
// answering that points lying ON it are a radius away: one curve, two footprints. Every
// angle comparison in this file goes through this one wrap.
fn ang_wrap(d: float) -> float {
v = d;
while v < 0.0 { v = v + 6.283185307179586; }
while v >= 6.283185307179586 { v = v - 6.283185307179586; }
v
}
// One segment's length. The unit `track_len` sums and the unit a position along the track
// is measured in.
pub fn seg_len(t: Track, i: integer) -> float {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
dx = pv(t, i, 2) - pv(t, i, 0);
dy = pv(t, i, 3) - pv(t, i, 1);
return sqrt(dx * dx + dy * dy) ?? 0.0;
}
d = pv(t, i, 4) - pv(t, i, 3);
if d < 0.0 { d = 0.0 - d; }
d * pv(t, i, 2)
}
// The whole track's length, for pacing a mover or normalising a position to 0..1.
pub fn track_len(t: Track) -> float {
s = 0.0;
for i in 0..t.tk_n { s = s + seg_len(t, i); }
s
}
// Point at arc-length `s` along segment i.
pub fn seg_point(t: Track, i: integer, s: float) -> (float, float) {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
seg_l = seg_len(t, i);
if seg_l < 0.000001 { return (pv(t, i, 0), pv(t, i, 1)); }
u = s / seg_l ?? 0.0;
return (pv(t, i, 0) + (pv(t, i, 2) - pv(t, i, 0)) * u,
pv(t, i, 1) + (pv(t, i, 3) - pv(t, i, 1)) * u);
}
r = pv(t, i, 2);
a0 = pv(t, i, 3);
a1 = pv(t, i, 4);
dir = 1.0;
if a1 < a0 { dir = 0.0 - 1.0; }
a = a0 + (s / r ?? 0.0) * dir;
(pv(t, i, 0) + r * cos(a), pv(t, i, 1) + r * sin(a))
}
// The direction of travel a distance `s` into segment `i` — what a mover faces, and what a
// carriage or a camera is aligned to.
pub fn seg_tangent(t: Track, i: integer, s: float) -> (float, float) {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
seg_l = seg_len(t, i);
if seg_l < 0.000001 { return (1.0, 0.0); }
return ((pv(t, i, 2) - pv(t, i, 0)) / seg_l ?? 0.0,
(pv(t, i, 3) - pv(t, i, 1)) / seg_l ?? 0.0);
}
r = pv(t, i, 2);
a0 = pv(t, i, 3);
a1 = pv(t, i, 4);
dir = 1.0;
if a1 < a0 { dir = 0.0 - 1.0; }
a = a0 + (s / r ?? 0.0) * dir;
(0.0 - sin(a) * dir, cos(a) * dir)
}
// Signed curvature: + turning left (centre on the left of travel), - turning right.
//
// The same left-positive convention `track_offset` uses, which is why the two are checked
// against each other: on a turn signed positive, the positive offset is the INSIDE rail.
//
// Example: @HXY-001
pub fn seg_curvature(t: Track, i: integer) -> float {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT { return 0.0; }
r = pv(t, i, 2);
if r < 0.000001 { return 0.0; }
if pv(t, i, 4) >= pv(t, i, 3) { return 1.0 / r ?? 0.0; }
0.0 - (1.0 / r ?? 0.0)
}
// I-CURV — an offset degenerates when |d| >= R. This IS the minimum curve radius, and
// it falls out of the geometry rather than being a tuning constant.
//
// |d|, not d, because a way has BOTH rails: the outer one is never in trouble and the
// inner one collapses at the tightest turn anywhere on the track. So the question is
// "may this WAY be this wide", and past the answer `track_offset` still returns a Track —
// one whose inner arc has a NEGATIVE length and is no longer equidistant.
//
// Example: @HXY-003
pub fn offset_legal(t: Track, d: float) -> boolean {
ad = d; if ad < 0.0 { ad = 0.0 - ad; }
ok = true;
for i in 0..t.tk_n {
if (t.tk_kind[i] ?? 0) == WAY_ARC {
if ad >= pv(t, i, 2) { ok = false; }
}
}
ok
}
// The parallel track at distance d (left normal positive). For straights and arcs the
// offset is again a straight / a concentric arc, so equidistance is EXACT here — the
// only cost anywhere is flattening, err ~ c^2/(8R).
//
// EQUIDISTANCE DOES NOT PICK A SIDE. A left offset of a counter-clockwise turn moves
// TOWARDS the centre, so its radius SHRINKS by d; only a right offset grows. Reading the
// sweep direction the other way up would put every arc on the far side of the way from
// its straights — still exactly d from the centreline, still passing an equidistance
// gate, and no longer JOINED. The number that sees it is the gap at a joint.
//
// Example: @HXY-001
pub fn track_offset(t: Track, d: float) -> Track {
o = track_new();
for i in 0..t.tk_n {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
(tx, ty) = seg_tangent(t, i, 0.0);
nx = 0.0 - ty;
ny = tx;
track_straight(o, pv(t, i, 0) + nx * d, pv(t, i, 1) + ny * d,
pv(t, i, 2) + nx * d, pv(t, i, 3) + ny * d);
} else {
dir = 1.0;
if pv(t, i, 4) < pv(t, i, 3) { dir = 0.0 - 1.0; }
// Minus, not plus: travelling counter-clockwise the centre is on the LEFT, so the
// left offset is the SMALLER concentric arc.
track_arc(o, pv(t, i, 0), pv(t, i, 1), pv(t, i, 2) - d * dir,
pv(t, i, 3), pv(t, i, 4));
}
}
o
}
// Distance from a point to the centreline (min over segments).
pub fn seg_distance(t: Track, i: integer, px: float, py: float) -> float {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
ax = pv(t, i, 0); ay = pv(t, i, 1);
vx = pv(t, i, 2) - ax; vy = pv(t, i, 3) - ay;
len2 = vx * vx + vy * vy;
u = 0.0;
if len2 > 0.000000001 { u = ((px - ax) * vx + (py - ay) * vy) / len2 ?? 0.0; }
if u < 0.0 { u = 0.0; }
if u > 1.0 { u = 1.0; }
dx = px - (ax + u * vx);
dy = py - (ay + u * vy);
return sqrt(dx * dx + dy * dy) ?? 0.0;
}
cx = pv(t, i, 0); cy = pv(t, i, 1); r = pv(t, i, 2);
dx = px - cx; dy = py - cy;
dd = sqrt(dx * dx + dy * dy) ?? 0.0;
a = atan2(dy, dx);
lo = pv(t, i, 3); hi = pv(t, i, 4);
if hi < lo { tmp = lo; lo = hi; hi = tmp; }
// ⚠ THE OFFSET FROM `lo`, WRAPPED, AGAINST THE SPAN — never a slide up to `lo` alone.
//
// Example: @HXY-007
// A sweep of 2pi or more covers the circle, and `da <= span` says so without a case.
if ang_wrap(a - lo) <= hi - lo {
v = dd - r;
if v < 0.0 { v = 0.0 - v; }
return v;
}
(e0x, e0y) = seg_point(t, i, 0.0);
(e1x, e1y) = seg_point(t, i, seg_len(t, i));
d0 = sqrt((px - e0x) * (px - e0x) + (py - e0y) * (py - e0y)) ?? 0.0;
d1 = sqrt((px - e1x) * (px - e1x) + (py - e1y) * (py - e1y)) ?? 0.0;
if d0 < d1 { return d0; }
d1
}
// How far a point lies from the track. The test for "am I on the road", and the reason the
// per-segment distance is kept cheap.
pub fn track_distance(t: Track, px: float, py: float) -> float {
best = 1000000.0;
for i in 0..t.tk_n {
d = seg_distance(t, i, px, py);
if d < best { best = d; }
}
best
}
// ── the one pass: cells + edges + surface ids together ───────────────────────
//
// Walks the centreline once, marks every cell within `halfwidth`, then cuts the boundary
// into the EdgeSet — recording, per edge, WHICH segment of the way blocks it. That is
// the whole reason no lookup index is needed later.
// Register this track's segments as surfaces; returns the id of the FIRST one (ids are
// contiguous, so segment i has id first+i).
//
// Example: @HXY-004
pub fn way_surfaces(t: Track, sf: Surfaces) -> integer {
first = 0;
for i in 0..t.tk_n {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
(tx, ty) = seg_tangent(t, i, 0.0);
id = surf_straight(sf, 0.0 - ty, tx,
(0.0 - ty) * pv(t, i, 0) + tx * pv(t, i, 1));
if i == 0 { first = id; }
} else {
id = surf_arc(sf, pv(t, i, 0), pv(t, i, 1), pv(t, i, 2));
if i == 0 { first = id; }
}
}
first
}
// Which segment of the track is nearest this point? Nearest wins, ties to the lower
// index — deterministic and order-free, the same rule `cut_arb` applies to surfaces.
pub fn nearest_seg(t: Track, px: float, py: float) -> integer {
best = -1;
bd = 1000000.0;
for i in 0..t.tk_n {
d = seg_distance(t, i, px, py);
if d < bd - 0.000000001 { bd = d; best = i; }
}
best
}
// MARK ONLY — phase 1 of the two-phase build.
//
// Marking cells and cutting edges must NOT be interleaved per part. If each
// part cuts its own boundary as it lands, a later part can turn an already-blocked edge
// into an interior one — and that stale edge is never cleared, so the result depends on
// which part ran first. Mark every part, THEN cut once.
//
// A cell is marked on its CENTRE, so the footprint quantises and `halfwidth` does not
// survive the trip: between two rings of centres, every width gives the same cells.
//
// Example: @HXY-002
// Example: @HXY-004
pub fn way_mark(t: Track, halfwidth: float, s: HexSet) {
for r in hexset_r0(s)..hexset_r0(s) + hexset_h(s) {
for q in hexset_q0(s)..hexset_q0(s) + hexset_w(s) {
px = (lattice_k(q, r) as float) * 0.8660254037844386;
py = (lattice_m(q, r) as float) * 0.5;
if track_distance(t, px, py) <= halfwidth { hexset_set(s, q, r, true); }
}
}
}
// CUT ONCE — phase 2. For each boundary edge of the FINAL footprint, pick the nearest
// surface among ALL registered ones (ties to the lower id). Order-independent by
// construction: it never looks at what was stamped when, only at the finished cells.
//
// Example: @HXY-004
pub fn cut_arb(s: HexSet, e: EdgeSet, sf: Surfaces, nsurf: integer, mat: integer) {
for r in hexset_r0(s)..hexset_r0(s) + hexset_h(s) {
for q in hexset_q0(s)..hexset_q0(s) + hexset_w(s) {
if hexset_get(s, q, r) {
for d in 0..6 {
nq = nb_q(q, r, d);
nr = nb_r(q, r, d);
if !hexset_get(s, nq, nr) {
(ex, ey) = edge_point(q, r, nq, nr);
best = -1;
bd = 1000000.0;
for si in 1..nsurf {
dd = surf_distance(sf, si, ex, ey);
if dd < bd - 0.000000001 { bd = dd; best = si; }
}
if best >= 0 { edge_block_full(e, q, r, nq, nr, best, mat); }
}
}
}
}
}
}
// Mark the way's footprint into `s`, cut its boundary into `e`, and tag every blocked
// edge with the surface of the NEAREST segment. One walk, one source of truth.
//
// For ONE way. Calling it once per part of a multi-part way is the interleaving the
// phase-1 note above forbids — same cells, walls left standing inside the finished
// footprint, and their owner decided by the stamping order.
//
// Example: @HXY-004
pub fn way_stamp(t: Track, halfwidth: float, s: HexSet, e: EdgeSet,
first_surf: integer, mat: integer) {
for r in hexset_r0(s)..hexset_r0(s) + hexset_h(s) {
for q in hexset_q0(s)..hexset_q0(s) + hexset_w(s) {
px = (lattice_k(q, r) as float) * 0.8660254037844386;
py = (lattice_m(q, r) as float) * 0.5;
if track_distance(t, px, py) <= halfwidth { hexset_set(s, q, r, true); }
}
}
for r in hexset_r0(s)..hexset_r0(s) + hexset_h(s) {
for q in hexset_q0(s)..hexset_q0(s) + hexset_w(s) {
if hexset_get(s, q, r) {
for d in 0..6 {
nq = nb_q(q, r, d);
nr = nb_r(q, r, d);
if !hexset_get(s, nq, nr) {
(ex, ey) = edge_point(q, r, nq, nr);
si = nearest_seg(t, ex, ey);
if si >= 0 { edge_block_full(e, q, r, nq, nr, first_surf + si, mat); }
}
}
}
}
}
}
// ── ALONG-WAY PARAMETER, AND STEPS ──────────────────────────────────────────
// Arc length from the start of segment i to the point on it nearest (px,py).
//
// Example: @HXY-005
pub fn seg_param(t: Track, i: integer, px: float, py: float) -> float {
if (t.tk_kind[i] ?? 0) == WAY_STRAIGHT {
ax = t.tk_p[i * 6 + 0] ?? 0.0;
ay = t.tk_p[i * 6 + 1] ?? 0.0;
bx = t.tk_p[i * 6 + 2] ?? 0.0;
by = t.tk_p[i * 6 + 3] ?? 0.0;
vx = bx - ax;
vy = by - ay;
l2 = vx * vx + vy * vy;
if l2 < 0.000000001 { return 0.0; }
u = ((px - ax) * vx + (py - ay) * vy) / l2 ?? 0.0;
if u < 0.0 { u = 0.0; }
if u > 1.0 { u = 1.0; }
return u * (sqrt(l2) ?? 0.0);
}
cx = t.tk_p[i * 6 + 0] ?? 0.0;
cy = t.tk_p[i * 6 + 1] ?? 0.0;
rr = t.tk_p[i * 6 + 2] ?? 0.0;
a0 = t.tk_p[i * 6 + 3] ?? 0.0;
a1 = t.tk_p[i * 6 + 4] ?? 0.0;
a = atan2(py - cy, px - cx);
sweep = a1 - a0;
// Bring the offset into [0, 2pi), then measure it ALONG the arc's own direction.
da = ang_wrap(a - a0);
span = sweep;
if span < 0.0 { span = 0.0 - span; }
tp = da;
if sweep < 0.0 {
tp = 6.283185307179586 - da;
if da < 0.000000001 { tp = 0.0; }
}
// Outside the span, snap to the NEARER end. Clamping blindly to the far end is the
// trap: a point sitting just BEFORE the start wraps to nearly 2pi, and clamping that
// to `span` hands the first cell of a flight the height of the last one — a full-flight
// riser between two neighbouring cells.
if tp > span {
if tp - span < 6.283185307179586 - tp { tp = span; } else { tp = 0.0; }
}
tp * rr
}
// Arc length from the START OF THE WAY to the point on it nearest (px,py). This is the
// milepost: the parameter stations, signals, steps and mileage markers are all stated in.
//
// Example: @HXY-005
pub fn way_param(t: Track, px: float, py: float) -> float {
best = -1;
bd = 1000000000.0;
bacc = 0.0;
acc = 0.0;
for i in 0..t.tk_n {
dd = seg_distance(t, i, px, py);
if dd < bd { bd = dd; best = i; bacc = acc; }
acc = acc + seg_len(t, i);
}
if best < 0 { return 0.0; }
bacc + seg_param(t, best, px, py)
}
// A STAIRCASE: every `tread` of travel along the way raises the ground by `rise`.
//
// The same routine is a terraced hillside, or a ramp at rise = 0. What makes it a good
// test of the whole model is that the tread is quantised by the CELL ROWS, and the row
// spacing depends on the direction of travel (the 24-direction tiers) — so a tread that
// is clean along one heading skips whole steps along another, which shows up as a DOUBLE
// RISER between adjacent cells. That is a real defect (a trip hazard), and it is what
// sets the minimum tread per direction.
//
// The number: a flight's shortest safe tread is the spacing of its own cells along the
// way — sqrt(3) = 1.732 down a row, 1.5 down a column. There is no tread that is fine
// enough for every heading.
//
// Example: @HXY-006
pub fn way_steps(t: Track, s: HexSet, f: Heights, rise: float, tread: float) {
for r in hexset_r0(s)..hexset_r0(s) + hexset_h(s) {
for q in hexset_q0(s)..hexset_q0(s) + hexset_w(s) {
if hexset_get(s, q, r) {
cx = ((lattice_k(q, r)) as float) * 0.8660254037844386;
cy = ((lattice_m(q, r)) as float) * 0.5;
p = way_param(t, cx, cy);
n = (p / tread ?? 0.0) as integer;
height_set(f, q, r, (n as float) * rise);
}
}
}
}
tests/01-hex-way.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_way — the conformance tests: a way is an exact CENTRELINE plus offsets, never a
// rasterised band, and its arc length, tangent and offsets follow from that.
//
// These assert the library's CONTRACT, not its depth. The exhaustive measurement lives in
// hexbody's gates (`tests/line.loft`, `tests/arb.loft`), which print their evidence and carry a
// control that must fire. This file is what CI runs under `LOFT_DENY_WARNINGS=1`.
use hex_way::*;
use hex_edge::*;
use hex_field::*;
fn main() {
t = track_new();
assert(track_len(t) == 0.0, "an empty track has zero length");
}
fn test_a_straight_segment_has_its_euclidean_length() {
t = track_new();
track_straight(t, 0.0, 0.0, 3.0, 4.0); // the 3-4-5 triangle
l = track_len(t);
assert(l > 4.999 && l < 5.001, "a 3-4 straight is 5 long, got {l}");
assert(seg_len(t, 0) > 4.999, "the one segment carries that length");
}
fn test_a_point_along_a_segment_interpolates_exactly() {
t = track_new();
track_straight(t, 0.0, 0.0, 10.0, 0.0);
(x, y) = seg_point(t, 0, 5.0); // halfway
assert(x > 4.999 && x < 5.001, "halfway along is x = 5, got {x}");
assert(y > -0.001 && y < 0.001, "and stays on the line, got y = {y}");
(tx, ty) = seg_tangent(t, 0, 5.0);
assert(tx > 0.999 && ty > -0.001 && ty < 0.001, "the tangent points +x, got ({tx}, {ty})");
}
fn test_an_arc_segment_measures_its_true_arc_length() {
t = track_new();
// a quarter circle of radius 2: length = 2 * pi/2 = pi
track_arc(t, 0.0, 0.0, 2.0, 0.0, 1.5707963267948966);
l = track_len(t);
assert(l > 3.140 && l < 3.143, "a quarter arc of r=2 is pi long, got {l}");
}
fn test_distance_to_the_centreline_is_measured_from_the_line_not_a_band() {
// `X8`: a way is a centreline plus offsets. A point 3 off a straight run is 3 away --
// if the way were stored as a rasterised band this would quantise, and it must not.
t = track_new();
track_straight(t, 0.0, 0.0, 10.0, 0.0);
d = track_distance(t, 5.0, 3.0);
assert(d > 2.999 && d < 3.001, "a point 3 off the centreline is 3 away, got {d}");
}
tests/02-worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for `hex_way` (@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: @HXY-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-way.loft`.
//
// THE ONE IDEA. A way is authored as ONE exact centreline, and every other
// line it has — rails, kerbs, platform faces, lane markings — is an OFFSET of
// that centreline, computed on demand. Nothing but the centreline is stored.
//
// WHY IT IS WORTH THE TROUBLE. The obvious alternative is to store the band of
// cells and read the edges back off it. That works, and it quantises: the cells
// come from a lattice, so the width you asked for is not the width you get, and
// the error does not shrink as you refine the request. `test_the_rasterised
// _band_has_a_floor_the_offset_does_not` measures the floor — and it is not one
// number, it is one number PER HEADING.
//
// WHAT GOES WRONG ANYWAY. Every trap below is the same species: a call that
// answers something entirely plausible, passes every cheap check, and is wrong.
// An offset on the far side of the way is still exactly `d` from the centreline.
// A band asked for at half the width comes back the same size. A road stamped
// part by part covers the same cells as one stamped correctly. In each case the
// number that can tell them apart is NOT the one a caller reaches for first, and
// naming that number is what these examples are for.
use hex_way::*;
use hex_field::*;
use hex_edge::*;
const Q0 = -6;
const R0 = -6;
const W = 13;
const H = 13;
// The lattice -> world map every distance here is measured in. A cell is one unit
// from centre to corner, so it is `sqrt(3)` = 1.732 across the flats and 1.5 from
// one row to the next — an anisotropy two of the examples below are about.
fn wx(q: integer, r: integer) -> float { ((lattice_k(q, r)) as float) * 0.8660254037844386 }
fn wy(q: integer, r: integer) -> float { ((lattice_m(q, r)) as float) * 0.5 }
// How far apart the end of one segment and the start of the next actually are.
fn joint_gap(t: Track, i: integer) -> float {
(ax, ay) = seg_point(t, i, seg_len(t, i));
(bx, by) = seg_point(t, i + 1, 0.0);
sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by)) ?? 0.0
}
fn abs_f(v: float) -> float { if v < 0.0 { return 0.0 - v; } v }
// The cell count a band of this halfwidth rasterises to.
fn band_cells(t: Track, hw: float) -> integer {
s = hexset_chunk(-8, -8, 17, 17);
way_mark(t, hw, s);
hexset_count(s)
}
// Edges that are blocked while BOTH their cells are inside the footprint — a wall
// standing in the middle of the road. Each such edge is reached from both of its
// cells, so the raw count is halved.
fn walls_inside(s: HexSet, e: EdgeSet) -> integer {
n = 0;
for r in R0..R0 + H {
for q in Q0..Q0 + W {
if hexset_get(s, q, r) {
for d in 0..6 {
nq = nb_q(q, r, d);
nr = nb_r(q, r, d);
if hexset_get(s, nq, nr) && edge_blocked(e, q, r, nq, nr) { n = n + 1; }
}
}
}
}
n / 2 ?? 0
}
// The biggest height difference between two NEIGHBOURING cells of a flight, and how
// many neighbouring pairs there were to look at — a flight whose cells are not
// neighbours has no risers to measure, and would otherwise read as perfectly smooth.
fn worst_riser(s: HexSet, f: Heights) -> float {
m = 0.0;
for r in R0..R0 + H {
for q in Q0..Q0 + W {
if hexset_get(s, q, r) {
for d in 0..6 {
nq = nb_q(q, r, d);
nr = nb_r(q, r, d);
if hexset_get(s, nq, nr) {
dz = abs_f(height_get(f, q, r) - height_get(f, nq, nr));
if dz > m { m = dz; }
}
}
}
}
}
m
}
fn adjacent_pairs(s: HexSet) -> integer {
n = 0;
for r in R0..R0 + H {
for q in Q0..Q0 + W {
if hexset_get(s, q, r) {
for d in 0..6 {
if hexset_get(s, nb_q(q, r, d), nb_r(q, r, d)) { n = n + 1; }
}
}
}
}
n
}
fn main() {
assert(track_len(track_new()) == 0.0, "an empty way is zero long");
}
// @HXY-001 — AN OFFSET HAS TO JOIN, AND EQUIDISTANCE CANNOT SEE THAT IT DOES.
//
// `track_offset` is the whole reason a way is stored as a centreline: the rail,
// the kerb, the platform face are all this one call at different `d`. So the
// property everyone reaches for is equidistance — every point of the offset is
// `d` from the centreline — and it is the wrong property to check ALONE, because
// it holds just as exactly on the wrong side.
//
// Offset a way that goes straight and then turns. If the turn's offset lands on
// the far side, the result is still a valid pair of curves, each still exactly
// `d` from the line, and the rail now JUMPS by `2d` where the straight meets the
// turn. A train would need to be lifted over it.
//
// The number that sees it is the gap at the joint, and it is zero here for every
// combination of turn direction and side — which is what "left normal positive"
// has to mean for arcs too. An equidistance gate alone cannot see an arc branch
// that reads the sweep direction the other way up; this joint test does.
fn test_an_offset_has_to_join_where_the_centreline_joins() {
// A left turn: 10 units of straight, then a quarter circle about (10, 5).
left = track_new();
track_straight(left, 0.0, 0.0, 10.0, 0.0);
track_arc(left, 10.0, 5.0, 5.0, -1.5707963267948966, 0.0);
// A right turn: the mirror, about (10, -5), swept the other way.
right = track_new();
track_straight(right, 0.0, 0.0, 10.0, 0.0);
track_arc(right, 10.0, -5.0, 5.0, 1.5707963267948966, 0.0);
assert(joint_gap(left, 0) < 0.000000001, "the centreline itself joins");
assert(joint_gap(right, 0) < 0.000000001, "and so does the mirrored one");
// `d` positive is the LEFT of travel, for a straight and for a turn alike, so
// the left rail of the left turn starts at (10, 1) — the same point the offset
// straight ends at.
lo = track_offset(left, 1.0);
(bx, by) = seg_point(lo, 1, 0.0);
assert(abs_f(bx - 10.0) < 0.000000001 && abs_f(by - 1.0) < 0.000000001,
"the left rail of a left turn starts at (10, 1), got ({bx}, {by})");
// All four combinations of turn direction and side, and the gap is the check.
k = 1;
while k <= 8 {
d = (k as float) * 0.5; // 0.5 .. 4.0, all inside the limit
assert(joint_gap(track_offset(left, d), 0) < 0.000000001,
"left turn, left rail at d={d} joins");
assert(joint_gap(track_offset(left, 0.0 - d), 0) < 0.000000001,
"left turn, right rail at d={d} joins");
assert(joint_gap(track_offset(right, d), 0) < 0.000000001,
"right turn, left rail at d={d} joins");
assert(joint_gap(track_offset(right, 0.0 - d), 0) < 0.000000001,
"right turn, right rail at d={d} joins");
k = k + 1;
}
// And the property that could not tell the two apart, so nobody mistakes this
// for a test of it: equidistance holds — it held before the fix too.
o = track_offset(left, 1.0);
(mx, my) = seg_point(o, 1, seg_len(o, 1) * 0.5);
assert(abs_f(track_distance(left, mx, my) - 1.0) < 0.000000001,
"the offset is exactly 1.0 from the centreline");
// `seg_curvature` states the same left/right convention as a number, and the two
// have to agree: on a turn signed POSITIVE the positive offset is the INSIDE one,
// so its rail is the shorter of the two. Disagree, and one of them is the bug.
assert(abs_f(seg_curvature(left, 1) - 0.2) < 0.000000001,
"a left turn of radius 5 has curvature +0.2, got {seg_curvature(left, 1)}");
assert(abs_f(seg_curvature(right, 1) + 0.2) < 0.000000001,
"the right turn has -0.2, got {seg_curvature(right, 1)}");
assert(seg_curvature(left, 0) == 0.0, "and a straight has none");
assert(seg_len(track_offset(left, 1.0), 1) < seg_len(left, 1),
"the left rail of a left turn is the inside one, so it is shorter");
assert(seg_len(track_offset(right, 1.0), 1) > seg_len(right, 1),
"and of a right turn it is the outside one, so it is longer");
}
// @HXY-002 — THE RASTERISED BAND HAS A FLOOR; THE OFFSET DOES NOT.
//
// This is why nothing but the centreline is stored. `way_mark` marks every cell
// whose CENTRE is within `halfwidth` of the way, so the width you get is the
// width of the nearest ring of cell centres — and between two rings, a whole
// interval of requested widths gives the identical footprint.
//
// The interval is the floor, and the module header quotes it as "around 0.5 hex
// widths". That is the best case, not the case. Measured here: a way along a
// ROW cannot resolve anything finer than 1.5 (0.87 hex widths); the same way
// along a COLUMN resolves 0.866 (0.5 hex widths). The floor is a function of
// the heading, and the two headings differ by sqrt(3).
//
// So a caller asking for a 0.1-wide footpath and a 1.4-wide lane along a row
// gets the same cells twice. `track_offset` at those two widths gets two
// different exact lines, which is the point: the cells are a rasterisation of
// the way, never the way.
fn test_the_rasterised_band_has_a_floor_the_offset_does_not() {
along_row = track_new();
track_straight(along_row, -20.0, 0.0, 20.0, 0.0);
along_col = track_new();
track_straight(along_col, 0.0, -20.0, 0.0, 20.0);
// Along a row the rings of cell centres are 1.5 apart.
narrow = band_cells(along_row, 0.1);
assert(band_cells(along_row, 1.4999) == narrow,
"a 0.1 path and a 1.4999 lane rasterise identically along a row");
assert(band_cells(along_row, 1.5001) > narrow,
"and the footprint only grows once 1.5 is crossed");
// Along a column they are sqrt(3)/2 = 0.866 apart — a finer floor, same way.
thin = band_cells(along_col, 0.1);
assert(band_cells(along_col, 0.8659) == thin,
"the same two widths along a column rasterise identically too");
assert(band_cells(along_col, 0.8661) > thin,
"and that footprint grows at 0.866, not at 1.5");
// The two floors are a factor sqrt(3) apart: the SAME requested width resolves
// differently depending only on which way the road points.
assert(band_cells(along_row, 1.0) == narrow, "1.0 is below the row floor");
assert(band_cells(along_col, 1.0) > thin, "and above the column floor");
// The offset has no floor at all: ask for the two widths, get two lines.
a = track_offset(along_row, 0.1);
b = track_offset(along_row, 1.4999);
(ax, ay) = seg_point(a, 0, 5.0);
(bx, by) = seg_point(b, 0, 5.0);
assert(abs_f(ay - 0.1) < 0.000000001, "the 0.1 offset is at y = 0.1, got {ay}");
assert(abs_f(by - 1.4999) < 0.000000001, "the 1.4999 offset is at y = 1.4999, got {by}");
assert(abs_f(ax - bx) < 0.000000001, "both run along the same x");
}
// @HXY-003 — `offset_legal` IS A WIDTH TEST, NOT AN OFFSET TEST.
//
// It takes `d` and uses `|d|`, which looks like a needless restriction until you
// remember what a way is: the left rail AND the right rail, at +d and -d. The
// outer one is never in trouble; the inner one collapses when `|d|` reaches the
// tightest turn radius on the whole track. So the question `offset_legal`
// answers is "may this WAY be this wide", and the answer is set by the sharpest
// turn anywhere along it — which is the minimum curve radius, falling out of the
// geometry rather than being chosen.
//
// Past the limit `track_offset` still returns a Track, and it is nonsense in two
// measurable ways: the inner arc's radius goes negative, so its length is
// NEGATIVE and the way gets shorter as it gets wider; and equidistance — the one
// guarantee the whole design rests on — stops holding. Asked for 6.0 on a
// radius-5 turn, the "offset" sits 5.71 from the centreline.
fn test_offset_legal_is_a_width_test_set_by_the_sharpest_turn() {
t = track_new();
track_straight(t, 0.0, 0.0, 10.0, 0.0);
track_arc(t, 10.0, 5.0, 5.0, -1.5707963267948966, 0.0); // R = 5
assert(offset_legal(t, 4.5), "a way 9 wide fits a radius-5 turn");
assert(offset_legal(t, -4.5), "and the sign does not matter — it is a width");
assert(!offset_legal(t, 5.0), "at the turn radius the inner rail is a point");
assert(!offset_legal(t, -5.0), "from either side");
// Just inside the limit everything still holds exactly.
ok = track_offset(t, 4.5);
assert(seg_len(ok, 1) > 0.0, "the inner arc still has a positive length");
(px, py) = seg_point(ok, 1, seg_len(ok, 1) * 0.5);
assert(abs_f(track_distance(t, px, py) - 4.5) < 0.000000001,
"and is exactly 4.5 from the centreline");
// Just outside it, both failures are visible.
bad = track_offset(t, 6.0);
assert(seg_len(bad, 1) < 0.0,
"past the limit the inner arc has a NEGATIVE length, got {seg_len(bad, 1)}");
assert(track_len(bad) < track_len(ok),
"so the way gets SHORTER as it gets wider");
(qx, qy) = seg_point(bad, 1, seg_len(bad, 1) * 0.5);
assert(abs_f(track_distance(t, qx, qy) - 6.0) > 0.25,
"and equidistance is gone: asked 6.0, sits {track_distance(t, qx, qy)} out");
// It is the whole track's tightest turn that decides, not the one being read.
two = track_new();
track_arc(two, 0.0, 0.0, 9.0, 0.0, 0.5); // a gentle sweep
track_arc(two, 0.0, 0.0, 2.0, 0.0, 0.5); // and a tight one
assert(offset_legal(two, 1.5), "1.5 clears the tight turn as well as the gentle one");
assert(!offset_legal(two, 2.0), "2.0 does not — the R=2 turn sets the limit for both");
}
// @HXY-004 — MARK EVERY PART, THEN CUT ONCE. STAMPING PART BY PART IS WRONG,
// AND THE FOOTPRINT CANNOT TELL YOU SO.
//
// `way_stamp` marks a way's cells and cuts its boundary in one call, which is
// exactly what you want for ONE way and exactly what you must not do for two.
// Cutting turns "outside the footprint" into a blocked edge — and the footprint
// grows when the next part lands, so an edge that was a boundary becomes an
// interior edge with a wall still standing in it. Nothing ever clears it.
//
// Two lanes of a road, side by side. All three routes below cover the SAME 23
// cells, so a footprint check passes on all three. Stamping them one at a time
// leaves 22 walls down the middle of the road, and WHICH surface owns them
// depends on which lane was stamped first — the order-dependence the two-phase
// rule exists to remove. `way_mark` both, then `cut_arb` once: no walls, and
// the answer never looks at what was stamped when.
fn test_mark_every_part_then_cut_once() {
a = track_new();
track_straight(a, -9.0, 0.0, 9.0, 0.0);
b = track_new();
track_straight(b, -9.0, 1.5, 9.0, 1.5); // the neighbouring row of cells
sf = surfaces_new();
fa = way_surfaces(a, sf);
fb = way_surfaces(b, sf);
// `way_surfaces` answers the FIRST id and promises the rest are contiguous, which
// is what lets `way_stamp` name a segment's surface as `first + i` with no lookup.
assert(fa == 1 && fb == fa + a.tk_n,
"ids are contiguous across parts: {fa} then {fb}");
s1 = hexset_chunk(Q0, R0, W, H);
e1 = edgeset_new(Q0, R0, W, H);
way_stamp(a, 0.7, s1, e1, fa, 1);
way_stamp(b, 0.7, s1, e1, fb, 1);
s2 = hexset_chunk(Q0, R0, W, H);
e2 = edgeset_new(Q0, R0, W, H);
way_stamp(b, 0.7, s2, e2, fb, 1); // the same two lanes, other order
way_stamp(a, 0.7, s2, e2, fa, 1);
s3 = hexset_chunk(Q0, R0, W, H);
e3 = edgeset_new(Q0, R0, W, H);
way_mark(a, 0.7, s3);
way_mark(b, 0.7, s3);
cut_arb(s3, e3, sf, fb + b.tk_n, 1);
// The check a caller would make, and it cannot see any of this.
assert(hexset_count(s1) == 23, "lane A then B covers 23 cells");
assert(hexset_count(s2) == 23, "B then A covers the same 23");
assert(hexset_count(s3) == 23, "and so does mark-both-then-cut");
assert(walls_inside(s1, e1) == 22, "stamping part by part leaves 22 walls inside the road");
assert(walls_inside(s2, e2) == 22, "the other order leaves 22 as well");
assert(walls_inside(s3, e3) == 0, "marking both and cutting once leaves none");
// And the stale walls are order-dependent: the same edge, two different owners.
assert(edge_surf(e1, 0, 0, 0, 1) == fa, "A stamped first owns the shared edge");
assert(edge_surf(e2, 0, 0, 0, 1) == fb, "B stamped first owns the very same edge");
assert(edge_surf(e3, 0, 0, 0, 1) == 0, "cut once, nothing owns it — it is not a wall");
}
// @HXY-005 — A MILEPOST NEAR THE START OF A TURN SNAPS TO THE NEARER END, NOT
// THE FARTHER ONE.
//
// `way_param` is the parameter everything along a way is stated in — stations,
// signals, the steps below. On an arc it is an ANGLE turned into a length, and
// an angle wraps: a point that sits just BEFORE the start of the turn is at
// -0.1 radians, which is 2*pi - 0.1 going the arc's way round, i.e. very nearly
// a full circle of travel.
//
// Clamping that into the arc's span would hand it the parameter of the END of
// the turn. On a staircase that is the last step's height given to the first
// cell of the flight — a full-flight riser between two neighbouring cells, and
// the reason this is a worked example rather than a footnote. `seg_param`
// instead snaps to whichever end is angularly nearer, so a point before the
// start reads 0.
fn test_a_milepost_before_a_turn_reads_zero_not_a_full_lap() {
arc = track_new();
track_arc(arc, 0.0, 0.0, 4.0, 0.0, 1.5707963267948966); // R = 4, a quarter turn
span = seg_len(arc, 0);
assert(abs_f(span - 6.283185307179586) < 0.000001, "the quarter turn is 6.283 long");
// On the arc, the parameter is the arc length as expected.
mid = seg_param(arc, 0, 4.0 * cos(0.7853981633974483), 4.0 * sin(0.7853981633974483));
assert(abs_f(mid - span * 0.5) < 0.000001, "halfway round reads half the length, got {mid}");
// 0.1 radians BEFORE the start. The wrap says 6.18; the answer is 0.
before = seg_param(arc, 0, 4.0 * cos(-0.1), 4.0 * sin(-0.1));
assert(before == 0.0, "a point before the turn reads 0, got {before}");
assert(before < span * 0.5, "and emphatically not the far end of the turn");
// 0.1 radians after the end, which really does belong at the far end.
after = seg_param(arc, 0, 4.0 * cos(1.67), 4.0 * sin(1.67));
assert(abs_f(after - span) < 0.000001, "a point past the turn reads the full span, got {after}");
}
// @HXY-006 — THE SHORTEST SAFE TREAD IS A FUNCTION OF THE HEADING.
//
// `way_steps` raises the ground by `rise` every `tread` of travel, which is a
// staircase, a terrace, or a ramp at rise 0. The tread is continuous; the cells
// it is quantised onto are not, and their spacing ALONG the way depends on which
// way the way points — sqrt(3) = 1.732 apart down a row, 1.5 apart down a
// column. Ask for a tread finer than that spacing and consecutive cells skip a
// step: a DOUBLE RISER between two neighbours, which is a trip hazard on a stair
// and a cliff on a terrace.
//
// So there is no single "fine enough" tread. 1.5 is exactly right along a
// column and produces double risers along a row. Whatever the flight's heading,
// the shortest tread it can carry is the spacing of its own cells — and this
// example measures both, rather than quoting one.
//
// This is @HXY-002's anisotropy seen end-on: the lattice that makes the ACROSS
// resolution coarser along a row is the one that makes the ALONG spacing coarser
// there too.
fn test_the_shortest_safe_tread_depends_on_the_heading() {
along_row = track_new();
track_straight(along_row, -12.0, 0.0, 12.0, 0.0);
along_col = track_new();
track_straight(along_col, 0.0, -12.0, 0.0, 12.0);
// A halfwidth of 1.0 clears the column floor (0.866) and not the row floor
// (1.5), which is what makes BOTH footprints connected — a flight whose cells
// are not neighbours has no risers, and would read as flawless.
sr = hexset_chunk(Q0, R0, W, H);
way_mark(along_row, 1.0, sr);
sc = hexset_chunk(Q0, R0, W, H);
way_mark(along_col, 1.0, sc);
assert(adjacent_pairs(sr) > 0 && adjacent_pairs(sc) > 0,
"both flights are connected, so there is something to measure");
// A tread of 1.5: right for the column, one step short for the row.
fr = heights_new(Q0, R0, W, H);
way_steps(along_row, sr, fr, 1.0, 1.5);
fc = heights_new(Q0, R0, W, H);
way_steps(along_col, sc, fc, 1.0, 1.5);
assert(worst_riser(sr, fr) > 1.5,
"along a row, a 1.5 tread skips steps: riser {worst_riser(sr, fr)} for a 1.0 rise");
assert(worst_riser(sc, fc) < 1.5,
"along a column the same tread is clean: riser {worst_riser(sc, fc)}");
// Give the row its own cell spacing and it is clean too.
fr2 = heights_new(Q0, R0, W, H);
way_steps(along_row, sr, fr2, 1.0, 1.7320508075688772);
assert(worst_riser(sr, fr2) < 1.5,
"at a tread of sqrt(3) the row flight is clean: riser {worst_riser(sr, fr2)}");
// And the column tolerates nothing finer than its own: 1.4 breaks it.
fc2 = heights_new(Q0, R0, W, H);
way_steps(along_col, sc, fc2, 1.0, 1.4);
assert(worst_riser(sc, fc2) > 1.5,
"just under 1.5 the column flight skips: riser {worst_riser(sc, fc2)}");
}
// @HXY-007 — AN ANGLE IS A DIRECTION, NOT A QUANTITY.
//
// `track_arc` takes plain floats, so the angles an arc is authored with are
// whatever the caller had to hand: a heading that has been decremented a few
// times, an `atan2` result minus a turn, a sweep counted backwards from zero.
// Every one of those describes the same curve in the plane — an angle names a
// direction, and adding or removing 2*pi does not move anything.
//
// A normalisation in ONE direction — `while aa < lo { aa = aa + 2*pi; }` and no
// companion going down — is right for an arc authored ABOVE the atan2 range and
// silently wrong for one authored below it: a point taken FROM the arc, by
// `seg_point`, would report itself as most of a radius away.
//
// WHY IT MATTERS MORE THAN A DISTANCE. `seg_distance` is not a leaf:
// `track_distance`, `nearest_seg`, `way_mark`, `way_stamp`, `cut_arb` and
// `way_param`'s own choice of segment all reach it. So the failure a caller
// would see is not a wrong number, it is a way rasterised somewhere else: one
// curve, two footprints. `seg_distance` and `seg_param` both normalise through
// `ang_wrap`, which wraps up AND down, so the arc below is the same arc at either
// authoring.
//
// ⚠ This block DEFINES the tag, so it must not also cite one: a citation line
// anywhere in a block cancels a tag named in its prose. The citations live in
// `src/hex_way.loft`, on the two functions this is about.
fn test_the_same_arc_authored_a_turn_lower_is_the_same_arc() {
// Two spellings of ONE quarter turn: radius 4 about the origin, from due east
// to due north.
hi = track_new();
track_arc(hi, 0.0, 0.0, 4.0, 0.0, 1.5707963267948966);
lo = track_new();
track_arc(lo, 0.0, 0.0, 4.0, 0.0 - 6.283185307179586,
0.0 - 6.283185307179586 + 1.5707963267948966);
// ⚠ THE POINTS COME FROM `seg_point`, NOT FROM A FORMULA WRITTEN HERE. They are
// on the curve by construction, so a non-zero distance is the subject's answer
// and not this test's arithmetic.
span = seg_len(hi, 0);
worst_hi = 0.0;
worst_lo = 0.0;
for k in 0..11 {
s = span * (k as float) / 10.0 ?? 0.0;
(hx, hy) = seg_point(hi, 0, s);
(lx, ly) = seg_point(lo, 0, s);
dh = abs_f(seg_distance(hi, 0, hx, hy));
dl = abs_f(seg_distance(lo, 0, lx, ly));
if dh > worst_hi { worst_hi = dh; }
if dl > worst_lo { worst_lo = dl; }
}
assert(worst_hi < 0.000001, "a point on the [0, pi/2] arc is on it, got {worst_hi}");
assert(worst_lo < 0.000001,
"the SAME arc authored at [-2pi, -2pi+pi/2] reports its own points "
+ "{worst_lo} away — an angle is a direction, not a quantity");
// …and the consumer-visible half: one curve, one footprint.
a = track_new();
track_arc(a, 0.0, 0.0, 4.0, 0.0, 1.5707963267948966);
b = track_new();
track_arc(b, 0.0, 0.0, 4.0, 0.0 - 6.283185307179586,
0.0 - 6.283185307179586 + 1.5707963267948966);
sa = hexset_chunk(Q0, R0, W, H);
sb = hexset_chunk(Q0, R0, W, H);
way_mark(a, 0.9, sa);
way_mark(b, 0.9, sb);
na = 0;
nb = 0;
for r in R0..R0 + H {
for q in Q0..Q0 + W {
if hexset_get(sa, q, r) { na = na + 1; }
if hexset_get(sb, q, r) { nb = nb + 1; }
}
}
assert(na > 0, "the fixture marked nothing at all — this row cannot be surprised");
assert(na == nb,
"the same arc marks {na} cells one way and {nb} the other — the way is "
+ "rasterised somewhere else entirely, and nothing about the shape changed");
}