Editor/Road/ArchCrossover.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Answered here and nowhere else: derived twice, drive and verge drifted apart.
public static class ArchCrossover {
// A drive serves the road it points at; RoadId overrides when two face one wall.
public const float DriveReach = 2400f;
// A drive stopping a foot from the kerb is nobody's intent.
const float Merges = 48f;
public static bool Serves( ArchPlan plan, ArchApproachPart approach, out ArchRoadPart road, out ArchFrame frame, out float reach ) {
road = null;
frame = default;
reach = 0f;
if ( plan is null || approach.Kind != ApproachKind.Ramp ) {
return false;
}
var outward = ArchApproachAxes.Outward( approach );
var named = plan.FindRoad( approach.RoadId );
var best = float.MaxValue;
var splineEnd = approach.IsSpline
? new Vector2( approach.Nodes[^1].Position.x, approach.Nodes[^1].Position.y )
: default;
foreach ( var candidate in plan.Roads() ) {
if ( named is not null && candidate != named ) {
continue;
}
var curve = candidate.Curve();
if ( curve.Length < 1f || !curve.Nearest( approach.IsSpline ? splineEnd : approach.Origin, out var station, out var nearest ) ) {
continue;
}
if ( approach.IsSpline ) {
var side = SideOf( station, splineEnd );
var right = side == RoadSide.Right;
var edge = candidate.Reach( right, station.WidthScale );
var edgeGap = MathF.Abs( nearest - edge );
if ( edgeGap > Merges || edgeGap >= best ) {
continue;
}
best = edgeGap;
road = candidate;
frame = station;
reach = approach.Curve().Length;
continue;
}
var centre = new Vector2( station.Position.x, station.Position.y );
var gap = Vector2.Dot( centre - approach.Origin, outward ) - (candidate.HalfWidth + MathF.Max( 0f, candidate.PavementWidth ));
// Straight in front of the drive, not merely nearby.
if ( gap < 24f || gap > DriveReach || gap >= best ) {
continue;
}
best = gap;
road = candidate;
frame = station;
reach = gap;
}
return road is not null;
}
// One run, one fall: stopping short of the verge keeps its length, lands on grade.
public static bool Landing( ArchPlan plan, ArchApproachPart approach, ArchKit kit, float drag, out float run, out float height ) {
run = 0f;
height = 0f;
if ( !Serves( plan, approach, out var road, out var frame, out var reach ) || drag < reach - Merges ) {
return false;
}
run = reach;
height = frame.Position.z + Back( road, kit );
return true;
}
// Level all along, so the drive lands level.
public static float Back( ArchRoadPart road, ArchKit kit ) => road.Back( kit );
// A crossover lowers the kerb; it never breaks the pavement.
public static float KerbTop( ArchRoadPart road, IReadOnlyList<ArchRoadCrossing> crossings, bool right, float distance, ArchKit kit ) {
var full = MathF.Max( 0f, road.KerbHeight );
var dropped = Math.Clamp( kit.DroppedKerb, 0f, full );
var transition = MathF.Max( 4f, kit.KerbTransition );
var height = full;
foreach ( var crossing in crossings ) {
if ( !road.Carries( crossing.Side, right ) ) {
continue;
}
var half = Half( crossing );
var reach = MathF.Abs( distance - crossing.Distance );
if ( reach >= half + transition ) {
continue;
}
var t = reach <= half ? 0f : (reach - half) / transition;
height = MathF.Min( height, MathX.Lerp( dropped, full, t * t * (3f - 2f * t) ) );
}
return height;
}
// Full splay at the stone, tapering to the drive's own width at the plot.
public static float Reach( ArchRoadCrossing crossing, float across, float field ) {
var half = MathF.Max( 12f, crossing.Width ) * 0.5f;
var flare = MathF.Max( 0f, crossing.Splay );
var toward = field < 1f ? 0f : Math.Clamp( 1f - across / field, 0f, 1f );
return half + flare * toward * toward;
}
// The drop spans the splay at the stone, the mouth a car actually crosses.
public static float Half( ArchRoadCrossing crossing ) => Reach( crossing, 0f, 1f );
// Authored per crossover: how smooth a wide splay must look is not one map-wide number.
public static int Segments( ArchRoadCrossing crossing, ArchKit kit ) {
return Math.Clamp( crossing.SplaySegments > 0 ? crossing.SplaySegments : kit.CrossoverSegments, 2, 64 );
}
// Inverse of Reach: the road-swept section cuts on depth, not width.
public static float Depth( ArchRoadCrossing crossing, float distance, float field ) {
var half = MathF.Max( 12f, crossing.Width ) * 0.5f;
var flare = MathF.Max( 0f, crossing.Splay );
var reach = MathF.Abs( distance - crossing.Distance );
if ( reach <= half ) {
return field;
}
if ( flare < 0.01f || reach >= half + flare ) {
return 0f;
}
return field * (1f - MathF.Sqrt( (reach - half) / flare ));
}
// Nothing between crossovers reaches, so the rib collapses onto the kerb's back.
public static float Depth( IReadOnlyList<ArchRoadCrossing> paved, float distance, float field ) {
var depth = 0f;
foreach ( var crossing in paved ) {
depth = MathF.Max( depth, Depth( crossing, distance, field ) );
}
return depth;
}
// Usually none: skipping the per-face lookup is the common case.
public static List<ArchRoadCrossing> Paved( ArchRoadPart road, IReadOnlyList<ArchRoadCrossing> crossings, bool right ) {
var paved = new List<ArchRoadCrossing>();
foreach ( var crossing in crossings ) {
if ( crossing.Apron && crossing.Kind != CrossingKind.Zebra && road.Carries( crossing.Side, right ) ) {
paved.Add( crossing );
}
}
return paved;
}
// Without these stations, frame simplification collapses the dropped kerb to nothing.
public static IEnumerable<float> Stations( ArchRoadPart road, IReadOnlyList<ArchRoadCrossing> crossings, ArchKit kit ) {
if ( crossings is null ) {
yield break;
}
foreach ( var crossing in crossings ) {
foreach ( var edge in Edges( road, crossing, kit ) ) {
yield return crossing.Distance - edge;
yield return crossing.Distance + edge;
}
}
}
// Knots across the pavement: sampled along, the curve is coarse exactly where it turns.
static IEnumerable<float> Edges( ArchRoadPart road, ArchRoadCrossing crossing, ArchKit kit ) {
var field = ArchRoadKerb.FieldWidth( road );
var segments = (float)Segments( crossing, kit );
for ( var knot = 0; knot <= segments; knot++ ) {
yield return Reach( crossing, field * knot / segments, field );
}
var half = Half( crossing );
var transition = MathF.Max( 4f, kit.KerbTransition );
for ( var part = 1; part <= 4; part++ ) {
yield return half + transition * part * 0.25f;
}
}
// A crossing linked to a driveway follows it, so moving the garage moves the kerb.
public static List<ArchRoadCrossing> Crossings( ArchRoadPart road, ArchPlan plan, ArchCurve curve ) {
var resolved = new List<ArchRoadCrossing>();
foreach ( var crossing in ArchLayerGate.Enabled( road.Crossings ) ) {
var approach = crossing.ApproachId == 0 ? null : Approach( plan, crossing.ApproachId, out _, out _ );
if ( approach is null ) {
resolved.Add( crossing );
continue;
}
var outward = ArchApproachAxes.Outward( approach );
var mouth = approach.IsSpline
? new Vector2( approach.Nodes[^1].Position.x, approach.Nodes[^1].Position.y )
: approach.Origin + outward * MathF.Max( 12f, approach.Run );
if ( !curve.Nearest( mouth, out var frame, out _ ) ) {
resolved.Add( crossing );
continue;
}
resolved.Add( Cut( approach, frame, crossing.Id, crossing.Name, crossing.Kind, crossing.Apron ) );
}
Derived( road, plan, resolved );
return resolved;
}
// Every drive gets a crossover, authored or not - one to remember goes missing.
static void Derived( ArchRoadPart road, ArchPlan plan, List<ArchRoadCrossing> resolved ) {
if ( plan is null ) {
return;
}
foreach ( var room in plan.AllRooms() ) {
foreach ( var approach in room.Approaches ) {
if ( resolved.Any( entry => entry.ApproachId == approach.Id ) ) {
continue;
}
if ( !Serves( plan, approach, out var served, out var frame, out _ ) || served != road ) {
continue;
}
resolved.Add( Cut( approach, frame, approach.Id, $"Crossover{approach.Id}", CrossingKind.Driveway, true ) );
}
}
}
static ArchRoadCrossing Cut( ArchApproachPart approach, ArchFrame frame, int id, string name, CrossingKind kind, bool apron ) {
return new ArchRoadCrossing {
Id = id,
Name = name,
Kind = kind,
Distance = frame.Distance,
Width = MathF.Max( 24f, approach.IsSpline ? ArchApproachAxes.WidthAt( approach, approach.Nodes.Count - 1 ) : approach.Width ),
Splay = MathF.Max( 0f, approach.Splay ),
SplaySegments = approach.SplaySegments,
Side = SideOf( frame, approach.IsSpline
? new Vector2( approach.Nodes[^1].Position.x, approach.Nodes[^1].Position.y )
: approach.Origin ),
ApproachId = approach.Id,
Apron = apron
};
}
// Read the drive's origin, never its mouth, which can sit on the far pavement.
static RoadSide SideOf( ArchFrame frame, Vector2 point ) {
var probe = frame.Side( 1f, 0f );
var toward = new Vector2( probe.x, probe.y ) - frame.Flat;
return Vector2.Dot( point - frame.Flat, toward ) > 0f ? RoadSide.Right : RoadSide.Left;
}
public static ArchApproachPart Approach( ArchPlan plan, int id, out ArchRoom room, out ArchBuilding building ) {
foreach ( var candidate in plan.Buildings ) {
foreach ( var entry in candidate.Rooms ) {
var approach = entry.Approaches.FirstOrDefault( part => part.Id == id );
if ( approach is null ) {
continue;
}
room = entry;
building = candidate;
return approach;
}
}
room = null;
building = null;
return null;
}
}