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:
+198
-42
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user