Editor-side class that composes carved architectural volumes into a final mesh-friendly shape. It collects additive solids and subtractive cuts, lays them into a grid, computes bands, caps and flanks, and emits faces and cells with oriented vertex lists for downstream mesh generation.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Solid minus volume, every face classified so the base generators can dress them.
// Deliberately NOT a CSG kernel: triangles break the quad rule and the append-only surface roles.
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 ) );
}
// Lay the grid in the flight's own frame: its steps stay square, and only the pad is angled.
public ArchCarve In( float yaw )
{
// Rounded only to kill float noise - the frame is an angle, not a grid value.
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();
// A cut is partial in z, so every piece survives and knows its neighbours - seam or reveal.
var grid = ArchCarveCells.Over( standing.Concat( taking ).ToList() );
shape.Grid = grid;
shape.Frame = frame;
// Every band end carries the volume face that put it there, so nothing downstream has to guess whether a
// height is a solid's own surface or the boundary a bite stopped at.
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 );
}
}
// A cut on the face is a reveal; orientation alone paints an uncut platform's top as a sill.
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 );
// Split before it is lifted: the stations the neighbours put on this cap's edges are corners like any
// other, and a cap carrying seven of them is an n-gon whatever its outline measures.
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 ) );
}
}
// A cap carries every vertex its NEIGHBOURS put on the edges it shares, because a neighbour split part way
// along a shared edge leaves a vertex in the middle of it. Meet that with one long edge and the two surfaces
// are stitched by a hanging vertex, which is a hairline the moment anything welds or nudges a corner. The
// stations are the same adjacency stretches the flanks are already cut on - one answer, both sides.
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;
}
// Solid neighbour = seam drawn by neither; cut-away = jamb; never there = outside.
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] ) )
{
// Through the grid's own vertex table: the same station measured along the neighbour's edge is a
// tenth of an inch away otherwise, and the two walls then miss each other by that tenth.
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 };
}
// The range is the SPAN a face fills, surfaces and all - a jamb's foot is the floor of the hole beside it,
// which is the one thing that lets a raked bite's reveal meet the raked cap it exposed.
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;
}
// Split where the NEIGHBOUR's bands stop, the same way the stretch splits where its neighbours change:
// the neighbour's cap dies on this edge at that height, so a wall running past it meets one edge with
// two and stitches the pair with a hanging vertex.
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 ) );
}
// Every height ANYTHING in the arrangement has a band edge at - the courses every wall in it is cut on, and
// the reason it is one list rather than a neighbour's. A wall cut on its own neighbour's bands and the wall
// round the corner cut on another's disagree about the vertical edge they share: one arrives whole, the other
// in two, and the pair is stitched by a hanging vertex, which cracks the moment anything moves a corner.
// Consistency has to be arrangement-wide because it travels: agreeing with one neighbour puts you out with
// the next. Splitting where nothing needed it costs nothing - ArchMesh dissolves the seam straight back.
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();
}
// Wound as ArchMesh.Box, heights straight off the surface that end of the band belongs to - so a cap under a
// raked bite rakes with it and nothing has to ask whether the boundary was the solid's or the cut's.
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 };
}
// A band split by a COURSE is level at that course - a surface only reaches the end it actually put there.
// Reading one at every split is what would drive an interior seam through the solid.
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
};
}
// This cell's own surface first, then the span's: the cell wins because that is the material actually being
// skinned, and the span answers for an end that dies inside the band - the floor of the hole beside it.
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;
}
// Measured at the footprint centre; a plane far off would be a depth the volume does not have.
var centre = volume.Footprint.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / volume.Footprint.Count;
return volume.Ceiling.At( centre ) - volume.Floor.At( centre ) > Grain;
}
}