Editor/Road/ArchJunction.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchRoadSpan {
public float From { get; init; }
public float To { get; init; }
public float Length => MathF.Max( 0f, To - From );
}
// DERIVED: a crossing IS a junction; legs ordered by bearing, a road through contributes two.
public static class ArchJunction {
const float DetectStep = 48f;
internal const float ShortestLeg = 24f;
public static List<ArchJunctionNode> Derived( ArchPlan plan, ArchKit kit ) {
return new ArchJunctionDerivationService( plan, kit ).Create();
}
// No section sweeps through a node's mouth, or it drives through the apron.
public static List<ArchRoadSpan> Spans( ArchRoadPart road, IReadOnlyList<ArchJunctionNode> nodes, float length ) {
return new ArchJunctionSpanService( nodes, length ).For( road );
}
// End stations land on the cuts, where the junction expects the face.
public static List<ArchFrame> Clipped( ArchCurve curve, IReadOnlyList<ArchFrame> frames, ArchRoadSpan span ) {
var clipped = frames
.Where( frame => frame.Distance > span.From + 1f && frame.Distance < span.To - 1f )
.ToList();
if ( curve.Sample( span.From, out var start ) ) {
clipped.Insert( 0, start );
}
if ( curve.Sample( span.To, out var end ) ) {
clipped.Add( end );
}
return clipped;
}
internal static ArchJunctionNode Clustered( List<ArchJunctionNode> nodes, Vector2 at, float height, ArchKit kit ) {
foreach ( var node in nodes ) {
if ( (node.Centre - at).Length <= MathF.Max( 24f, kit.JunctionSpread ) ) {
return node;
}
}
var fresh = new ArchJunctionNode { Centre = at, Height = height };
nodes.Add( fresh );
return fresh;
}
// Crossing in plan is not meeting: different heights pass over, never cut back.
internal static IEnumerable<(Vector2 At, float Height, float Along, float Across)> Meetings( ArchRoadWalk first, ArchRoadWalk second, ArchKit kit ) {
if ( !first.Bounds.Overlaps( second.Bounds ) ) {
yield break;
}
var level = MathF.Max( 1f, kit.JunctionLevel );
for ( var here = 0; here < first.Line.Count - 1; here++ ) {
for ( var there = 0; there < second.Line.Count - 1; there++ ) {
if ( !Crosses( first.Line[here], first.Line[here + 1], second.Line[there], second.Line[there + 1],
out var at, out var a, out var b ) ) {
continue;
}
var along = MathX.Lerp( first.Stations[here], first.Stations[here + 1], a );
var across = MathX.Lerp( second.Stations[there], second.Stations[there + 1], b );
var over = first.Height( along );
var under = second.Height( across );
if ( MathF.Abs( over - under ) > level ) {
continue;
}
yield return (at, (over + under) * 0.5f, along, across);
}
}
}
static bool Crosses( Vector2 from, Vector2 to, Vector2 other, Vector2 end, out Vector2 at, out float first, out float second ) {
at = default;
first = 0f;
second = 0f;
var span = to - from;
var reach = end - other;
var denominator = span.x * reach.y - span.y * reach.x;
if ( MathF.Abs( denominator ) < 0.0001f ) {
return false;
}
var gap = other - from;
first = (gap.x * reach.y - gap.y * reach.x) / denominator;
second = (gap.x * span.y - gap.y * span.x) / denominator;
if ( first < 0f || first > 1f || second < 0f || second > 1f ) {
return false;
}
at = from + span * first;
return true;
}
}
public sealed class ArchJunctionDerivationService {
readonly ArchPlan plan;
readonly ArchKit kit;
readonly List<ArchRoadWalk> roads;
readonly List<ArchJunctionNode> nodes = new();
public ArchJunctionDerivationService( ArchPlan plan, ArchKit kit ) {
this.plan = plan;
this.kit = kit;
roads = plan.Roads()
.Select( road => new ArchRoadWalk( road ) )
.Where( walk => walk.Usable )
.ToList();
}
public List<ArchJunctionNode> Create() {
for ( var index = 0; index < roads.Count; index++ ) {
for ( var other = index + 1; other < roads.Count; other++ ) {
Connect( roads[index], roads[other] );
}
}
foreach ( var node in nodes ) {
node.Resolve( kit );
}
return nodes.Where( node => node.Legs.Count >= 3 ).ToList();
}
void Connect( ArchRoadWalk first, ArchRoadWalk second ) {
foreach ( var meeting in ArchJunction.Meetings( first, second, kit ) ) {
var node = ArchJunction.Clustered( nodes, meeting.At, meeting.Height, kit );
node.Take( first, meeting.Along, ArchJunction.ShortestLeg );
node.Take( second, meeting.Across, ArchJunction.ShortestLeg );
}
}
}
public sealed class ArchJunctionSpanService {
readonly IReadOnlyList<ArchJunctionNode> nodes;
readonly float length;
public ArchJunctionSpanService( IReadOnlyList<ArchJunctionNode> nodes, float length ) {
this.nodes = nodes;
this.length = length;
}
public List<ArchRoadSpan> For( ArchRoadPart road ) {
var mouths = nodes
.Select( node => node.Mouth( road ) )
.Where( mouth => mouth.Length > 0f )
.OrderBy( mouth => mouth.From )
.ToList();
var spans = new List<ArchRoadSpan>();
var open = 0f;
foreach ( var mouth in mouths ) {
if ( mouth.From > open + ArchJunction.ShortestLeg ) {
spans.Add( new ArchRoadSpan { From = open, To = MathF.Min( mouth.From, length ) } );
}
open = MathF.Max( open, mouth.To );
}
if ( length > open + ArchJunction.ShortestLeg ) {
spans.Add( new ArchRoadSpan { From = open, To = length } );
}
return spans;
}
}
// Flattened walk with a station per point, so crossings report as distances along the curve.
public sealed class ArchRoadWalk {
public ArchRoadWalk( ArchRoadPart road ) {
Road = road;
Curve = road.Curve();
if ( !Curve.IsUsable ) {
return;
}
var frames = Curve.Walk( 48f );
Line = frames.Select( frame => frame.Flat ).ToList();
Stations = frames.Select( frame => frame.Distance ).ToList();
Length = Curve.Length;
Bounds = BBox.FromPoints( frames.Select( frame => frame.Position ) ).Grow( 8f );
Usable = Line.Count >= 2;
}
public ArchRoadPart Road { get; }
public ArchCurve Curve { get; }
public List<Vector2> Line { get; } = new();
public List<float> Stations { get; } = new();
public float Length { get; }
public BBox Bounds { get; }
public bool Usable { get; }
public float Height( float station ) => Curve.Sample( station, out var frame ) ? frame.Position.z : 0f;
}