Disable PA on/off ramps; restore mainline to pre-ramp state

Three iterations of the Daikoku PA ramps never read as a real
interchange. Measured why, rather than tuning further:

  * the entry and exit decks INTERSECTED -- 2547 sample pairs within
    30 studs horizontally at <12 studs dY, worst case 0.5 studs apart
    at dY=0.0. On screen that is the bowtie tangle of white rails.
  * both real Daikoku ramps turn the SAME way (clockwise; the guide
    video says "right, right"). This build mirrored them into each
    other.
  * the real arc wraps OUTSIDE the lot and forms its north/east
    boundary. This build cut across the apron interior.

Gated behind BUILD_PA_RAMPS=false rather than deleted -- the radius
was already correct (470 studs vs an OSM-measured mean of 469) and
the merge lands tangent at 0.1-0.2 deg divergence. Both worth keeping.

Also removed two leftovers that would have outlived the ramps:

  * rampOpen punched holes in the mainline's outer parapet, railing
    and kill wall for ramps that no longer exist -- a barrier gap with
    a drop behind it. Sealed via RAMP_EDGE_OPENINGS=false. Verified:
    Barrier/EdgeRail/KillWall = 1646 each = 823 samples x 2 edges,
    0 road samples without a guard within 90 studs.
  * gore zebra hatching painted 1024 bars + 64 boundary lines marking
    a junction that is not there. Sealed via RAMP_GORE_MARKINGS=false.

Verified after a cache-busting fresh rebuild (Studio's require() cache
otherwise serves the previous map and reports a false pass):
  parts 22628 -> 15988
  all ramp part classes = 0
  Road = 823 (unchanged), spawn pad + PASpawn intact
  clip probe TRUSTWORTHY (control +11/-0), CLIPPING=0

Research and rebuild spec kept for when this is revisited:
reference/ramps/BUILD_SPEC.md, INDEX.md (10 CC-licensed Commons
photos), docs/daikoku-reference-spec.md (OSM circle fits).
This commit is contained in:
2026-08-03 21:15:25 -04:00
parent 1d23c187ad
commit c297b20aed
34 changed files with 2206 additions and 46 deletions
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Eyes-on shots of the PA flyover ramps.
Camera targets are QUERIED FROM THE LIVE GEOMETRY on every run, never
hardcoded. The previous capture script pinned literal coordinates at the
old defect sites; after the ramps were redesigned those coords pointed at
empty air and the shots silently framed the wrong thing. Same class of bug
as the hardcoded yFork=96 -- a constant that stops matching the model.
Each shot names a part and an offset; the part's real position is looked up
in Studio at capture time. If a named part is missing the shot is SKIPPED
with a loud message rather than falling back to a stale guess.
"""
import shutil
import sys
import time
sys.path.insert(0, "/tmp")
import rbx # noqa: E402
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
# name, anchor part, camera offset from anchor, look-at offset from anchor, fov
SHOTS = [
# The flyover itself, from the side -- the money shot: ramp over deck.
("F1_flyover_side", "PAEntryRampCross_100", (300, 150, 300), (0, -20, 0), 55),
# Underneath the crossing, looking up: proves real clearance.
("F2_flyover_under", "PAEntryRampCross_100", (140, -70, 140), (0, 10, 0), 75),
# The spiral descending into the PA apron.
("F3_spiral_into_pa", "PAEntryRampTrunk_200", (340, 220, 340), (-120, -60, -120), 60),
# Exit ramp climbing out of the PA.
("F4_exit_climb", "PAExitRampTrunk_200", (-340, 200, 300), (60, -40, 40), 60),
# Where the descent lands on the far carriageway.
("F5_landing_merge", "PAEntryRampNear_200", (220, 130, 220), (0, -15, 0), 55),
# Wide establishing shot of the whole interchange.
("F6_wide", "PAEntryRampCross_100", (900, 620, 900), (-250, -120, -250), 60),
]
POS_QUERY = """
local ws=game:GetService("Workspace"); local map=ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local want={}
for _,n in ipairs({%s}) do want[n]=true end
local found={}
for _,d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and want[d.Name] then
found[#found+1]=("%%s=%%.1f,%%.1f,%%.1f"):format(d.Name,d.Position.X,d.Position.Y,d.Position.Z)
end
end
return table.concat(found,"|")
"""
CAM = """
local cam = workspace.CurrentCamera
cam.CameraType = Enum.CameraType.Scriptable
cam.FieldOfView = {fov}
cam.CFrame = CFrame.lookAt(Vector3.new({px},{py},{pz}), Vector3.new({tx},{ty},{tz}))
return "ok"
"""
def fetch_anchors(names):
"""Look up each anchor part's real position in the live datamodel."""
lst = ",".join('"%s"' % n for n in sorted(set(names)))
r = rbx.call(
"execute_luau",
{"datamodel_type": "Edit", "code": POS_QUERY % lst},
wait=40,
timeout=400,
)
txt = str(r)
out = {}
for chunk in txt.split("|"):
if "=" not in chunk:
continue
nm, _, vals = chunk.partition("=")
nm = nm.strip().strip("'\"[]{}() ")
parts = vals.strip().strip("'\"[]{}() ").split(",")
if len(parts) != 3:
continue
try:
out[nm] = tuple(float(v) for v in parts)
except ValueError:
continue
return out
def main():
anchors = fetch_anchors([s[1] for s in SHOTS])
print(f"resolved {len(anchors)} anchors: {sorted(anchors)}")
made, skipped = [], []
for name, anchor, cam_off, look_off, fov in SHOTS:
if anchor not in anchors:
print(f"{name}: SKIP -- anchor {anchor} not found in model")
skipped.append(name)
continue
ax, ay, az = anchors[anchor]
px, py, pz = ax + cam_off[0], ay + cam_off[1], az + cam_off[2]
tx, ty, tz = ax + look_off[0], ay + look_off[1], az + look_off[2]
r = rbx.call(
"execute_luau",
{
"datamodel_type": "Edit",
"code": CAM.format(
fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz
),
},
wait=12,
timeout=300,
)
if isinstance(r, dict) and r.get("isError"):
print(f"{name}: CAM FAIL {str(r)[:120]}")
skipped.append(name)
continue
time.sleep(1.5)
cap = rbx.call(
"screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300
)
imgs = cap.get("images") if isinstance(cap, dict) else None
if not imgs:
print(f"{name}: CAPTURE FAIL {str(cap)[:140]}")
skipped.append(name)
continue
dest = f"{OUT}/{name}.jpg"
shutil.copy(imgs[0], dest)
made.append(dest)
print(f"{name}: OK ({anchor} @ {ax:.0f},{ay:.0f},{az:.0f}) -> {dest}")
print(f"\nMADE {len(made)} SKIPPED {len(skipped)} {skipped if skipped else ''}")
if __name__ == "__main__":
main()