Editor/Carve/ArchCarveCells.Sweep.cs

Editor-side static partial class methods for carving architecture cells by sweeping volumes along one axis. It computes edge segments from volume footprints, splits edges at crossings, builds x-coordinate stations, assembles trapezoid strips between successive stations, and returns loops where the strip centroid is inside any volume.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Swept in ONE direction, not crossed into a grid. A station is taken at every corner and every crossing, and each
// slab between two stations is sliced only by the edges that run the whole way across it - so a piece is the
// trapezoid between two edges. Every edge lands on a piece boundary end for end, which is what the bands and the
// adjacency need, and no piece straddles a boundary.
//
// A grid asks for the same guarantee in two directions at once and then has to cut its tiles again with every
// slanted edge, which is where the wedges came from: a turned hole donated four x lines, four y lines and four
// diagonals, so the ring of material round it came back as sixteen tiles chopped into triangles and n-gons.
static partial class ArchCarveCells
{
	static List<List<Vector2>> Swept( IReadOnlyList<ArchCarveVolume> volumes )
	{
		var edges = Crossed( volumes );
		var stations = Stations( edges );
		var loops = new List<List<Vector2>>();

		for ( var index = 0; index + 1 < stations.Count; index++ )
		{
			var from = stations[index];
			var to = stations[index + 1];

			if ( to - from < ArchCarve.Grain )
			{
				continue;
			}

			var middle = (from + to) * 0.5f;
			var across = edges.Where( edge => Spans( edge, from, to ) ).OrderBy( edge => Height( edge, middle ) ).ToList();

			for ( var lower = 0; lower + 1 < across.Count; lower++ )
			{
				var strip = Strip( across[lower], across[lower + 1], from, to );

				if ( Fills( volumes, strip ) )
				{
					loops.Add( strip );
				}
			}
		}

		return loops;
	}

	// An edge steeper than the grain runs across no slab at all - it only donates the station its ends stand on.
	static bool Spans( (Vector2 From, Vector2 To) edge, float from, float to )
	{
		return edge.To.x - edge.From.x > ArchCarve.Grain
			&& edge.From.x <= from + ArchCarve.Grain
			&& edge.To.x >= to - ArchCarve.Grain;
	}

	static float Height( (Vector2 From, Vector2 To) edge, float at )
	{
		var span = edge.To.x - edge.From.x;

		return edge.From.y + (edge.To.y - edge.From.y) * Math.Clamp( (at - edge.From.x) / span, 0f, 1f );
	}

	static List<Vector2> Strip( (Vector2 From, Vector2 To) lower, (Vector2 From, Vector2 To) upper, float from, float to )
	{
		return new List<Vector2>
		{
			new( from, Height( lower, from ) ),
			new( to, Height( lower, to ) ),
			new( to, Height( upper, to ) ),
			new( from, Height( upper, from ) )
		};
	}

	// The centroid of a trapezoid stands inside it, so one containment test settles the whole strip. A strip
	// inside nothing at all is left out: the flank against a neighbour that was never there reads the same as
	// the flank against no neighbour, which is the solid's own outside either way.
	static bool Fills( IReadOnlyList<ArchCarveVolume> volumes, IReadOnlyList<Vector2> strip )
	{
		var centre = strip.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / strip.Count;

		return volumes.Any( volume => volume.Covers( centre ) );
	}

	// Split where they cross, so a crossing is a station rather than something that happens inside a slab.
	static List<(Vector2 From, Vector2 To)> Crossed( IReadOnlyList<ArchCarveVolume> volumes )
	{
		var split = new List<(Vector2, Vector2)>();

		foreach ( var footprint in volumes.Select( volume => volume.Footprint ) )
		{
			for ( var index = 0; index < footprint.Count; index++ )
			{
				var from = footprint[index];
				var to = footprint[(index + 1) % footprint.Count];
				var marks = new List<float> { 0f, 1f };

				foreach ( var other in volumes )
				{
					marks.AddRange( ArchFootprint.Crossings( other.Footprint, from, to ) );
				}

				marks.Sort();

				for ( var mark = 0; mark + 1 < marks.Count; mark++ )
				{
					if ( marks[mark + 1] - marks[mark] < 0.0001f )
					{
						continue;
					}

					var start = Vector2.Lerp( from, to, marks[mark] );
					var finish = Vector2.Lerp( from, to, marks[mark + 1] );

					split.Add( start.x <= finish.x ? (start, finish) : (finish, start) );
				}
			}
		}

		return split;
	}

	static List<float> Stations( IReadOnlyList<(Vector2 From, Vector2 To)> edges )
	{
		return edges
			.SelectMany( edge => new[] { edge.From.x, edge.To.x } )
			.Select( value => MathF.Round( value / ArchCarve.Grain ) * ArchCarve.Grain )
			.Distinct()
			.OrderBy( value => value )
			.ToList();
	}
}