<!-- SOLID CITY · part 3017 -->
er edge), never
 *     by the centre sample alone: that one rule kills both the buried
 *     downhill edge and the floating uphill edge;
 *   the FRAMES — a right vector carried from the world up, with banking
 *     applied as a deliberate roll, derived from curvature and capped,
 *     because our vehicles read heights, not bank — banking here is for
 *     the eye and for standing truth, and five degrees is its law;
 *   the EDGEKIT — curbs with a restrained gold line, a sidewalk on the
 *     uphill side, and skirts that fall to the ground on the fill side
 *     and rise to meet it on the cut side, so no edge ever floats;
 *   the REGISTRAR — every standing surface (ribbon, curbs, sidewalk,
 *     arrival fans) written into one triangle list, the same list the
 *     walkers and the wheels will read.
 * Pure arrays in, pure arrays out; THREE belongs to the caller.
 * By the hand of Solid, his good King.
 */

export function createRoadSmith(o) {
  var groundAt = o.groundAt;

  /* ---------- small vector helpers (plan + height) ---------- */
  function wrapAngle(a) { while (a > Math.PI) a -= Math.PI * 2; while (a < -Math.PI) a += Math.PI * 2; return a; }

  /* ---------- geometry emission: flat-normal triangles ---------- */
  function newSurf() { return { pos: [], nor: [] }; }
  function tri(S, a, b, c, outward) {
    var ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
    var vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
    var nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
    var L = Math.hypot(nx, ny, nz) || 1; nx /= L; ny /= L; nz /= L;
    if (outward) { if (nx * outward[0] + nz * outward[1] < 0) { var t = b; b = c; c = t; nx = -nx; ny = -ny; nz = -nz; } }
    else if (ny < 0) { var t2 = b; b = c; c = t2; nx = -nx; ny = -ny; nz = -nz; }
    S.pos.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
    for (var i = 0; i < 3; i++) S.nor.push(nx, ny, nz);
  }
  function quad(S, a, b, c, d, outward) { tri(S, a, b, c, outward); tri(S, a, c, d, outward); }
  /* a standing triangle list is the same corners, flattened */
  function standQuad(T, a, b, c, d) { T.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]); T.push(a[0], a[1], a[2], c[0], c[1], c[2], d[0], d[1], d[2]); }

  /* ---------- the centerline: Catmull–Rom through the waypoints ---------- */
  function splinePoints(pts, stepM) {
    /* CENTRIPETAL Catmull-Rom (Barry–Goldman), not the uniform
     * spline this factory was born with. The uniform curve reads
     * every surveyed leg as equal time, so where the King's points
     * run uneven — a 3.8 m leg beside a 40 m leg on Road B — it
     * overshoots, loops, and invents two-metre radii under a
     * 7.6 m carriageway: the folds in his screenshots. The
     * centripetal form weights each span by the square root of
     * its length and CANNOT cusp, loop, or overshoot its segment;
     * it is the spline road-builders use because it stays true
     * to a surveyed line. */
    var P = [pts[0]].concat(pts, [pts[pts.length - 1]]);
    var dense = [];
    function lp(a, b, w) { return [a[0] + (b[0] - a[0]) * w, a[1] + (b[1] - a[1]) * w]; }
    for (var i = 1; i < P.length - 2; i++) {
      var p0 = P[i - 1], p1 = P[i], p2 = P[i + 1], p3 = P[i + 2];
      var t0 = 0;
      var t1 = t0 + Math.pow(Math.max(Math.hypot(p1[0] - p0[0], p1[1] - p0[1]), 1e-4), 0.5);
      var t2 = t1 + Math.pow(Math.max(Math.hypot(p2[0] - p1[0], p2[1] - p1[1]), 1e-4), 0.5);
      var t3 = t2 + Math.pow(Math.max(Math.hypot(p3[0] - p2[0], p3[1] - p2[1]), 1e-4), 0.5);
      var span = Math.hypot(p2[0] - p1[0], p2[1] - p1[1]);
      if (span < 1e-4) continue;
      var n = Math.max(2, Math.ceil(span / stepM));
      for (var j = (i === 1 ? 0 : 1); j <= n; j++) {
        var t = t1 + (t2 - t1) * (j / n);
        var A1 = lp(p0, p1, (t - t0) / (t1 - t0)), A2 = lp(p1, p2, (t - t1) / (t2 - t1)), A3 = lp(p2, p3, (t - t2) / (t3 - t2));
        var B1 = lp(A1, A2, (t - t0) / (t2 - t0)), B2 = lp(A2, A3, (t - t1) / (t3 - t1));
        dense.push(lp(B1, B2, (t - t1) / (t2 - t1)));
      }
    }
    return dense;
  }
  function resample(dense, stepM) {
    var out = [dense[0]], acc = 0;
    for (var i = 1; i < dense.length; i++) {
      var d = Math.hypot(dense[i][0] - dense[i - 1][0], dense[i][1] - dense[i - 1][1]);
      acc += d;
      if (acc >= stepM) { out.push(dense[i]); acc = 0; }
    }
    if (out[out.length - 1] !== dense[dense.length - 1]) out.push(dense[dense.length - 1]);
    return out;
  }

  /* ---------- buildRoad: the whole craft ---------- */
  function buildRoad(spec) {
    var pts = spec.points, W = spec.width || 7.6, hw = W / 2;
    var stepM = spec.stationStep || (spec.furniture === 'bench' ? 1.25 : 2);
    var gradeCap = spec.gradeCap || 0.38;
    var bankCap = (spec.bankCapDeg || 5) * Math.PI / 180;
    var dense = splinePoints(pts, 0.5);
    var line = resample(dense, stepM);
    /* a spline that doubles back on itself