Aux-lane suppression + PA geometry fixes; probe/verify tooling
CI / quality (push) Failing after 1m9s

MapBuilder.luau
  - Suppress the aux lane across samples 525-550 (AUX_SUPPRESS_FROM/TO),
    honoring the user's hand deletion in Studio so a rebuild no longer
    regenerates it.
  - Smoothstep the width ramp over AUX_BLEND=5 samples on each side. A hard
    cut produced a 26-stud lateral fence jog; the blend removes it.
  - DO NOT remove this suppression. Re-enabling it resurrects geometry the
    user deleted by hand.

rokit.toml
  - rojo 7.5.1 -> 7.7.0 to match the auto-updating Studio plugin.
    Server older than plugin fails the handshake with
    "attempt to index number with 'protocolVersion'".

tools/
  - 34 read-only probe/verify/capture scripts used to measure the datamodel
    instead of reasoning from source.

Verified-good baseline at this commit:
  total parts       16788
  PAWestSlip        60 segs, 34.4 deg total bend, worst 1.4, 0 kinks >5
  PAEastSlip        60 segs, 30.7 deg total bend, worst 1.3, 0 kinks >5
  AuxLane parts     30 (samples 525-550 deliberately absent)
  barrier coverage  823 samples, 0 unguarded
  fence jogs >10st  0
This commit is contained in:
2026-08-06 08:27:47 -04:00
parent c297b20aed
commit 6e50f9718d
36 changed files with 3723 additions and 1 deletions
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Capture the speed-change (aux) lanes ANCHORED to the parts themselves.
Hardcoded camera offsets are how you end up photographing empty air after the
geometry moves. Every shot here is framed from the LIVE position of the aux
lane / slip parts queried out of the DataModel at capture time, so the camera
cannot drift away from the thing under test.
"""
import shutil
import sys
import time
sys.path.insert(0, "/tmp")
import rbx # noqa: E402
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
# Ask Studio where the aux lanes actually are, then frame from that.
# Returns "x y z | x y z ..." anchors: west aux mid, east aux mid, and the
# two slip mouths.
ANCHOR_CODE = """
local map = workspace:FindFirstChild("Map")
if not map then return "NOMAP" end
local aux = {}
local mouths = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = d.Name:match("^AuxLane_(%d+)$")
if ai then table.insert(aux, {i = tonumber(ai), p = d.Position}) end
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) <= 1 then mouths[s] = d.Position end
end
end
end
if #aux == 0 then return "NOAUX" end
table.sort(aux, function(a, b) return a.i < b.i end)
-- Split the aux segments into two clusters (west run and east run) by index gap.
local groups, cur = {}, {aux[1]}
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(groups, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(groups, cur)
local parts = {}
for _, g in ipairs(groups) do
local mid = g[math.max(1, math.floor(#g / 2))].p
table.insert(parts, ("%.1f %.1f %.1f"):format(mid.X, mid.Y, mid.Z))
end
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local m = mouths[s]
if m then table.insert(parts, ("%.1f %.1f %.1f"):format(m.X, m.Y, m.Z)) end
end
return table.concat(parts, " | ")
"""
CAM_TPL = """
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 "cam ok"
"""
def grab(name, px, py, pz, tx, ty, tz, fov=55):
code = CAM_TPL.format(fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz)
r = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": code})
if isinstance(r, dict) and r.get("isError"):
print(f"{name}: CAM FAIL {r}")
return None
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)[:160]}")
return None
dest = f"{OUT}/{name}.jpg"
shutil.copy(imgs[0], dest)
print(f"{name}: OK -> {dest}")
return dest
def main():
a = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": ANCHOR_CODE},
wait=40, timeout=300)
txt = a if isinstance(a, str) else str(a)
if "NOMAP" in txt or "NOAUX" in txt:
print("ANCHOR FAIL: " + txt[:200])
return
# Pull the coordinate triples out of whatever wrapper the bridge returns.
import re
trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt)
if not trip:
print("ANCHOR PARSE FAIL: " + txt[:300])
return
pts = [tuple(float(v) for v in t) for t in trip]
print("anchors: " + str(pts))
made = []
for n, (x, y, z) in enumerate(pts[:2], start=1):
# Oblique from outside the deck, looking back at the merge.
d = grab(f"aux{n}_oblique", x + 260, y + 150, z + 260, x, y, z, fov=50)
if d:
made.append(d)
# Top-down: the merge taper reads clearest from directly above.
d = grab(f"aux{n}_topdown", x + 3, y + 430, z + 3, x, y, z, fov=42)
if d:
made.append(d)
# Driver's-eye down the lane.
d = grab(f"aux{n}_driver", x - 190, y + 16, z - 60, x + 120, y + 8, z + 40, fov=68)
if d:
made.append(d)
print("\nMADE " + str(len(made)) + " shots")
for m in made:
print(m)
if __name__ == "__main__":
main()