#!/usr/bin/env python3 """Capture the PA slip ramps from the user's own viewpoint, LIVE-ANCHORED. The user's screenshot looks along the mainline from above the deck with the ramp descending on the left. Reproduce that framing from the actual ramp geometry so the shot cannot drift onto empty air after an edit. """ import re import shutil import sys import time sys.path.insert(0, "/tmp") import rbx # noqa: E402 OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots" # Anchors: for each slip -> mouth (first deck seg), mid-span, landing (last seg). ANCHOR_CODE = """ local map = workspace:FindFirstChild("Map") if not map then return "NOMAP" end local segs = {} for _, d in ipairs(map:GetDescendants()) do if d:IsA("BasePart") then for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do local k = d.Name:match("^" .. s .. "_Deck_(%d+)$") if k then segs[s] = segs[s] or {} table.insert(segs[s], {i = tonumber(k), p = d.Position}) end end end end local out = {} for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do local g = segs[s] if g and #g > 2 then table.sort(g, function(a, b) return a.i < b.i end) local mouth = g[1].p local mid = g[math.floor(#g / 2)].p local land = g[#g].p table.insert(out, ("%.1f %.1f %.1f"):format(mouth.X, mouth.Y, mouth.Z)) table.insert(out, ("%.1f %.1f %.1f"):format(mid.X, mid.Y, mid.Z)) table.insert(out, ("%.1f %.1f %.1f"):format(land.X, land.Y, land.Z)) end end if #out == 0 then return "NOSLIP" end return table.concat(out, " | ") """ 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 "NOSLIP" in txt: print("ANCHOR FAIL: " + txt[:200]) return trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt) if len(trip) < 6: 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, base in enumerate((0, 3), start=1): mouth, mid, land = pts[base], pts[base + 1], pts[base + 2] tag = ["west", "east"][n - 1] # 1. The user's view: high, behind the mouth, looking down the ramp. made.append(grab( f"ramp_{tag}_userview", mouth[0] - 300, mouth[1] + 120, mouth[2] - 300, land[0], land[1], land[2], fov=60)) # 2. Side elevation: shows the descent profile against the deck. made.append(grab( f"ramp_{tag}_side", mid[0] + 420, mid[1] + 90, mid[2], mid[0], mid[1], mid[2], fov=48)) # 3. Top-down over mid-span: is it parallel to the deck or cutting away? made.append(grab( f"ramp_{tag}_topdown", mid[0], mid[1] + 560, mid[2], mid[0], mid[1], mid[2], fov=55)) made = [m for m in made if m] print("\nMADE " + str(len(made)) + " shots") for m in made: print(m) if __name__ == "__main__": main()