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).
@@ -0,0 +1,64 @@
|
|||||||
|
# PA Ramp Build Spec — reconciled
|
||||||
|
|
||||||
|
Written 2026-08-03 after research + measurement. Supersedes ad-hoc azimuth
|
||||||
|
tuning in `MapBuilder.luau`.
|
||||||
|
|
||||||
|
## Source conflict — READ THIS FIRST
|
||||||
|
|
||||||
|
Two sources disagree, and the disagreement matters:
|
||||||
|
|
||||||
|
| | Generic standard (AASHTO/FHWA/JP, via research) | Measured Daikoku (OSM circle fits) |
|
||||||
|
|---|---|---|
|
||||||
|
| Ramp form | short shallow **parallel slip road** | **loop helix**, 150–220° of turn |
|
||||||
|
| Radius | 80–120 m (285–430 studs) | 71–204 m, mean 131 m (**469 studs**) |
|
||||||
|
| Arc | 30–45° | 217° (main helix, rms 1.2 = truly circular) |
|
||||||
|
|
||||||
|
**Resolution: measured Daikoku wins for THIS build.** The generic spec describes
|
||||||
|
a typical roadside PA on a flat ground-level expressway. Daikoku PA is not that
|
||||||
|
— it sits at the bottom of a **five-level junction stack** on reclaimed land, so
|
||||||
|
its ramps are genuinely helical loops, not slip roads. The satellite plan view
|
||||||
|
(`aerial/daikoku_z19.jpg`) shows one large-radius arc sweeping around the lot.
|
||||||
|
|
||||||
|
The generic spec is still authoritative for the **merge detail** (below), which
|
||||||
|
is not Daikoku-specific.
|
||||||
|
|
||||||
|
## Numbers to build to
|
||||||
|
|
||||||
|
| Quantity | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Scale | 3.571 studs/m (1 stud = 0.28 m) | project |
|
||||||
|
| Main entry helix radius | **525 studs** (147 m) | OSM way 32403901, rms 1.2 |
|
||||||
|
| Secondary helix radius | **440 studs** (124 m) | OSM way 779045459, rms 0.7 |
|
||||||
|
| Tight connector radius | 255 studs (71 m) | OSM way 32355896 |
|
||||||
|
| Turn direction | **clockwise (right) — BOTH ramps** | 6 of 8 OSM ways; video 「右、右」 |
|
||||||
|
| Divergence at merge | 2–5° (current build: 0.1–0.2°, tangent) | AASHTO/FHWA |
|
||||||
|
| Deceleration lane | 150–200 m before gore | AASHTO |
|
||||||
|
| Acceleration lane | 200–300 m after gore | AASHTO |
|
||||||
|
| Gore apex angle | < 5°, chevron-painted over 30–50 m | AASHTO |
|
||||||
|
| Superelevation | 4–8% bank on curves | AASHTO |
|
||||||
|
| Deck-to-deck spacing | 36–43 studs/level (10–12 m) | JP standard |
|
||||||
|
|
||||||
|
## Hard constraints (checkable in a probe)
|
||||||
|
|
||||||
|
1. **Entry and exit ramps must NEVER intersect.** Current build fails this:
|
||||||
|
2547 point-pairs within 30 studs planar at < 12 studs dY, worst case
|
||||||
|
**0.5 studs apart at dY = 0.0**. This is the dominant visual defect —
|
||||||
|
it reads as a bowtie tangle. Probe must assert 0 such pairs.
|
||||||
|
2. Both ramps curve **the same direction** (clockwise). They are at opposite
|
||||||
|
ends of the lot and wrap the same way; they do not mirror into each other.
|
||||||
|
3. Ramp arc wraps **outside** the parking apron, forming its boundary — it does
|
||||||
|
not cut across the lot interior.
|
||||||
|
4. Merge is tangent (already achieved: 0.1–0.2°).
|
||||||
|
5. No ramp part intersects any Workspace-level building model (obstacle scope
|
||||||
|
must be INVERTED, not a name whitelist — see verification skill).
|
||||||
|
|
||||||
|
## Edge / structure treatment (from onboard video + Commons photos)
|
||||||
|
|
||||||
|
- Edge = **low concrete jersey base + see-through chain-link mesh fence above**.
|
||||||
|
NOT a tall solid parapet. Current build's heavy kerb chain is why the ramps
|
||||||
|
read as thick white rails.
|
||||||
|
- **Red/orange chevron shoulder paint** through every ramp curve — present in
|
||||||
|
all five reference videos, day and night. Cheapest high-value detail.
|
||||||
|
- Piers: **large cylindrical columns**, widely spaced, under a **trapezoidal
|
||||||
|
box-girder** deck underside. White/pale concrete.
|
||||||
|
- White zebra hatching fills the gore wedge.
|
||||||
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 134 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
# PA Ramp Reference Images
|
||||||
|
|
||||||
|
Downloaded 2026-08-03. All files verified present on disk and integrity-checked
|
||||||
|
with PIL (12/12 valid, 0 corrupt).
|
||||||
|
|
||||||
|
## Wikimedia Commons (fetched by research agent)
|
||||||
|
|
||||||
|
| File | Px | Shows | License |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `Daikoku-PA.JPG` | 2487x1797 | Daikoku PA overview | CC BY 3.0 |
|
||||||
|
| `Daikoku-JCT.JPG` | 2560x1920 | Daikoku JCT elevated stack | CC BY 3.0 |
|
||||||
|
| `DaikokuJCT-Bnishi.JPG` | 640x480 | Daikoku JCT bayside west | CC BY-SA 3.0 |
|
||||||
|
| `Shutoko_daikokuPA.jpg` | 1280x960 | Daikoku PA elevated structure | CC BY-SA 3.0 |
|
||||||
|
| `Shutoko_oiPA_east.jpg` | 1280x960 | Oi PA east | CC BY-SA 3.0 |
|
||||||
|
| `Shutoko_oiPA_west.jpg` | 1280x960 | Oi PA west (ground level, facility bldg) | CC BY-SA 3.0 |
|
||||||
|
| `Shutoko_heiwajimaPA_up.jpg` | 1280x960 | Heiwajima PA inbound | CC BY-SA 3.0 |
|
||||||
|
| `Shutoko_heiwajimaPA_down.jpg` | 1280x960 | Heiwajima PA outbound | CC BY-SA 3.0 |
|
||||||
|
| `Shutoko_shibauraPA.jpg` | 1280x960 | Shibaura PA | CC BY-SA 3.0 |
|
||||||
|
| `Shuto_expressway_shibaura_pa.jpg` | 1000x680 | Shibaura PA / expressway | CC BY-SA 3.0 |
|
||||||
|
|
||||||
|
License note: all CC BY / CC BY-SA. Reference-only for shape study. Do NOT
|
||||||
|
trace directly into shipped assets without attribution (see
|
||||||
|
`reference-image-research` skill, license matrix).
|
||||||
|
|
||||||
|
## From project NAS archive (higher value for geometry)
|
||||||
|
|
||||||
|
| File | Px | Shows |
|
||||||
|
|---|---|---|
|
||||||
|
| `aerial/daikoku_z19.jpg` | Esri z19 | **Best plan view.** Satellite of Daikoku PA: single large-radius arc wrapping the lot |
|
||||||
|
| `aerial/daikoku_bldg_big.jpg` | | Daikoku facility building aerial |
|
||||||
|
| `daikoku_jct_sheet2.jpg` | 1728x972 | Onboard contact sheet, Daikoku JCT |
|
||||||
|
| `exit_to_baybridge_sheet2.jpg` | 1728x1296 | **Best ground view.** Off-ramp divergence, gore hatching, jersey+mesh edge, box girder |
|
||||||
|
|
||||||
|
## Measured source of truth
|
||||||
|
|
||||||
|
`docs/daikoku-reference-spec.md` — OSM Overpass geometry with Kasa circle fits
|
||||||
|
of the actual ramp ways. This is measured, not estimated, and outranks generic
|
||||||
|
standards for Daikoku-specific radii.
|
||||||
|
After Width: | Height: | Size: 659 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 524 KiB |
|
After Width: | Height: | Size: 554 KiB |
|
After Width: | Height: | Size: 524 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 592 KiB |
|
After Width: | Height: | Size: 645 KiB |
|
After Width: | Height: | Size: 606 KiB |
|
After Width: | Height: | Size: 208 KiB |
@@ -0,0 +1,160 @@
|
|||||||
|
# Daikoku Futo PA — Measured Reference Spec
|
||||||
|
|
||||||
|
Source-of-truth numbers for building the Daikoku PA entrance/exit ramps and
|
||||||
|
apron in `MapBuilder.luau`. Everything here is **measured**, not estimated:
|
||||||
|
OSM geometry via Overpass (`/tmp/osm_daikoku.py`), Esri aerial imagery, and
|
||||||
|
frame-sampled onboard video.
|
||||||
|
|
||||||
|
Generated 2026-08-03.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Scale
|
||||||
|
|
||||||
|
| Quantity | Value |
|
||||||
|
|---|---|
|
||||||
|
| Project scale | `M = 1/0.28` = **3.571 studs per metre** (`MapBuilder.luau:69`) |
|
||||||
|
| Reciprocal | 1 stud = 0.28 m |
|
||||||
|
| `Circuit.STUDS_TO_KM` | 0.28 / 1000 |
|
||||||
|
|
||||||
|
> NOTE: `c1_georeference.json` records `scale_studs_per_meter: 4.3` for the C1
|
||||||
|
> loop georeference. That is a **different** mapping (real-Tokyo lat/lon → C1
|
||||||
|
> world) with a stated `median_error_m: 366`. Do **not** use 4.3 for PA
|
||||||
|
> geometry. PA work uses 3.571.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The real PA footprint (OSM)
|
||||||
|
|
||||||
|
Overpass, bbox 35.4545/139.6700 → 35.4705/139.6935. ENU metres about
|
||||||
|
`35.46246657, 139.68149001` (user's Google Earth point).
|
||||||
|
|
||||||
|
| OSM way | Type | Extent (m) | Perimeter | Name |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `405966005` | `highway=rest_area` | **203 × 227** | 646 m | 大黒PA |
|
||||||
|
| `59608205` | `amenity=parking` | **181 × 174** | 507 m | 大黒ふ頭パーキングエリア |
|
||||||
|
| `88702527` | `amenity=parking` | 102 × 107 | 359 m | (satellite lot, W of main) |
|
||||||
|
|
||||||
|
**Current build:** apron is `APRON_X = 210*M`, `APRON_Z = 200*M`
|
||||||
|
(`MapBuilder.luau:991`) = 210 × 200 m. That sits between the parking polygon
|
||||||
|
(181×174) and the rest_area polygon (203×227) — **already a good match.** The
|
||||||
|
apron extent is not the problem.
|
||||||
|
|
||||||
|
The *shape* is the divergence: the build is an axis-aligned square; the real
|
||||||
|
lot is a rounded wedge bounded on the N/E by the loop ramp arc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Loop ramp radii — MEASURED
|
||||||
|
|
||||||
|
Kasa circle fit over each OSM ramp way with |cumulative turn| ≥ 120°.
|
||||||
|
`rms` = fit residual (low = genuinely circular).
|
||||||
|
|
||||||
|
| OSM way | Length | Turn | **Radius** | rms | Layer | Direction |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `32355896` | 475 m | −272° | **70.9 m** | 14.4 | — | CW / right |
|
||||||
|
| `32403901` | 601 m | −217° | **147.4 m** | **1.2** | 5 | CW / right |
|
||||||
|
| `32592491` | 729 m | +208° | **197.4 m** | 12.7 | 2 | CCW / left |
|
||||||
|
| `307397558` | 167 m | −178° | **27.5 m** | 9.0 | — | CW / right |
|
||||||
|
| `779045459` | 371 m | +161° | **123.6 m** | **0.7** | 3 | CCW / left |
|
||||||
|
| `32403902` | 656 m | −150° | **204.2 m** | 13.3 | 3 | CW / right |
|
||||||
|
| `32592481` | 359 m | −146° | **120.4 m** | 12.0 | 1 | CW / right |
|
||||||
|
| `32592487` | 488 m | −122° | **158.8 m** | 11.3 | 2 | CW / right |
|
||||||
|
|
||||||
|
**Headline numbers**
|
||||||
|
|
||||||
|
- Radius range **27.5 – 204 m**; mean **131 m** = **469 studs**
|
||||||
|
- The two cleanest circular fits (rms < 1.5) are the true helixes:
|
||||||
|
- `32403901` — **147 m** (526 studs), 217° of turn, layer 5
|
||||||
|
- `779045459` — **124 m** (441 studs), 161° of turn, layer 3
|
||||||
|
- **6 of 8 curve clockwise (right).** Matches the video captions exactly:
|
||||||
|
`右、右と覚えておくとPAに着きます` — "remember right, right".
|
||||||
|
|
||||||
|
**Recommended build values**
|
||||||
|
|
||||||
|
| Ramp | Radius (m) | Radius (studs) | Turn |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Main entry helix | 147 | **525** | ~220° CW |
|
||||||
|
| Secondary helix | 124 | **440** | ~160° CCW |
|
||||||
|
| Tight connector | 71 | **255** | ~270° CW |
|
||||||
|
|
||||||
|
Current `Circuit.PA_RADIUS = 260` studs = 72.8 m — which is almost exactly the
|
||||||
|
tight connector (`32355896`, 70.9 m). The *main* helixes are roughly 2× that.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Deck layers — the vertical stack
|
||||||
|
|
||||||
|
OSM `layer` tags on bridge ways in the interchange:
|
||||||
|
|
||||||
|
| Layer | Bridge ways | Total length |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 11 | 17,696 m |
|
||||||
|
| 2 | 7 | 5,637 m |
|
||||||
|
| 3 | 8 | 5,368 m |
|
||||||
|
| 4 | 10 | 10,096 m |
|
||||||
|
| 5 | 6 | 1,417 m |
|
||||||
|
|
||||||
|
**Five distinct deck levels.** This is the single most important structural
|
||||||
|
fact: Daikoku JCT is a five-level stack, and the PA sits at the bottom of it.
|
||||||
|
|
||||||
|
Current `Circuit.PA_LIFT = 56` studs (15.7 m) of clearance over the apron —
|
||||||
|
that is one level. A faithful build wants at least 3 visible levels overhead.
|
||||||
|
|
||||||
|
Typical Japanese expressway deck-to-deck spacing is ~10–12 m; at 3.571 studs/m
|
||||||
|
that is **36–43 studs per level**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Video-derived detail
|
||||||
|
|
||||||
|
Sources archived at
|
||||||
|
`unraid-1:/mnt/user/data/yy-roblox-games/0-tokyo-wangan-racing/004-video-reference/`
|
||||||
|
|
||||||
|
| Route | Video ID | Len | Light | Value |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| CMT TOKYO "How to go to?" | `91suWmNr62c` | 2m50 | night | narrated turn-by-turn |
|
||||||
|
| Bay Bridge up (ベイブリッジ〜大黒PA) | `PK4BachUlYo` | 2m36 | night | full entry sequence |
|
||||||
|
| Yokohane down (横羽線下り) | `zlmdaR0opgE` | 5m15 | **day** | best structural read |
|
||||||
|
| Honmoku JCT (攻略車載#18) | `fNHucFnFQ1c` | 10m07 | **day** | approach + JCT |
|
||||||
|
| Night walkaround | `7i0-KCwMDyc` | 10m40 | night | apron-level atmosphere |
|
||||||
|
|
||||||
|
### Confirmed visual details
|
||||||
|
|
||||||
|
1. **Red/orange chevron shoulder paint** on the outer kerb through every ramp
|
||||||
|
curve. Present in all five videos, day and night. Highest-value cheap
|
||||||
|
detail available.
|
||||||
|
2. **White splayed / V-shaped piers** with trapezoidal box-girder deck
|
||||||
|
underside (clearest at `yokohane_down` t=218–242s).
|
||||||
|
3. **Chain-link mesh fencing** along ramp edges — not solid barrier. Above a
|
||||||
|
low concrete jersey base.
|
||||||
|
4. **Green route shields** `大黒線 K5`; blue circular regulatory signs;
|
||||||
|
`50` km/h limit discs on ramps.
|
||||||
|
5. **`/P` + up-right arrow gantry** marks the parking entry (`91suWmNr62c`
|
||||||
|
t=104s).
|
||||||
|
6. Apron at night: warm sodium pole lighting, **white stall lines clearly
|
||||||
|
visible**, painted directional arrows, low guardrail perimeter.
|
||||||
|
7. Arrival reads as **descending into a canyon** — decks close in overhead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Gaps / not yet measured
|
||||||
|
|
||||||
|
- **Deck heights above ground.** OSM `layer` gives ordering, not elevation.
|
||||||
|
Needs either a DEM lookup or estimation from video pier counts.
|
||||||
|
- **Third approach route.** Confirmed to exist (video states "3 ways"); two
|
||||||
|
are captured in full, the Honmoku run covers the approach only.
|
||||||
|
- **Exit ramp geometry.** All captured footage is *entry*. Exits not yet
|
||||||
|
sampled.
|
||||||
|
- **Stall grid angle.** Aerial shows a diagonal ~40–45° grid; exact bearing
|
||||||
|
not yet fitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Reproduce
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 /tmp/osm_daikoku.py # OSM geometry + circle fits
|
||||||
|
python3 /tmp/route.py <videoid> <name> <step_s> # video -> contact sheets
|
||||||
|
python3 /tmp/sat.py # Esri aerial tiles
|
||||||
|
```
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pull real Daikoku JCT / PA geometry from OpenStreetMap via Overpass.
|
||||||
|
|
||||||
|
Outputs measured, real-world numbers:
|
||||||
|
- PA service-area polygon (extent, area)
|
||||||
|
- Each ramp/link way: length, turn direction, fitted circle radius
|
||||||
|
- Bridge levels present
|
||||||
|
|
||||||
|
All distances in metres. Local ENU projection about the PA centre so we can
|
||||||
|
fit circles in a flat plane without map-projection distortion.
|
||||||
|
"""
|
||||||
|
import json, math, urllib.request, urllib.parse, sys
|
||||||
|
|
||||||
|
LAT0, LON0 = 35.46246657, 139.68149001 # user's Google Earth point
|
||||||
|
R_EARTH = 6378137.0
|
||||||
|
|
||||||
|
# Generous bbox around the whole interchange
|
||||||
|
S, W, N, E = 35.4545, 139.6700, 35.4705, 139.6935
|
||||||
|
|
||||||
|
Q = f"""
|
||||||
|
[out:json][timeout:120];
|
||||||
|
(
|
||||||
|
way["highway"]({S},{W},{N},{E});
|
||||||
|
way["amenity"="parking"]({S},{W},{N},{E});
|
||||||
|
way["highway"="services"]({S},{W},{N},{E});
|
||||||
|
way["highway"="rest_area"]({S},{W},{N},{E});
|
||||||
|
);
|
||||||
|
out body geom;
|
||||||
|
"""
|
||||||
|
|
||||||
|
ENDPOINTS = [
|
||||||
|
"https://overpass-api.de/api/interpreter",
|
||||||
|
"https://overpass.kumi.systems/api/interpreter",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch():
|
||||||
|
last = None
|
||||||
|
for ep in ENDPOINTS:
|
||||||
|
try:
|
||||||
|
data = urllib.parse.urlencode({"data": Q}).encode()
|
||||||
|
req = urllib.request.Request(ep, data=data,
|
||||||
|
headers={"User-Agent": "daikoku-ref/1.0"})
|
||||||
|
with urllib.request.urlopen(req, timeout=150) as r:
|
||||||
|
return json.loads(r.read().decode())
|
||||||
|
except Exception as e:
|
||||||
|
last = f"{ep}: {e}"
|
||||||
|
print(f" endpoint failed {last}", file=sys.stderr)
|
||||||
|
raise SystemExit(f"all overpass endpoints failed: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def enu(lat, lon):
|
||||||
|
"""Local east/north metres relative to (LAT0, LON0)."""
|
||||||
|
x = math.radians(lon - LON0) * R_EARTH * math.cos(math.radians(LAT0))
|
||||||
|
y = math.radians(lat - LAT0) * R_EARTH
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
|
||||||
|
def polyline_len(pts):
|
||||||
|
return sum(math.dist(pts[i], pts[i + 1]) for i in range(len(pts) - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def fit_circle(pts):
|
||||||
|
"""Algebraic (Kasa) circle fit. Returns (cx, cy, R, rms_residual)."""
|
||||||
|
n = len(pts)
|
||||||
|
if n < 3:
|
||||||
|
return None
|
||||||
|
sx = sum(p[0] for p in pts) / n
|
||||||
|
sy = sum(p[1] for p in pts) / n
|
||||||
|
u = [p[0] - sx for p in pts]
|
||||||
|
v = [p[1] - sy for p in pts]
|
||||||
|
Suu = sum(a * a for a in u); Svv = sum(b * b for b in v)
|
||||||
|
Suv = sum(u[i] * v[i] for i in range(n))
|
||||||
|
Suuu = sum(a ** 3 for a in u); Svvv = sum(b ** 3 for b in v)
|
||||||
|
Suvv = sum(u[i] * v[i] * v[i] for i in range(n))
|
||||||
|
Svuu = sum(v[i] * u[i] * u[i] for i in range(n))
|
||||||
|
d = 2 * (Suu * Svv - Suv * Suv)
|
||||||
|
if abs(d) < 1e-9:
|
||||||
|
return None
|
||||||
|
uc = (Svv * (Suuu + Suvv) - Suv * (Svvv + Svuu)) / d
|
||||||
|
vc = (Suu * (Svvv + Svuu) - Suv * (Suuu + Suvv)) / d
|
||||||
|
cx, cy = uc + sx, vc + sy
|
||||||
|
rs = [math.dist(p, (cx, cy)) for p in pts]
|
||||||
|
R = sum(rs) / len(rs)
|
||||||
|
rms = math.sqrt(sum((r - R) ** 2 for r in rs) / len(rs))
|
||||||
|
return cx, cy, R, rms
|
||||||
|
|
||||||
|
|
||||||
|
def total_turn(pts):
|
||||||
|
"""Signed cumulative heading change in degrees. +ccw / -cw."""
|
||||||
|
tot = 0.0
|
||||||
|
for i in range(1, len(pts) - 1):
|
||||||
|
a1 = math.atan2(pts[i][1] - pts[i - 1][1], pts[i][0] - pts[i - 1][0])
|
||||||
|
a2 = math.atan2(pts[i + 1][1] - pts[i][1], pts[i + 1][0] - pts[i][0])
|
||||||
|
d = math.degrees(a2 - a1)
|
||||||
|
while d > 180: d -= 360
|
||||||
|
while d < -180: d += 360
|
||||||
|
tot += d
|
||||||
|
return tot
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data = fetch()
|
||||||
|
els = data.get("elements", [])
|
||||||
|
print(f"overpass returned {len(els)} ways\n")
|
||||||
|
|
||||||
|
ramps, areas = [], []
|
||||||
|
for el in els:
|
||||||
|
if el.get("type") != "way" or "geometry" not in el:
|
||||||
|
continue
|
||||||
|
t = el.get("tags", {})
|
||||||
|
pts = [enu(g["lat"], g["lon"]) for g in el["geometry"]]
|
||||||
|
if len(pts) < 2:
|
||||||
|
continue
|
||||||
|
rec = {"id": el["id"], "tags": t, "pts": pts,
|
||||||
|
"len": polyline_len(pts)}
|
||||||
|
hw = t.get("highway", "")
|
||||||
|
if hw in ("services", "rest_area") or t.get("amenity") == "parking":
|
||||||
|
areas.append(rec)
|
||||||
|
elif hw.endswith("_link") or t.get("oneway") == "yes":
|
||||||
|
ramps.append(rec)
|
||||||
|
|
||||||
|
# ---- PA area ----
|
||||||
|
print("=" * 70)
|
||||||
|
print("PARKING / SERVICE AREA POLYGONS")
|
||||||
|
print("=" * 70)
|
||||||
|
for a in sorted(areas, key=lambda r: -r["len"])[:6]:
|
||||||
|
xs = [p[0] for p in a["pts"]]; ys = [p[1] for p in a["pts"]]
|
||||||
|
w = max(xs) - min(xs); h = max(ys) - min(ys)
|
||||||
|
nm = a["tags"].get("name", a["tags"].get("name:en", "-"))
|
||||||
|
print(f" way {a['id']:>12} {a['tags'].get('highway') or a['tags'].get('amenity'):<10}"
|
||||||
|
f" extent {w:6.0f} x {h:6.0f} m perim {a['len']:7.0f} m {nm}")
|
||||||
|
print(f" E {min(xs):7.0f}..{max(xs):7.0f} N {min(ys):7.0f}..{max(ys):7.0f}")
|
||||||
|
|
||||||
|
# ---- curved ramps: the loops ----
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("CURVED RAMPS (|turn| >= 120 deg) -- the helix loops")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f" {'way id':>12} {'len(m)':>7} {'turn':>8} {'radius(m)':>10} {'rms':>6} layer/bridge")
|
||||||
|
loops = []
|
||||||
|
for r in ramps:
|
||||||
|
turn = total_turn(r["pts"])
|
||||||
|
if abs(turn) < 120 or r["len"] < 60:
|
||||||
|
continue
|
||||||
|
fc = fit_circle(r["pts"])
|
||||||
|
if not fc:
|
||||||
|
continue
|
||||||
|
cx, cy, R, rms = fc
|
||||||
|
if R > 400: # not a tight loop
|
||||||
|
continue
|
||||||
|
loops.append((r, turn, cx, cy, R, rms))
|
||||||
|
for r, turn, cx, cy, R, rms in sorted(loops, key=lambda z: -abs(z[1])):
|
||||||
|
t = r["tags"]
|
||||||
|
lb = f"layer={t.get('layer','-')} bridge={t.get('bridge','-')}"
|
||||||
|
print(f" {r['id']:>12} {r['len']:7.0f} {turn:7.0f}\u00b0 {R:10.1f} {rms:6.1f} {lb}")
|
||||||
|
print(f" centre E {cx:7.0f} N {cy:7.0f} "
|
||||||
|
f"{'CCW/left' if turn>0 else 'CW/right'}")
|
||||||
|
|
||||||
|
if loops:
|
||||||
|
rr = [z[4] for z in loops]
|
||||||
|
print(f"\n loop radius: min {min(rr):.0f} m max {max(rr):.0f} m "
|
||||||
|
f"mean {sum(rr)/len(rr):.0f} m")
|
||||||
|
print(f" in studs (1 stud = 0.28 m): mean {sum(rr)/len(rr)/0.28:.0f} studs")
|
||||||
|
|
||||||
|
# ---- layers present ----
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("DECK LAYERS (bridge ways)")
|
||||||
|
print("=" * 70)
|
||||||
|
lay = {}
|
||||||
|
for r in ramps:
|
||||||
|
if r["tags"].get("bridge"):
|
||||||
|
L = r["tags"].get("layer", "0")
|
||||||
|
lay.setdefault(L, []).append(r["len"])
|
||||||
|
for L in sorted(lay, key=lambda s: int(s) if s.lstrip("-").isdigit() else 0):
|
||||||
|
v = lay[L]
|
||||||
|
print(f" layer {L:>3}: {len(v):3d} bridge ways, total {sum(v):8.0f} m")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/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}")
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fetch + stitch Esri World Imagery tiles (no API key) around a lat/lon.
|
||||||
|
|
||||||
|
Esri World Imagery over Japan is high-res aerial. Tile scheme is standard
|
||||||
|
Web Mercator XYZ, same as OSM.
|
||||||
|
"""
|
||||||
|
import math, sys, urllib.request, io
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
URL = "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
||||||
|
UA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) research/1.0"}
|
||||||
|
|
||||||
|
|
||||||
|
def deg2tile(lat, lon, z):
|
||||||
|
lat_r = math.radians(lat)
|
||||||
|
n = 2.0 ** z
|
||||||
|
xt = (lon + 180.0) / 360.0 * n
|
||||||
|
yt = (1.0 - math.asinh(math.tan(lat_r)) / math.pi) / 2.0 * n
|
||||||
|
return xt, yt
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(z, x, y):
|
||||||
|
u = URL.format(z=z, x=x, y=y)
|
||||||
|
req = urllib.request.Request(u, headers=UA)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return Image.open(io.BytesIO(r.read())).convert("RGB")
|
||||||
|
|
||||||
|
|
||||||
|
def build(lat, lon, z, half, out):
|
||||||
|
xt, yt = deg2tile(lat, lon, z)
|
||||||
|
cx, cy = int(xt), int(yt)
|
||||||
|
n = half * 2 + 1
|
||||||
|
canvas = Image.new("RGB", (256 * n, 256 * n))
|
||||||
|
ok = 0
|
||||||
|
for i, x in enumerate(range(cx - half, cx + half + 1)):
|
||||||
|
for j, y in enumerate(range(cy - half, cy + half + 1)):
|
||||||
|
try:
|
||||||
|
canvas.paste(fetch(z, x, y), (i * 256, j * 256))
|
||||||
|
ok += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" tile {z}/{x}/{y} FAIL {e}", file=sys.stderr)
|
||||||
|
canvas.save(out, quality=92)
|
||||||
|
# metres per pixel at this latitude
|
||||||
|
mpp = 156543.03392 * math.cos(math.radians(lat)) / (2 ** z)
|
||||||
|
span = mpp * 256 * n
|
||||||
|
print(f"{out} z={z} tiles={ok}/{n*n} size={canvas.size} "
|
||||||
|
f"mpp={mpp:.3f} span={span:.0f}m")
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
LAT, LON = 35.46246657, 139.68149001
|
||||||
|
build(LAT, LON, 17, 2, "/tmp/daikoku_z17.jpg") # wide: whole loop
|
||||||
|
build(LAT, LON, 18, 2, "/tmp/daikoku_z18.jpg") # closer
|
||||||
|
build(LAT, LON, 19, 2, "/tmp/daikoku_z19.jpg") # detail: stalls
|
||||||
|
After Width: | Height: | Size: 303 KiB |
@@ -92,6 +92,9 @@ local LANE_W = 3.25 * M -- 11.6 studs; narrow urban lane used on the C1
|
|||||||
-- every 1.0 m, bounded by a 15-20 cm edge line.
|
-- every 1.0 m, bounded by a 15-20 cm edge line.
|
||||||
local ZEBRA_W = 0.45 * M
|
local ZEBRA_W = 0.45 * M
|
||||||
local ZEBRA_PITCH = 1.0 * M
|
local ZEBRA_PITCH = 1.0 * M
|
||||||
|
-- Gore hatching + speed-change-lane boundary lines only make sense where a
|
||||||
|
-- ramp actually diverges. Tied to BUILD_PA_RAMPS in buildPA (2026-08-03).
|
||||||
|
local RAMP_GORE_MARKINGS = false
|
||||||
local MARK_Y = 0.1 -- sits just proud of the deck to avoid z-fighting
|
local MARK_Y = 0.1 -- sits just proud of the deck to avoid z-fighting
|
||||||
local MARK_H = 2.1
|
local MARK_H = 2.1
|
||||||
-- Heading change per 60-stud sample above which the curve counts as tight
|
-- Heading change per 60-stud sample above which the curve counts as tight
|
||||||
@@ -399,10 +402,21 @@ local function planPA()
|
|||||||
kind = "off",
|
kind = "off",
|
||||||
k = 7, -- fork at sample 547, bearing 32 deg
|
k = 7, -- fork at sample 547, bearing 32 deg
|
||||||
forkR = 470.0, -- mainline is 533 out at that bearing: 63 in from it
|
forkR = 470.0, -- mainline is 533 out at that bearing: 63 in from it
|
||||||
az = 270.0, -- ring gate on the PA's south edge
|
-- GATE MOVED TO THE PA's NORTH EDGE (2026-08-03).
|
||||||
turn = 1, -- CCW: 270 -> 392 (=32), down the east side
|
-- It used to be az=270 -- the SOUTH edge -- with a 122 deg sweep,
|
||||||
sweep = 122.0,
|
-- which walked the trunk right around the PA on a r=470 arc. The
|
||||||
yFork = 96,
|
-- shop models sit on a ring of radius ~460-515 across the south
|
||||||
|
-- and west sectors (Fast Food Restaurant, McDonald's, Restaurant1,
|
||||||
|
-- Updated Ramen Shop, すき家, 7 eleven), so that arc drove straight
|
||||||
|
-- through them: 183 measured clips. The mainline is on the +Z
|
||||||
|
-- (north) side of the PA, and the north sector Z>-7325 is clear of
|
||||||
|
-- shops, so the gate belongs there. A short sweep also answers the
|
||||||
|
-- "angle is too round" note -- the ramp now leaves the perimeter
|
||||||
|
-- almost straight at the mainline instead of curling around it.
|
||||||
|
az = 95.0, -- ring gate on the PA's north edge, facing the mainline
|
||||||
|
turn = 1, -- CCW, short arc across the clear north sector
|
||||||
|
sweep = 48.0,
|
||||||
|
-- yFork is derived from the deck at the merge sample (see below).
|
||||||
m = -5, -- diverges from the carriageway at sample 542
|
m = -5, -- diverges from the carriageway at sample 542
|
||||||
out = 0, -- unused: no under-deck crossing any more
|
out = 0, -- unused: no under-deck crossing any more
|
||||||
nearSide = -1, -- PA is on the -Z side of the mainline here
|
nearSide = -1, -- PA is on the -Z side of the mainline here
|
||||||
@@ -412,10 +426,14 @@ local function planPA()
|
|||||||
kind = "on",
|
kind = "on",
|
||||||
k = -6, -- fork at sample 534, bearing 132 deg
|
k = -6, -- fork at sample 534, bearing 132 deg
|
||||||
forkR = 470.0,
|
forkR = 470.0,
|
||||||
az = 250.0, -- ring gate on the south edge, 135 studs west of red's
|
-- Gate also moved to the north edge (see PAEntryRamp above); it
|
||||||
turn = -1, -- CW: 250 -> 132, up the west side
|
-- was az=250 sweeping 118 deg through the west-sector shops.
|
||||||
sweep = 118.0,
|
-- 65 deg keeps ~250 studs of clear kerb between the two ramp
|
||||||
yFork = 96,
|
-- mouths at r=470 so they don't share an opening.
|
||||||
|
az = 65.0, -- ring gate on the north edge, east of red's
|
||||||
|
turn = -1, -- CW, short arc across the clear north sector
|
||||||
|
sweep = 44.0,
|
||||||
|
-- yFork derived (see below); a constant caused the dY -6.0 step.
|
||||||
m = 3, -- merges onto the carriageway at sample 537, heading east
|
m = 3, -- merges onto the carriageway at sample 537, heading east
|
||||||
out = 0,
|
out = 0,
|
||||||
nearSide = -1,
|
nearSide = -1,
|
||||||
@@ -443,7 +461,10 @@ local function planPA()
|
|||||||
local nx0 = samples[(i0 % n) + 1].pos
|
local nx0 = samples[(i0 % n) + 1].pos
|
||||||
local h0 = math.atan2(nx0.Z - s0.Z, nx0.X - s0.X)
|
local h0 = math.atan2(nx0.Z - s0.Z, nx0.X - s0.X)
|
||||||
local nrm = Vector3.new(-math.sin(h0), 0, math.cos(h0))
|
local nrm = Vector3.new(-math.sin(h0), 0, math.cos(h0))
|
||||||
local emerge = Vector3.new(basePos.X, def.yFork, basePos.Z) - nrm * (def.out * def.nearSide)
|
-- Deck height at the merge sample: the ramp must hand off at exactly
|
||||||
|
-- the mainline's local Y or it walls off (was a hardcoded 96 vs 102).
|
||||||
|
local yForkDerived = samples[nearIdxM].pos.Y
|
||||||
|
local emerge = Vector3.new(basePos.X, yForkDerived, basePos.Z) - nrm * (def.out * def.nearSide)
|
||||||
table.insert(junctionDefs, {
|
table.insert(junctionDefs, {
|
||||||
name = def.name,
|
name = def.name,
|
||||||
idx = i0,
|
idx = i0,
|
||||||
@@ -453,7 +474,7 @@ local function planPA()
|
|||||||
az = math.rad(def.az),
|
az = math.rad(def.az),
|
||||||
turn = def.turn,
|
turn = def.turn,
|
||||||
sweep = math.rad(def.sweep),
|
sweep = math.rad(def.sweep),
|
||||||
yFork = def.yFork,
|
yFork = yForkDerived,
|
||||||
nearSide = def.nearSide,
|
nearSide = def.nearSide,
|
||||||
-- `side` kept for the barrier/median code, which still thinks in
|
-- `side` kept for the barrier/median code, which still thinks in
|
||||||
-- terms of one carriageway per junction; the near side is the one
|
-- terms of one carriageway per junction; the near side is the one
|
||||||
@@ -468,7 +489,7 @@ local function planPA()
|
|||||||
-- gates) hangs off this.
|
-- gates) hangs off this.
|
||||||
fork = Vector3.new(
|
fork = Vector3.new(
|
||||||
c.X + math.cos(math.rad(def.az) + def.turn * math.rad(def.sweep)) * def.forkR,
|
c.X + math.cos(math.rad(def.az) + def.turn * math.rad(def.sweep)) * def.forkR,
|
||||||
def.yFork,
|
yForkDerived,
|
||||||
c.Z + math.sin(math.rad(def.az) + def.turn * math.rad(def.sweep)) * def.forkR
|
c.Z + math.sin(math.rad(def.az) + def.turn * math.rad(def.sweep)) * def.forkR
|
||||||
),
|
),
|
||||||
pos = nearPos,
|
pos = nearPos,
|
||||||
@@ -510,11 +531,19 @@ local function planPA()
|
|||||||
-- would strip the parapet off the far carriageway again (the 2026-08-02
|
-- would strip the parapet off the far carriageway again (the 2026-08-02
|
||||||
-- "no walls near the parking area" bug, in a new form).
|
-- "no walls near the parking area" bug, in a new form).
|
||||||
local rampOpenSide = {}
|
local rampOpenSide = {}
|
||||||
for _, jd in ipairs(junctionDefs) do
|
-- RAMPS DISABLED (2026-08-03): with no ramp deck built, an opening in the
|
||||||
for k = -RAMP_SPAN, RAMP_SPAN do
|
-- outer parapet is a hole in the barrier with a drop behind it -- the
|
||||||
local i = ((jd.idx + k - 1) % n) + 1
|
-- exact "no walls near the parking area" failure described above, just
|
||||||
rampOpen[i] = true
|
-- caused from the other end. Leave the edge sealed while the ramps are
|
||||||
rampOpenSide[i] = jd.side
|
-- off. Re-enable together with BUILD_PA_RAMPS in buildPA.
|
||||||
|
local RAMP_EDGE_OPENINGS = false
|
||||||
|
if RAMP_EDGE_OPENINGS then
|
||||||
|
for _, jd in ipairs(junctionDefs) do
|
||||||
|
for k = -RAMP_SPAN, RAMP_SPAN do
|
||||||
|
local i = ((jd.idx + k - 1) % n) + 1
|
||||||
|
rampOpen[i] = true
|
||||||
|
rampOpenSide[i] = jd.side
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
PAPlan = {
|
PAPlan = {
|
||||||
@@ -945,7 +974,13 @@ local function buildRoad(map: Folder)
|
|||||||
-- Where the ramp splits off the mainline the real road paints a
|
-- Where the ramp splits off the mainline the real road paints a
|
||||||
-- zebra-hatched no-drive wedge in the gore. 45 cm bars every 1.0 m
|
-- zebra-hatched no-drive wedge in the gore. 45 cm bars every 1.0 m
|
||||||
-- per 全標協 2018 pp.97-98.
|
-- per 全標協 2018 pp.97-98.
|
||||||
if plan.medianOpen[i] then
|
--
|
||||||
|
-- RAMPS DISABLED (2026-08-03): a gore is the wedge BETWEEN a mainline
|
||||||
|
-- and a diverging ramp. With no ramp there is no gore, so this paints
|
||||||
|
-- 1024 hatch bars and 64 boundary lines marking a junction that does
|
||||||
|
-- not exist -- it reads as random white striping across the deck.
|
||||||
|
-- Re-enable together with BUILD_PA_RAMPS in buildPA.
|
||||||
|
if plan.medianOpen[i] and RAMP_GORE_MARKINGS then
|
||||||
local nBars = math.floor(delta.Magnitude / ZEBRA_PITCH)
|
local nBars = math.floor(delta.Magnitude / ZEBRA_PITCH)
|
||||||
for k = 0, nBars - 1 do
|
for k = 0, nBars - 1 do
|
||||||
local z = (k * ZEBRA_PITCH - delta.Magnitude / 2) + ZEBRA_PITCH / 2
|
local z = (k * ZEBRA_PITCH - delta.Magnitude / 2) + ZEBRA_PITCH / 2
|
||||||
@@ -1578,6 +1613,33 @@ local function buildPA(map: Folder)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- PA ON/OFF RAMPS -- DISABLED (2026-08-03, user request).
|
||||||
|
--
|
||||||
|
-- Held off deliberately, not abandoned. Three iterations all failed to
|
||||||
|
-- look like a real interchange, and the measurements said why:
|
||||||
|
-- * 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. That reads on screen as a bowtie tangle of white rails.
|
||||||
|
-- * both real Daikoku ramps turn the SAME way (clockwise; the guide
|
||||||
|
-- video says 「右、右」). 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.
|
||||||
|
--
|
||||||
|
-- What was already right and should be kept when this is revisited:
|
||||||
|
-- radius (470 studs vs a measured OSM mean of 469) and the merge, which
|
||||||
|
-- lands tangent at 0.1-0.2 degrees of divergence.
|
||||||
|
--
|
||||||
|
-- Rebuild spec, reference imagery and the checkable constraint list:
|
||||||
|
-- reference/ramps/BUILD_SPEC.md
|
||||||
|
-- reference/ramps/INDEX.md
|
||||||
|
-- reference/ramps/docs/daikoku-reference-spec.md (OSM circle fits)
|
||||||
|
--
|
||||||
|
-- Flip this to true to re-enable the existing (defective) generator.
|
||||||
|
-- The ring wall keeps its gate openings either way -- they are placed
|
||||||
|
-- from `junctions` above, independently of this flag, so the PA stays
|
||||||
|
-- drivable and the perimeter stays closed with the ramps off.
|
||||||
|
local BUILD_PA_RAMPS = false
|
||||||
|
|
||||||
-- Two ramps: mainline -> ring (entry) and ring -> mainline (exit).
|
-- Two ramps: mainline -> ring (entry) and ring -> mainline (exit).
|
||||||
-- `junctions` was resolved above (the ring wall needed it to place gates);
|
-- `junctions` was resolved above (the ring wall needed it to place gates);
|
||||||
-- reuse it so the ramp deck and the wall opening can never disagree.
|
-- reuse it so the ramp deck and the wall opening can never disagree.
|
||||||
@@ -1688,20 +1750,47 @@ local function buildPA(map: Folder)
|
|||||||
-- run to eliminate. Those spans are short and are carried by the
|
-- run to eliminate. Those spans are short and are carried by the
|
||||||
-- piers on either side.
|
-- piers on either side.
|
||||||
local blocked = underMainline(segMid, RAMP_W)
|
local blocked = underMainline(segMid, RAMP_W)
|
||||||
if h > 10 and s % 2 == 0 and not blocked then
|
-- SUPPORT SPACING + FORM (2026-08-03, user note: "supports are
|
||||||
|
-- supposed to be beams / poles like in real life").
|
||||||
|
-- Was `s % 2 == 0` -- a column every 2 segments, 162 of them,
|
||||||
|
-- which reads as a solid palisade under the deck rather than
|
||||||
|
-- discrete piers. Real elevated ramps carry the deck on widely
|
||||||
|
-- spaced slender columns, each topped by a transverse HEAD BEAM
|
||||||
|
-- (cap) that the deck sits on. Every 14th segment gives roughly
|
||||||
|
-- one bent per 30-40 m of span, which matches the reference
|
||||||
|
-- footage under Daikoku.
|
||||||
|
local PIER_EVERY = 14
|
||||||
|
if h > 10 and s % PIER_EVERY == 0 and not blocked then
|
||||||
|
-- slimmer column: 4.5 studs instead of 6, so it reads as a
|
||||||
|
-- pole rather than a fat block
|
||||||
local pil = part({
|
local pil = part({
|
||||||
Name = "PARampPillar",
|
Name = "PARampPillar",
|
||||||
-- Cylinder axis is local X, so Size.X is the height and
|
-- Cylinder axis is local X, so Size.X is the height and
|
||||||
-- Size.Y/Z the diameter; the Z rotation stands it upright.
|
-- Size.Y/Z the diameter; the Z rotation stands it upright.
|
||||||
-- (Same transposition that had all 80 viaduct PierColumns
|
-- (Same transposition that had all 80 viaduct PierColumns
|
||||||
-- built as 8-stud discs -- see the PierColumn note above.)
|
-- built as 8-stud discs -- see the PierColumn note above.)
|
||||||
Size = Vector3.new(h, 6, 6),
|
Size = Vector3.new(h, 4.5, 4.5),
|
||||||
CFrame = CFrame.new(segMid.X, segMid.Y - h / 2, segMid.Z) * CFrame.Angles(0, 0, math.rad(90)),
|
CFrame = CFrame.new(segMid.X, segMid.Y - h / 2, segMid.Z) * CFrame.Angles(0, 0, math.rad(90)),
|
||||||
Color = C_CONCRETE,
|
Color = C_CONCRETE,
|
||||||
Material = Enum.Material.Concrete,
|
Material = Enum.Material.Concrete,
|
||||||
Shape = Enum.PartType.Cylinder,
|
Shape = Enum.PartType.Cylinder,
|
||||||
})
|
})
|
||||||
pil.Parent = pa
|
pil.Parent = pa
|
||||||
|
-- HEAD BEAM: a short transverse girder across the top of the
|
||||||
|
-- column, directly under the deck, so the ramp visibly rests
|
||||||
|
-- on something instead of balancing on a stick. Oriented
|
||||||
|
-- across the run using the segment's own heading.
|
||||||
|
-- (a and b are this segment's endpoints; there is no segDir
|
||||||
|
-- in scope -- deriving it here rather than assuming one.)
|
||||||
|
local hdg = math.atan2(b.Z - a.Z, b.X - a.X)
|
||||||
|
local cap = part({
|
||||||
|
Name = "PARampPierCap",
|
||||||
|
Size = Vector3.new(3.0, 2.6, RAMP_W * 0.92),
|
||||||
|
CFrame = CFrame.new(segMid.X, segMid.Y - 1.6, segMid.Z) * CFrame.Angles(0, -hdg, 0),
|
||||||
|
Color = C_CONCRETE,
|
||||||
|
Material = Enum.Material.Concrete,
|
||||||
|
})
|
||||||
|
cap.Parent = pa
|
||||||
end
|
end
|
||||||
-- Edge kerbs so a car can't slide off the side of a raised ramp.
|
-- Edge kerbs so a car can't slide off the side of a raised ramp.
|
||||||
--
|
--
|
||||||
@@ -1830,8 +1919,10 @@ local function buildPA(map: Folder)
|
|||||||
end
|
end
|
||||||
|
|
||||||
for _, jn in ipairs(junctions) do
|
for _, jn in ipairs(junctions) do
|
||||||
|
if not BUILD_PA_RAMPS then
|
||||||
|
continue
|
||||||
|
end
|
||||||
local rampR = jn.radius
|
local rampR = jn.radius
|
||||||
local yf = jn.yFork
|
|
||||||
local ns = jn.nearSide
|
local ns = jn.nearSide
|
||||||
local mergeIdx = jn.mergeIdx
|
local mergeIdx = jn.mergeIdx
|
||||||
-- ACTUAL local deck height at the merge, not a constant 102. The
|
-- ACTUAL local deck height at the merge, not a constant 102. The
|
||||||
@@ -1848,46 +1939,111 @@ local function buildPA(map: Folder)
|
|||||||
-- drawing. Climb is spread over the full run too: 50 studs across a
|
-- drawing. Climb is spread over the full run too: 50 studs across a
|
||||||
-- ~900-stud arc is ~5.5%, well inside the 9% limit, so there is no
|
-- ~900-stud arc is ~5.5%, well inside the 9% limit, so there is no
|
||||||
-- reason to front-load it.
|
-- reason to front-load it.
|
||||||
|
--
|
||||||
|
-- DECK_CLEAR is the flyover's soffit clearance above the mainline
|
||||||
|
-- deck. Japanese expressway minimum vertical clearance is 4.5 m;
|
||||||
|
-- at this map's scale (1 m = 3.57 studs) that is ~16 studs to the
|
||||||
|
-- UNDERSIDE, so add the ramp slab depth on top.
|
||||||
|
local DECK_CLEAR = 34
|
||||||
|
-- 1. TRUNK -- perimeter spiral, ring gate out to the fork point. The
|
||||||
|
-- radius eases across the WHOLE sweep so the ramp hugs the PA edge
|
||||||
|
-- and flares out toward the mainline -- the shape in the drawing.
|
||||||
|
--
|
||||||
|
-- The trunk now climbs to OVER_Y (above the deck) rather than to deck
|
||||||
|
-- level: BOTH ramps cross the expressway on a flyover and serve the
|
||||||
|
-- FAR carriageway, per the drawing -- red comes off the highway, over
|
||||||
|
-- the deck and circles down into the PA; yellow leaves the PA, over
|
||||||
|
-- the deck, and merges into the northbound (far) lanes. The previous
|
||||||
|
-- pass deleted this crossing and substituted a median gap; that is
|
||||||
|
-- what kept both ramps stranded on the near side.
|
||||||
|
local OVER_Y = yd + DECK_CLEAR
|
||||||
local trunk = {}
|
local trunk = {}
|
||||||
for s = 0, 400 do
|
for s = 0, 400 do
|
||||||
local f = s / 400
|
local f = s / 400
|
||||||
local th = jn.th0 + jn.turn * jn.sweep * f
|
local th = jn.th0 + jn.turn * jn.sweep * f
|
||||||
local r = RING_R + (rampR - RING_R) * ease(f)
|
local r = RING_R + (rampR - RING_R) * ease(f)
|
||||||
local y = APRON_Y + (yf - APRON_Y) * ease(f)
|
local y = APRON_Y + (OVER_Y - APRON_Y) * ease(f)
|
||||||
table.insert(trunk, Vector3.new(c.X + math.cos(th) * r, y, c.Z + math.sin(th) * r))
|
table.insert(trunk, Vector3.new(c.X + math.cos(th) * r, y, c.Z + math.sin(th) * r))
|
||||||
end
|
end
|
||||||
local fork = trunk[#trunk]
|
local fork = trunk[#trunk]
|
||||||
|
|
||||||
-- 2. SLIP -- fork to the carriageway on the PA side, at deck level.
|
-- 2. CROSSING -- fork straight out over BOTH carriageways at constant
|
||||||
-- There is only ONE slip now. The far carriageway is reached through
|
-- OVER_Y. This is the flyover proper: it must clear the deck, so it
|
||||||
-- the gap in the centre divider (see `open` in planPA), which is what
|
-- holds height all the way across and only descends afterwards.
|
||||||
-- the user's drawing is about: "this is why the middle
|
local farSide = -ns
|
||||||
-- barriers/dividers are important because each side will have access
|
local overFar = carriagewayPoint(mergeIdx, farSide)
|
||||||
-- to on and off ramps". Both carriageways sit at the same deck height
|
local cross = {}
|
||||||
-- and are directly adjacent, so a second slip diving under the
|
|
||||||
-- expressway was solving a problem that does not exist.
|
|
||||||
local tgtNear = carriagewayPoint(mergeIdx, ns)
|
|
||||||
local near = {}
|
|
||||||
for s = 0, 200 do
|
for s = 0, 200 do
|
||||||
local f = s / 200
|
local f = s / 200
|
||||||
table.insert(
|
table.insert(
|
||||||
near,
|
cross,
|
||||||
Vector3.new(
|
Vector3.new(fork.X + (overFar.X - fork.X) * f, OVER_Y, fork.Z + (overFar.Z - fork.Z) * f)
|
||||||
fork.X + (tgtNear.X - fork.X) * f,
|
|
||||||
yf + (yd - yf) * ease(f),
|
|
||||||
fork.Z + (tgtNear.Z - fork.Z) * f
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 3. DESCENT -- from above the far carriageway, run ALONGSIDE the
|
||||||
|
-- mainline and come down onto it. The along-road length is what makes
|
||||||
|
-- the grade legal: dropping DECK_CLEAR studs over ~6 samples of road
|
||||||
|
-- (~370 studs) is ~5.4%, inside the 9% limit.
|
||||||
|
--
|
||||||
|
-- Which way it runs is set by the traffic direction, not by taste.
|
||||||
|
-- The far carriageway travels toward INCREASING sample index, so an
|
||||||
|
-- off-ramp must diverge BEFORE the crossing (lower index) and an
|
||||||
|
-- on-ramp must merge AFTER it (higher index) -- either one pointing
|
||||||
|
-- the same way as the traffic it joins or leaves.
|
||||||
|
local LAND_SPAN = 6
|
||||||
|
-- MERGE AT THE EDGE, NOT ON TOP OF THE DECK (2026-08-03).
|
||||||
|
-- The descent used to target carriagewayPoint(), which is the CENTRE
|
||||||
|
-- of the far carriageway (LANE_OFF = ROAD_WIDTH/4 from the
|
||||||
|
-- centreline). Landing there drops the ramp mouth straight onto the
|
||||||
|
-- running lanes -- the "top of the ramps are sitting right on the
|
||||||
|
-- highway" note. A real ramp runs up the OUTER shoulder and tapers in
|
||||||
|
-- over an acceleration lane.
|
||||||
|
--
|
||||||
|
-- So the ramp now tracks the carriageway's OUTER EDGE for most of the
|
||||||
|
-- descent and only drifts to the lane centre over the last stretch:
|
||||||
|
-- edgeOff = ROAD_WIDTH/2 - RAMP_W/2 (outer kerb, ramp fully on deck)
|
||||||
|
-- laneOff = LANE_OFF (lane centre, fully merged)
|
||||||
|
-- taper() is 0 along the shoulder run and eases to 1 at the mouth,
|
||||||
|
-- so the merge is a gradual convergence rather than a drop-in.
|
||||||
|
local edgeOff = ROAD_WIDTH / 2 - RAMP_W / 2
|
||||||
|
local function landPoint(gi, f)
|
||||||
|
local i0f = math.floor(gi)
|
||||||
|
local frac = gi - i0f
|
||||||
|
local ia = i0f + 1
|
||||||
|
local ib = ((i0f + 1) % nS) + 1
|
||||||
|
local sa, sb = samples[ia].pos, samples[ib].pos
|
||||||
|
local p = sa + (sb - sa) * frac
|
||||||
|
-- local heading -> normal, so the offset is perpendicular to the
|
||||||
|
-- road at this point rather than a global axis
|
||||||
|
local nxt = samples[(ia % nS) + 1].pos
|
||||||
|
local h = math.atan2(nxt.Z - sa.Z, nxt.X - sa.X)
|
||||||
|
local nx, nz = -math.sin(h), math.cos(h)
|
||||||
|
-- for an off-ramp f runs mouth->highway, so invert the taper
|
||||||
|
local t = (jn.kind == "off") and (1 - f) or f
|
||||||
|
-- hold the shoulder for the first 65%, then converge
|
||||||
|
local conv = math.clamp((t - 0.65) / 0.35, 0, 1)
|
||||||
|
conv = conv * conv * (3 - 2 * conv)
|
||||||
|
local off = edgeOff + (LANE_OFF - edgeOff) * conv
|
||||||
|
return Vector3.new(p.X + nx * off * farSide, p.Y, p.Z + nz * off * farSide)
|
||||||
|
end
|
||||||
|
local descent = {}
|
||||||
|
for s = 0, 200 do
|
||||||
|
local f = s / 200
|
||||||
|
-- follow the carriageway's own curve rather than a straight
|
||||||
|
-- chord, so the ramp lands tangent instead of at an angle
|
||||||
|
local gi = ((mergeIdx - 1 + (jn.kind == "off" and -LAND_SPAN or LAND_SPAN) * f) % nS)
|
||||||
|
local p = landPoint(gi, f)
|
||||||
|
table.insert(descent, Vector3.new(p.X, OVER_Y + (yd - OVER_Y) * ease(f), p.Z))
|
||||||
|
end
|
||||||
|
|
||||||
-- An on-ramp is this same geometry driven the other way -- the deck is
|
-- An on-ramp is this same geometry driven the other way -- the deck is
|
||||||
-- undirected, so only which ends are MOUTHS matters, and every run
|
-- undirected, so only which ends are MOUTHS matters, and every run
|
||||||
-- here is open at both ends (ring gate / fork, fork / carriageway).
|
-- here is open at both ends. Chevrons go on the trunk only: that is
|
||||||
-- Chevrons go on the trunk only: that is the curved helix they exist
|
-- the curved helix they exist to make legible.
|
||||||
-- to make legible, and on the straight slip they would just be a red
|
|
||||||
-- wall alongside the merge.
|
|
||||||
emitRun(jn.name .. "Trunk", trunk, true, true, true)
|
emitRun(jn.name .. "Trunk", trunk, true, true, true)
|
||||||
emitRun(jn.name .. "Near", near, true, true, false)
|
emitRun(jn.name .. "Cross", cross, true, true, false)
|
||||||
|
emitRun(jn.name .. "Near", descent, true, true, false)
|
||||||
|
|
||||||
-- Flare the ramp mouth across the outer gate so the opening in the
|
-- Flare the ramp mouth across the outer gate so the opening in the
|
||||||
-- ring parapet is fully backed by deck -- the trunk meets the ring at
|
-- ring parapet is fully backed by deck -- the trunk meets the ring at
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Eyes-on shots at the two REPORTED defects (not generic beauty angles).
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
A entry ramp start (-4122, 47, -7860) -- the 390-stud access gap
|
||||||
|
B exit ramp start (-4258, 47, -7836)
|
||||||
|
C mainline junction (-3700, 102, -7184) -- the dY -6.0 step
|
||||||
|
D ramp vs nearest building(-4419, 223, -6751)
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, "/tmp")
|
||||||
|
import rbx # noqa: E402
|
||||||
|
|
||||||
|
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
|
||||||
|
|
||||||
|
SHOTS = [
|
||||||
|
# name, camera pos, look-at
|
||||||
|
("D1_entry_ramp_start", (-4122 + 210, 47 + 120, -7860 + 210), (-4122, 47, -7860)),
|
||||||
|
("D2_entry_start_low", (-4122 + 70, 47 + 25, -7860 + 70), (-4122, 55, -7860)),
|
||||||
|
("D3_exit_ramp_start", (-4258 - 200, 47 + 110, -7836 + 200), (-4258, 47, -7836)),
|
||||||
|
("D4_junction_step", (-3700 + 190, 102 + 90, -7184 + 190), (-3700, 102, -7184)),
|
||||||
|
("D5_ramp_vs_building", (-4419 + 520, 223 + 180, -6751 + 520), (-4300, 120, -7100)),
|
||||||
|
("D6_corridor_along", (-4122, 47 + 260, -7860 + 480), (-4300, 110, -7200)),
|
||||||
|
]
|
||||||
|
|
||||||
|
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 main():
|
||||||
|
made = []
|
||||||
|
for name, (px, py, pz), (tx, ty, tz) in SHOTS:
|
||||||
|
fov = 70 if "low" in name or "along" in name else 55
|
||||||
|
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]}")
|
||||||
|
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]}")
|
||||||
|
continue
|
||||||
|
dest = f"{OUT}/{name}.jpg"
|
||||||
|
shutil.copy(imgs[0], dest)
|
||||||
|
made.append(dest)
|
||||||
|
print(f"{name}: OK -> {dest}")
|
||||||
|
print(f"\nMADE {len(made)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#!/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()
|
||||||
@@ -16,7 +16,14 @@ for _, d in ipairs(map:GetDescendants()) do
|
|||||||
if nm:find("PierColumn") or nm:find("PAMast") or nm:find("GirderRib") then
|
if nm:find("PierColumn") or nm:find("PAMast") or nm:find("GirderRib") then
|
||||||
table.insert(structural, d)
|
table.insert(structural, d)
|
||||||
end
|
end
|
||||||
for _, base in ipairs({ "PAEntryRampTrunk", "PAEntryRampNear", "PAExitRampTrunk", "PAExitRampNear" }) do
|
for _, base in ipairs({
|
||||||
|
"PAEntryRampTrunk",
|
||||||
|
"PAEntryRampCross",
|
||||||
|
"PAEntryRampNear",
|
||||||
|
"PAExitRampTrunk",
|
||||||
|
"PAExitRampCross",
|
||||||
|
"PAExitRampNear",
|
||||||
|
}) do
|
||||||
if nm:sub(1, #base + 1) == base .. "_" then
|
if nm:sub(1, #base + 1) == base .. "_" then
|
||||||
runs[base] = runs[base] or {}
|
runs[base] = runs[base] or {}
|
||||||
table.insert(runs[base], d)
|
table.insert(runs[base], d)
|
||||||
@@ -56,7 +63,14 @@ table.insert(out, "structuralParts=" .. #structural)
|
|||||||
local function sortKey(p)
|
local function sortKey(p)
|
||||||
return tonumber(p.Name:match("_(%d+)$")) or 0
|
return tonumber(p.Name:match("_(%d+)$")) or 0
|
||||||
end
|
end
|
||||||
for _, base in ipairs({ "PAEntryRampTrunk", "PAEntryRampNear", "PAExitRampTrunk", "PAExitRampNear" }) do
|
for _, base in ipairs({
|
||||||
|
"PAEntryRampTrunk",
|
||||||
|
"PAEntryRampCross",
|
||||||
|
"PAEntryRampNear",
|
||||||
|
"PAExitRampTrunk",
|
||||||
|
"PAExitRampCross",
|
||||||
|
"PAExitRampNear",
|
||||||
|
}) do
|
||||||
local parts = runs[base]
|
local parts = runs[base]
|
||||||
if not parts then
|
if not parts then
|
||||||
table.insert(out, base .. ": MISSING")
|
table.insert(out, base .. ": MISSING")
|
||||||
@@ -73,8 +87,15 @@ for _, base in ipairs({ "PAEntryRampTrunk", "PAEntryRampNear", "PAExitRampTrunk"
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Trunk -> Near handoff
|
-- Handoffs along the flyover chain: Trunk -> Cross -> Near. The ramps now
|
||||||
for _, pair in ipairs({ { "PAEntryRampTrunk", "PAEntryRampNear" }, { "PAExitRampTrunk", "PAExitRampNear" } }) do
|
-- cross OVER the mainline, so `Cross` sits between the spiral and the
|
||||||
|
-- descent; checking Trunk->Near directly would report a false gap.
|
||||||
|
for _, pair in ipairs({
|
||||||
|
{ "PAEntryRampTrunk", "PAEntryRampCross" },
|
||||||
|
{ "PAEntryRampCross", "PAEntryRampNear" },
|
||||||
|
{ "PAExitRampTrunk", "PAExitRampCross" },
|
||||||
|
{ "PAExitRampCross", "PAExitRampNear" },
|
||||||
|
}) do
|
||||||
local t, nr = runs[pair[1]], runs[pair[2]]
|
local t, nr = runs[pair[1]], runs[pair[2]]
|
||||||
if t and nr then
|
if t and nr then
|
||||||
table.sort(t, function(a, b)
|
table.sort(t, function(a, b)
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
-- Diagnose the two reported defects:
|
||||||
|
-- (1) ramps not accessible in either direction (connectivity)
|
||||||
|
-- (2) ramps pass through buildings (clipping)
|
||||||
|
-- Uses native GetPartBoundsInBox so this stays fast on ~22k parts.
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local BUILDING = {
|
||||||
|
Building = true,
|
||||||
|
TokyoTower = true,
|
||||||
|
TowerDeck = true,
|
||||||
|
TowerAntenna = true,
|
||||||
|
ShiodomeTower = true,
|
||||||
|
TowerCrown = true,
|
||||||
|
DietTower = true,
|
||||||
|
RoppongiHills = true,
|
||||||
|
TokyoStation = true,
|
||||||
|
StationDome = true,
|
||||||
|
Skytree = true,
|
||||||
|
GinzaBlock = true,
|
||||||
|
NationalDiet = true,
|
||||||
|
BridgeTower = true,
|
||||||
|
TowerCrossbeam = true,
|
||||||
|
Structure = true,
|
||||||
|
PalaceWall = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ramp, drivable = {}, {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
local n = d.Name
|
||||||
|
if n:find("PAEntryRamp") or n:find("PAExitRamp") then
|
||||||
|
table.insert(ramp, d)
|
||||||
|
elseif n:match("^Road_%d+$") or n == "PAFloor" then
|
||||||
|
table.insert(drivable, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #ramp == 0 then
|
||||||
|
return "no ramp parts"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------- (2) BUILDING CLIP CHECK ----------
|
||||||
|
local op = OverlapParams.new()
|
||||||
|
op.MaxParts = 40
|
||||||
|
local clipHits, clipNames, worst = 0, {}, {}
|
||||||
|
for _, r in ipairs(ramp) do
|
||||||
|
local hits = ws:GetPartBoundsInBox(r.CFrame, r.Size, op)
|
||||||
|
for _, h in ipairs(hits) do
|
||||||
|
if h ~= r and BUILDING[h.Name] then
|
||||||
|
clipHits += 1
|
||||||
|
clipNames[h.Name] = (clipNames[h.Name] or 0) + 1
|
||||||
|
if #worst < 5 then
|
||||||
|
table.insert(worst, ("%s@%.0f,%.0f,%.0f"):format(r.Name, r.Position.X, r.Position.Y, r.Position.Z))
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- CONTROL: same probe against mainline Road_ parts (known-good, should be ~0)
|
||||||
|
local ctrlHits, ctrlN = 0, 0
|
||||||
|
for i = 1, #drivable, 9 do
|
||||||
|
local d = drivable[i]
|
||||||
|
ctrlN += 1
|
||||||
|
local hits = ws:GetPartBoundsInBox(d.CFrame, d.Size, op)
|
||||||
|
for _, h in ipairs(hits) do
|
||||||
|
if h ~= d and BUILDING[h.Name] then
|
||||||
|
ctrlHits += 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local cn = {}
|
||||||
|
for k, v in pairs(clipNames) do
|
||||||
|
table.insert(cn, k .. "=" .. v)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------- (1) CONNECTIVITY ----------
|
||||||
|
-- Ramp basename groups, then endpoints of each group by trailing index.
|
||||||
|
local groups = {}
|
||||||
|
for _, r in ipairs(ramp) do
|
||||||
|
local base, idx = r.Name:match("^(.-)_?(%d+)$")
|
||||||
|
base = base or r.Name
|
||||||
|
idx = tonumber(idx) or 0
|
||||||
|
local g = groups[base]
|
||||||
|
if not g then
|
||||||
|
g = { lo = math.huge, hi = -math.huge, loP = nil, hiP = nil, n = 0 }
|
||||||
|
groups[base] = g
|
||||||
|
end
|
||||||
|
g.n += 1
|
||||||
|
if idx < g.lo then
|
||||||
|
g.lo, g.loP = idx, r
|
||||||
|
end
|
||||||
|
if idx > g.hi then
|
||||||
|
g.hi, g.hiP = idx, r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For an endpoint, find nearest drivable surface and report gap + height delta.
|
||||||
|
local function nearestDrivable(p)
|
||||||
|
local bd, bp = math.huge, nil
|
||||||
|
for _, d in ipairs(drivable) do
|
||||||
|
local dd = (d.Position - p).Magnitude
|
||||||
|
if dd < bd then
|
||||||
|
bd, bp = dd, d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return bd, bp
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for base, g in pairs(groups) do
|
||||||
|
if g.loP and g.hiP then
|
||||||
|
local d1, p1 = nearestDrivable(g.loP.Position)
|
||||||
|
local d2, p2 = nearestDrivable(g.hiP.Position)
|
||||||
|
table.insert(
|
||||||
|
lines,
|
||||||
|
("%s n=%d | START gap=%.0f dY=%.1f to %s | END gap=%.0f dY=%.1f to %s"):format(
|
||||||
|
base,
|
||||||
|
g.n,
|
||||||
|
d1,
|
||||||
|
p1 and (g.loP.Position.Y - p1.Position.Y) or -999,
|
||||||
|
p1 and p1.Name or "nil",
|
||||||
|
d2,
|
||||||
|
p2 and (g.hiP.Position.Y - p2.Position.Y) or -999,
|
||||||
|
p2 and p2.Name or "nil"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ("CLIP ramp=%d/%d parts hit buildings [%s] samples=[%s] || CONTROL road %d/%d hit\n%s"):format(
|
||||||
|
clipHits,
|
||||||
|
#ramp,
|
||||||
|
table.concat(cn, ", "),
|
||||||
|
table.concat(worst, " ; "),
|
||||||
|
ctrlHits,
|
||||||
|
ctrlN,
|
||||||
|
table.concat(lines, "\n")
|
||||||
|
)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
-- Why did the building-clip probe return 0 with a 0 control? Diagnose the metric.
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
|
||||||
|
local BUILDING = {
|
||||||
|
Building = true,
|
||||||
|
TokyoTower = true,
|
||||||
|
TowerDeck = true,
|
||||||
|
TowerAntenna = true,
|
||||||
|
ShiodomeTower = true,
|
||||||
|
TowerCrown = true,
|
||||||
|
DietTower = true,
|
||||||
|
RoppongiHills = true,
|
||||||
|
TokyoStation = true,
|
||||||
|
StationDome = true,
|
||||||
|
Skytree = true,
|
||||||
|
GinzaBlock = true,
|
||||||
|
NationalDiet = true,
|
||||||
|
BridgeTower = true,
|
||||||
|
TowerCrossbeam = true,
|
||||||
|
Structure = true,
|
||||||
|
PalaceWall = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ramp, bld = {}, {}
|
||||||
|
local anyQueryOff, anyCollideOff = 0, 0
|
||||||
|
for _, d in ipairs(ws:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
if d.Name:find("PAEntryRamp") or d.Name:find("PAExitRamp") then
|
||||||
|
table.insert(ramp, d)
|
||||||
|
elseif BUILDING[d.Name] then
|
||||||
|
table.insert(bld, d)
|
||||||
|
if not d.CanQuery then
|
||||||
|
anyQueryOff += 1
|
||||||
|
end
|
||||||
|
if not d.CanCollide then
|
||||||
|
anyCollideOff += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #bld == 0 then
|
||||||
|
return "NO building parts found in Workspace at all (name set wrong?)"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- nearest building to the ramp chain (subsample ramps for speed)
|
||||||
|
local bd, br, bb = math.huge, nil, nil
|
||||||
|
for i = 1, #ramp, 7 do
|
||||||
|
local r = ramp[i]
|
||||||
|
for _, b in ipairs(bld) do
|
||||||
|
local dd = (r.Position - b.Position).Magnitude
|
||||||
|
if dd < bd then
|
||||||
|
bd, br, bb = dd, r, b
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- how many buildings are anywhere near the PA at all?
|
||||||
|
local cen = Vector3.new(-4128, 81, -7438)
|
||||||
|
local near2000 = 0
|
||||||
|
for _, b in ipairs(bld) do
|
||||||
|
if (b.Position - cen).Magnitude < 2000 then
|
||||||
|
near2000 += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- does the raw overlap API return ANYTHING for a ramp part? (metric sanity)
|
||||||
|
local op = OverlapParams.new()
|
||||||
|
op.MaxParts = 50
|
||||||
|
local sample = ramp[math.floor(#ramp / 2)]
|
||||||
|
local hits = ws:GetPartBoundsInBox(sample.CFrame, sample.Size * 1.2, op)
|
||||||
|
local hn = {}
|
||||||
|
for i = 1, math.min(#hits, 8) do
|
||||||
|
table.insert(hn, hits[i].Name)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ("buildings=%d (CanQuery off=%d, CanCollide off=%d) | nearestBldToRamp=%.0f (%s -> %s @%.0f,%.0f,%.0f) | bldWithin2000ofPA=%d | overlapSanity hits=%d [%s]"):format(
|
||||||
|
#bld,
|
||||||
|
anyQueryOff,
|
||||||
|
anyCollideOff,
|
||||||
|
bd,
|
||||||
|
br and br.Name or "nil",
|
||||||
|
bb and bb.Name or "nil",
|
||||||
|
bb and bb.Position.X or 0,
|
||||||
|
bb and bb.Position.Y or 0,
|
||||||
|
bb and bb.Position.Z or 0,
|
||||||
|
near2000,
|
||||||
|
#hits,
|
||||||
|
table.concat(hn, ",")
|
||||||
|
)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
-- Building-clip probe, CORRECTLY SCOPED.
|
||||||
|
--
|
||||||
|
-- The previous version tested only PAEntryRamp*/PAExitRamp* against buildings
|
||||||
|
-- and reported 0/1200 -- but its control ALSO returned 0, which makes the
|
||||||
|
-- result vacuous. Worse, the part visibly speared by a tower is a Road_*
|
||||||
|
-- segment, i.e. it was sitting in the control group, not the test group.
|
||||||
|
--
|
||||||
|
-- This version:
|
||||||
|
-- * tests EVERY drivable part (Road_*, ramps, PA surfaces) -- 21k parts
|
||||||
|
-- * proves the metric can detect a hit BEFORE trusting a zero (positive
|
||||||
|
-- control: a synthetic part deliberately placed inside a building)
|
||||||
|
-- * reports the worst offenders with coordinates so they can be shot
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local BUILDING = {
|
||||||
|
Building = true,
|
||||||
|
TokyoTower = true,
|
||||||
|
TowerDeck = true,
|
||||||
|
TowerAntenna = true,
|
||||||
|
ShiodomeTower = true,
|
||||||
|
TowerCrown = true,
|
||||||
|
DietTower = true,
|
||||||
|
RoppongiHills = true,
|
||||||
|
TokyoStation = true,
|
||||||
|
StationDome = true,
|
||||||
|
Skytree = true,
|
||||||
|
GinzaBlock = true,
|
||||||
|
NationalDiet = true,
|
||||||
|
BridgeTower = true,
|
||||||
|
TowerCrossbeam = true,
|
||||||
|
PalaceWall = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isDrivable(n)
|
||||||
|
return n:match("^Road_")
|
||||||
|
or n:find("Ramp")
|
||||||
|
or n == "PAFloor"
|
||||||
|
or n:match("^Apron")
|
||||||
|
or n:match("^Slip")
|
||||||
|
or n:match("^Merge")
|
||||||
|
end
|
||||||
|
|
||||||
|
local drive, bld = {}, {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
if BUILDING[d.Name] then
|
||||||
|
table.insert(bld, d)
|
||||||
|
elseif isDrivable(d.Name) then
|
||||||
|
table.insert(drive, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #bld == 0 or #drive == 0 then
|
||||||
|
return ("SCOPE FAIL drive=%d bld=%d"):format(#drive, #bld)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build an OverlapParams limited to building parts only.
|
||||||
|
local op = OverlapParams.new()
|
||||||
|
op.FilterType = Enum.RaycastFilterType.Include
|
||||||
|
op.FilterDescendantsInstances = bld
|
||||||
|
op.MaxParts = 12
|
||||||
|
|
||||||
|
-- POSITIVE CONTROL: put a probe cube dead-centre inside a building and
|
||||||
|
-- confirm the metric reports it. If this returns 0, the metric is broken and
|
||||||
|
-- every other number below is meaningless.
|
||||||
|
local target = bld[math.floor(#bld / 2)]
|
||||||
|
local ctrlHits = ws:GetPartBoundsInBox(CFrame.new(target.Position), Vector3.new(4, 4, 4), op)
|
||||||
|
local controlOK = #ctrlHits > 0
|
||||||
|
|
||||||
|
local hits, samples = 0, {}
|
||||||
|
for _, p in ipairs(drive) do
|
||||||
|
-- shrink slightly so parts merely TOUCHING a facade aren't flagged;
|
||||||
|
-- we want genuine interpenetration
|
||||||
|
local probe = p.Size - Vector3.new(1.5, 1.5, 1.5)
|
||||||
|
if probe.X > 0.5 and probe.Y > 0.5 and probe.Z > 0.5 then
|
||||||
|
local h = ws:GetPartBoundsInBox(p.CFrame, probe, op)
|
||||||
|
if #h > 0 then
|
||||||
|
hits += 1
|
||||||
|
if #samples < 6 then
|
||||||
|
table.insert(
|
||||||
|
samples,
|
||||||
|
("%s@%.0f,%.0f,%.0f in %s"):format(
|
||||||
|
p.Name,
|
||||||
|
p.Position.X,
|
||||||
|
p.Position.Y,
|
||||||
|
p.Position.Z,
|
||||||
|
h[1].Name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ("CONTROL=%s (%d hits on a part inside a building) || drivable=%d bld=%d CLIPPING=%d [%s]"):format(
|
||||||
|
controlOK and "PASS" or "FAIL-METRIC-BROKEN",
|
||||||
|
#ctrlHits,
|
||||||
|
#drive,
|
||||||
|
#bld,
|
||||||
|
hits,
|
||||||
|
table.concat(samples, " ; ")
|
||||||
|
)
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
-- CLIP PROBE v3 -- INVERTED SCOPE.
|
||||||
|
--
|
||||||
|
-- v2 tested drivable parts against a HARDCODED WHITELIST of 16 building
|
||||||
|
-- names (Building, TokyoTower, GinzaBlock, ...). It reported CLIPPING=0 with
|
||||||
|
-- a passing control, and I reported that as fact. The user can see ramps
|
||||||
|
-- going through buildings, so the zero was false.
|
||||||
|
--
|
||||||
|
-- The bug is the whitelist: any obstacle whose name isn't one of those 16 is
|
||||||
|
-- invisible to the test. The PA structures (shops, kiosks, canopies) are the
|
||||||
|
-- prime suspect -- they sit exactly where the ramp ends land.
|
||||||
|
--
|
||||||
|
-- v3 inverts the scope: an obstacle is ANY part that is not drivable and not
|
||||||
|
-- infrastructure (pier/rail/sign/ground). No name list to go stale.
|
||||||
|
-- It also reports offenders GROUPED BY OBSTACLE NAME, so the answer names
|
||||||
|
-- the thing being speared instead of just counting.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isDrivable(n)
|
||||||
|
return n:match("^Road_")
|
||||||
|
or n:find("Ramp")
|
||||||
|
or n == "PAFloor"
|
||||||
|
or n:match("^Apron")
|
||||||
|
or n:match("^Slip")
|
||||||
|
or n:match("^Merge")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- things that are ALLOWED to intersect a road (structure, decor, markings)
|
||||||
|
local function isInfra(n)
|
||||||
|
return n:find("Pier")
|
||||||
|
or n:find("Pillar")
|
||||||
|
or n:find("Column")
|
||||||
|
or n:find("Support")
|
||||||
|
or n:find("Beam")
|
||||||
|
or n:find("Rail")
|
||||||
|
or n:find("Guard")
|
||||||
|
or n:find("Barrier")
|
||||||
|
or n:find("Sign")
|
||||||
|
or n:find("Light")
|
||||||
|
or n:find("Lamp")
|
||||||
|
or n:find("Line")
|
||||||
|
or n:find("Mark")
|
||||||
|
or n:find("Stripe")
|
||||||
|
or n:find("Deck")
|
||||||
|
or n:find("Water")
|
||||||
|
or n == "Ground"
|
||||||
|
or n:find("Grounds")
|
||||||
|
or n:find("Median")
|
||||||
|
or n:find("Curb")
|
||||||
|
or n:find("Wall")
|
||||||
|
end
|
||||||
|
|
||||||
|
local drive, obst = {}, {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
local n = d.Name
|
||||||
|
if isDrivable(n) then
|
||||||
|
table.insert(drive, d)
|
||||||
|
elseif not isInfra(n) then
|
||||||
|
table.insert(obst, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #obst == 0 or #drive == 0 then
|
||||||
|
return ("SCOPE FAIL drive=%d obst=%d"):format(#drive, #obst)
|
||||||
|
end
|
||||||
|
|
||||||
|
local op = OverlapParams.new()
|
||||||
|
op.FilterType = Enum.RaycastFilterType.Include
|
||||||
|
op.FilterDescendantsInstances = obst
|
||||||
|
op.MaxParts = 12
|
||||||
|
|
||||||
|
-- POSITIVE CONTROL: probe dead-centre of a known obstacle must report a hit.
|
||||||
|
local target = obst[math.floor(#obst / 2)]
|
||||||
|
local ctrl = ws:GetPartBoundsInBox(CFrame.new(target.Position), Vector3.new(4, 4, 4), op)
|
||||||
|
local controlOK = #ctrl > 0
|
||||||
|
|
||||||
|
-- SECOND CONTROL: a box far out in empty sky must report ZERO. Guards the
|
||||||
|
-- opposite failure -- a filter so broad everything "hits".
|
||||||
|
local negCtrl = ws:GetPartBoundsInBox(CFrame.new(0, 40000, 0), Vector3.new(8, 8, 8), op)
|
||||||
|
local negOK = #negCtrl == 0
|
||||||
|
|
||||||
|
local hits = 0
|
||||||
|
local byObst, byRamp = {}, {}
|
||||||
|
local samples = {}
|
||||||
|
for _, p in ipairs(drive) do
|
||||||
|
local probe = p.Size - Vector3.new(1.5, 1.5, 1.5)
|
||||||
|
if probe.X > 0.5 and probe.Y > 0.5 and probe.Z > 0.5 then
|
||||||
|
local h = ws:GetPartBoundsInBox(p.CFrame, probe, op)
|
||||||
|
if #h > 0 then
|
||||||
|
hits += 1
|
||||||
|
local on = h[1].Name
|
||||||
|
byObst[on] = (byObst[on] or 0) + 1
|
||||||
|
local rn = p.Name:gsub("_%d+$", "")
|
||||||
|
byRamp[rn] = (byRamp[rn] or 0) + 1
|
||||||
|
if #samples < 10 then
|
||||||
|
table.insert(
|
||||||
|
samples,
|
||||||
|
("%s@%.0f,%.0f,%.0f IN %s"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z, on)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
("CONTROL+=%s(%d) CONTROL-=%s(%d) -> probe %s"):format(
|
||||||
|
controlOK and "PASS" or "FAIL",
|
||||||
|
#ctrl,
|
||||||
|
negOK and "PASS" or "FAIL",
|
||||||
|
#negCtrl,
|
||||||
|
(controlOK and negOK) and "TRUSTWORTHY" or "BROKEN-IGNORE-RESULTS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
table.insert(out, ("drivable=%d obstacles=%d CLIPPING=%d"):format(#drive, #obst, hits))
|
||||||
|
|
||||||
|
local function dump(t, label)
|
||||||
|
local l = {}
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
table.insert(l, ("%s=%d"):format(k, v))
|
||||||
|
end
|
||||||
|
table.sort(l, function(a, b)
|
||||||
|
return a > b
|
||||||
|
end)
|
||||||
|
local keep = {}
|
||||||
|
for i = 1, math.min(#l, 12) do
|
||||||
|
table.insert(keep, l[i])
|
||||||
|
end
|
||||||
|
table.insert(out, label .. ": " .. table.concat(keep, " "))
|
||||||
|
end
|
||||||
|
if hits > 0 then
|
||||||
|
dump(byObst, "speared obstacles")
|
||||||
|
dump(byRamp, "offending road groups")
|
||||||
|
for _, s in ipairs(samples) do
|
||||||
|
table.insert(out, " " .. s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out, "\n")
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
-- CLIP PROBE v4 -- FULL-WORKSPACE SCOPE. This is the one that finds the
|
||||||
|
-- defect the user can see.
|
||||||
|
--
|
||||||
|
-- WHY v2 AND v3 BOTH RETURNED A FALSE ZERO:
|
||||||
|
-- both scoped obstacles to the `Map` folder. The PA buildings are NOT in
|
||||||
|
-- Map -- they are Creator-Store models parented directly to Workspace as
|
||||||
|
-- siblings: McDonald's (1666 parts), Gas Station 1 (1463), 7 eleven (627),
|
||||||
|
-- Restaurant1, Updated Ramen Shop, Fast Food Restaurant, すき家, Parking lot.
|
||||||
|
-- The probes literally could not see the things being speared. A passing
|
||||||
|
-- positive control did NOT save me: the control proved the metric works,
|
||||||
|
-- not that the SCOPE covers the real obstacles.
|
||||||
|
--
|
||||||
|
-- v4 collects obstacles from the ENTIRE workspace, excluding only the Map
|
||||||
|
-- folder's own drivable/infra parts. Reports per-model hit counts so we know
|
||||||
|
-- exactly which shop each ramp is driving through.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isDrivable(n)
|
||||||
|
return n:match("^Road_")
|
||||||
|
or n:find("Ramp")
|
||||||
|
or n == "PAFloor"
|
||||||
|
or n:match("^Apron")
|
||||||
|
or n:match("^Slip")
|
||||||
|
or n:match("^Merge")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- collect drivable parts (ramps especially) from Map
|
||||||
|
local drive = {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") and isDrivable(d.Name) then
|
||||||
|
table.insert(drive, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- collect obstacles: every BasePart in workspace that is NOT inside Map.
|
||||||
|
-- These are the imported building models.
|
||||||
|
local obst = {}
|
||||||
|
local modelOf = {}
|
||||||
|
for _, top in ipairs(ws:GetChildren()) do
|
||||||
|
if top ~= map and not top:IsA("Terrain") and not top:IsA("Camera") then
|
||||||
|
if top:IsA("BasePart") then
|
||||||
|
-- loose parts like stray PAPerimeter/PARampKerb: skip, they're infra
|
||||||
|
if not isDrivable(top.Name) and not top.Name:find("Perimeter") and not top.Name:find("Kerb") then
|
||||||
|
table.insert(obst, top)
|
||||||
|
modelOf[top] = top.Name
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for _, d in ipairs(top:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
table.insert(obst, d)
|
||||||
|
modelOf[d] = top.Name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #obst == 0 then
|
||||||
|
return "SCOPE FAIL: no obstacles found outside Map"
|
||||||
|
end
|
||||||
|
|
||||||
|
local op = OverlapParams.new()
|
||||||
|
op.FilterType = Enum.RaycastFilterType.Include
|
||||||
|
op.FilterDescendantsInstances = obst
|
||||||
|
op.MaxParts = 20
|
||||||
|
|
||||||
|
-- CONTROL+ : a box at a known obstacle's centre must report a hit
|
||||||
|
local tgt = obst[math.floor(#obst / 2)]
|
||||||
|
local cPos = ws:GetPartBoundsInBox(CFrame.new(tgt.Position), Vector3.new(4, 4, 4), op)
|
||||||
|
-- CONTROL- : empty sky must report zero
|
||||||
|
local cNeg = ws:GetPartBoundsInBox(CFrame.new(0, 40000, 0), Vector3.new(8, 8, 8), op)
|
||||||
|
local trustworthy = #cPos > 0 and #cNeg == 0
|
||||||
|
|
||||||
|
local hits = 0
|
||||||
|
local byModel, byRamp = {}, {}
|
||||||
|
local samples = {}
|
||||||
|
for _, p in ipairs(drive) do
|
||||||
|
local probe = p.Size - Vector3.new(1.0, 1.0, 1.0)
|
||||||
|
if probe.X > 0.5 and probe.Y > 0.5 and probe.Z > 0.5 then
|
||||||
|
local h = ws:GetPartBoundsInBox(p.CFrame, probe, op)
|
||||||
|
if #h > 0 then
|
||||||
|
hits += 1
|
||||||
|
local mn = modelOf[h[1]] or h[1].Name
|
||||||
|
byModel[mn] = (byModel[mn] or 0) + 1
|
||||||
|
local rn = p.Name:gsub("_%d+$", "")
|
||||||
|
byRamp[rn] = (byRamp[rn] or 0) + 1
|
||||||
|
if #samples < 10 then
|
||||||
|
table.insert(
|
||||||
|
samples,
|
||||||
|
("%s@%.0f,%.0f,%.0f IN %s"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z, mn)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
("CONTROL+=%d CONTROL-=%d -> %s"):format(#cPos, #cNeg, trustworthy and "TRUSTWORTHY" or "BROKEN-IGNORE")
|
||||||
|
)
|
||||||
|
table.insert(out, ("drivable=%d obstacles(outside Map)=%d CLIPPING=%d"):format(#drive, #obst, hits))
|
||||||
|
|
||||||
|
local function dump(t, label)
|
||||||
|
local l = {}
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
table.insert(l, ("%s=%d"):format(k, v))
|
||||||
|
end
|
||||||
|
table.sort(l)
|
||||||
|
table.insert(out, label .. ": " .. table.concat(l, " | "))
|
||||||
|
end
|
||||||
|
if hits > 0 then
|
||||||
|
dump(byModel, "MODELS SPEARED")
|
||||||
|
dump(byRamp, "offending ramp groups")
|
||||||
|
for _, s in ipairs(samples) do
|
||||||
|
table.insert(out, " " .. s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out, "\n")
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- FLOATING SCENERY PROBE.
|
||||||
|
--
|
||||||
|
-- The wide captures show grey slabs/towers hanging in the sky with nothing
|
||||||
|
-- under them. No existing probe catches this: the clip probe only asks
|
||||||
|
-- "does road intersect building", which is 0 and correct. Floating geometry
|
||||||
|
-- is the opposite failure -- parts that touch NOTHING at all.
|
||||||
|
--
|
||||||
|
-- Metric: for each candidate scenery part, raycast straight DOWN from its
|
||||||
|
-- underside and report the distance to the first thing hit. A part sitting
|
||||||
|
-- on ground/deck returns ~0. A floating part returns a large number or nil
|
||||||
|
-- (nothing beneath it at all).
|
||||||
|
--
|
||||||
|
-- CONTROL: the same ray cast from a part known to be seated (a Road part)
|
||||||
|
-- must return a small distance. If the control comes back nil/huge the
|
||||||
|
-- raycast itself is broken and every "floating" result is meaningless --
|
||||||
|
-- the silent-zero trap that produced two phantom defects earlier.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local candidates, roads = {}, {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
local nm = d.Name
|
||||||
|
if nm:sub(1, 5) == "Road_" then
|
||||||
|
table.insert(roads, d)
|
||||||
|
elseif
|
||||||
|
nm:find("Building")
|
||||||
|
or nm:find("Tower")
|
||||||
|
or nm:find("Skyline")
|
||||||
|
or nm:find("Block")
|
||||||
|
or nm:find("Slab")
|
||||||
|
or nm:find("Distant")
|
||||||
|
then
|
||||||
|
table.insert(candidates, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local params = RaycastParams.new()
|
||||||
|
params.FilterType = Enum.RaycastFilterType.Exclude
|
||||||
|
|
||||||
|
-- distance from a part's underside straight down to the first hit
|
||||||
|
local function dropDist(p)
|
||||||
|
params.FilterDescendantsInstances = { p }
|
||||||
|
local origin = p.Position - Vector3.new(0, p.Size.Y / 2 + 1, 0)
|
||||||
|
local res = ws:Raycast(origin, Vector3.new(0, -6000, 0), params)
|
||||||
|
if not res then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return (origin - res.Position).Magnitude
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
|
||||||
|
-- CONTROL: a road part must find something close beneath it (deck/pier/ground).
|
||||||
|
local ctl, ctlN = nil, "none"
|
||||||
|
for _, rd in ipairs(roads) do
|
||||||
|
local d = dropDist(rd)
|
||||||
|
if d then
|
||||||
|
ctl, ctlN = d, rd.Name
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ctl == nil then
|
||||||
|
table.insert(out, "CONTROL=FAIL raycast found nothing under ANY road -- probe is broken, ignore results below")
|
||||||
|
else
|
||||||
|
table.insert(out, ("CONTROL=PASS %.1f studs under %s (raycast works)"):format(ctl, ctlN))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Classify candidates.
|
||||||
|
local FLOAT_MIN = 60 -- studs of empty air below = floating
|
||||||
|
local floating, seated, voidBelow = 0, 0, 0
|
||||||
|
local worst, worstNm, worstPos = -1, "none", nil
|
||||||
|
local samples = {}
|
||||||
|
for _, p in ipairs(candidates) do
|
||||||
|
local d = dropDist(p)
|
||||||
|
if d == nil then
|
||||||
|
voidBelow += 1
|
||||||
|
if #samples < 8 then
|
||||||
|
table.insert(
|
||||||
|
samples,
|
||||||
|
("%s@%.0f,%.0f,%.0f NOTHING-BELOW"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
elseif d > FLOAT_MIN then
|
||||||
|
floating += 1
|
||||||
|
if d > worst then
|
||||||
|
worst, worstNm, worstPos = d, p.Name, p.Position
|
||||||
|
end
|
||||||
|
if #samples < 8 then
|
||||||
|
table.insert(
|
||||||
|
samples,
|
||||||
|
("%s@%.0f,%.0f,%.0f drop=%.0f"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z, d)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
seated += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
("candidates=%d roads=%d | seated=%d floating(>%d)=%d nothingBelow=%d"):format(
|
||||||
|
#candidates,
|
||||||
|
#roads,
|
||||||
|
seated,
|
||||||
|
FLOAT_MIN,
|
||||||
|
floating,
|
||||||
|
voidBelow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if worstPos then
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
("WORST %s drop=%.0f at %.0f,%.0f,%.0f"):format(worstNm, worst, worstPos.X, worstPos.Y, worstPos.Z)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
for _, s in ipairs(samples) do
|
||||||
|
table.insert(out, " " .. s)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Name histogram so we can see WHICH generator made them.
|
||||||
|
local hist = {}
|
||||||
|
for _, p in ipairs(candidates) do
|
||||||
|
local base = p.Name:gsub("_%d+$", "")
|
||||||
|
hist[base] = (hist[base] or 0) + 1
|
||||||
|
end
|
||||||
|
local hl = {}
|
||||||
|
for k, v in pairs(hist) do
|
||||||
|
table.insert(hl, ("%s=%d"):format(k, v))
|
||||||
|
end
|
||||||
|
table.sort(hl)
|
||||||
|
table.insert(out, "byName: " .. table.concat(hl, " "))
|
||||||
|
|
||||||
|
return table.concat(out, "\n")
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
-- FLYOVER PROBE. Two things the connectivity check cannot tell you:
|
||||||
|
--
|
||||||
|
-- 1. Does the crossing actually pass OVER the mainline deck, with real
|
||||||
|
-- vertical clearance? A "flyover" that intersects the deck is just a
|
||||||
|
-- wall. Measured as: for every Cross part, find mainline Road parts
|
||||||
|
-- directly beneath it in plan, and report the worst (smallest) gap
|
||||||
|
-- between the ramp underside and the road topside.
|
||||||
|
--
|
||||||
|
-- 2. Does the descent land tangent and flush on the FAR carriageway?
|
||||||
|
-- Reported as the dY at the last descent part vs the road beneath it.
|
||||||
|
--
|
||||||
|
-- Every metric carries a CONTROL that must come back non-zero, because a
|
||||||
|
-- silent 0 from a mis-scoped query is indistinguishable from a genuine pass
|
||||||
|
-- -- that exact trap produced two phantom "defects" earlier in this build.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local cross, descent, roads = {}, {}, {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
local nm = d.Name
|
||||||
|
if nm:find("RampCross_") then
|
||||||
|
table.insert(cross, d)
|
||||||
|
elseif nm:find("RampNear_") then
|
||||||
|
table.insert(descent, d)
|
||||||
|
elseif nm:sub(1, 5) == "Road_" then
|
||||||
|
table.insert(roads, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- plan-view overlap (ignores Y entirely)
|
||||||
|
local function planOverlap(a, b)
|
||||||
|
return math.abs(a.Position.X - b.Position.X) < (a.Size.X + b.Size.X) / 2
|
||||||
|
and math.abs(a.Position.Z - b.Position.Z) < (a.Size.Z + b.Size.Z) / 2
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
|
||||||
|
-- CONTROL 1: a road part must plan-overlap itself.
|
||||||
|
local ctlSelf = (#roads > 0 and planOverlap(roads[1], roads[1])) and 1 or 0
|
||||||
|
table.insert(out, ("CONTROL planOverlap-self=%d (want 1)"):format(ctlSelf))
|
||||||
|
|
||||||
|
-- CLEARANCE over the deck.
|
||||||
|
local worst, worstAt, nOver = 1e9, "none", 0
|
||||||
|
for _, cp in ipairs(cross) do
|
||||||
|
for _, rd in ipairs(roads) do
|
||||||
|
if planOverlap(cp, rd) then
|
||||||
|
nOver += 1
|
||||||
|
-- ramp underside minus road topside
|
||||||
|
local gap = (cp.Position.Y - cp.Size.Y / 2) - (rd.Position.Y + rd.Size.Y / 2)
|
||||||
|
if gap < worst then
|
||||||
|
worst, worstAt = gap, cp.Name .. " over " .. rd.Name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if nOver == 0 then
|
||||||
|
table.insert(out, "CROSSING: 0 road parts under any Cross part -- ramp does NOT span the deck")
|
||||||
|
else
|
||||||
|
table.insert(out, ("CROSSING: %d overlaps | worst vertical gap=%.1f at %s"):format(nOver, worst, worstAt))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- LANDING: last descent part of each ramp vs the road under it.
|
||||||
|
local function tailOf(prefix)
|
||||||
|
local best, bestIdx = nil, -1
|
||||||
|
for _, p in ipairs(descent) do
|
||||||
|
if p.Name:sub(1, #prefix) == prefix then
|
||||||
|
local i = tonumber(p.Name:match("_(%d+)$")) or 0
|
||||||
|
if i > bestIdx then
|
||||||
|
best, bestIdx = p, i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
for _, pre in ipairs({ "PAEntryRampNear", "PAExitRampNear" }) do
|
||||||
|
local t = tailOf(pre)
|
||||||
|
if not t then
|
||||||
|
table.insert(out, pre .. ": no tail part")
|
||||||
|
else
|
||||||
|
local bestGap, bestNm = 1e9, "none"
|
||||||
|
for _, rd in ipairs(roads) do
|
||||||
|
if planOverlap(t, rd) then
|
||||||
|
local dy = (t.Position.Y - t.Size.Y / 2) - (rd.Position.Y + rd.Size.Y / 2)
|
||||||
|
if math.abs(dy) < math.abs(bestGap) then
|
||||||
|
bestGap, bestNm = dy, rd.Name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if bestNm == "none" then
|
||||||
|
table.insert(out, ("%s LANDING: tail is over NO road -- lands off the carriageway"):format(pre))
|
||||||
|
else
|
||||||
|
table.insert(out, ("%s LANDING: dY=%.1f onto %s"):format(pre, bestGap, bestNm))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(out, ("counts cross=%d descent=%d roads=%d"):format(#cross, #descent, #roads))
|
||||||
|
return table.concat(out, "\n")
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
-- GROUND-TRUTH PROBE for the floating-scenery finding.
|
||||||
|
--
|
||||||
|
-- probe_floating reported 664 parts with "nothing below". That number alone
|
||||||
|
-- is ambiguous: it reads identically whether (a) the buildings genuinely
|
||||||
|
-- hang in the air, or (b) the map has no ground/terrain plane at all, so
|
||||||
|
-- NOTHING can ever return a hit. Distinguish before calling it a defect.
|
||||||
|
--
|
||||||
|
-- Measure:
|
||||||
|
-- 1. Does the map contain terrain / a baseplate / a water or ground slab?
|
||||||
|
-- 2. Bottom-face Y of each scenery part vs the road deck Y and vs 0.
|
||||||
|
-- A building whose bottom is at/below ~0 is "planted" even with no
|
||||||
|
-- ground mesh; one whose bottom is hundreds of studs up is FLOATING.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
|
||||||
|
-- 1. terrain / ground candidates
|
||||||
|
local terr = ws:FindFirstChildOfClass("Terrain")
|
||||||
|
local terrHas = false
|
||||||
|
if terr then
|
||||||
|
local ok, reg = pcall(function()
|
||||||
|
return terr.MaxExtents
|
||||||
|
end)
|
||||||
|
-- MaxExtents is a Region3int16; its Min/Max are Vector3int16 which have
|
||||||
|
-- no .Magnitude. Compare components instead.
|
||||||
|
if ok and reg then
|
||||||
|
local mn, mx = reg.Min, reg.Max
|
||||||
|
terrHas = (mx.X - mn.X) > 1 or (mx.Y - mn.Y) > 1 or (mx.Z - mn.Z) > 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(out, ("Terrain present=%s hasVoxels=%s"):format(tostring(terr ~= nil), tostring(terrHas)))
|
||||||
|
|
||||||
|
local groundish = {}
|
||||||
|
for _, d in ipairs(ws:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
local n = d.Name:lower()
|
||||||
|
if
|
||||||
|
n:find("ground")
|
||||||
|
or n:find("base")
|
||||||
|
or n:find("water")
|
||||||
|
or n:find("sea")
|
||||||
|
or n:find("bay")
|
||||||
|
or n:find("floor")
|
||||||
|
then
|
||||||
|
local sz = d.Size
|
||||||
|
if sz.X > 200 or sz.Z > 200 then
|
||||||
|
table.insert(
|
||||||
|
groundish,
|
||||||
|
("%s size=%.0fx%.0f topY=%.1f"):format(d.Name, sz.X, sz.Z, d.Position.Y + sz.Y / 2)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(out, ("large ground-like slabs=%d"):format(#groundish))
|
||||||
|
for i = 1, math.min(#groundish, 6) do
|
||||||
|
table.insert(out, " " .. groundish[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. bottom-face Y of scenery vs deck
|
||||||
|
local roadY, roadN = {}, 0
|
||||||
|
local scen = {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
local nm = d.Name
|
||||||
|
if nm:sub(1, 5) == "Road_" then
|
||||||
|
roadN += 1
|
||||||
|
table.insert(roadY, d.Position.Y)
|
||||||
|
elseif
|
||||||
|
nm:find("Building")
|
||||||
|
or nm:find("Tower")
|
||||||
|
or nm:find("Ginza")
|
||||||
|
or nm:find("Shiodome")
|
||||||
|
or nm:find("Diet")
|
||||||
|
then
|
||||||
|
table.insert(scen, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(roadY)
|
||||||
|
local medRoad = roadY[math.max(1, math.floor(#roadY / 2))] or 0
|
||||||
|
table.insert(out, ("median road Y=%.1f (n=%d)"):format(medRoad, roadN))
|
||||||
|
|
||||||
|
local planted, floatHigh = 0, 0
|
||||||
|
local hist = {}
|
||||||
|
local worst, worstNm, worstP = -1e9, "none", nil
|
||||||
|
for _, p in ipairs(scen) do
|
||||||
|
local btm = p.Position.Y - p.Size.Y / 2
|
||||||
|
if btm <= 5 then
|
||||||
|
planted += 1
|
||||||
|
else
|
||||||
|
floatHigh += 1
|
||||||
|
local base = p.Name:gsub("_%d+$", "")
|
||||||
|
hist[base] = (hist[base] or 0) + 1
|
||||||
|
if btm > worst then
|
||||||
|
worst, worstNm, worstP = btm, p.Name, p.Position
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(out, ("scenery=%d | bottom<=5 (planted)=%d | bottom>5 (airborne)=%d"):format(#scen, planted, floatHigh))
|
||||||
|
if worstP then
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
("HIGHEST bottom: %s btmY=%.0f at %.0f,%.0f,%.0f"):format(worstNm, worst, worstP.X, worstP.Y, worstP.Z)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
local hl = {}
|
||||||
|
for k, v in pairs(hist) do
|
||||||
|
table.insert(hl, ("%s=%d"):format(k, v))
|
||||||
|
end
|
||||||
|
table.sort(hl)
|
||||||
|
table.insert(out, "airborne byName: " .. (#hl > 0 and table.concat(hl, " ") or "(none)"))
|
||||||
|
|
||||||
|
return table.concat(out, "\n")
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
-- What actually sits near the four ramp ENDS? The user reports ramps going
|
||||||
|
-- through buildings; the clip probe only found pillars vs perimeter wall.
|
||||||
|
-- So: name every non-drivable part within 140 studs of each ramp end, and
|
||||||
|
-- separately list PA-area structures with their footprints, so the ramp
|
||||||
|
-- can be re-aimed at a genuinely clear corridor instead of guessed at.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
if not map then
|
||||||
|
return "NO MAP"
|
||||||
|
end
|
||||||
|
|
||||||
|
local WANT = {
|
||||||
|
PAEntryRampNear_200 = true,
|
||||||
|
PAExitRampNear_200 = true,
|
||||||
|
PAEntryRampTrunk_1 = true,
|
||||||
|
PAExitRampTrunk_1 = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ends = {}
|
||||||
|
local all = {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
if WANT[d.Name] then
|
||||||
|
ends[d.Name] = d.Position
|
||||||
|
end
|
||||||
|
table.insert(all, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drivable(n)
|
||||||
|
return n:match("^Road_") or n:find("Ramp") or n == "PAFloor" or n == "Ground" or n:find("Water")
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
for n, p in pairs(ends) do
|
||||||
|
local near = {}
|
||||||
|
local closest, closeName = 1e9, "none"
|
||||||
|
for _, d in ipairs(all) do
|
||||||
|
if not drivable(d.Name) then
|
||||||
|
local dist = (d.Position - p).Magnitude
|
||||||
|
if dist < 140 then
|
||||||
|
near[d.Name] = (near[d.Name] or 0) + 1
|
||||||
|
if dist < closest then
|
||||||
|
closest, closeName = dist, d.Name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local l = {}
|
||||||
|
for k, v in pairs(near) do
|
||||||
|
table.insert(l, ("%s x%d"):format(k, v))
|
||||||
|
end
|
||||||
|
table.sort(l)
|
||||||
|
local keep = {}
|
||||||
|
for i = 1, math.min(#l, 10) do
|
||||||
|
table.insert(keep, l[i])
|
||||||
|
end
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
("%s @%.0f,%.0f,%.0f closest=%s(%.0f) near: %s"):format(
|
||||||
|
n,
|
||||||
|
p.X,
|
||||||
|
p.Y,
|
||||||
|
p.Z,
|
||||||
|
closeName,
|
||||||
|
closest,
|
||||||
|
table.concat(keep, ", ")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- PA-area structures with footprints so we can aim at a clear corridor.
|
||||||
|
table.insert(out, "--- PA structures (within 700 of PA centre) ---")
|
||||||
|
local paf = nil
|
||||||
|
for _, d in ipairs(all) do
|
||||||
|
if d.Name == "PAFloor" then
|
||||||
|
paf = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if paf then
|
||||||
|
local c = paf.Position
|
||||||
|
table.insert(out, ("PAFloor centre %.0f,%.0f,%.0f size %.0fx%.0f"):format(c.X, c.Y, c.Z, paf.Size.X, paf.Size.Z))
|
||||||
|
local seen = {}
|
||||||
|
local rows = {}
|
||||||
|
for _, d in ipairs(all) do
|
||||||
|
if not drivable(d.Name) and (d.Position - c).Magnitude < 700 then
|
||||||
|
local base = d.Name:gsub("_%d+$", "")
|
||||||
|
if not seen[base] then
|
||||||
|
seen[base] = { minx = 1e9, maxx = -1e9, minz = 1e9, maxz = -1e9, n = 0 }
|
||||||
|
end
|
||||||
|
local s = seen[base]
|
||||||
|
s.n += 1
|
||||||
|
s.minx = math.min(s.minx, d.Position.X - d.Size.X / 2)
|
||||||
|
s.maxx = math.max(s.maxx, d.Position.X + d.Size.X / 2)
|
||||||
|
s.minz = math.min(s.minz, d.Position.Z - d.Size.Z / 2)
|
||||||
|
s.maxz = math.max(s.maxz, d.Position.Z + d.Size.Z / 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for k, s in pairs(seen) do
|
||||||
|
table.insert(rows, ("%s n=%d X[%.0f..%.0f] Z[%.0f..%.0f]"):format(k, s.n, s.minx, s.maxx, s.minz, s.maxz))
|
||||||
|
end
|
||||||
|
table.sort(rows)
|
||||||
|
for _, r in ipairs(rows) do
|
||||||
|
table.insert(out, " " .. r)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out, "\n")
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
-- Geometry facts needed to re-cut the ramps so they (a) merge at the highway
|
||||||
|
-- EDGE instead of landing on top of it, (b) run a corridor that misses the PA
|
||||||
|
-- shops, and (c) get pole/beam supports instead of whatever's there now.
|
||||||
|
--
|
||||||
|
-- Outputs:
|
||||||
|
-- 1. Mainline cross-section at the PA: per-carriageway edge X/Z + width, so
|
||||||
|
-- a merge can target the OUTER edge rather than the deck centreline.
|
||||||
|
-- 2. Plan-footprint of every shop model near the PA, as an X/Z box, so a
|
||||||
|
-- clear corridor can be computed instead of guessed.
|
||||||
|
-- 3. Current support part sizes/shapes, to see what needs replacing.
|
||||||
|
|
||||||
|
local ws = game:GetService("Workspace")
|
||||||
|
local map = ws:FindFirstChild("Map")
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
|
||||||
|
-- 1. mainline cross-section near the PA (sample by Z band)
|
||||||
|
local band = {}
|
||||||
|
for _, d in ipairs(map:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") and d.Name:match("^Road_") then
|
||||||
|
local p = d.Position
|
||||||
|
if p.Z > -7250 and p.Z < -7050 and p.X > -4700 and p.X < -3400 then
|
||||||
|
table.insert(band, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(band, function(a, b)
|
||||||
|
return a.Position.Z < b.Position.Z
|
||||||
|
end)
|
||||||
|
table.insert(out, ("mainline sample parts in band=%d"):format(#band))
|
||||||
|
-- cluster by Z to find the two carriageways
|
||||||
|
local zmin, zmax = 1e9, -1e9
|
||||||
|
for _, d in ipairs(band) do
|
||||||
|
zmin = math.min(zmin, d.Position.Z - d.Size.Z / 2)
|
||||||
|
zmax = math.max(zmax, d.Position.Z + d.Size.Z / 2)
|
||||||
|
end
|
||||||
|
table.insert(out, ("mainline Z extent %.1f .. %.1f (total %.1f)"):format(zmin, zmax, zmax - zmin))
|
||||||
|
for i = 1, math.min(#band, 6) do
|
||||||
|
local d = band[i]
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
(" %s pos %.0f,%.1f,%.1f size %.1fx%.1f rotY=%.0f"):format(
|
||||||
|
d.Name,
|
||||||
|
d.Position.X,
|
||||||
|
d.Position.Y,
|
||||||
|
d.Position.Z,
|
||||||
|
d.Size.X,
|
||||||
|
d.Size.Z,
|
||||||
|
math.deg(select(2, d.CFrame:ToEulerAnglesYXZ()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. shop model footprints near the PA
|
||||||
|
table.insert(out, "--- shop model footprints (plan boxes) ---")
|
||||||
|
for _, top in ipairs(ws:GetChildren()) do
|
||||||
|
if top ~= map and not top:IsA("Terrain") and not top:IsA("Camera") and not top:IsA("BasePart") then
|
||||||
|
local x0, x1, z0, z1, y0, y1, n = 1e9, -1e9, 1e9, -1e9, 1e9, -1e9, 0
|
||||||
|
for _, d in ipairs(top:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") then
|
||||||
|
n += 1
|
||||||
|
x0 = math.min(x0, d.Position.X - d.Size.X / 2)
|
||||||
|
x1 = math.max(x1, d.Position.X + d.Size.X / 2)
|
||||||
|
z0 = math.min(z0, d.Position.Z - d.Size.Z / 2)
|
||||||
|
z1 = math.max(z1, d.Position.Z + d.Size.Z / 2)
|
||||||
|
y0 = math.min(y0, d.Position.Y - d.Size.Y / 2)
|
||||||
|
y1 = math.max(y1, d.Position.Y + d.Size.Y / 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if n > 0 and x0 > -6000 and x1 < -2500 and z0 > -9000 and z1 < -6000 then
|
||||||
|
table.insert(
|
||||||
|
out,
|
||||||
|
(" %-42s n=%-5d X[%.0f..%.0f] Z[%.0f..%.0f] Y[%.0f..%.0f]"):format(top.Name, n, x0, x1, z0, z1, y0, y1)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 3. what the current supports look like
|
||||||
|
table.insert(out, "--- current ramp supports ---")
|
||||||
|
local sup = {}
|
||||||
|
for _, d in ipairs(ws:GetDescendants()) do
|
||||||
|
if d:IsA("BasePart") and (d.Name:find("RampPillar") or d.Name:find("PierColumn") or d.Name:find("PierCap")) then
|
||||||
|
local k = ("%s shape=%s size=%.1fx%.1fx%.1f"):format(
|
||||||
|
d.Name,
|
||||||
|
d.Shape and d.Shape.Name or "n/a",
|
||||||
|
d.Size.X,
|
||||||
|
d.Size.Y,
|
||||||
|
d.Size.Z
|
||||||
|
)
|
||||||
|
sup[k] = (sup[k] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sl = {}
|
||||||
|
for k, v in pairs(sup) do
|
||||||
|
table.insert(sl, ("%s x%d"):format(k, v))
|
||||||
|
end
|
||||||
|
table.sort(sl)
|
||||||
|
for i = 1, math.min(#sl, 12) do
|
||||||
|
table.insert(out, " " .. sl[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out, "\n")
|
||||||