// PROCITY Lane D — R40 §40.5: door-point → footpath correction (pure geometry, no THREE, no rng). // // The shell derives each patronage door point as `lot centre + front-normal · (d/2 + 0.6)` // (web/index.html:433-437, the +sin/+cos side since Lane B's R39 heading fix). That lands ON the // footpath for almost every shop, but the residue class — lots whose setback or corner geometry // puts the point a metre or two outside the verge band — left the R8 claim ("peds duck into shops // they pass") false at the kerb: the ped steered to a point just off the walkable strip. // // This module clamps a door point into the footpath band of its NEAREST footpath-bearing street — // registry.js's published `vergeBand` corridor law, over the same edge objects the sim already // walks. Graph-only on purpose: the sim never sees lots, and this needs none. // // Guard rails (all measured before shipping — tools/qa/door_footpath_check.mjs is the gate): // • CAP (default 6 m): a point further than this from its band is NOT a kerb miss — it is a // market stall fronting the market square or an arcade shop fronting the covered lane. Those // fronts are correct where they are (the square IS their front); dragging one 50 m to a street // it does not front would re-break the front-door truth this exists to hold. Leave it. // • MIN_BAND (0.8 m): a `lane` is all carriageway and an `arcade` band can degenerate — an edge // with no walkable strip can never be a snap target. // • MARGIN (0.45 m): snapped points sit inside the band, not on the kerb line, so a float wobble // can't re-classify them. // • Deterministic: pure function of (point, edges) — same plan → same doors, forever. // // Measured on the R39 four (raw → snapped, walkable-front rate): synthetic 95.9→95.9 (its residue // is stall-vs-stall inside the square, out of this function's reach and over the floor anyway), // katoomba 98.6→100, fitzroy 99.3→100, bowral 93.3→100 — and the transition check says it fixes 4 // and breaks 0 corpus-wide. import { vergeBand } from '../core/registry.js'; const EPS = 1e-6; export const DOOR_SNAP_CAP = 6; // m — beyond this the point fronts a plaza, not this street export const DOOR_SNAP_MARGIN = 0.45; // m inside the band edges export const DOOR_SNAP_MIN_BAND = 0.8; // m — narrower than this is not a walkable strip // edges: the sim's prepared edge list — [{ A:{x,z}, B:{x,z}, width, kind, ... }] (vergeBand reads // width/kind; A/B are the resolved endpoints). Returns { x, z, moved } — the input point (moved: // false) when it already sits in its nearest band, has no footpath edge to snap to, or is a // plaza-front beyond the cap. export function snapDoorToFootpath(x, z, edges, { cap = DOOR_SNAP_CAP, margin = DOOR_SNAP_MARGIN, minBand = DOOR_SNAP_MIN_BAND } = {}) { let best = null, bd = Infinity, bqx = 0, bqz = 0; for (const e of edges) { const [vi, vo] = vergeBand(e); if (vo - vi < minBand) continue; // no walkable strip on this edge const ax = e.A.x, az = e.A.z, dx = e.B.x - ax, dz = e.B.z - az; const L2 = dx * dx + dz * dz; let t = L2 < EPS ? 0 : ((x - ax) * dx + (z - az) * dz) / L2; t = t < 0 ? 0 : t > 1 ? 1 : t; const qx = ax + t * dx, qz = az + t * dz; const d = Math.hypot(x - qx, z - qz); if (d < bd) { bd = d; best = e; bqx = qx; bqz = qz; } } if (!best || bd < EPS) return { x, z, moved: false }; // no footpath in town / on the centreline const [vi, vo] = vergeBand(best); const m = Math.min(margin, (vo - vi) / 2); const target = Math.max(vi + m, Math.min(vo - m, bd)); // clamp into [kerb+m, corridor-edge−m] const mv = Math.abs(target - bd); if (mv < EPS || mv > cap) return { x, z, moved: false }; // already in band · or a plaza front const ux = (x - bqx) / bd, uz = (z - bqz) / bd; // outward from the centreline return { x: bqx + ux * target, z: bqz + uz * target, moved: true }; }