Editor utility for computing arch bay segments along walkable runs. It defines ArchBay and ArchBayGate structs, computes bays from run paths with spacing and gates, yields post stations, computes total reach, finds nearest station to a point, and maps a global distance to a station.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A stretch of the walk that wants no infill - where a gate swings, or where a flight comes down through.
public readonly struct ArchBayGate
{
public float At { get; init; }
public float Width { get; init; }
public bool Spans( float from, float to ) => Width > 0f && to > At && from < At + Width;
}
// A gated bay keeps its posts and loses its infill - the gap a gate or a flight uses.
public readonly struct ArchBay
{
public ArchStation From { get; init; }
public ArchStation To { get; init; }
public bool Gated { get; init; }
public bool Last { get; init; }
public float Span => (To.Point - From.Point).Length;
public ArchBay Narrowed( float by )
{
return new ArchBay
{
From = ArchStation.Between( From.Point, To.Point, by, From.Height, From.Distance + by ),
To = ArchStation.Between( From.Point, To.Point, MathF.Max( by, Span - by ), To.Height, To.Distance - by ),
Gated = Gated,
Last = Last
};
}
}
// Distance runs across the WHOLE walk, never restarted per edge.
public static class ArchBays
{
public static List<ArchBay> Over( IEnumerable<ArchRunPath> runs, float spacing, float minimum, IReadOnlyList<ArchBayGate> gates = null )
{
var bays = new List<ArchBay>();
var travelled = 0f;
foreach ( var run in runs )
{
for ( var edge = 0; edge < run.Edges; edge++ )
{
var from = run.At( edge );
var to = run.At( edge + 1 );
var length = (to - from).Length;
// A leg too short to post still counts against the tape.
if ( length < MathF.Max( 0.05f, minimum ) )
{
travelled += length;
continue;
}
var division = ArchDivide.AtMost( length, spacing );
var closing = !run.Closed && edge == run.Edges - 1;
for ( var index = 0; index < division.Count; index++ )
{
var (start, end) = division.Bay( index );
bays.Add( new ArchBay
{
From = ArchStation.Between( from, to, start, run.Height, travelled + start ),
To = ArchStation.Between( from, to, end, run.Height, travelled + end ),
Gated = Opened( gates, travelled + start, travelled + end ),
Last = closing && index == division.Count - 1
} );
}
travelled += length;
}
}
return bays;
}
static bool Opened( IReadOnlyList<ArchBayGate> gates, float from, float to )
{
if ( gates is null )
{
return false;
}
foreach ( var gate in gates )
{
if ( gate.Spans( from, to ) )
{
return true;
}
}
return false;
}
// A closed run needs no closing post - the first stands on the return point.
public static IEnumerable<ArchStation> Posts( IEnumerable<ArchBay> bays )
{
foreach ( var bay in bays )
{
yield return bay.From;
if ( bay.Last )
{
yield return bay.To;
}
}
}
public static float Reach( IEnumerable<ArchRunPath> runs ) => runs.Sum( run => run.Length );
// The inverse of Station, and it lives beside it: a flight already standing on the deck says where the
// balustrade must open, so the gap and the steps cannot be two opinions about one gesture.
public static bool Nearest( IEnumerable<ArchRunPath> runs, Vector2 point, out ArchStation station )
{
var travelled = 0f;
var closest = float.MaxValue;
station = default;
foreach ( var run in runs )
{
for ( var edge = 0; edge < run.Edges; edge++ )
{
var from = run.At( edge );
var to = run.At( edge + 1 );
var span = to - from;
var length = span.Length;
if ( length < 0.05f )
{
continue;
}
var along = Math.Clamp( Vector2.Dot( point - from, span / length ), 0f, length );
var gap = (from + span / length * along - point).Length;
if ( gap < closest )
{
closest = gap;
station = ArchStation.Between( from, to, along, run.Height, travelled + along );
}
travelled += length;
}
}
return closest < float.MaxValue;
}
public static bool Station( IEnumerable<ArchRunPath> runs, float distance, out ArchStation station )
{
var travelled = 0f;
foreach ( var run in runs )
{
if ( run.Station( distance - travelled, out var found ) )
{
station = found with { Distance = distance };
return true;
}
travelled += run.Length;
}
station = default;
return false;
}
}