Editor/Carve/ArchCarveCells.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One piece's edge, kept whole so an adjacency can be MEASURED rather than taken from the line it hashed to.
readonly struct ArchCarveEdge {
public ArchCarvePiece Piece { get; init; }
public int Index { get; init; }
public Vector2 From { get; init; }
public Vector2 To { get; init; }
public float Low { get; init; }
public float High { get; init; }
// The line it hashed to, kept rather than re-derived: the adjacency scan asked for it a second time per edge.
public (long, long, long) Line { get; init; }
}
// One stretch of an edge; a T-junction or a neighbour change splits an edge into several.
readonly struct ArchCarveTouch {
public ArchCarvePiece Piece { get; init; }
public float From { get; init; }
public float To { get; init; }
}
sealed class ArchCarvePiece {
public List<Vector2> Loop { get; init; }
public Vector2 Centre { get; init; }
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
public ArchCarveBands Whole { get; set; }
public ArchCarveBands Standing { get; set; }
public List<ArchCarveTouch>[] Beside { get; set; }
}
sealed class ArchCarveGrid {
public List<ArchCarvePiece> Pieces { get; init; } = new();
// Weld table — all manufactured points snap to the grid to prevent slivers from rounding
readonly Dictionary<(long, long), Vector2> claimed = new();
public const float WeldReach = ArchGridService.FinestSize;
public Vector2 Welded( Vector2 point ) {
var cell = ((long)MathF.Floor( point.x / WeldReach ), (long)MathF.Floor( point.y / WeldReach ));
for ( var dx = -1; dx <= 1; dx++ ) {
for ( var dy = -1; dy <= 1; dy++ ) {
if ( claimed.TryGetValue( (cell.Item1 + dx, cell.Item2 + dy), out var held ) && (held - point).Length < WeldReach ) {
return held;
}
}
}
claimed[cell] = point;
return point;
}
}
static class ArchCarveFrame {
public static Vector2 Turn( Vector2 point, float yaw ) {
if ( yaw == 0f ) {
return point;
}
var radians = yaw.DegreeToRadian();
var sin = MathF.Sin( radians );
var cos = MathF.Cos( radians );
return new Vector2( point.x * cos - point.y * sin, point.x * sin + point.y * cos );
}
public static Vector3 Turn( Vector3 point, float yaw ) {
var flat = Turn( new Vector2( point.x, point.y ), yaw );
return new Vector3( flat.x, flat.y, point.z );
}
// Orthogonal: turning the origin and the fall leaves every height the plane answers unchanged.
public static ArchCarvePlane Turn( ArchCarvePlane plane, float yaw ) {
return new ArchCarvePlane {
Datum = plane.Datum,
Origin = Turn( plane.Origin, yaw ),
Fall = Turn( plane.Fall, yaw )
};
}
public static ArchCarveCell Turn( ArchCarveCell cell, float yaw ) {
var loop = cell.Loop.Select( point => Turn( point, yaw ) ).ToList();
ArchFootprint.Bounds( loop, out var min, out var max );
return new ArchCarveCell {
Min = min,
Max = max,
Centre = Turn( cell.Centre, yaw ),
Loop = loop,
From = cell.From,
To = cell.To,
Foot = Turn( cell.Foot, yaw ),
Head = Turn( cell.Head, yaw )
};
}
}
// The arrangement every carve is read off: pieces that tile the volumes and never straddle one's boundary, each
// knowing the neighbours it shares an edge with, end for end.
static partial class ArchCarveCells {
public static ArchCarveGrid Over( IReadOnlyList<ArchCarveVolume> volumes ) {
var grid = new ArchCarveGrid();
var loops = Fewest( volumes );
Weld( grid, loops );
foreach ( var loop in loops ) {
var tidied = Tidied( loop );
if ( Real( tidied ) ) {
grid.Pieces.Add( Piece( tidied ) );
}
}
Link( grid.Pieces );
return grid;
}
// Ring produces true quads; sweep produces rectangles with mid-side vertices
static List<List<Vector2>> Fewest( IReadOnlyList<ArchCarveVolume> volumes ) {
return ArchCarveQuads.Ring( volumes ) ?? Swept( volumes );
}
static void Weld( ArchCarveGrid grid, List<List<Vector2>> loops ) {
foreach ( var loop in loops ) {
for ( var index = 0; index < loop.Count; index++ ) {
loop[index] = grid.Welded( loop[index] );
}
}
}
static List<Vector2> Tidied( List<Vector2> loop ) {
var kept = new List<Vector2>();
foreach ( var point in loop ) {
Push( kept, point );
}
return kept;
}
static ArchCarvePiece Piece( List<Vector2> loop ) {
ArchFootprint.Bounds( loop, out var min, out var max );
var centre = Vector2.Zero;
foreach ( var point in loop ) {
centre += point;
}
return new ArchCarvePiece {
Loop = loop,
Centre = centre / loop.Count,
Min = min,
Max = max
};
}
static void Link( List<ArchCarvePiece> pieces ) {
var edges = new List<ArchCarveEdge>();
var lines = new Dictionary<(long, long, long), List<int>>();
foreach ( var piece in pieces ) {
piece.Beside = new List<ArchCarveTouch>[piece.Loop.Count];
for ( var index = 0; index < piece.Loop.Count; index++ ) {
piece.Beside[index] = new List<ArchCarveTouch>();
var from = piece.Loop[index];
var to = piece.Loop[(index + 1) % piece.Loop.Count];
var line = Line( from, to );
if ( !lines.TryGetValue( line.Key, out var along ) ) {
along = new List<int>();
lines[line.Key] = along;
}
along.Add( edges.Count );
edges.Add( new ArchCarveEdge {
Piece = piece,
Index = index,
From = from,
To = to,
Low = Vector2.Dot( from, line.Along ),
High = Vector2.Dot( to, line.Along ),
Line = line.Key
} );
}
}
// Check neighbouring buckets — rounding can split collinear edges across boundaries
for ( var index = 0; index < edges.Count; index++ ) {
foreach ( var beside in Nearby( lines, edges[index].Line ) ) {
if ( beside > index ) {
Touch( edges[index], edges[beside] );
}
}
}
}
static IEnumerable<int> Nearby( Dictionary<(long, long, long), List<int>> lines, (long, long, long) key ) {
for ( var along = -1L; along <= 1L; along++ ) {
for ( var across = -1L; across <= 1L; across++ ) {
for ( var offset = -1L; offset <= 1L; offset++ ) {
if ( lines.TryGetValue( (key.Item1 + along, key.Item2 + across, key.Item3 + offset), out var held ) ) {
foreach ( var index in held ) {
yield return index;
}
}
}
}
}
}
static void Touch( ArchCarveEdge left, ArchCarveEdge right ) {
if ( ReferenceEquals( left.Piece, right.Piece ) || !Shares( left, right ) ) {
return;
}
var low = MathF.Max( MathF.Min( left.Low, left.High ), MathF.Min( right.Low, right.High ) );
var high = MathF.Min( MathF.Max( left.Low, left.High ), MathF.Max( right.Low, right.High ) );
if ( high - low < ArchCarve.Grain ) {
return;
}
left.Piece.Beside[left.Index].Add( Stretch( left, right.Piece, low, high ) );
right.Piece.Beside[right.Index].Add( Stretch( right, left.Piece, low, high ) );
}
// Bucket key only proposes a shared line — verify collinearity precisely
static bool Shares( ArchCarveEdge left, ArchCarveEdge right ) {
return MathF.Abs( Cross( left.From, left.To, right.From ) ) < ArchCarve.Grain
&& MathF.Abs( Cross( left.From, left.To, right.To ) ) < ArchCarve.Grain;
}
static ArchCarveTouch Stretch( ArchCarveEdge edge, ArchCarvePiece beside, float low, float high ) {
var first = Fraction( edge, low );
var second = Fraction( edge, high );
return new ArchCarveTouch {
Piece = beside,
From = MathF.Min( first, second ),
To = MathF.Max( first, second )
};
}
static float Fraction( ArchCarveEdge edge, float at ) {
var span = edge.High - edge.Low;
return MathF.Abs( span ) < ArchCarve.Grain ? 0f : Math.Clamp( (at - edge.Low) / span, 0f, 1f );
}
static ((long, long, long) Key, Vector2 Along) Line( Vector2 from, Vector2 to ) {
var along = (to - from).Normal;
if ( along.x < -0.0001f || MathF.Abs( along.x ) < 0.0001f && along.y < 0f ) {
along = -along;
}
var normal = new Vector2( -along.y, along.x );
var offset = Vector2.Dot( normal, from );
return ((Grained( along.x ), Grained( along.y ), Grained( offset )), along);
}
static long Grained( float value ) => (long)MathF.Round( value / ArchCarve.Grain );
static (long, long) Quantised( Vector2 point ) {
return ((long)MathF.Round( point.x / ArchCarve.Grain ), (long)MathF.Round( point.y / ArchCarve.Grain ));
}
static void Push( List<Vector2> loop, Vector2 point ) {
if ( loop.Count > 0 && (loop[^1] - point).Length < ArchCarve.Grain ) {
return;
}
if ( loop.Count > 1 && (loop[0] - point).Length < ArchCarve.Grain ) {
return;
}
loop.Add( point );
}
static float Cross( Vector2 a, Vector2 b, Vector2 point ) {
var span = b - a;
var length = span.Length;
if ( length < 0.0001f ) {
return 0f;
}
return (span.x * (point.y - a.y) - span.y * (point.x - a.x)) / length;
}
static bool Real( List<Vector2> loop ) {
if ( loop.Count < 3 ) {
return false;
}
var area = MathF.Abs( ArchFootprint.SignedArea( loop ) );
var longest = 0f;
for ( var index = 0; index < loop.Count; index++ ) {
longest = MathF.Max( longest, (loop[(index + 1) % loop.Count] - loop[index]).Length );
}
return longest > ArchCarve.Grain && area / longest > ArchCarve.Grain;
}
public static List<List<Vector2>> Boundary( IEnumerable<ArchCarvePiece> pieces, float height ) {
var standing = pieces.Where( piece => Holds( piece, height ) ).ToHashSet();
var next = new Dictionary<(long, long), List<Vector2>>();
foreach ( var piece in standing ) {
for ( var index = 0; index < piece.Loop.Count; index++ ) {
if ( piece.Beside[index].Any( touch => standing.Contains( touch.Piece ) ) ) {
continue;
}
var from = piece.Loop[index];
var to = piece.Loop[(index + 1) % piece.Loop.Count];
if ( !next.TryGetValue( Quantised( from ), out var ends ) ) {
ends = new List<Vector2>();
next[Quantised( from )] = ends;
}
ends.Add( to );
}
}
var loops = new List<List<Vector2>>();
while ( next.Count > 0 ) {
var start = next.Keys.First();
var loop = new List<Vector2>();
var at = start;
while ( next.TryGetValue( at, out var ends ) && ends.Count > 0 ) {
var step = ends[0];
ends.RemoveAt( 0 );
if ( ends.Count == 0 ) {
next.Remove( at );
}
loop.Add( step );
at = Quantised( step );
if ( at == start ) {
break;
}
}
var kept = Straightened( loop );
if ( kept.Count >= 3 ) {
loops.Add( kept );
}
}
return loops;
}
static bool Holds( ArchCarvePiece piece, float height ) {
return piece.Standing.Spans.Any( span => height > span.From - ArchCarve.Grain && height < span.To + ArchCarve.Grain );
}
static List<Vector2> Straightened( List<Vector2> loop ) {
var kept = new List<Vector2>();
for ( var index = 0; index < loop.Count; index++ ) {
var previous = loop[(index - 1 + loop.Count) % loop.Count];
var current = loop[index];
var next = loop[(index + 1) % loop.Count];
if ( MathF.Abs( Cross( previous, next, current ) ) > ArchCarve.Grain ) {
kept.Add( current );
}
}
return kept;
}
}