Editor utility for bridge placement. Computes blocking ranges where roads intersect a bridge frame and returns cleared gaps along the bridge span for placing piers.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A stretch across a bent a pier may stand in, left to right - same sense as ArchFrame.Across.
public readonly struct ArchGap
{
public float From { get; init; }
public float To { get; init; }
public float Width => MathF.Max( 0f, To - From );
public float Middle => (From + To) * 0.5f;
public bool Holds( float width ) => Width >= MathF.Max( 1f, width );
}
// The junction derivation looks away at JunctionLevel - without this a pier lands in the fast lane it flies over.
public static class ArchBridgeGap
{
// Walk cells only just touch - a bend's outside leaves a wedge of daylight; they overlap instead.
const float Overlap = 12f;
// Resolved once per bridge, not per candidate station - which roads pass below is a per-cell soffit question.
public static List<ArchRoadWalk> Roads( ArchPlan plan )
{
if ( plan is null )
{
return new List<ArchRoadWalk>();
}
return plan.Roads()
.Select( road => new ArchRoadWalk( road ) )
.Where( walk => walk.Usable )
.ToList();
}
// A pier's depth along the road is part of the test - a box clears a carriageway by its corner.
public static List<ArchGap> Across(
IReadOnlyList<ArchRoadWalk> roads,
ArchFrame frame,
float soffit,
float depth,
ArchGap domain,
ArchKit kit )
{
var blocked = new List<ArchGap>();
foreach ( var walk in roads )
{
Blocked( walk, frame, soffit, depth, kit, blocked );
}
return Cleared( blocked, domain );
}
// The carried road excludes itself by deck height, not by name - loops under their own deck stay honest.
static void Blocked( ArchRoadWalk walk, ArchFrame frame, float soffit, float depth, ArchKit kit, List<ArchGap> blocked )
{
var origin = frame.Flat;
var across = new Vector2( frame.Across.x, frame.Across.y );
var along = new Vector2( frame.Along.x, frame.Along.y );
var level = MathF.Max( 1f, kit.JunctionLevel );
var margin = MathF.Max( 0f, kit.PierClearance );
for ( var index = 0; index < walk.Line.Count - 1; index++ )
{
var middle = MathX.Lerp( walk.Stations[index], walk.Stations[index + 1], 0.5f );
if ( walk.Height( middle ) > soffit - level )
{
continue;
}
var start = walk.Line[index];
var span = walk.Line[index + 1] - start;
var length = span.Length;
if ( length < 0.01f )
{
continue;
}
var direction = span / length;
var normal = new Vector2( -direction.y, direction.x );
var reach = walk.Road.Reach() + margin + MathF.Abs( Vector2.Dot( along, normal ) ) * depth * 0.5f;
if ( !Band( Vector2.Dot( origin - start, normal ), Vector2.Dot( across, normal ), -reach, reach, out var lower, out var upper ) )
{
continue;
}
if ( !Band( Vector2.Dot( origin - start, direction ), Vector2.Dot( across, direction ), -Overlap, length + Overlap, out var near, out var far ) )
{
continue;
}
var opens = MathF.Max( lower, near );
var closes = MathF.Min( upper, far );
if ( closes > opens )
{
blocked.Add( new ArchGap { From = opens, To = closes } );
}
}
}
// A rate of nothing is the parallel case - a road running ALONG the bent: whole line or none.
static bool Band( float offset, float rate, float lower, float upper, out float from, out float to )
{
from = float.MinValue;
to = float.MaxValue;
if ( MathF.Abs( rate ) < 0.0001f )
{
return offset >= lower && offset <= upper;
}
var first = (lower - offset) / rate;
var second = (upper - offset) / rate;
from = MathF.Min( first, second );
to = MathF.Max( first, second );
return true;
}
static List<ArchGap> Cleared( List<ArchGap> blocked, ArchGap domain )
{
var gaps = new List<ArchGap>();
var open = domain.From;
foreach ( var band in blocked.OrderBy( band => band.From ) )
{
if ( band.To <= domain.From || band.From >= domain.To )
{
continue;
}
var opens = MathF.Max( domain.From, band.From );
if ( opens > open )
{
gaps.Add( new ArchGap { From = open, To = opens } );
}
open = MathF.Max( open, MathF.Min( domain.To, band.To ) );
}
if ( domain.To > open )
{
gaps.Add( new ArchGap { From = open, To = domain.To } );
}
return gaps;
}
}