Verification first, and B's R39 heading fix did NOT clear the gate alone. Measured today (tools/qa/door_footpath_check.mjs, new): synthetic 447/493 foot (90.7%) · katoomba 71/72 · fitzroy 138/139 · bowral 28/30 (93.3%) — from R39's 0/493, 0/72, 0/139, 0/30. Two residue species, handled differently ON PURPOSE: 1. A 0.85m kerb miss on real towns — plan_osm's setback puts the facade past the corridor edge, so the shell's d/2+0.6 door point lands just off the verge. Fixed by new D-owned door_snap.js, applied in sim.setShops: clamps each door into the registry vergeBand of its nearest footpath-bearing edge, 6m cap. Corpus: fixes 4, breaks 0. Live-shell verified (493 points, 2 moved, max 5.41m — matches node exactly). Real towns now 72/72, 139/139, 30/30. 2. The synthetic's 36 strict misses are market stalls and arcade shops WHOSE FRONT IS THE MARKET SQUARE. An uncapped snap would 'pass' them by teleporting a door 59.85m to a street they do not front — passing the gate by breaking the truth it measures. The gate instead scores walkable front ground (footpath ∪ market/arcade block ground) = 473/493 (95.9%) and reports the strict number alongside. The check ships its own falsifiability CONTROL (the R8-R39 back point must score under the floor: reads 0.4-13.3%) and a fix-only arm (clamp breaks 0, asserted forever). Deterministic. Remaining 20 synthetic misses are artefacts of the fixed nudge at index.html:433-437 — not D's file; measured, exact fix documented for F. Goldens untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
3.9 KiB
JavaScript
63 lines
3.9 KiB
JavaScript
// 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 };
|
||
}
|