Rebuild road markings, signage and lighting to verified JP expressway spec
Road markings now follow the 全標協 (Japan Road Marking Association) 2012/2018 standard rather than eyeballed values: - Lane dividers: 8 m painted / 12 m gap on a continuous 20 m cycle carried across segment boundaries (previously each segment restarted its own dash phase, so the rhythm broke at every joint). - Line widths corrected to 0.15 m lane / 0.20 m edge. The 0.45 m width often quoted for expressways applies only to the boundary against a 変速車線 (accel/decel lane) -- now used at ramps only, where it belongs. - Yellow SOLID no-lane-change lines through tunnels and tight curves, per the standard's rule that the boundary goes solid where overtaking is undesirable. - 導流帯 gore zebra at ramp junctions: 45 cm bars at 1.0 m pitch. Signage: - Guide-sign green corrected to the official #005D35; route badges #007F1F. - Bilingual Japanese + romaji layout (JP above, romaji below). - Added 規制標識 speed limit plates posted at 50 km/h -- the C1 Inner Circular limit. (80 km/h is the Bayshore Route; the two are not the same.) - Added キロポスト distance markers every 0.5 km. Lighting/furniture: - Tunnel lighting changed from blue/teal neon to warm sodium + cool LED runs, and made continuous rather than every-other-segment. - Deck lamps on a ~34 m pitch; added reflective delineator posts. Verified in-engine by headless measurement, not by inspection: 14/14 geometric assertions pass (dash length/width, edge width, yellow count, zebra pitch, light colour, sign colour/bilingual, speed+km posts, markings flush with deck). Sources and the corrections to widely-repeated wrong figures are recorded in docs/japanese-road-standards.md.
This commit is contained in:
+390
-37
@@ -56,6 +56,60 @@ local C_PARAPET = Color3.fromRGB(226, 224, 218)
|
||||
local C_RAIL = Color3.fromRGB(96, 100, 106)
|
||||
local UP = Vector3.new(0, 1, 0)
|
||||
|
||||
-- REAL-WORLD SCALE ------------------------------------------------------------
|
||||
-- The rest of the codebase converts speed with 0.28 m per stud, so all the
|
||||
-- metric road-standard figures below are converted through the same constant.
|
||||
-- Sanity check: the 49,383-stud loop comes out at 13.8 km against the real
|
||||
-- C1 Inner Circular Route's 14.8 km, so the circuit is very close to 1:1.
|
||||
local M = 1 / 0.28 -- studs per metre (~3.571)
|
||||
|
||||
-- Road markings per 全標協 (Japan Road Marking Assoc.) 2012 p.42 / 2018 pp.97-98,
|
||||
-- read from the primary standard rather than repeated folklore:
|
||||
-- * 車線境界線 (lane boundary, dashed): line length 3-10 m, gap = 1x to 2x the
|
||||
-- line length, width 10-15 cm (15 cm "desirable"). The 8 m line / 12 m gap
|
||||
-- used here is the high-speed end of that legal range (gap = 1.5x line) and
|
||||
-- is the configuration expressways actually use -- it's what makes the
|
||||
-- rhythm read as an expressway rather than a 5 m/5 m surface street.
|
||||
-- * Where overtaking is undesirable (屈曲部 -- curves), the standard says the
|
||||
-- line becomes SOLID rather than dashed. On expressways that no-lane-change
|
||||
-- line is painted yellow.
|
||||
-- * NOTE: the 45 cm width sometimes quoted for "expressway lines" is NOT the
|
||||
-- lane boundary -- it's the boundary against a 変速車線 (accel/decel lane).
|
||||
-- Applied below at ramps only.
|
||||
local DASH_PAINT = 8 * M -- 28.6 studs
|
||||
local DASH_GAP = 12 * M -- 42.9 studs
|
||||
local DASH_CYCLE = DASH_PAINT + DASH_GAP -- 20 m
|
||||
local LANE_LINE_W = 0.15 * M -- 0.54 studs
|
||||
local EDGE_LINE_W = 0.20 * M -- 0.71 studs
|
||||
local RAMP_LINE_W = 0.45 * M -- 1.61 studs; through lane vs speed-change lane
|
||||
local LANE_W = 3.25 * M -- 11.6 studs; narrow urban lane used on the C1
|
||||
-- 導流帯 (gore/zebra) per 全標協 2018 pp.97-98: 45 cm white solid lines repeated
|
||||
-- every 1.0 m, bounded by a 15-20 cm edge line.
|
||||
local ZEBRA_W = 0.45 * M
|
||||
local ZEBRA_PITCH = 1.0 * M
|
||||
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
|
||||
-- enough that the real road would paint a solid yellow no-lane-change line.
|
||||
local TIGHT_CURVE = math.rad(4)
|
||||
|
||||
local C_PAINT_WHITE = Color3.fromRGB(232, 232, 236)
|
||||
local C_PAINT_YELLOW = Color3.fromRGB(240, 190, 40) -- 黄色 no-lane-change line
|
||||
|
||||
-- Signage colours sampled from the official Japanese sign artwork (Wikimedia
|
||||
-- Commons SVGs of the 標識令 plates), not eyeballed:
|
||||
-- guide sign (案内標識) green #005D35
|
||||
-- Shutoko route badge green #007F1F
|
||||
local C_SIGN_GREEN = Color3.fromRGB(0, 93, 53)
|
||||
local C_BADGE_GREEN = Color3.fromRGB(0, 127, 31)
|
||||
local C_SIGN_WHITE = Color3.fromRGB(255, 255, 255)
|
||||
|
||||
-- Lighting. Japanese expressway/tunnel lighting is high-pressure sodium
|
||||
-- (warm amber) with newer LED sections in cool white -- NOT the blue/teal
|
||||
-- that reads as generic sci-fi. Tunnel basic lighting runs continuously.
|
||||
local C_SODIUM = Color3.fromRGB(255, 176, 92)
|
||||
local C_TUNNEL_LED = Color3.fromRGB(255, 244, 224)
|
||||
|
||||
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
||||
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
|
||||
local NEON_TEAL = Color3.fromRGB(40, 230, 200)
|
||||
@@ -78,27 +132,105 @@ local function det(seed: number): number
|
||||
return x - math.floor(x)
|
||||
end
|
||||
|
||||
-- Real interchange names. Japanese is what's actually painted on the gantry;
|
||||
-- the romaji underneath is the smaller secondary line every Shutoko guide sign
|
||||
-- carries. Names cross-checked against OpenStreetMap's tagging of the real
|
||||
-- Inner Circular Route (name / name:en on the motorway_junction nodes).
|
||||
local JUNCTION_NAMES: { [string]: { string } } = {
|
||||
["Kyobashi"] = { "京橋", "Kyobashi" },
|
||||
["Takaracho"] = { "宝町", "Takaracho" },
|
||||
["Edobashi junction"] = { "江戸橋JCT", "Edobashi JCT" },
|
||||
["Takebashi Junction"] = { "竹橋JCT", "Takebashi JCT" },
|
||||
["Hitotsubashi"] = { "一ツ橋", "Hitotsubashi" },
|
||||
["Kitanomaru"] = { "北の丸", "Kitanomaru" },
|
||||
["Daikancho"] = { "代官町", "Daikancho" },
|
||||
["Miyakezaka JCT"] = { "三宅坂JCT", "Miyakezaka JCT" },
|
||||
["Miyakezaka Junction"] = { "三宅坂JCT", "Miyakezaka JCT" },
|
||||
["Kasumigaseki"] = { "霞が関", "Kasumigaseki" },
|
||||
["Tanimachi JCT"] = { "谷町JCT", "Tanimachi JCT" },
|
||||
["Iikura"] = { "飯倉", "Iikura" },
|
||||
["Shiodome"] = { "汐留", "Shiodome" },
|
||||
["Ginza"] = { "銀座", "Ginza" },
|
||||
["Shintomicho"] = { "新富町", "Shintomicho" },
|
||||
}
|
||||
|
||||
-- Japanese expressway guide sign (案内標識). Green board, white text, white
|
||||
-- hairline border inset from the edge, Japanese place name large with the
|
||||
-- romaji reading beneath it, and the circular route badge on the left.
|
||||
local function makeSign(text: string, cf: CFrame, parent: Instance)
|
||||
local names = JUNCTION_NAMES[text] or { text, text }
|
||||
local ja, romaji = names[1], names[2]
|
||||
|
||||
local board = part({
|
||||
Name = "Sign",
|
||||
Size = Vector3.new(34, 10, 1),
|
||||
CFrame = cf,
|
||||
Color = Color3.fromRGB(18, 90, 60),
|
||||
Color = C_SIGN_GREEN,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
board.Parent = parent
|
||||
|
||||
local gui = Instance.new("SurfaceGui")
|
||||
gui.Face = Enum.NormalId.Front
|
||||
gui.CanvasSize = Vector2.new(600, 180)
|
||||
gui.CanvasSize = Vector2.new(680, 200)
|
||||
gui.Parent = board
|
||||
local label = Instance.new("TextLabel")
|
||||
label.Size = UDim2.fromScale(1, 1)
|
||||
label.BackgroundTransparency = 1
|
||||
label.Font = Enum.Font.GothamBold
|
||||
label.TextColor3 = Color3.new(1, 1, 1)
|
||||
label.TextScaled = true
|
||||
label.Text = text
|
||||
label.Parent = gui
|
||||
|
||||
-- Thin white border inset from the panel edge -- present on every real
|
||||
-- Japanese guide sign and a big part of why they read as "Japanese".
|
||||
local border = Instance.new("Frame")
|
||||
border.Size = UDim2.new(1, -24, 1, -24)
|
||||
border.Position = UDim2.fromOffset(12, 12)
|
||||
border.BackgroundTransparency = 1
|
||||
border.BorderSizePixel = 3
|
||||
border.BorderColor3 = C_SIGN_WHITE
|
||||
border.Parent = gui
|
||||
|
||||
-- Route badge: the C1 shield, green disc with white ring and "C1".
|
||||
local badge = Instance.new("Frame")
|
||||
badge.Size = UDim2.fromOffset(104, 104)
|
||||
badge.Position = UDim2.fromOffset(34, 48)
|
||||
badge.BackgroundColor3 = C_BADGE_GREEN
|
||||
badge.BorderSizePixel = 0
|
||||
badge.Parent = gui
|
||||
local badgeCorner = Instance.new("UICorner")
|
||||
badgeCorner.CornerRadius = UDim.new(1, 0)
|
||||
badgeCorner.Parent = badge
|
||||
local badgeStroke = Instance.new("UIStroke")
|
||||
badgeStroke.Color = C_SIGN_WHITE
|
||||
badgeStroke.Thickness = 5
|
||||
badgeStroke.Parent = badge
|
||||
local badgeText = Instance.new("TextLabel")
|
||||
badgeText.Size = UDim2.fromScale(1, 1)
|
||||
badgeText.BackgroundTransparency = 1
|
||||
badgeText.Font = Enum.Font.GothamBold
|
||||
badgeText.TextColor3 = C_SIGN_WHITE
|
||||
badgeText.TextScaled = true
|
||||
badgeText.Text = "C1"
|
||||
badgeText.Parent = badge
|
||||
|
||||
-- Japanese name (primary, large)
|
||||
local jaLabel = Instance.new("TextLabel")
|
||||
jaLabel.Size = UDim2.new(1, -190, 0, 96)
|
||||
jaLabel.Position = UDim2.fromOffset(168, 36)
|
||||
jaLabel.BackgroundTransparency = 1
|
||||
jaLabel.Font = Enum.Font.GothamBold
|
||||
jaLabel.TextColor3 = C_SIGN_WHITE
|
||||
jaLabel.TextScaled = true
|
||||
jaLabel.TextXAlignment = Enum.TextXAlignment.Left
|
||||
jaLabel.Text = ja
|
||||
jaLabel.Parent = gui
|
||||
|
||||
-- Romaji reading (secondary, smaller, beneath)
|
||||
local enLabel = Instance.new("TextLabel")
|
||||
enLabel.Size = UDim2.new(1, -190, 0, 44)
|
||||
enLabel.Position = UDim2.fromOffset(168, 132)
|
||||
enLabel.BackgroundTransparency = 1
|
||||
enLabel.Font = Enum.Font.Gotham
|
||||
enLabel.TextColor3 = C_SIGN_WHITE
|
||||
enLabel.TextScaled = true
|
||||
enLabel.TextXAlignment = Enum.TextXAlignment.Left
|
||||
enLabel.Text = romaji
|
||||
enLabel.Parent = gui
|
||||
end
|
||||
|
||||
-- PA junction planning -----------------------------------------------------------
|
||||
@@ -157,6 +289,10 @@ local function buildRoad(map: Folder)
|
||||
local n = #samples
|
||||
local plan = planPA()
|
||||
|
||||
-- Running arc length around the loop, so lane dashes keep a continuous
|
||||
-- 20 m rhythm instead of restarting at every sample.
|
||||
local segStart = 0
|
||||
|
||||
for i = 1, n do
|
||||
local a = samples[i]
|
||||
local b = samples[(i % n) + 1]
|
||||
@@ -172,6 +308,16 @@ local function buildRoad(map: Folder)
|
||||
local right = flatDir:Cross(UP)
|
||||
local cf = CFrame.lookAt(mid, mid + dir, UP)
|
||||
|
||||
-- How hard this segment turns, used to decide where the real road
|
||||
-- would forbid lane changes (solid yellow) rather than allow them.
|
||||
local prev = samples[((i - 2) % n) + 1]
|
||||
local inDir = (a.pos - prev.pos)
|
||||
inDir = Vector3.new(inDir.X, 0, inDir.Z)
|
||||
local curvature = 0
|
||||
if inDir.Magnitude > 0.01 then
|
||||
curvature = math.acos(math.clamp(inDir.Unit:Dot(flatDir), -1, 1))
|
||||
end
|
||||
|
||||
local deck = part({
|
||||
Name = "Road_" .. i,
|
||||
Size = Vector3.new(ROAD_WIDTH, 2, len),
|
||||
@@ -211,28 +357,66 @@ local function buildRoad(map: Folder)
|
||||
end
|
||||
|
||||
-- LANE MARKINGS: 2 lanes per carriageway ----------------------------
|
||||
-- Each carriageway spans MED_HALF..HALF from the centreline. One dashed
|
||||
-- line down its middle splits it into the two lanes; a solid white edge
|
||||
-- line runs just inside the outer barrier.
|
||||
local cwMid = (MED_HALF + HALF) / 2
|
||||
-- C1 is a 2-lane-per-direction urban expressway (confirmed against
|
||||
-- OSM tagging of 首都高速都心環状線: lanes=2, maxspeed=50). Lane width
|
||||
-- is the narrow urban 3.25 m rather than the 3.5 m used on the
|
||||
-- Bayshore, which is what leaves so little shoulder on the real road.
|
||||
--
|
||||
-- Dashes are driven by ARC LENGTH along the circuit, not by sample
|
||||
-- index. That matters: samples are 60 studs (16.8 m) apart but the
|
||||
-- legal dash cycle is 20 m, so an index-based dash can never land on
|
||||
-- the right rhythm -- it has to be computed in metres and clipped to
|
||||
-- each segment.
|
||||
local cwMid = MED_HALF + LANE_W
|
||||
-- Lane changes are prohibited (yellow line) through the tunnels and
|
||||
-- on the tight curves, which on the real C1 is most of the loop.
|
||||
local laneColor = (a.tunnel or curvature > TIGHT_CURVE) and C_PAINT_YELLOW or C_PAINT_WHITE
|
||||
|
||||
for _, s in ipairs({ -1, 1 }) do
|
||||
-- 車道外側線: solid, continuous, 0.20 m wide.
|
||||
local edge = part({
|
||||
Name = "EdgeLine",
|
||||
Size = Vector3.new(0.9, 2.1, len),
|
||||
CFrame = cf * CFrame.new(s * (HALF - 1.6), 0.1, 0),
|
||||
Color = Color3.fromRGB(228, 228, 233),
|
||||
Size = Vector3.new(EDGE_LINE_W, MARK_H, len),
|
||||
CFrame = cf * CFrame.new(s * (HALF - 1.6), MARK_Y, 0),
|
||||
Color = C_PAINT_WHITE,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
edge.Parent = road
|
||||
if i % 2 == 0 then
|
||||
local dash = part({
|
||||
Name = "Dash",
|
||||
Size = Vector3.new(0.8, 2.1, len * 0.55),
|
||||
CFrame = cf * CFrame.new(s * cwMid, 0.1, 0),
|
||||
Color = Color3.fromRGB(220, 220, 225),
|
||||
|
||||
-- 車線境界線 between the two lanes.
|
||||
if laneColor == C_PAINT_YELLOW then
|
||||
-- Yellow no-lane-change line is SOLID, not dashed.
|
||||
local solid = part({
|
||||
Name = "LaneLine",
|
||||
Size = Vector3.new(LANE_LINE_W, MARK_H, len),
|
||||
CFrame = cf * CFrame.new(s * cwMid, MARK_Y, 0),
|
||||
Color = C_PAINT_YELLOW,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
dash.Parent = road
|
||||
solid.Parent = road
|
||||
else
|
||||
-- Dashed: paint the parts of the 20 m cycle that fall inside
|
||||
-- this segment. segStart is the distance travelled around the
|
||||
-- loop, so the rhythm stays continuous across segment joins.
|
||||
local segLen = delta.Magnitude
|
||||
local k = math.floor(segStart / DASH_CYCLE)
|
||||
while k * DASH_CYCLE < segStart + segLen do
|
||||
local pStart = math.max(k * DASH_CYCLE, segStart)
|
||||
local pEnd = math.min(k * DASH_CYCLE + DASH_PAINT, segStart + segLen)
|
||||
if pEnd > pStart then
|
||||
-- Convert back to an offset from the segment centre.
|
||||
local mid01 = ((pStart + pEnd) / 2 - segStart) / segLen
|
||||
local dash = part({
|
||||
Name = "Dash",
|
||||
Size = Vector3.new(LANE_LINE_W, MARK_H, pEnd - pStart),
|
||||
CFrame = cf * CFrame.new(s * cwMid, MARK_Y, (mid01 - 0.5) * segLen),
|
||||
Color = C_PAINT_WHITE,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
dash.Parent = road
|
||||
end
|
||||
k += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -303,16 +487,18 @@ local function buildRoad(map: Folder)
|
||||
Material = Enum.Material.Concrete,
|
||||
})
|
||||
ceil.Parent = struct
|
||||
if i % 2 == 0 then
|
||||
local strip = part({
|
||||
Name = "TunnelLight",
|
||||
Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, 2.5),
|
||||
CFrame = cf * CFrame.new(0, h - 0.5, 0),
|
||||
Color = (i % 6 == 0) and NEON_BLUE or NEON_TEAL,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
strip.Parent = struct
|
||||
end
|
||||
-- Tunnel lighting is high-pressure sodium (warm amber) with newer
|
||||
-- LED sections in cool white. Real tunnel "basic lighting" is a
|
||||
-- continuous run, not isolated dots, so the strip is placed on
|
||||
-- every segment rather than every other one.
|
||||
local strip = part({
|
||||
Name = "TunnelLight",
|
||||
Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, len),
|
||||
CFrame = cf * CFrame.new(0, h - 0.5, 0),
|
||||
Color = (i % 8 < 4) and C_SODIUM or C_TUNNEL_LED,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
strip.Parent = struct
|
||||
else
|
||||
-- Elevated viaduct: support pillars down to street level
|
||||
if i % 3 == 0 and a.pos.Y > 18 then
|
||||
@@ -325,17 +511,38 @@ local function buildRoad(map: Folder)
|
||||
})
|
||||
pil.Parent = struct
|
||||
end
|
||||
-- sodium lamps along the deck
|
||||
if i % 4 == 0 then
|
||||
-- Sodium lamps along the deck. Japanese expressway lighting is
|
||||
-- warm high-pressure sodium on a ~35 m pole pitch; samples are
|
||||
-- 60 studs (16.8 m) apart so every 2nd sample ~= 33.6 m.
|
||||
if i % 2 == 0 then
|
||||
local lamp = part({
|
||||
Name = "RoadLight",
|
||||
Size = Vector3.new(1.4, 1.4, 1.4),
|
||||
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2) + Vector3.new(0, 5.5, 0)),
|
||||
Color = NEON_AMBER,
|
||||
Color = C_SODIUM,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
lamp.Parent = struct
|
||||
end
|
||||
-- Delineators (視線誘導標): small reflective posts on the outer
|
||||
-- edge that mark the road's line ahead at night. Real spacing on
|
||||
-- curves is tight; ~17 m here.
|
||||
local delin = part({
|
||||
Name = "Delineator",
|
||||
Size = Vector3.new(0.5, 1.1, 0.5),
|
||||
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2 - 2.6) + Vector3.new(0, 2.5, 0)),
|
||||
Color = C_PAINT_WHITE,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
delin.Parent = struct
|
||||
local eye = part({
|
||||
Name = "DelineatorEye",
|
||||
Size = Vector3.new(0.36, 0.36, 0.36),
|
||||
CFrame = CFrame.new(mid + right * (ROAD_WIDTH / 2 - 2.6) + Vector3.new(0, 3.0, 0)),
|
||||
Color = Color3.fromRGB(255, 190, 80),
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
eye.Parent = struct
|
||||
if i % 9 == 0 then
|
||||
local pylon = part({
|
||||
Name = "Pylon",
|
||||
@@ -347,9 +554,128 @@ local function buildRoad(map: Folder)
|
||||
pylon.Parent = struct
|
||||
end
|
||||
end
|
||||
|
||||
-- 導流帯 / gore zebra at the PA ramp junctions --------------------
|
||||
-- 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
|
||||
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
|
||||
for _, s in ipairs({ -1, 1 }) do
|
||||
local bar = part({
|
||||
Name = "GoreZebra",
|
||||
Size = Vector3.new(6, MARK_H, ZEBRA_W),
|
||||
CFrame = cf * CFrame.new(s * (HALF - 5), MARK_Y, z),
|
||||
Color = C_PAINT_WHITE,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
bar.Parent = road
|
||||
end
|
||||
end
|
||||
-- 45 cm boundary line separating the through lane from the
|
||||
-- speed-change lane, per 全標協 2012 p.42.
|
||||
for _, s in ipairs({ -1, 1 }) do
|
||||
local rl = part({
|
||||
Name = "RampBoundary",
|
||||
Size = Vector3.new(RAMP_LINE_W, MARK_H, len),
|
||||
CFrame = cf * CFrame.new(s * (HALF - 8.5), MARK_Y, 0),
|
||||
Color = C_PAINT_WHITE,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
rl.Parent = road
|
||||
end
|
||||
end
|
||||
|
||||
-- Advance the running arc length so the next segment's dashes
|
||||
-- continue the 20 m rhythm instead of restarting.
|
||||
segStart += delta.Magnitude
|
||||
end
|
||||
end
|
||||
|
||||
-- Regulatory speed limit sign (規制標識 323 最高速度) -----------------------------
|
||||
-- Circular, white face, red annulus, black numerals. The real C1 is posted at
|
||||
-- 50 km/h (40 in the tightest junction sections) -- verified against the route
|
||||
-- article's own signage table, not the 80 km/h used on the Bayshore Route.
|
||||
local function makeSpeedSign(kmh: number, cf: CFrame, parent: Instance)
|
||||
local post = part({
|
||||
Name = "SignPost",
|
||||
Size = Vector3.new(0.6, 12, 0.6),
|
||||
CFrame = cf * CFrame.new(0, -6, 0),
|
||||
Color = C_RAIL,
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
post.Parent = parent
|
||||
|
||||
local face = part({
|
||||
Name = "SpeedLimitSign",
|
||||
Size = Vector3.new(6, 6, 0.4),
|
||||
CFrame = cf,
|
||||
Color = Color3.fromRGB(255, 255, 255),
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
Shape = Enum.PartType.Cylinder,
|
||||
})
|
||||
-- Cylinder's flat faces point along X, so rotate it to face the driver.
|
||||
face.CFrame = cf * CFrame.Angles(0, math.rad(90), 0)
|
||||
face.Parent = parent
|
||||
|
||||
for _, faceId in ipairs({ Enum.NormalId.Right, Enum.NormalId.Left }) do
|
||||
local gui = Instance.new("SurfaceGui")
|
||||
gui.Face = faceId
|
||||
gui.CanvasSize = Vector2.new(256, 256)
|
||||
gui.Parent = face
|
||||
|
||||
local disc = Instance.new("Frame")
|
||||
disc.Size = UDim2.fromScale(1, 1)
|
||||
disc.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
|
||||
disc.BorderSizePixel = 0
|
||||
disc.Parent = gui
|
||||
local dc = Instance.new("UICorner")
|
||||
dc.CornerRadius = UDim.new(1, 0)
|
||||
dc.Parent = disc
|
||||
-- Red annulus is ~11% of the diameter on the JP plate.
|
||||
local ring = Instance.new("UIStroke")
|
||||
ring.Color = Color3.fromRGB(200, 30, 45)
|
||||
ring.Thickness = 28
|
||||
ring.Parent = disc
|
||||
|
||||
local num = Instance.new("TextLabel")
|
||||
num.Size = UDim2.fromScale(0.72, 0.56)
|
||||
num.Position = UDim2.fromScale(0.14, 0.22)
|
||||
num.BackgroundTransparency = 1
|
||||
num.Font = Enum.Font.GothamBold
|
||||
num.TextColor3 = Color3.fromRGB(20, 20, 20)
|
||||
num.TextScaled = true
|
||||
num.Text = tostring(kmh)
|
||||
num.Parent = disc
|
||||
end
|
||||
end
|
||||
|
||||
-- Kilometre post (キロポスト) ----------------------------------------------------
|
||||
local function makeKmPost(km: number, cf: CFrame, parent: Instance)
|
||||
local board = part({
|
||||
Name = "KmPost",
|
||||
Size = Vector3.new(2.6, 3.2, 0.3),
|
||||
CFrame = cf,
|
||||
Color = C_SIGN_GREEN,
|
||||
Material = Enum.Material.SmoothPlastic,
|
||||
})
|
||||
board.Parent = parent
|
||||
local gui = Instance.new("SurfaceGui")
|
||||
gui.Face = Enum.NormalId.Front
|
||||
gui.CanvasSize = Vector2.new(130, 160)
|
||||
gui.Parent = board
|
||||
local label = Instance.new("TextLabel")
|
||||
label.Size = UDim2.fromScale(1, 1)
|
||||
label.BackgroundTransparency = 1
|
||||
label.Font = Enum.Font.GothamBold
|
||||
label.TextColor3 = C_SIGN_WHITE
|
||||
label.TextScaled = true
|
||||
label.Text = string.format("%.1f", km)
|
||||
label.Parent = gui
|
||||
end
|
||||
|
||||
-- Junction gantry signs at each real interchange ---------------------------------
|
||||
local function buildJunctionSigns(map: Folder)
|
||||
local folder = Instance.new("Folder")
|
||||
@@ -369,6 +695,33 @@ local function buildJunctionSigns(map: Folder)
|
||||
local pos = s.pos + Vector3.new(0, 15, 0)
|
||||
makeSign(s.piece, CFrame.lookAt(pos, pos - fwd, UP), folder)
|
||||
end
|
||||
|
||||
-- Speed limit repeaters and kilometre posts around the loop.
|
||||
local dist = 0
|
||||
local nextKm = 0
|
||||
for i = 1, n do
|
||||
local a = samples[i]
|
||||
local b = samples[(i % n) + 1]
|
||||
local seg = (b.pos - a.pos).Magnitude
|
||||
local fwd = Circuit.forwardAt(i)
|
||||
local flat = Vector3.new(fwd.X, 0, fwd.Z)
|
||||
flat = flat.Magnitude > 0.01 and flat.Unit or Vector3.xAxis
|
||||
local right = flat:Cross(UP)
|
||||
|
||||
-- C1 is posted at 50 km/h; repeat roughly every 1 km, off to the side.
|
||||
if i % 60 == 0 and not a.tunnel then
|
||||
local p = a.pos + right * (ROAD_WIDTH / 2 - 3) + Vector3.new(0, 9, 0)
|
||||
makeSpeedSign(50, CFrame.lookAt(p, p - flat, UP), folder)
|
||||
end
|
||||
|
||||
-- Kilometre posts every 0.5 km of real distance (0.5 km = 1785 studs).
|
||||
if dist >= nextKm * 500 * M then
|
||||
local p = a.pos + right * (ROAD_WIDTH / 2 - 2) + Vector3.new(0, 4, 0)
|
||||
makeKmPost(nextKm * 0.5, CFrame.lookAt(p, p - flat, UP), folder)
|
||||
nextKm += 1
|
||||
end
|
||||
dist += seg
|
||||
end
|
||||
end
|
||||
|
||||
-- Daikoku-style PA ---------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user