Editor utility for carving and tessellating 2D polygon loops into quads/triangles for an architectural carving tool. It splits complex loops into wedges, fuses triangles into quads, tests ring/hole relations between two volumes, simulates break/jitter on a hole, stitches faces between inner and outer loops, and validates tiling and convexity.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One shape inside another is a RING, and a ring is stitched corner to corner: each inner corner is chorded to the
// outer corner standing nearest it round the same centre, so what is left round a turned hole is four trapezoids
// rather than the strips a sweep has to cut it into. It is what a modeller draws by hand, and it holds its four
// faces however far the hole is turned.
//
// Nothing here is taken on trust. A chord across a reflex corner leaves geometry outside the solid it was carved
// from, so the stitch is MEASURED against the area it has to cover and handed back only when it tiles it exactly -
// otherwise the caller sweeps instead.
static class ArchCarveQuads
{
const int BreakSalt = 71;
// Twice the finest coordinate the tool has, which is past the furthest a snap can carry a corner off a
// turned loop - under it a break would be rounding rather than damage.
const float LeastBite = ArchGridService.FinestSize * 2f;
// A face with more corners than a quad, cut into quads and the odd triangle using ONLY the corners it already
// has - so every boundary edge survives and the neighbours across them still meet it edge for edge. Triangles
// first because a triangulation is easy to be sure of, then pairs of them fused back into the squarest quads
// available, which is how a quad mesher gets its quads without inventing vertices to hang them on.
public static IEnumerable<List<Vector2>> Split( List<Vector2> loop )
{
if ( loop.Count <= 4 )
{
return new[] { loop };
}
var wedges = Wedges( loop );
return wedges.Count == 0 ? new[] { loop } : Fused( wedges );
}
static List<List<Vector2>> Wedges( List<Vector2> loop )
{
var remaining = new List<Vector2>( loop );
var wedges = new List<List<Vector2>>();
while ( remaining.Count > 3 )
{
var clipped = Clipped( remaining );
if ( clipped < 0 )
{
return new List<List<Vector2>>();
}
var behind = remaining[(clipped - 1 + remaining.Count) % remaining.Count];
var ahead = remaining[(clipped + 1) % remaining.Count];
wedges.Add( new List<Vector2> { behind, remaining[clipped], ahead } );
remaining.RemoveAt( clipped );
}
wedges.Add( remaining );
return wedges;
}
// The corner to take off: convex, and with none of the rest standing inside the wedge it would leave.
static int Clipped( List<Vector2> loop )
{
for ( var index = 0; index < loop.Count; index++ )
{
var behind = loop[(index - 1 + loop.Count) % loop.Count];
var here = loop[index];
var ahead = loop[(index + 1) % loop.Count];
if ( Turn( behind, here, ahead ) < ArchCarve.Grain )
{
continue;
}
var wedge = new List<Vector2> { behind, here, ahead };
if ( loop.Where( corner => !Corner( wedge, corner ) ).Any( corner => ArchFootprint.Contains( wedge, corner ) ) )
{
continue;
}
return index;
}
return -1;
}
static bool Corner( List<Vector2> wedge, Vector2 corner ) => wedge.Any( held => (held - corner).Length < ArchCarve.Grain );
static float Turn( Vector2 behind, Vector2 here, Vector2 ahead )
{
var into = here - behind;
var away = ahead - here;
return into.x * away.y - into.y * away.x;
}
// Squarest first, so the pairing spends its triangles on the quads worth having rather than on whichever two
// happened to be adjacent. Each triangle goes into at most one quad; what is left over stays a triangle.
static List<List<Vector2>> Fused( List<List<Vector2>> wedges )
{
var taken = new bool[wedges.Count];
var quads = new List<List<Vector2>>();
var candidates = new List<(float Cost, int Left, int Right, List<Vector2> Quad)>();
for ( var left = 0; left < wedges.Count; left++ )
{
for ( var right = left + 1; right < wedges.Count; right++ )
{
if ( Fuse( wedges[left], wedges[right] ) is { } quad )
{
candidates.Add( (Skew( quad ), left, right, quad) );
}
}
}
foreach ( var (_, left, right, quad) in candidates.OrderBy( candidate => candidate.Cost ) )
{
if ( taken[left] || taken[right] )
{
continue;
}
taken[left] = true;
taken[right] = true;
quads.Add( quad );
}
for ( var index = 0; index < wedges.Count; index++ )
{
if ( !taken[index] )
{
quads.Add( wedges[index] );
}
}
return quads;
}
// The quad two triangles either side of a shared edge make, or null when they share no edge or the result
// would fold back on itself.
static List<Vector2> Fuse( List<Vector2> left, List<Vector2> right )
{
for ( var index = 0; index < left.Count; index++ )
{
var from = left[index];
var to = left[(index + 1) % left.Count];
if ( !Corner( right, from ) || !Corner( right, to ) )
{
continue;
}
var beyond = right.FirstOrDefault( corner => !Corner( new List<Vector2> { from, to }, corner ) );
var behind = left[(index + 2) % left.Count];
var quad = new List<Vector2> { behind, from, beyond, to };
return Convex( quad ) ? quad : null;
}
return null;
}
static bool Convex( List<Vector2> quad )
{
for ( var index = 0; index < quad.Count; index++ )
{
var behind = quad[(index - 1 + quad.Count) % quad.Count];
var ahead = quad[(index + 1) % quad.Count];
if ( Turn( behind, quad[index], ahead ) < ArchCarve.Grain )
{
return false;
}
}
return true;
}
// How far off square its corners stand - a rectangle scores nothing, a sliver scores four.
static float Skew( List<Vector2> quad )
{
var skew = 0f;
for ( var index = 0; index < quad.Count; index++ )
{
var into = (quad[index] - quad[(index - 1 + quad.Count) % quad.Count]).Normal;
var away = (quad[(index + 1) % quad.Count] - quad[index]).Normal;
skew += MathF.Abs( Vector2.Dot( into, away ) );
}
return skew;
}
public static List<List<Vector2>> Ring( IReadOnlyList<ArchCarveVolume> volumes )
{
if ( volumes.Count != 2 )
{
return null;
}
var first = Wound( volumes[0].Footprint );
var second = Wound( volumes[1].Footprint );
if ( first is null || second is null )
{
return null;
}
var host = Holds( first, second ) ? first : Holds( second, first ) ? second : null;
if ( host is null )
{
return null;
}
var standing = ReferenceEquals( host, first );
var hole = Broken( host, standing ? second : first, standing ? volumes[1].Break : volumes[0].Break );
var faces = Stitched( host, hole );
return faces is not null && Tiles( faces, host ) ? faces : null;
}
static List<Vector2> Wound( IReadOnlyList<Vector2> footprint )
{
return footprint is { Count: >= 3 } ? ArchFootprint.Wind( footprint.ToList() ) : null;
}
// Strictly inside: every corner enclosed and every edge inside along its whole length, so a hole that touches
// or crosses the boundary is no ring and is left to the sweep.
static bool Holds( List<Vector2> outer, List<Vector2> inner )
{
if ( inner.Any( corner => !ArchFootprint.Contains( outer, corner ) ) )
{
return false;
}
for ( var index = 0; index < inner.Count; index++ )
{
var spans = ArchFootprint.Inside( outer, inner[index], inner[(index + 1) % inner.Count] ).ToList();
if ( spans.Count != 1 || spans[0].From > 0.001f || spans[0].To < 0.999f )
{
return false;
}
}
return true;
}
// Disrepair only ever subtracts. A break pulls each CONVEX corner of the hole in along its own bisector - never
// out into material that was standing - so the loop it hands back stands inside the loop that was dragged and
// the ring round it can only lose area. A reflex corner is left where it is: moved inward it would push the
// edges either side of it out past the clean loop, which is the one way an inward offset could add.
static List<Vector2> Broken( List<Vector2> host, List<Vector2> hole, ArchCarveBreak breaking )
{
if ( !breaking.Breaks )
{
return hole;
}
var bite = Bite( host, hole, breaking.Jitter );
if ( bite < LeastBite )
{
return hole;
}
var broken = new List<Vector2>();
for ( var index = 0; index < hole.Count; index++ )
{
var behind = hole[(index - 1 + hole.Count) % hole.Count];
var here = hole[index];
var ahead = hole[(index + 1) % hole.Count];
var inward = Turn( behind, here, ahead ) < ArchCarve.Grain ? Vector2.Zero : Inward( behind, here, ahead );
var offset = bite * ArchBarrierShape.Noise( breaking.Seed, index, BreakSalt );
// A corner either bites or stays exactly where it was authored - never snapped on the spot. A reflex
// corner, a spike, and a bite the grid would round away all stay: that rounding is the one thing that
// could carry a corner back OUT of the loop it was dragged with.
var bitten = inward.Length > 0.5f && offset >= LeastBite;
broken.Add( bitten ? ArchGridService.Fine( here + inward * offset ) : here );
}
return broken;
}
// No further than a third of the way across the NARROWEST material standing round the hole, and no further than
// a quarter of the hole's own narrowest span. Past the first, a ring piece's centre lands inside the cut and the
// whole piece goes with it - which is the cut taking material nobody dragged it over. Past the second, the loop
// folds through itself.
static float Bite( List<Vector2> host, List<Vector2> hole, float jitter )
{
ArchFootprint.Bounds( hole, out var min, out var max );
var gap = MathF.Min( Gap( host, hole ), Gap( hole, host ) );
return MathF.Min( jitter, MathF.Min( gap / 3f, MathF.Min( max.x - min.x, max.y - min.y ) * 0.25f ) );
}
// Corner to boundary, both ways round: the closest the two loops come is always a corner of one against an
// edge of the other, so the pair of scans measures the ring at its thinnest wherever that falls.
static float Gap( List<Vector2> loop, List<Vector2> against )
{
var boundary = ArchRunPath.Of( loop, 0f, true );
var gap = float.MaxValue;
foreach ( var corner in against )
{
if ( boundary.Nearest( corner, out _, out var reach ) )
{
gap = MathF.Min( gap, reach );
}
}
return gap;
}
// Wound by ArchFootprint.Wind, so the loop turns anticlockwise and its inside is to the LEFT of every edge.
static Vector2 Inward( Vector2 behind, Vector2 here, Vector2 ahead )
{
var into = (here - behind).Normal;
var away = (ahead - here).Normal;
var bisector = new Vector2( -into.y, into.x ) + new Vector2( -away.y, away.x );
return bisector.Length < 0.0001f ? Vector2.Zero : bisector.Normal;
}
// One face per inner edge, taking the outer edges the chords at its two ends stepped over: none and it is a
// triangle, one and it is the quad this exists for, more and the surplus outer edges fan off the corner the
// second chord landed on.
static List<List<Vector2>> Stitched( List<Vector2> host, List<Vector2> hole )
{
var reach = Reach( host, hole );
if ( Around( reach, host.Count ) != host.Count )
{
return null;
}
var faces = new List<List<Vector2>> { new( hole ) };
for ( var index = 0; index < hole.Count; index++ )
{
var next = (index + 1) % hole.Count;
var corner = reach[index];
var step = Step( reach[index], reach[next], host.Count );
if ( step == 0 )
{
faces.Add( new List<Vector2> { host[corner], hole[next], hole[index] } );
continue;
}
faces.Add( new List<Vector2> { host[corner], host[(corner + 1) % host.Count], hole[next], hole[index] } );
for ( var extra = 1; extra < step; extra++ )
{
var at = (corner + extra) % host.Count;
faces.Add( new List<Vector2> { host[at], host[(at + 1) % host.Count], hole[next] } );
}
}
return faces;
}
// Winding exactly once round the outer loop is what keeps the chords from crossing each other.
static int Around( List<int> reach, int corners )
{
var travelled = 0;
for ( var index = 0; index < reach.Count; index++ )
{
travelled += Step( reach[index], reach[(index + 1) % reach.Count], corners );
}
return travelled;
}
static int Step( int from, int to, int corners ) => (to - from + corners) % corners;
// Which outer corner each inner corner chords to. Corner for corner when the two loops have the same number
// of them - every offset tried and the shortest set of chords kept, because the pairing wanted is by CORNER:
// a hole in the end of a long slab chords to the two corners beside it, which angles about the hole's own
// centre get wrong the moment it sits off centre. That mistake spends two chords on one corner and leaves a
// triangle and a four-corner fan where four quads were there to be had.
static List<int> Reach( List<Vector2> host, List<Vector2> hole )
{
if ( host.Count == hole.Count )
{
return Paired( host, hole );
}
var centre = hole.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / hole.Count;
return hole.Select( corner => Nearest( host, centre, corner ) ).ToList();
}
// Both loops wind the same way, so an offset is the whole choice - there are only as many pairings as corners.
static List<int> Paired( List<Vector2> host, List<Vector2> hole )
{
var best = 0;
var shortest = float.MaxValue;
for ( var offset = 0; offset < host.Count; offset++ )
{
var total = 0f;
for ( var index = 0; index < hole.Count; index++ )
{
total += (host[(index + offset) % host.Count] - hole[index]).Length;
}
if ( total < shortest )
{
shortest = total;
best = offset;
}
}
return Enumerable.Range( 0, hole.Count ).Select( index => (index + best) % host.Count ).ToList();
}
static int Nearest( List<Vector2> host, Vector2 centre, Vector2 corner )
{
var wanted = MathF.Atan2( corner.y - centre.y, corner.x - centre.x );
var best = 0;
var closest = float.MaxValue;
for ( var index = 0; index < host.Count; index++ )
{
var angle = MathF.Atan2( host[index].y - centre.y, host[index].x - centre.x ) - wanted;
var apart = MathF.Abs( MathF.Atan2( MathF.Sin( angle ), MathF.Cos( angle ) ) );
if ( apart < closest )
{
closest = apart;
best = index;
}
}
return best;
}
// The hole's own loop is one of the faces, so a stitch that tiles covers the whole host exactly once. Areas
// that add up while a face hangs outside would need another face folded back over the shortfall, which the
// per-face simplicity and containment tests rule out.
static bool Tiles( List<List<Vector2>> faces, List<Vector2> host )
{
var wanted = MathF.Abs( ArchFootprint.SignedArea( host ) );
var covered = 0f;
foreach ( var face in faces )
{
var area = ArchFootprint.SignedArea( face );
var centre = face.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / face.Count;
if ( area < ArchCarve.Grain || !ArchFootprint.Contains( host, centre ) )
{
return false;
}
if ( face.Count > 3 && !ArchFootprint.IsSimple( face ) )
{
return false;
}
covered += area;
}
return MathF.Abs( covered - wanted ) < MathF.Max( 1f, wanted * 0.0001f );
}
}