Editor/Carve/ArchCarve.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchCarve {
public const float Grain = 0.01f;
readonly List<ArchCarveVolume> solids = new();
readonly List<ArchCarveVolume> cuts = new();
float frame;
public static ArchCarve Prism( IReadOnlyList<Vector2> footprint, float from, float to ) {
return new ArchCarve().Plus( ArchCarveVolume.Over( footprint, from, to ) );
}
public static ArchCarve Wedge( IReadOnlyList<Vector2> footprint, float from, ArchCarvePlane top ) {
return new ArchCarve().Plus( ArchCarveVolume.Under( footprint, from, top ) );
}
public ArchCarve In( float yaw ) {
frame = MathF.Round( yaw % 90f, 4 );
return this;
}
public ArchCarve Plus( ArchCarveVolume volume ) {
if ( Real( volume ) ) {
solids.Add( volume );
}
return this;
}
public ArchCarve Less( ArchCarveVolume volume ) {
if ( Real( volume ) ) {
cuts.Add( volume );
}
return this;
}
public ArchCarveShape Resolve() {
var shape = new ArchCarveShape();
if ( solids.Count == 0 ) {
return shape;
}
var standing = solids.Select( Framed ).ToList();
var taking = cuts.Select( Framed ).ToList();
var grid = ArchCarveCells.Over( standing.Concat( taking ).ToList() );
shape.Grid = grid;
shape.Frame = frame;
foreach ( var piece in grid.Pieces ) {
var bands = new ArchCarveBands();
foreach ( var volume in standing.Where( volume => volume.Covers( piece.Centre ) ) ) {
bands.Add( volume.Floor.At( piece.Centre ), volume.Ceiling.At( piece.Centre ), volume.Floor, volume.Ceiling );
}
piece.Whole = bands.Copy();
foreach ( var cut in taking.Where( cut => cut.Covers( piece.Centre ) ) ) {
bands.Remove( cut.Floor.At( piece.Centre ), cut.Ceiling.At( piece.Centre ), cut.Floor, cut.Ceiling );
}
piece.Standing = bands;
}
var courses = Courses( grid );
foreach ( var piece in grid.Pieces ) {
var taken = piece.Standing.Missing( piece.Whole ).ToList();
foreach ( var band in piece.Standing.Spans ) {
var cell = new ArchCarveCell {
Min = piece.Min,
Max = piece.Max,
Centre = piece.Centre,
Loop = piece.Loop,
From = band.From,
To = band.To,
Foot = band.Foot,
Head = band.Head
};
shape.Cells.Add( cell );
Cap( shape, grid, cell, piece, taken );
Flank( shape, grid, cell, piece, courses );
}
}
Unframe( shape );
return shape;
}
ArchCarveVolume Framed( ArchCarveVolume volume ) {
if ( frame == 0f ) {
return volume;
}
return new ArchCarveVolume {
Footprint = volume.Footprint.Select( point => ArchCarveFrame.Turn( point, -frame ) ).ToList(),
Floor = ArchCarveFrame.Turn( volume.Floor, -frame ),
Ceiling = ArchCarveFrame.Turn( volume.Ceiling, -frame ),
Break = volume.Break
};
}
void Unframe( ArchCarveShape shape ) {
if ( frame == 0f ) {
return;
}
foreach ( var face in shape.Faces ) {
for ( var index = 0; index < face.Points.Count; index++ ) {
face.Points[index] = ArchCarveFrame.Turn( face.Points[index], frame );
}
}
for ( var index = 0; index < shape.Cells.Count; index++ ) {
shape.Cells[index] = ArchCarveFrame.Turn( shape.Cells[index], frame );
}
}
static void Cap( ArchCarveShape shape, ArchCarveGrid grid, ArchCarveCell cell, ArchCarvePiece piece, IReadOnlyList<ArchCarveSpan> taken ) {
var cut = taken.Any( gap => MathF.Abs( gap.From - cell.To ) < Grain );
var under = taken.Any( gap => MathF.Abs( gap.To - cell.From ) < Grain );
foreach ( var part in ArchCarveQuads.Split( Conforming( grid, cell, piece ) ) ) {
shape.Faces.Add( Level( part, cell.Head, true, cut ? ArchCarveSide.Sill : ArchCarveSide.Top ) );
shape.Faces.Add( Level( part, cell.Foot, false, under ? ArchCarveSide.Head : ArchCarveSide.Bottom ) );
}
}
// Inserts neighbour edge-split stations to avoid hanging vertices
static List<Vector2> Conforming( ArchCarveGrid grid, ArchCarveCell cell, ArchCarvePiece piece ) {
if ( piece?.Beside is null ) {
return cell.Loop.ToList();
}
var loop = new List<Vector2>();
for ( var index = 0; index < cell.Loop.Count; index++ ) {
var from = cell.Loop[index];
var to = cell.Loop[(index + 1) % cell.Loop.Count];
loop.Add( from );
foreach ( var station in Stations( piece.Beside[index] ) ) {
var at = grid.Welded( Vector2.Lerp( from, to, station ) );
if ( (at - from).Length > Grain && (at - to).Length > Grain ) {
loop.Add( at );
}
}
}
return loop;
}
static IEnumerable<float> Stations( IReadOnlyList<ArchCarveTouch> touching ) {
var marks = new List<float>();
foreach ( var at in touching.SelectMany( touch => new[] { touch.From, touch.To } ).OrderBy( at => at ) ) {
if ( at <= 0.001f || at >= 0.999f || marks.Any( held => MathF.Abs( held - at ) < 0.001f ) ) {
continue;
}
marks.Add( at );
}
return marks;
}
static void Flank( ArchCarveShape shape, ArchCarveGrid grid, ArchCarveCell cell, ArchCarvePiece piece, IReadOnlyList<float> courses ) {
var bands = Within( cell, courses );
for ( var index = 0; index < piece.Loop.Count; index++ ) {
var from = piece.Loop[index];
var to = piece.Loop[(index + 1) % piece.Loop.Count];
foreach ( var (start, end, beside) in Stretches( piece.Beside[index] ) ) {
var a = grid.Welded( Vector2.Lerp( from, to, start ) );
var b = grid.Welded( Vector2.Lerp( from, to, end ) );
if ( (b - a).Length < Grain ) {
continue;
}
if ( beside is null ) {
Clip( shape, cell, a, b, Whole( cell ), bands, ArchCarveSide.Face );
continue;
}
foreach ( var gap in beside.Standing.Missing( beside.Whole ) ) {
Clip( shape, cell, a, b, gap, bands, ArchCarveSide.Jamb );
}
foreach ( var bare in beside.Whole.Beyond( cell.From, cell.To ) ) {
Clip( shape, cell, a, b, bare, bands, ArchCarveSide.Face );
}
}
}
}
static IEnumerable<(float From, float To, ArchCarvePiece Beside)> Stretches( List<ArchCarveTouch> touching ) {
if ( touching.Count == 0 ) {
yield return (0f, 1f, null);
yield break;
}
var marks = new List<float> { 0f, 1f };
foreach ( var touch in touching ) {
marks.Add( touch.From );
marks.Add( touch.To );
}
marks.Sort();
for ( var index = 0; index + 1 < marks.Count; index++ ) {
var from = marks[index];
var to = marks[index + 1];
if ( to - from < 0.001f ) {
continue;
}
var middle = (from + to) * 0.5f;
yield return (from, to, touching.FirstOrDefault( touch => middle > touch.From && middle < touch.To ).Piece);
}
}
static ArchCarveSpan Whole( ArchCarveCell cell ) {
return new ArchCarveSpan { From = cell.From, To = cell.To, Foot = cell.Foot, Head = cell.Head };
}
static void Clip(
ArchCarveShape shape,
ArchCarveCell cell,
Vector2 edgeFrom,
Vector2 edgeTo,
ArchCarveSpan range,
IReadOnlyList<float> bands,
ArchCarveSide side ) {
var from = MathF.Max( range.From, cell.From );
var to = MathF.Min( range.To, cell.To );
if ( to - from < Grain ) {
return;
}
var course = from;
foreach ( var height in bands.Where( height => height > from + Grain && height < to - Grain ) ) {
shape.Faces.Add( Side( cell, range, edgeFrom, edgeTo, course, height, side ) );
course = height;
}
shape.Faces.Add( Side( cell, range, edgeFrom, edgeTo, course, to, side ) );
}
// Arrangement-wide band heights — consistency must be global to avoid hanging vertices
static List<float> Courses( ArchCarveGrid grid ) {
var heights = new List<float>();
void Mark( float height ) {
if ( heights.Any( held => MathF.Abs( held - height ) < Grain ) ) {
return;
}
heights.Add( height );
}
foreach ( var piece in grid.Pieces ) {
foreach ( var span in piece.Whole.Spans.Concat( piece.Standing.Spans ) ) {
Mark( span.From );
Mark( span.To );
}
}
heights.Sort();
return heights;
}
static List<float> Within( ArchCarveCell cell, IReadOnlyList<float> courses ) {
return courses.Where( height => height > cell.From + Grain && height < cell.To - Grain ).ToList();
}
static ArchCarveFace Level( IReadOnlyList<Vector2> loop, ArchCarvePlane plane, bool up, ArchCarveSide side ) {
var points = loop.Select( point => new Vector3( point.x, point.y, plane.At( point ) ) ).ToList();
if ( !up ) {
points = points.Take( 1 ).Concat( points.Skip( 1 ).Reverse() ).ToList();
}
return new ArchCarveFace { Points = points, Side = side };
}
// Course splits are level — only the band's own ends carry the surface plane
static ArchCarveFace Side( ArchCarveCell cell, ArchCarveSpan range, Vector2 from, Vector2 to, float bottom, float top, ArchCarveSide side ) {
var head = Ending( cell, range, top, true );
var foot = Ending( cell, range, bottom, false );
return new ArchCarveFace {
Points = new List<Vector3>
{
new( from.x, from.y, foot.At( from ) ),
new( to.x, to.y, foot.At( to ) ),
new( to.x, to.y, head.At( to ) ),
new( from.x, from.y, head.At( from ) )
},
Side = side
};
}
// Cell's own surface wins; span answers for an end that dies inside the band
static ArchCarvePlane Ending( ArchCarveCell cell, ArchCarveSpan range, float height, bool up ) {
if ( MathF.Abs( height - (up ? cell.To : cell.From) ) < Grain ) {
return up ? cell.Head : cell.Foot;
}
if ( MathF.Abs( height - (up ? range.To : range.From) ) < Grain ) {
return up ? range.Head : range.Foot;
}
return ArchCarvePlane.Level( height );
}
static bool Real( ArchCarveVolume volume ) {
if ( volume.Footprint is not { Count: >= 3 } ) {
return false;
}
var centre = volume.Footprint.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / volume.Footprint.Count;
return volume.Ceiling.At( centre ) - volume.Floor.At( centre ) > Grain;
}
}