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.
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();
}
}