PROCITY/web/js/citizens/door_snap.js
m3ultra 3e1ff312a5 Lane D R40 §40.5: the peds come in the front door — 100% on every real town
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>
2026-08-07 12:28:25 +10:00

63 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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-edgem]
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 };
}