Daikoku PA: lift mainline over the apron, rebuild both ramps
The PA ring road and the expressway mainline were both at ground level, so the ring parapet stood INSIDE the carriageway (measured -10.0 studs of vertical overlap: a wall in the middle of the road). Reference photos of the real Daikoku Futo PA show the expressway flying OVER the parking apron on pillars, with the PA at ground level and ramps climbing up to meet it. Rebuilt to match: - mainline lifted 38 studs through the junction, eased in/out (max 5.26% grade, within real expressway limits) - PA ring + apron stay at ground level; legacy PARingPillar props removed - both on/off ramps regraded over the full rise with smoothstep easing, segments overlapped 3 studs so wheels can't catch a seam, plus support pillars and edge kerbs where the deck is high Verified in-engine against built parts (tools/diag_daikoku.luau), not just source: clearance now +28.0 studs, both ramps continuous (max step 5.62, no gaps). tools/plot_daikoku.py renders the plan/section for comparison.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
plot_wallmap.py -- render the JSON dumped by tools/diag_wallmap.luau as a
|
||||
top-down PNG.
|
||||
|
||||
WHY THIS EXISTS: the Studio viewport cannot be screenshotted on this machine
|
||||
(Vinegar/Wine on nested X display :1, DXVK-rendered -> XGetImage returns
|
||||
black). For "is there a gap / does it look jumbled / which way does it face"
|
||||
questions, plotting the datamodel geometry top-down is both possible and more
|
||||
precise than a screenshot.
|
||||
|
||||
Usage:
|
||||
python3 tools/plot_wallmap.py /tmp/wallmap_raw.txt /tmp/wallmap.png ["Title"]
|
||||
|
||||
The input file is whatever rbx.call('execute_luau', ...) returned for
|
||||
diag_wallmap.luau -- it may be JSON or a Python-repr dict, both are handled.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
STYLE = {
|
||||
"Road": ("#3a3a44", 0.5),
|
||||
"Ramp": ("#4a4a58", 0.5),
|
||||
"Apron": ("#2e2e36", 0.5),
|
||||
"Barrier": ("#ffffff", 0.95),
|
||||
"EdgeRail": ("#8a8a9a", 0.95),
|
||||
"KillWall": ("#ff4444", 0.95),
|
||||
"PARingWall": ("#00e5ff", 0.95),
|
||||
"PARingRail": ("#0090a0", 0.95),
|
||||
"Median": ("#ffd400", 0.95),
|
||||
"MedianFence": ("#c0a000", 0.95),
|
||||
}
|
||||
# Draw decks first so walls land on top of them.
|
||||
ORDER = [
|
||||
"Road", "Ramp", "Apron", "Median", "MedianFence",
|
||||
"KillWall", "EdgeRail", "Barrier", "PARingWall", "PARingRail",
|
||||
]
|
||||
LEGEND = [
|
||||
("PARingWall (ring parapet)", "#00e5ff"),
|
||||
("Barrier (mainline)", "#ffffff"),
|
||||
("KillWall", "#ff4444"),
|
||||
("Median", "#ffd400"),
|
||||
("Road/Ramp deck", "#3a3a44"),
|
||||
]
|
||||
|
||||
|
||||
def load(path):
|
||||
raw = open(path).read()
|
||||
start = raw.find("{")
|
||||
if start < 0:
|
||||
raise SystemExit(f"no payload found in {path}")
|
||||
body = raw[start:]
|
||||
try:
|
||||
return json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
# The bridge often hands back a Python repr (single quotes, True/None).
|
||||
return ast.literal_eval(body)
|
||||
|
||||
|
||||
def analyse(parts):
|
||||
"""Print the numbers that answer 'is it sealed' before drawing anything."""
|
||||
ring = [p for p in parts if p["k"] == "PARingWall"]
|
||||
if not ring:
|
||||
return None, None
|
||||
cx = sum(p["x"] for p in ring) / len(ring)
|
||||
cz = sum(p["z"] for p in ring) / len(ring)
|
||||
rad = [math.hypot(p["x"] - cx, p["z"] - cz) for p in ring]
|
||||
split = (min(rad) + max(rad)) / 2
|
||||
inner = [p for p in ring if math.hypot(p["x"] - cx, p["z"] - cz) < split]
|
||||
outer = [p for p in ring if math.hypot(p["x"] - cx, p["z"] - cz) >= split]
|
||||
print(f"PA centre ~({cx:.0f},{cz:.0f}) radii {min(rad):.0f}..{max(rad):.0f}")
|
||||
print(f"inner-edge walls {len(inner)}/72 outer-edge walls {len(outer)}/72")
|
||||
|
||||
angs = sorted(math.degrees(math.atan2(p["z"] - cz, p["x"] - cx)) % 360 for p in inner)
|
||||
gaps = []
|
||||
for i, a in enumerate(angs):
|
||||
nxt = angs[(i + 1) % len(angs)]
|
||||
d = (nxt - a) % 360
|
||||
if d > 7.5: # one segment is 5 deg; >7.5 means at least one is absent
|
||||
gaps.append((round(a, 1), round(nxt, 1), round(d, 1)))
|
||||
print("INNER-wall gaps >7.5deg (a usable exit shows here):", gaps or "NONE - sealed")
|
||||
|
||||
road = sum(1 for p in parts if p["k"] == "Road")
|
||||
bar = sum(1 for p in parts if p["k"] == "Barrier")
|
||||
if road:
|
||||
# One barrier per side of every deck segment => healthy ratio ~2.0.
|
||||
print(f"mainline Road={road} Barrier={bar} ratio={bar/road:.2f} (healthy ~2.0)")
|
||||
return cx, cz
|
||||
|
||||
|
||||
def render(parts, cx, cz, out_png, title, span=1000.0, size=1400):
|
||||
def sx(x):
|
||||
return (x - cx) / span * (size / 2) + size / 2
|
||||
|
||||
def sy(z):
|
||||
return (z - cz) / span * (size / 2) + size / 2
|
||||
|
||||
svg = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'
|
||||
f'<rect width="{size}" height="{size}" fill="#0e0e12"/>'
|
||||
]
|
||||
for kind in ORDER:
|
||||
col, op = STYLE.get(kind, ("#888888", 0.9))
|
||||
for p in parts:
|
||||
if p["k"] != kind:
|
||||
continue
|
||||
w = max(p["sx"] / span * (size / 2), 1.5)
|
||||
h = max(p["sz"] / span * (size / 2), 1.5)
|
||||
# Negate yaw: world +Z runs down the screen in this projection.
|
||||
svg.append(
|
||||
f'<g transform="translate({sx(p["x"]):.1f},{sy(p["z"]):.1f}) rotate({-p["r"]:.1f})">'
|
||||
f'<rect x="{-w/2:.1f}" y="{-h/2:.1f}" width="{w:.1f}" height="{h:.1f}"'
|
||||
f' fill="{col}" opacity="{op}"/></g>'
|
||||
)
|
||||
svg.append(f'<circle cx="{sx(cx):.0f}" cy="{sy(cz):.0f}" r="5" fill="#39ff14"/>')
|
||||
svg.append(
|
||||
f'<text x="20" y="34" fill="#eee" font-family="monospace" font-size="21">{title}</text>'
|
||||
)
|
||||
for i, (lab, col) in enumerate(LEGEND):
|
||||
yy = 64 + i * 25
|
||||
svg.append(
|
||||
f'<rect x="20" y="{yy-11}" width="16" height="13" fill="{col}"/>'
|
||||
f'<text x="44" y="{yy}" fill="#bbb" font-family="monospace" font-size="16">{lab}</text>'
|
||||
)
|
||||
svg.append("</svg>")
|
||||
|
||||
svg_path = out_png.rsplit(".", 1)[0] + ".svg"
|
||||
open(svg_path, "w").write("\n".join(svg))
|
||||
# rsvg-convert gives cleaner output but is often not installed; fall back to
|
||||
# ImageMagick. FileNotFoundError (binary absent) must be caught too -- a
|
||||
# returncode check alone does not cover it.
|
||||
try:
|
||||
rc = subprocess.run(
|
||||
["rsvg-convert", "-w", str(size), svg_path, "-o", out_png],
|
||||
capture_output=True,
|
||||
).returncode
|
||||
except FileNotFoundError:
|
||||
rc = 1
|
||||
if rc != 0:
|
||||
subprocess.run(["convert", "-density", "96", svg_path, out_png], check=True)
|
||||
print("wrote", out_png)
|
||||
|
||||
|
||||
def main():
|
||||
src = sys.argv[1] if len(sys.argv) > 1 else "/tmp/wallmap_raw.txt"
|
||||
dst = sys.argv[2] if len(sys.argv) > 2 else "/tmp/wallmap.png"
|
||||
title = sys.argv[3] if len(sys.argv) > 3 else "Daikoku PA -- wall map"
|
||||
data = load(src)
|
||||
parts = data["parts"]
|
||||
print("counts (whole map):", data.get("counts"))
|
||||
print("plotted parts:", len(parts))
|
||||
cx, cz = analyse(parts)
|
||||
if cx is None:
|
||||
raise SystemExit("no PARingWall parts found -- nothing to centre on")
|
||||
render(parts, cx, cz, dst, title)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user