Editor/Porch/ArchBays.cs
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;
}
}