Editor/Road/ArchBridgeGap.cs

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.

File Access
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;
	}
}