Files
jesse c297b20aed 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).
2026-08-03 21:15:25 -04:00

92 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Download a route video, sample frames every N seconds, and build labelled
contact sheets. Built for Daikoku entrance/exit reference gathering.
Usage: route.py <videoid> <shortname> [step_seconds]
"""
import os, re, subprocess, sys, glob, json
from PIL import Image, ImageDraw
YTDLP = os.path.expanduser("~/.local/bin/yt-dlp")
ROOT = "/tmp/routes"
def run(cmd, **kw):
return subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)
def meta(vid):
r = run(f'{YTDLP} --no-warnings -J --skip-download "https://www.youtube.com/watch?v={vid}" 2>/dev/null')
try:
d = json.loads(r.stdout)
except Exception:
return {}
return {k: d.get(k) for k in ("title", "uploader", "duration", "upload_date", "width", "height")}
def fetch(vid, name):
d = f"{ROOT}/{name}"
os.makedirs(d, exist_ok=True)
mp4 = f"{d}/{name}.mp4"
if not os.path.exists(mp4):
r = run(f'{YTDLP} -f "bv*[height<=1080][ext=mp4]/bv*[height<=1080]/b" '
f'--no-warnings -o "{mp4}" "https://www.youtube.com/watch?v={vid}"')
if not os.path.exists(mp4):
print(f" DOWNLOAD FAILED: {r.stderr[-300:]}")
return None
return mp4
def sample(mp4, d, step):
dur = float(run(f'ffprobe -v error -show_entries format=duration '
f'-of default=nw=1:nk=1 "{mp4}"').stdout.strip() or 0)
for f in glob.glob(f"{d}/fr_*.jpg"):
os.remove(f)
ts = list(range(2, int(dur), step))
for t in ts:
run(f'ffmpeg -v error -ss {t} -i "{mp4}" -frames:v 1 -q:v 2 '
f'-vf scale=960:-1 "{d}/fr_{t:04d}.jpg" -y')
return dur, ts
def sheets(d, name, cols=3, per=9):
fs = sorted(glob.glob(f"{d}/fr_*.jpg"))
out = []
for page in range((len(fs) + per - 1) // per):
sub = fs[page * per:(page + 1) * per]
ims = [Image.open(f).convert("RGB") for f in sub]
w, h = ims[0].size
rows = (len(sub) + cols - 1) // cols
s = Image.new("RGB", (w * cols, h * rows), (10, 10, 10))
for i, im in enumerate(ims):
s.paste(im, ((i % cols) * w, (i // cols) * h))
dr = ImageDraw.Draw(s)
for i, f in enumerate(sub):
t = int(re.search(r"fr_(\d+)", f).group(1))
x, y = (i % cols) * w, (i // cols) * h
dr.rectangle([x + 6, y + 6, x + 190, y + 54], fill=(0, 0, 0))
dr.text((x + 20, y + 22), f"t={t}s", fill=(255, 225, 0))
s = s.resize((int(s.width * 0.60), int(s.height * 0.60)), Image.LANCZOS)
o = f"{d}/{name}_sheet{page+1}.jpg"
s.save(o, quality=90)
out.append(o)
return out
if __name__ == "__main__":
vid, name = sys.argv[1], sys.argv[2]
step = int(sys.argv[3]) if len(sys.argv) > 3 else 8
m = meta(vid)
print(f"=== {name} [{vid}] ===")
print(f" title: {m.get('title')}")
print(f" dur : {m.get('duration')}s {m.get('width')}x{m.get('height')}")
mp4 = fetch(vid, name)
if not mp4:
sys.exit(1)
d = f"{ROOT}/{name}"
dur, ts = sample(mp4, d, step)
sh = sheets(d, name)
print(f" frames: {len(ts)} @ {step}s sheets: {len(sh)}")
for s in sh:
print(f" {s}")