Editor/Bands/ArchCorniceRing.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A segment where the ring steps out by Depth and back, shoulders mitred like any other corner.
public readonly record struct ArchCorniceReturn( Vector2 From, Vector2 To, float Depth );
// Swept on the centreline, not an offset loop — an offset mitres each vertex independently, skewing oblique segments.
public sealed class ArchCorniceRing {
public ArchCorniceStyle Crown { get; init; }
public IReadOnlyList<Vector2> Outline { get; init; }
public float Head { get; init; }
public float Thickness { get; init; }
public Func<Vector2, Vector2, bool> Abuts { get; init; }
public IReadOnlyList<ArchCorniceReturn> Returns { get; init; } = Array.Empty<ArchCorniceReturn>();
public ArchKit Kit { get; init; }
public ArchStyle Style { get; init; }
public ArchPalette[] Chain { get; init; }
public ArchPlan Plan { get; init; }
public int Level { get; init; }
public int HostId { get; init; }
// Whether a pier may step ABOVE the ring. A deck edge is the only head with nothing standing on it, so it is the
// only one that breaks its line upward - stepped off a storey head the blocks stand inside the storey above.
public bool Steps { get; init; } = true;
// What a pier standing under the ring dies at: the seat of its lowest course, less a bite so the pier's head and
// the entablature's underside are not one plane. The one answer, so nothing measures the entablature twice.
public static float Soffit( ArchCorniceStyle crown, float head, ArchKit kit ) {
var layers = ArchCorniceGen.Layers( crown, head );
return layers.Count == 0 ? head : layers[0].Lid( kit ) + ArchLap.Bite( kit );
}
public void Lay( ArchMesh canvas ) {
if ( Crown is not { Stands: true } || Outline is not { Count: >= 3 } ) {
return;
}
var half = MathF.Max( 1f, Thickness ) * 0.5f;
var bite = ArchLap.Bite( Kit );
var back = half - bite;
var outline = ArchFootprint.Wind( Outline.ToList() );
var loop = Stepped( outline );
var layers = ArchCorniceGen.Layers( Crown, Head );
foreach ( var layer in layers ) {
var brush = Style.Brush( layer.Course.Field, Chain );
switch ( layer.Course.Kind ) {
case CorniceCourse.Coping:
Band( canvas, loop, -half - layer.Projection, Thickness + layer.Projection * 2f,
layer.Seat( Kit ), layer.Lid( Kit ), brush );
break;
// SWEPT like every other proud course, not beamed edge by edge: a corona laid as a beam per edge is
// as many solids as the loop has sides, each authoring its own texcoords, and the run reads as
// blocks with a seam at every mitre where the reference has one continuous band.
case CorniceCourse.Band:
Swept( canvas, layer, loop, brush, Standing( Squared( layer, bite ), back ) );
break;
case CorniceCourse.Moulding:
Swept( canvas, layer, loop, brush, Standing( ArchProfiles.Named( Kit, layer.Course.Profile ), back ) );
break;
default:
Studded( canvas, layer, loop, back - bite, bite, brush );
break;
}
}
Brackets( canvas, loop, outline, layers, half - bite );
Piers( canvas, loop, half - bite );
}
// The path a proud shape pushes out of line. Each return lands on the edge it was measured against, so a pier
// wrapping a corner steps both of the runs meeting there and the two shoulders mitre into one another.
List<Vector2> Stepped( List<Vector2> loop ) {
if ( Returns.Count == 0 || loop.Count < 3 ) {
return loop;
}
var stepped = new List<Vector2>();
for ( var index = 0; index < loop.Count; index++ ) {
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
var span = to - from;
var length = span.Length;
stepped.Add( from );
if ( length < 0.5f ) {
continue;
}
var along = span / length;
var outward = ArchRegion.Outward( from, to );
foreach ( var crossing in Crossings( from, along, outward, length ) ) {
stepped.Add( from + along * crossing.From );
stepped.Add( from + along * crossing.From + outward * crossing.Depth );
stepped.Add( from + along * crossing.To + outward * crossing.Depth );
stepped.Add( from + along * crossing.To );
}
}
return stepped;
}
IEnumerable<(float From, float To, float Depth)> Crossings( Vector2 from, Vector2 along, Vector2 outward, float length ) {
var found = new List<(float From, float To, float Depth)>();
foreach ( var crossing in Returns ) {
if ( MathF.Abs( Vector2.Dot( crossing.From - from, outward ) ) > 1f
|| MathF.Abs( Vector2.Dot( crossing.To - from, outward ) ) > 1f ) {
continue;
}
var start = Math.Clamp( Vector2.Dot( crossing.From - from, along ), 0f, length );
var finish = Math.Clamp( Vector2.Dot( crossing.To - from, along ), 0f, length );
if ( finish - start > 0.5f && crossing.Depth > 0.05f ) {
found.Add( (start, finish, crossing.Depth) );
}
}
return found.OrderBy( crossing => crossing.From );
}
void Band( ArchMesh canvas, IReadOnlyList<Vector2> loop, float shift, float depth, float bottom, float top, ArchBrush brush ) {
var section = new ArchBandSection { Shift = shift, Depth = depth, Bottom = bottom, Top = top };
foreach ( var run in Broken( loop, bottom ) ) {
section.Emit( canvas, run, brush );
}
}
// A plain projecting course as a section, so it sweeps as one solid like every moulding does.
ArchProfile Squared( ArchCorniceLayer layer, float bite ) {
var reach = layer.Projection + bite;
var rise = layer.Lid( Kit ) - layer.Seat( Kit );
return new ArchProfile {
Name = "corona",
Points = new List<Vector2> { new( 0f, 0f ), new( reach, 0f ), new( reach, rise ), new( 0f, rise ) }
};
}
// The kit's own section stood off the centreline by the wall's half thickness, less a bite so its back is buried
// in the elevation rather than landing on it. Copied, never shifted in place - the kit's profile is shared.
static ArchProfile Standing( ArchProfile section, float shift ) {
if ( section is not { IsUsable: true } ) {
return null;
}
return new ArchProfile {
Name = section.Name,
Points = section.Points.Select( point => new Vector2( point.x + shift, point.y ) ).ToList(),
Closed = section.Closed,
Capped = section.Capped,
MapAlongPath = section.MapAlongPath
};
}
// Walked in REVERSE, because the outline is wound counter-clockwise and the extruder's right-hand side is the
// inside of such a loop. A section authored standing proud of the wall would otherwise crown the roof deck.
void Swept( ArchMesh canvas, ArchCorniceLayer layer, IReadOnlyList<Vector2> loop, ArchBrush brush, ArchProfile section ) {
if ( section is not { IsUsable: true } ) {
return;
}
foreach ( var run in Broken( loop, layer.Seat( Kit ) ) ) {
var path = run.Raised();
path.Reverse();
canvas.Extrude( path, section, 1f, Rotation.Identity, brush, run.Closed );
}
}
void Studded( ArchMesh canvas, ArchCorniceLayer layer, IReadOnlyList<Vector2> loop, float back, float bite, ArchBrush brush ) {
var section = ArchProfiles.Named( Kit, layer.Course.Profile );
var reach = layer.Projection + bite * 2f;
foreach ( var run in Broken( loop, layer.Bottom ) ) {
for ( var edge = 0; edge < run.Edges; edge++ ) {
var from = run.At( edge );
var along = run.At( edge + 1 ) - from;
var span = along.Length;
var head = CornerClearance( run, edge, reach );
var tail = CornerClearance( run, edge + 1, reach );
if ( span - head - tail < 1f ) {
continue;
}
var frame = new ArchBandFrame {
Origin = from,
Along = along / span,
Across = ArchRegion.Outward( from, run.At( edge + 1 ) )
};
ArchCorniceGen.Blocks( canvas, layer, Kit, frame, head, span - tail, back, reach, section, brush );
}
}
}
static float CornerClearance( ArchRunPath run, int corner, float reach ) {
if ( !run.Continues( corner ) ) {
return 0f;
}
var incoming = run.At( corner ) - run.At( corner - 1 );
var outgoing = run.At( corner + 1 ) - run.At( corner );
if ( incoming.Length < 0.5f || outgoing.Length < 0.5f ) {
return 0f;
}
incoming = incoming.Normal;
outgoing = outgoing.Normal;
var turn = MathF.Abs( incoming.x * outgoing.y - incoming.y * outgoing.x );
var opened = 1f + Vector2.Dot( incoming, outgoing );
return opened < 0.05f ? reach : MathF.Min( reach, reach * turn / opened );
}
IEnumerable<ArchRunPath> Broken( IReadOnlyList<Vector2> loop, float seat ) {
foreach ( var run in ArchBrokenRun.Of( loop, Abuts ).Level( seat ).Resolve() ) {
foreach ( var surviving in ArchCut.Runs( Plan, Kit, Level, HostId, run, ArchCutAffects.Trims ) ) {
yield return surviving;
}
}
}
// The corbel that carries the turn. A block row is held off every mitre, so the vertex is where a bracket goes -
// deeper than the blocks and hung under the corona, which is the course the brackets are seen to be carrying.
void Brackets( ArchMesh canvas, IReadOnlyList<Vector2> loop, IReadOnlyList<Vector2> outline, IReadOnlyList<ArchCorniceLayer> layers, float back ) {
var bracket = Crown.Bracket;
if ( !bracket.Stands || layers.Count == 0 ) {
return;
}
// It stands in the BLOCK ROW's own band, which is the course whose vertex it is there to fill; Drop corbels it
// further into the frieze. Hung anywhere else it is a solid driven through the mouldings behind it.
var row = layers.FirstOrDefault( layer => layer.Course.Kind is CorniceCourse.Dentils or CorniceCourse.Modillions );
var course = row.Course is null ? layers[0] : row;
var brush = Style.Brush( bracket.Field, Chain );
var corners = Turns( loop ).ToList();
foreach ( var corner in Turns( outline ) ) {
if ( corners.All( other => (other.At - corner.At).Length >= 0.5f
|| Vector2.Dot( other.Incoming, corner.Incoming ) < 0.99f
|| Vector2.Dot( other.Outgoing, corner.Outgoing ) < 0.99f ) ) {
corners.Add( corner );
}
}
foreach ( var corner in corners ) {
Block( canvas, corner.At, corner.Incoming, corner.Outgoing, bracket.Width, back + bracket.Projection,
course.Seat( Kit ) - bracket.Drop, course.Lid( Kit ), brush );
}
}
// One at every corner of the ring, which is where a street block breaks its parapet line.
void Piers( ArchMesh canvas, IReadOnlyList<Vector2> loop, float back ) {
var pier = Crown.Pier;
if ( !Steps || !pier.Stands ) {
return;
}
var brush = Style.Brush( pier.Field, Chain );
var cap = Style.Brush( ArchSurface.WallCap, Chain );
var shaft = MathF.Max( 0.5f, pier.Rise - MathF.Max( 0f, pier.Cap ) );
foreach ( var corner in Turns( loop ) ) {
Block( canvas, corner.At, corner.Incoming, corner.Outgoing, pier.Width, back + pier.Projection,
Head, Head + shaft, brush );
if ( pier.Cap > 0.05f ) {
Block( canvas, corner.At, corner.Incoming, corner.Outgoing, pier.Width + pier.CapOversail * 2f,
back + pier.Projection + pier.CapOversail, Head + shaft, Head + pier.Rise, cap );
}
}
}
// Only where the loop actually turns. A stepped loop carries repeated and collinear points, and a block on one
// of those is a corbel standing in the middle of an elevation.
static IEnumerable<(Vector2 At, Vector2 Incoming, Vector2 Outgoing)> Turns( IReadOnlyList<Vector2> loop ) {
for ( var corner = 0; corner < loop.Count; corner++ ) {
var at = loop[corner];
var incoming = Bearing( loop, corner, -1 );
var outgoing = Bearing( loop, corner, 1 );
if ( incoming.Length < 0.5f || outgoing.Length < 0.5f || Vector2.Dot( incoming, outgoing ) > 0.99f ) {
continue;
}
yield return (at, incoming, outgoing);
}
}
// The outward normal of the nearest edge with any length in it. A normal taken off a repeated point is NaN, and
// one NaN corner takes every block on the ring with it.
static Vector2 Bearing( IReadOnlyList<Vector2> loop, int corner, int step ) {
var at = loop[corner];
for ( var walk = 1; walk < loop.Count; walk++ ) {
var other = loop[((corner + step * walk) % loop.Count + loop.Count) % loop.Count];
if ( (other - at).Length > 0.5f ) {
return step > 0 ? ArchRegion.Outward( at, other ) : ArchRegion.Outward( other, at );
}
}
return Vector2.Zero;
}
// A block on a corner belongs to BOTH elevations, so each of its outer faces lines up with the course running
// away from it. Laid along one edge instead it stands square to that one and runs half its width out past the
// other's face, which is the overhang this replaced.
static void Block( ArchMesh canvas, Vector2 at, Vector2 incoming, Vector2 outgoing, float width, float reach, float bottom, float top, ArchBrush brush ) {
if ( top - bottom < 0.05f || width < 0.5f ) {
return;
}
var outer = Meeting( at, incoming, outgoing, reach );
var plan = ArchFootprint.Wind( new List<Vector2>
{
outer,
outer - incoming * width,
outer - (incoming + outgoing) * width,
outer - outgoing * width
} );
var seat = plan.Select( point => new Vector3( point.x, point.y, bottom ) ).ToList();
canvas.Prism( seat, seat.Select( point => point.WithZ( top ) ).ToList(), brush );
}
// Where the two offset faces actually cross, which is only the sum of the normals at a right angle. The same
// opened denominator ArchBandGen mitres a board on.
static Vector2 Meeting( Vector2 at, Vector2 incoming, Vector2 outgoing, float reach ) {
var opened = 1f + Vector2.Dot( incoming, outgoing );
return opened < 0.05f ? at + outgoing * reach : at + (incoming + outgoing) * (reach / opened);
}
}