Editor-side utility managing 1D bands (spans) used for carving architectural geometry. It stores spans with start/end scalars and planes, can add/merge spans, remove intervals (cutting out parts), compute missing gaps relative to another band set, and produce outside regions; it keeps spans sorted.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A stretch of solid, and the SURFACES that put each end where it is. The interval algebra is scalar because it
// is read at one point - the piece centre - but the answer a face is emitted from has to be the plane, or a
// surviving cap is drawn flat at whatever height the boundary measured to and a raked cut leaves a level shelf.
readonly struct ArchCarveSpan
{
public float From { get; init; }
public float To { get; init; }
public ArchCarvePlane Foot { get; init; }
public ArchCarvePlane Head { get; init; }
}
sealed class ArchCarveBands
{
readonly List<ArchCarveSpan> spans = new();
public IReadOnlyList<ArchCarveSpan> Spans => spans;
public void Add( float from, float to ) => Add( from, to, ArchCarvePlane.Level( from ), ArchCarvePlane.Level( to ) );
// A merge keeps the plane of the end that SURVIVES it: two solids sharing a band are one stretch, and the
// surface at its top belongs to whichever of them actually reached highest.
public void Add( float from, float to, ArchCarvePlane foot, ArchCarvePlane head )
{
if ( to - from < ArchCarve.Grain )
{
return;
}
var low = from;
var high = to;
var bottom = foot;
var top = head;
for ( var index = spans.Count - 1; index >= 0; index-- )
{
var span = spans[index];
if ( span.To < low - ArchCarve.Grain || span.From > high + ArchCarve.Grain )
{
continue;
}
if ( span.From < low )
{
low = span.From;
bottom = span.Foot;
}
if ( span.To > high )
{
high = span.To;
top = span.Head;
}
spans.RemoveAt( index );
}
Push( low, high, bottom, top );
}
public void Remove( float from, float to ) => Remove( from, to, ArchCarvePlane.Level( from ), ArchCarvePlane.Level( to ) );
// The threading that makes a raked cut leave a raked surface: what is left BELOW a bite is capped by the
// bite's own floor, and what is left above it stands on the bite's ceiling. Handed the scalars alone, both
// remainders were capped level at the height the boundary happened to measure at the piece centre.
public void Remove( float from, float to, ArchCarvePlane foot, ArchCarvePlane head )
{
if ( to - from < ArchCarve.Grain )
{
return;
}
for ( var index = spans.Count - 1; index >= 0; index-- )
{
var span = spans[index];
if ( span.To <= from + ArchCarve.Grain || span.From >= to - ArchCarve.Grain )
{
continue;
}
spans.RemoveAt( index );
if ( from - span.From > ArchCarve.Grain )
{
Push( span.From, from, span.Foot, foot );
}
if ( span.To - to > ArchCarve.Grain )
{
Push( to, span.To, head, span.Head );
}
}
Sort();
}
public ArchCarveBands Copy()
{
var copy = new ArchCarveBands();
copy.spans.AddRange( spans );
return copy;
}
// The stretches a cut took away - the only thing that makes a face a reveal. A gap's own surfaces are the
// inverse of the solid's round it: the floor of a hole is the head of whatever is left under it.
public IEnumerable<ArchCarveSpan> Missing( ArchCarveBands whole )
{
var gaps = whole.Copy();
foreach ( var span in spans )
{
gaps.Remove( span.From, span.To, span.Foot, span.Head );
}
return gaps.Spans;
}
// Where the neighbour was never there - the solid's outside, not a reveal.
public IEnumerable<ArchCarveSpan> Beyond( float from, float to )
{
var outside = new ArchCarveBands();
outside.Add( from, to );
foreach ( var span in spans )
{
outside.Remove( span.From, span.To, span.Foot, span.Head );
}
return outside.Spans;
}
void Push( float from, float to, ArchCarvePlane foot, ArchCarvePlane head )
{
spans.Add( new ArchCarveSpan { From = from, To = to, Foot = foot, Head = head } );
Sort();
}
void Sort() => spans.Sort( ( left, right ) => left.From.CompareTo( right.From ) );
}