1/**2 * Grid/pose math shared across the generation core.3 *4 * Conventions (must match the renderer):5 * - Cells are 1×1, centered on integer (x, z); cell (i, k) spans6 * x ∈ [i−0.5, i+0.5], z ∈ [k−0.5, k+0.5].7 * - Dir: 0=+x, 1=+z, 2=−x, 3=−z.8 * - A placement rotation r (quarter turns, rendered as rotation.y = r·π/2)9 * maps a local dir d to world dir (d − r + 4) % 4, and a local point10 * (x, z) to rotP(x, z, r).11 * - Piece defs are authored in "entry frame": entry port at (0, ·, −0.5)12 * traveling +z (dir 1), entry cell at (0, 0), y = 0 at entry base height.13 */14import type { CurveSeg, Dir, Vec3 } from "../shared/types.ts";1516export const DIRS: readonly Dir[] = [0, 1, 2, 3];1718/** Unit step for a dir, as [dx, dz]. */19export const DIR_STEP: readonly (readonly [number, number])[] = [20 [1, 0],21 [0, 1],22 [-1, 0],23 [0, -1],24];2526export function turnLeft(d: Dir): Dir {27 return ((d + 1) % 4) as Dir;28}29export function turnRight(d: Dir): Dir {30 return ((d + 3) % 4) as Dir;31}32export function opposite(d: Dir): Dir {33 return ((d + 2) % 4) as Dir;34}3536/** Move label for a cell entered heading `din` and left heading `dout`. */37export function moveLabel(din: Dir, dout: Dir): "F" | "L" | "R" {38 if (din === dout) return "F";39 if (dout === turnLeft(din)) return "L";40 if (dout === turnRight(din)) return "R";41 throw new Error(`impossible move ${din}→${dout}`);42}4344/** Rotate a local dir by r quarter turns (piece rotation convention). */45export function rotDir(d: Dir, r: Dir): Dir {46 return ((d - r + 4) % 4) as Dir;47}4849/** Rotate a point in the xz plane by r quarter turns (matches rotation.y = r·π/2). */50export function rotP(x: number, z: number, r: Dir): [number, number] {51 switch (r) {52 case 0:53 return [x, z];54 case 1:55 return [z, -x];56 case 2:57 return [-x, -z];58 case 3:59 return [-z, x];60 }61}6263export function rotVec3(v: Vec3, r: Dir): Vec3 {64 const [x, z] = rotP(v[0], v[2], r);65 return [x, v[1], z];66}6768/**69 * Rigid transform of entry-frame content into world:70 * rotate by r, then translate by (tx, ty, tz).71 */72export interface Xform {73 r: Dir;74 tx: number;75 ty: number;76 tz: number;77}7879export function applyXform(m: Xform, v: Vec3): Vec3 {80 const [x, z] = rotP(v[0], v[2], m.r);81 return [x + m.tx, v[1] + m.ty, z + m.tz];82}8384/**85 * Xform placing a def's entry frame so the entry cell lands on world cell86 * (cx, cz), heading `dir`, with entry base height `baseY`.87 * Canonical entry heading is +z (dir 1): r = (1 − dir + 4) % 4.88 */89export function windowXform(90 cx: number,91 cz: number,92 dir: Dir,93 baseY: number,94): Xform {95 return { r: ((1 - dir + 4) % 4) as Dir, tx: cx, ty: baseY, tz: cz };96}9798export function xformSeg(m: Xform, seg: CurveSeg): CurveSeg {99 switch (seg.kind) {100 case "line":101 return { kind: "line", a: applyXform(m, seg.a), b: applyXform(m, seg.b) };102 case "cubic":103 return {104 kind: "cubic",105 p: [106 applyXform(m, seg.p[0]),107 applyXform(m, seg.p[1]),108 applyXform(m, seg.p[2]),109 applyXform(m, seg.p[3]),110 ],111 };112 case "arc": {113 const c = applyXform(m, seg.center);114 // Rotation by r quarter turns maps angle θ → θ − r·π/2 in our115 // convention (point = center + radius·(cos θ, ·, sin θ), and116 // rotP(cos θ, sin θ, 1) = (sin θ, −cos θ) = (cos(θ−π/2), sin(θ−π/2))).117 const shift = -m.r * Math.PI / 2;118 return {119 kind: "arc",120 center: c,121 radius: seg.radius,122 a0: seg.a0 + shift,123 a1: seg.a1 + shift,124 y0: seg.y0 + m.ty,125 y1: seg.y1 + m.ty,126 };127 }128 }129}130131// ---------------------------------------------------------------------------132// Curve segment evaluation133// ---------------------------------------------------------------------------134135function lerp(a: number, b: number, t: number): number {136 return a + (b - a) * t;137}138139export function segPoint(seg: CurveSeg, t: number): Vec3 {140 switch (seg.kind) {141 case "line":142 return [143 lerp(seg.a[0], seg.b[0], t),144 lerp(seg.a[1], seg.b[1], t),145 lerp(seg.a[2], seg.b[2], t),146 ];147 case "arc": {148 const th = lerp(seg.a0, seg.a1, t);149 return [150 seg.center[0] + seg.radius * Math.cos(th),151 lerp(seg.y0, seg.y1, t),152 seg.center[2] + seg.radius * Math.sin(th),153 ];154 }155 case "cubic": {156 const [p0, p1, p2, p3] = seg.p;157 const u = 1 - t;158 const out: number[] = [];159 for (let i = 0; i < 3; i++) {160 out.push(161 u * u * u * p0[i] + 3 * u * u * t * p1[i] + 3 * u * t * t * p2[i] +162 t * t * t * p3[i],163 );164 }165 return out as unknown as Vec3;166 }167 }168}169170export function segLength(seg: CurveSeg): number {171 switch (seg.kind) {172 case "line": {173 const dx = seg.b[0] - seg.a[0];174 const dy = seg.b[1] - seg.a[1];175 const dz = seg.b[2] - seg.a[2];176 return Math.hypot(dx, dy, dz);177 }178 case "arc": {179 const horiz = Math.abs(seg.a1 - seg.a0) * seg.radius;180 return Math.hypot(horiz, seg.y1 - seg.y0);181 }182 case "cubic": {183 // Numeric: 16-step polyline is plenty at our scales.184 let len = 0;185 let prev = segPoint(seg, 0);186 for (let i = 1; i <= 16; i++) {187 const p = segPoint(seg, i / 16);188 len += Math.hypot(p[0] - prev[0], p[1] - prev[1], p[2] - prev[2]);189 prev = p;190 }191 return len;192 }193 }194}195196/** Reverse a segment's direction of travel. */197export function reverseSeg(seg: CurveSeg): CurveSeg {198 switch (seg.kind) {199 case "line":200 return { kind: "line", a: seg.b, b: seg.a };201 case "arc":202 return { ...seg, a0: seg.a1, a1: seg.a0, y0: seg.y1, y1: seg.y0 };203 case "cubic":204 return { kind: "cubic", p: [seg.p[3], seg.p[2], seg.p[1], seg.p[0]] };205 }206}