Editor/Roof/ArchFaceCarve.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Hip edges run at 45 degrees, which no rectilinear grid can hold - so cuts run per face.
// The rim is not emitted here: ArchRoofGen.JoinedSlab already walls every edge one face owns.
static class ArchFaceCarve {
const float Grain = ArchCarve.Grain;
public static List<List<Vector3>> Surround( IReadOnlyList<List<Vector3>> faces, IReadOnlyList<ArchCarveVolume> cuts, float thickness, float contact = 0f ) {
if ( cuts is not { Count: > 0 } ) {
return faces.ToList();
}
var kept = new List<List<Vector3>>();
foreach ( var face in faces ) {
var pieces = new List<List<Vector3>> { new( face ) };
foreach ( var cut in cuts ) {
pieces = Subtract( pieces, cut, thickness, contact );
}
kept.AddRange( pieces );
}
// A cut landing on a hip line strands a corner on the face nothing cut.
Conform( kept );
return kept;
}
static List<List<Vector3>> Subtract( List<List<Vector3>> pieces, ArchCarveVolume cut, float thickness, float contact ) {
var footprint = ArchFootprint.Wind( cut.Footprint.ToList() );
if ( footprint.Count < 3 ) {
return pieces;
}
var bounds = Bounds( footprint, cut, thickness, contact ).ToList();
var kept = new List<List<Vector3>>();
foreach ( var piece in pieces ) {
var inside = piece;
var outside = new List<List<Vector3>>();
for ( var index = 0; index < bounds.Count && inside is not null; index++ ) {
Split( inside, bounds[index], out inside, out var beyond );
if ( beyond is not null ) {
outside.Add( beyond );
}
}
if ( inside is null ) {
kept.Add( piece );
continue;
}
kept.AddRange( outside );
}
return kept;
}
// A deck RAKES, so the band is a boundary drawn across the face and not a yes or no about the whole of
// it: the footprint's edges say where the cut stands in plan, and its floor and ceiling say where the
// deck has climbed out of it. Judging by the overlap's extremes took a whole hip slope out for touching
// the eave, and left the ridge over a shaft that plainly reached it.
static IEnumerable<Func<Vector3, float>> Bounds( IReadOnlyList<Vector2> footprint, ArchCarveVolume cut, float thickness, float contact ) {
for ( var index = 0; index < footprint.Count; index++ ) {
var from = footprint[index];
var to = footprint[(index + 1) % footprint.Count];
yield return point => Cross( from, to, point );
}
yield return point => cut.Ceiling.At( Flat( point ) ) - point.z + contact;
yield return point => point.z + thickness - cut.Floor.At( Flat( point ) );
}
static Vector2 Flat( Vector3 point ) => new( point.x, point.y );
// Sutherland-Hodgman on one boundary: signed depth per corner, crossing interpolated in three.
static void Split( List<Vector3> piece, Func<Vector3, float> depth, out List<Vector3> inside, out List<Vector3> outside ) {
var within = new List<Vector3>();
var beyond = new List<Vector3>();
for ( var index = 0; index < piece.Count; index++ ) {
var current = piece[index];
var next = piece[(index + 1) % piece.Count];
var here = depth( current );
var there = depth( next );
if ( here >= -Grain ) within.Add( current );
if ( here <= Grain ) beyond.Add( current );
if ( here > Grain && there < -Grain || here < -Grain && there > Grain ) {
var crossing = current + (next - current) * (here / (here - there));
within.Add( crossing );
beyond.Add( crossing );
}
}
inside = Real( within ) ? within : null;
outside = Real( beyond ) ? beyond : null;
}
// A stranded corner must be inserted - a T left in becomes a wall across the deck.
static void Conform( List<List<Vector3>> pieces ) {
var corners = pieces.SelectMany( piece => piece ).ToList();
foreach ( var piece in pieces ) {
for ( var index = piece.Count - 1; index >= 0; index-- ) {
var from = piece[index];
var to = piece[(index + 1) % piece.Count];
var along = corners
.Where( corner => Between( from, to, corner ) )
.OrderBy( corner => (corner - from).Length )
.ToList();
for ( var step = along.Count - 1; step > 0; step-- ) {
if ( (along[step] - along[step - 1]).Length < Grain ) {
along.RemoveAt( step );
}
}
piece.InsertRange( index + 1, along );
}
}
}
static bool Between( Vector3 from, Vector3 to, Vector3 point ) {
var span = to - from;
var length = span.Length;
if ( length < Grain ) {
return false;
}
var along = Vector3.Dot( point - from, span ) / length;
if ( along < Grain || along > length - Grain ) {
return false;
}
return (from + span * (along / length) - point).Length < Grain;
}
static float Cross( Vector2 a, Vector2 b, Vector3 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;
}
// Narrower than anything the grid can author is clip noise, not deck. Measured against the FINEST grid
// step rather than the grain: a band boundary crossing a rake almost level with it leaves a hairline
// ribbon the length of the roof, and a ribbon is a face whose texture axis smears down the slope.
static bool Real( List<Vector3> loop ) {
if ( loop.Count < 3 ) {
return false;
}
var flat = loop.Select( point => new Vector2( point.x, point.y ) ).ToList();
var area = MathF.Abs( ArchFootprint.SignedArea( flat ) );
var longest = 0f;
for ( var index = 0; index < flat.Count; index++ ) {
longest = MathF.Max( longest, (flat[(index + 1) % flat.Count] - flat[index]).Length );
}
return longest > ArchGridService.FinestSize && area / longest > ArchGridService.FinestSize;
}
}