hex_way source

← 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.

ItemDefinedWorked exampleCall sites
WAY_STRAIGHT const25—46 80 99 118 140 181 … (9 total)
WAY_ARC const26—55 160
Track struct30—38 41 45 54 61 79 … (23 total)
track_new fn38—179 14 19 27 37 47 … (22 total)
track_straight fn45—185 20 28 48 138 143 … (13 total)
track_arc fn54—192 39 139 144 263 289 … (12 total)
seg_len fn79—93 100 119 229 427 23 … (17 total)
track_len fn91—15 21 40 115 281
seg_point fn98—228 229 29 45 46 153 … (13 total)
seg_tangent fn117—182 261 32
seg_curvature fn139HXY-001182 183 184 185 186
offset_legal fn156HXY-003265 266 267 268 291 292
track_offset fn178HXY-001152 161 163 165 167 174 … (12 total)
seg_distance fn200—241 279 425 490 491
track_distance fn238—302 349 49 176 274 284 … (7 total)
way_surfaces fn257HXY-004317 318
nearest_seg fn275—360
way_mark fn297HXY-00455 336 337 418 420 508 … (7 total)
cut_arb fn312HXY-004338
way_stamp fn343HXY-004326 327 331 332
seg_param fn374HXY-005430 377 381 386
way_param fn419HXY-005453
way_steps fn447HXY-006426 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");
}