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).
133 lines
4.7 KiB
Python
133 lines
4.7 KiB
Python
#!/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()
|