1/**2 * M8 — slalom proportion. The TRACK is bigger than the world.3 *4 * The generator plans in TRACK CELLS (the abstract grid all the planning5 * math lives in). At render scale, one track cell spans TRACK_SCALE world6 * units: track placements are positioned ×2 and their meshes scaled ×2,7 * while the marble, island blocks, and decor keep world scale 1 — the8 * channel becomes a slalom ≈ 3.5 marble diameters wide, so a physics9 * marble (M10) has real room before it can fall off.10 *11 * ENDLESS-ONLY: the finite loop keeps 1:1 (its seeds/goldens survive).12 * One track cell maps to a 2×2 patch of world-grid island cells; island13 * masses and their decor are generated directly on the world grid.14 */15import type { CurveSeg, Vec3 } from "../../shared/types.ts";1617export const TRACK_SCALE = 2;1819export function scaleVec(v: Vec3): Vec3 {20 return [v[0] * TRACK_SCALE, v[1] * TRACK_SCALE, v[2] * TRACK_SCALE];21}2223/** Uniformly scale a centerline segment into world units. */24export function scaleSeg(seg: CurveSeg): CurveSeg {25 switch (seg.kind) {26 case "line":27 return { kind: "line", a: scaleVec(seg.a), b: scaleVec(seg.b) };28 case "arc":29 return {30 kind: "arc",31 center: scaleVec(seg.center),32 radius: seg.radius * TRACK_SCALE,33 a0: seg.a0,34 a1: seg.a1,35 y0: seg.y0 * TRACK_SCALE,36 y1: seg.y1 * TRACK_SCALE,37 };38 case "cubic":39 return {40 kind: "cubic",41 p: [42 scaleVec(seg.p[0]),43 scaleVec(seg.p[1]),44 scaleVec(seg.p[2]),45 scaleVec(seg.p[3]),46 ],47 };48 }49}5051/** The four world-grid cell centers covered by one track cell. */52export function worldCellsOf(53 cx: number,54 cz: number,55): readonly (readonly [number, number])[] {56 const wx = cx * TRACK_SCALE;57 const wz = cz * TRACK_SCALE;58 return [59 [wx - 0.5, wz - 0.5],60 [wx + 0.5, wz - 0.5],61 [wx - 0.5, wz + 0.5],62 [wx + 0.5, wz + 0.5],63 ];64}