Editor/Carve/ArchCarveBands.cs

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.

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