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).
This commit is contained in:
2026-08-03 21:15:25 -04:00
parent 1d23c187ad
commit c297b20aed
34 changed files with 2206 additions and 46 deletions
+198 -42
View File
@@ -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.
local ZEBRA_W = 0.45 * 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_H = 2.1
-- Heading change per 60-stud sample above which the curve counts as tight
@@ -399,10 +402,21 @@ local function planPA()
kind = "off",
k = 7, -- fork at sample 547, bearing 32 deg
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
turn = 1, -- CCW: 270 -> 392 (=32), down the east side
sweep = 122.0,
yFork = 96,
-- GATE MOVED TO THE PA's NORTH EDGE (2026-08-03).
-- It used to be az=270 -- the SOUTH edge -- with a 122 deg sweep,
-- which walked the trunk right around the PA on a r=470 arc. The
-- 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
out = 0, -- unused: no under-deck crossing any more
nearSide = -1, -- PA is on the -Z side of the mainline here
@@ -412,10 +426,14 @@ local function planPA()
kind = "on",
k = -6, -- fork at sample 534, bearing 132 deg
forkR = 470.0,
az = 250.0, -- ring gate on the south edge, 135 studs west of red's
turn = -1, -- CW: 250 -> 132, up the west side
sweep = 118.0,
yFork = 96,
-- Gate also moved to the north edge (see PAEntryRamp above); it
-- was az=250 sweeping 118 deg through the west-sector shops.
-- 65 deg keeps ~250 studs of clear kerb between the two ramp
-- 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
out = 0,
nearSide = -1,
@@ -443,7 +461,10 @@ local function planPA()
local nx0 = samples[(i0 % n) + 1].pos
local h0 = math.atan2(nx0.Z - s0.Z, nx0.X - s0.X)
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, {
name = def.name,
idx = i0,
@@ -453,7 +474,7 @@ local function planPA()
az = math.rad(def.az),
turn = def.turn,
sweep = math.rad(def.sweep),
yFork = def.yFork,
yFork = yForkDerived,
nearSide = def.nearSide,
-- `side` kept for the barrier/median code, which still thinks in
-- terms of one carriageway per junction; the near side is the one
@@ -468,7 +489,7 @@ local function planPA()
-- gates) hangs off this.
fork = Vector3.new(
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
),
pos = nearPos,
@@ -510,11 +531,19 @@ local function planPA()
-- would strip the parapet off the far carriageway again (the 2026-08-02
-- "no walls near the parking area" bug, in a new form).
local rampOpenSide = {}
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
-- RAMPS DISABLED (2026-08-03): with no ramp deck built, an opening in the
-- outer parapet is a hole in the barrier with a drop behind it -- the
-- exact "no walls near the parking area" failure described above, just
-- caused from the other end. Leave the edge sealed while the ramps are
-- 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
PAPlan = {
@@ -945,7 +974,13 @@ local function buildRoad(map: Folder)
-- 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
-- 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)
for k = 0, nBars - 1 do
local z = (k * ZEBRA_PITCH - delta.Magnitude / 2) + ZEBRA_PITCH / 2
@@ -1578,6 +1613,33 @@ local function buildPA(map: Folder)
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).
-- `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.
@@ -1688,20 +1750,47 @@ local function buildPA(map: Folder)
-- run to eliminate. Those spans are short and are carried by the
-- piers on either side.
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({
Name = "PARampPillar",
-- Cylinder axis is local X, so Size.X is the height and
-- Size.Y/Z the diameter; the Z rotation stands it upright.
-- (Same transposition that had all 80 viaduct PierColumns
-- 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)),
Color = C_CONCRETE,
Material = Enum.Material.Concrete,
Shape = Enum.PartType.Cylinder,
})
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
-- 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
for _, jn in ipairs(junctions) do
if not BUILD_PA_RAMPS then
continue
end
local rampR = jn.radius
local yf = jn.yFork
local ns = jn.nearSide
local mergeIdx = jn.mergeIdx
-- 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
-- ~900-stud arc is ~5.5%, well inside the 9% limit, so there is no
-- 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 = {}
for s = 0, 400 do
local f = s / 400
local th = jn.th0 + jn.turn * jn.sweep * 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))
end
local fork = trunk[#trunk]
-- 2. SLIP -- fork to the carriageway on the PA side, at deck level.
-- There is only ONE slip now. The far carriageway is reached through
-- the gap in the centre divider (see `open` in planPA), which is what
-- the user's drawing is about: "this is why the middle
-- barriers/dividers are important because each side will have access
-- to on and off ramps". Both carriageways sit at the same deck height
-- 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 = {}
-- 2. CROSSING -- fork straight out over BOTH carriageways at constant
-- OVER_Y. This is the flyover proper: it must clear the deck, so it
-- holds height all the way across and only descends afterwards.
local farSide = -ns
local overFar = carriagewayPoint(mergeIdx, farSide)
local cross = {}
for s = 0, 200 do
local f = s / 200
table.insert(
near,
Vector3.new(
fork.X + (tgtNear.X - fork.X) * f,
yf + (yd - yf) * ease(f),
fork.Z + (tgtNear.Z - fork.Z) * f
)
cross,
Vector3.new(fork.X + (overFar.X - fork.X) * f, OVER_Y, fork.Z + (overFar.Z - fork.Z) * f)
)
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
-- undirected, so only which ends are MOUTHS matters, and every run
-- here is open at both ends (ring gate / fork, fork / carriageway).
-- Chevrons go on the trunk only: that is the curved helix they exist
-- to make legible, and on the straight slip they would just be a red
-- wall alongside the merge.
-- here is open at both ends. Chevrons go on the trunk only: that is
-- the curved helix they exist to make legible.
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
-- ring parapet is fully backed by deck -- the trunk meets the ring at