Editor/Pillar/ArchPillarGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly record struct ArchPillarRun( int FromX, int FromY, int ToX, int ToY, bool AlongX );
public readonly record struct ArchPillarColumn( Vector3 At, int X, int Y );
public static class ArchPillarGen {
const float Adrift = 1f;
// Resolves the column's base height onto the nearest slab unless Founded
public static float Seat( ArchPillarPart part, ArchKit kit, ArchPlan plan ) {
if ( part.Founded || plan is null ) {
return part.BaseHeight;
}
// Only seat on slabs below the column's own bury line
var poured = ArchPillarSeat.Poured( plan, kit, part.Origin, part.BaseHeight + ArchPillarSeat.Sunk( kit, part ) );
return poured is { } standing && MathF.Abs( standing - part.BaseHeight ) > Adrift ? standing : part.BaseHeight;
}
// Clamped to the soffit — level, not lapped, so the contact pass doesn't bury a face
public static float Height( ArchPillarPart part, ArchRoom room, ArchKit kit, ArchPlan plan = null ) {
var floor = Seat( part, kit, plan );
var slab = ArchPillarSoffit.Slab( plan, kit, part, floor );
if ( part.Height > 0f ) {
return slab < float.MaxValue ? MathF.Max( 8f, MathF.Min( part.Height, slab - floor ) ) : part.Height;
}
if ( room is null ) {
return slab < float.MaxValue ? MathF.Max( 8f, slab - floor ) : kit.WallHeight;
}
return MathF.Max( 8f, ArchPillarSoffit.Over( plan, kit, room, part, floor ) - floor );
}
public static List<ArchPillarRun> Runs( ArchPillarPart part ) {
var runs = new List<ArchPillarRun>();
if ( part.Span == PillarSpan.None || part.Placement != PillarPlacement.Grid ) {
return runs;
}
var countX = Math.Max( 1, part.CountX );
var countY = Math.Max( 1, part.CountY );
var edge = part.PerimeterOnly;
if ( part.SpanAlongX ) {
for ( var iy = 0; iy < countY; iy++ ) {
if ( edge && iy > 0 && iy < countY - 1 ) {
continue;
}
for ( var ix = 0; ix < countX - 1; ix++ ) {
runs.Add( new ArchPillarRun( ix, iy, ix + 1, iy, true ) );
}
}
}
if ( !part.SpanAlongY ) {
return runs;
}
for ( var ix = 0; ix < countX; ix++ ) {
if ( edge && ix > 0 && ix < countX - 1 ) {
continue;
}
for ( var iy = 0; iy < countY - 1; iy++ ) {
runs.Add( new ArchPillarRun( ix, iy, ix, iy + 1, false ) );
}
}
return runs;
}
public static HashSet<(int X, int Y)> Carried( ArchPillarPart part ) {
var carried = new HashSet<(int X, int Y)>();
foreach ( var run in Runs( part ) ) {
carried.Add( (run.FromX, run.FromY) );
carried.Add( (run.ToX, run.ToY) );
}
return carried;
}
public static void Build(
ArchMesh canvas,
ArchPillarPart part,
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchStyle style ) {
var chain = new[] { part.Palette, room.Palette, building.Palette };
var shaft = style.Brush( ArchSurface.Pillar, chain );
var cap = style.Brush( ArchSurface.PillarCap, chain );
var seat = Seat( part, kit, plan );
var height = Height( part, room, kit, plan );
if ( part.Placement == PillarPlacement.Pilaster ) {
Pilaster( canvas, part, room, height, shaft, cap, kit );
return;
}
var spanning = part.Span != PillarSpan.None && part.Placement == PillarPlacement.Grid;
var shaftHeight = spanning ? MathF.Max( 8f, height - part.SpanBand ) : height;
var carried = spanning ? Carried( part ) : new HashSet<(int X, int Y)>();
foreach ( var column in Columns( part, seat ) ) {
var columnHeight = carried.Contains( (column.X, column.Y) ) ? shaftHeight : height;
Column( canvas, part, column.At, columnHeight, shaft, cap,
Bites( plan, kit, part, room, building, column.At, columnHeight ) );
ArchDamage.PillarCorners( canvas, plan, kit, building, room, part, column.At, columnHeight, style );
}
if ( spanning ) {
using ( canvas.Part( ArchPieces.Span ) ) {
Spans( canvas, part, kit, seat, height, cap );
}
}
}
public static void Stand( ArchMesh canvas, ArchPillarPart part, ArchBrush shaft, ArchBrush cap ) {
foreach ( var column in Columns( part ) ) {
Column( canvas, part, column.At, MathF.Max( 1f, part.Height ), shaft, cap );
}
}
public static List<Vector3> Positions( ArchPillarPart part ) => Columns( part ).Select( column => column.At ).ToList();
public static List<ArchPillarColumn> Columns( ArchPillarPart part ) => Columns( part, part.BaseHeight );
public static List<ArchPillarColumn> Columns( ArchPillarPart part, float seat ) {
var columns = new List<ArchPillarColumn>();
var rotation = Rotation.FromYaw( part.Yaw );
if ( part.Placement != PillarPlacement.Grid ) {
columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x, part.Origin.y, seat ), 0, 0 ) );
return columns;
}
var countX = Math.Max( 1, part.CountX );
var countY = Math.Max( 1, part.CountY );
for ( var ix = 0; ix < countX; ix++ ) {
for ( var iy = 0; iy < countY; iy++ ) {
var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
var world = rotation * local;
columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x + world.x, part.Origin.y + world.y, seat ), ix, iy ) );
}
}
return columns;
}
readonly record struct Course( float Bottom, float Top, Vector2 Lower, Vector2 Upper, bool Dressed );
public static List<ArchCarveVolume> Bites( ArchPlan plan, ArchKit kit, ArchPillarPart part, ArchRoom room, ArchBuilding building, Vector3 basePoint, float height ) {
var bites = new List<ArchCarveVolume>();
if ( plan is null || room is null ) {
return bites;
}
var widest = part.Reach;
var flat = new Vector2( basePoint.x, basePoint.y );
var footprint = ArchFootprint.Rect( flat - new Vector2( widest, widest ), flat + new Vector2( widest, widest ) );
var foot = basePoint.z - MathF.Max( 0f, part.FootingBuried );
var head = basePoint.z + height;
foreach ( var cut in ArchCut.Over( plan, room.Floor, footprint, building?.Id ?? 0, ArchCutAffects.Pillars, part.Id ) ) {
bites.AddRange( ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, foot, head ) ) );
}
return bites;
}
static void Column( ArchMesh canvas, ArchPillarPart part, Vector3 basePoint, float height, ArchBrush shaft, ArchBrush cap, List<ArchCarveVolume> bites = null ) {
var courses = Courses( part, basePoint.z, height );
if ( courses.Count == 0 ) {
return;
}
var flat = new Vector2( basePoint.x, basePoint.y );
if ( bites is { Count: > 0 } ) {
Carved( canvas, part, flat, courses, bites, shaft, cap );
return;
}
Hulled( canvas, part, flat, courses, shaft, cap );
}
// One welded hull per column — keeps colonnades walkable
static void Hulled( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, ArchBrush shaft, ArchBrush cap ) {
var sides = part.Sides;
using var welding = canvas.Welding();
using var solid = canvas.Solid( ArchSolid.Hull( Rings( courses, flat, sides, part.Yaw ) ) );
canvas.Polygon( Ring( flat, courses[0].Lower, courses[0].Bottom, sides, part.Yaw ), courses[0].Dressed ? cap : shaft, true );
for ( var index = 0; index < courses.Count; index++ ) {
var course = courses[index];
var brush = course.Dressed ? cap : shaft;
Faces( canvas, flat, course, sides, part.Yaw, brush );
if ( index == courses.Count - 1 ) {
continue;
}
Step( canvas, flat, course, courses[index + 1], sides, part.Yaw, brush );
}
var last = courses[^1];
canvas.Polygon( Ring( flat, last.Upper, last.Top, sides, part.Yaw ), last.Dressed ? cap : shaft );
}
// Prism carve per course — no taper (only footings taper, and those are buried)
static void Carved( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, List<ArchCarveVolume> bites, ArchBrush shaft, ArchBrush cap ) {
using var welding = canvas.Welding();
foreach ( var course in courses ) {
var ring = Ring( flat, course.Lower, 0f, part.Sides, part.Yaw )
.Select( point => new Vector2( point.x, point.y ) )
.ToList();
var carve = ArchCarve.Prism( ring, course.Bottom, course.Top ).In( part.Yaw );
foreach ( var bite in bites ) {
carve.Less( bite );
}
foreach ( var face in carve.Resolve().Faces ) {
canvas.Polygon( face.Points, course.Dressed ? cap : shaft );
}
}
}
static List<Vector3> Rings( List<Course> courses, Vector2 flat, int sides, float yaw ) {
var points = new List<Vector3>();
foreach ( var course in courses ) {
points.AddRange( Ring( flat, course.Lower, course.Bottom, sides, yaw ) );
points.AddRange( Ring( flat, course.Upper, course.Top, sides, yaw ) );
}
return points;
}
static List<Course> Courses( ArchPillarPart part, float baseHeight, float height ) {
var courses = new List<Course>();
var section = part.Half;
var plinth = part.Plinth ? MathF.Max( 0f, part.PlinthHeight ) : 0f;
var capital = part.Capital ? MathF.Max( 0f, part.CapitalHeight ) : 0f;
var top = baseHeight + height;
var shaftBottom = baseHeight + plinth;
var shaftTop = top - capital;
Footing( part, courses, section, baseHeight );
if ( plinth > 0.05f ) {
var spread = section + new Vector2( part.PlinthOversize, part.PlinthOversize );
courses.Add( new Course( baseHeight, shaftBottom, spread, spread, true ) );
}
if ( shaftTop - shaftBottom > 0.05f ) {
courses.Add( new Course( shaftBottom, shaftTop, section, section, false ) );
}
if ( capital > 0.05f ) {
Head( part, courses, section, shaftTop, top );
}
return courses;
}
static void Footing( ArchPillarPart part, List<Course> courses, Vector2 section, float baseHeight ) {
if ( part.Footing == PillarFooting.None ) {
return;
}
var foot = baseHeight - MathF.Max( 0f, part.FootingBuried );
courses.AddRange( Pad( part.Footing, section, part.FootingReach,
foot, foot + MathF.Max( 1f, part.FootingHeight ) ) );
}
// Mirrors the footing pad — level with soffit, not lapped
static void Head( ArchPillarPart part, List<Course> courses, Vector2 section, float from, float top ) {
var mirrored = new List<Course>();
foreach ( var course in Pad( part.CapitalShape, section, MathF.Max( 0f, part.CapitalOversize ), from, top ) ) {
mirrored.Add( new Course( from + top - course.Top, from + top - course.Bottom,
course.Upper, course.Lower, course.Dressed ) );
}
mirrored.Reverse();
courses.AddRange( mirrored );
}
static IEnumerable<Course> Pad( PillarFooting kind, Vector2 section, float spread, float foot, float head ) {
var wide = section + new Vector2( spread, spread );
var depth = MathF.Max( 1f, head - foot );
switch ( kind ) {
case PillarFooting.Bevelled:
var shelf = foot + depth * 0.34f;
yield return new Course( foot, shelf, wide, wide, true );
yield return new Course( shelf, foot + depth, wide, section, true );
break;
case PillarFooting.Stepped:
var middle = foot + depth * 0.5f;
var upper = section + new Vector2( spread * 0.5f, spread * 0.5f );
yield return new Course( foot, middle, wide, wide, true );
yield return new Course( middle, foot + depth, upper, upper, true );
break;
default:
yield return new Course( foot, foot + depth, wide, wide, true );
break;
}
}
static void Faces( ArchMesh canvas, Vector2 centre, Course course, int sides, float yaw, ArchBrush brush ) {
var lower = Ring( centre, course.Lower, course.Bottom, sides, yaw );
var upper = Ring( centre, course.Upper, course.Top, sides, yaw );
for ( var index = 0; index < lower.Length; index++ ) {
var next = (index + 1) % lower.Length;
canvas.Quad( lower[index], lower[next], upper[next], upper[index], brush );
}
}
static void Step( ArchMesh canvas, Vector2 centre, Course below, Course above, int sides, float yaw, ArchBrush brush ) {
if ( MathF.Abs( below.Upper.x - above.Lower.x ) < 0.01f && MathF.Abs( below.Upper.y - above.Lower.y ) < 0.01f ) {
return;
}
var stepsIn = below.Upper.x >= above.Lower.x;
var wide = Ring( centre, stepsIn ? below.Upper : above.Lower, below.Top, sides, yaw );
var tight = Ring( centre, stepsIn ? above.Lower : below.Upper, below.Top, sides, yaw );
for ( var index = 0; index < wide.Length; index++ ) {
var next = (index + 1) % wide.Length;
if ( stepsIn ) {
canvas.Quad( wide[index], wide[next], tight[next], tight[index], brush );
continue;
}
canvas.Quad( tight[index], tight[next], wide[next], wide[index], brush );
}
}
static Vector3[] Ring( Vector2 centre, Vector2 half, float height, int sides, float yaw ) {
var corners = sides < 5
? new[]
{
new Vector2( centre.x - half.x, centre.y - half.y ),
new Vector2( centre.x + half.x, centre.y - half.y ),
new Vector2( centre.x + half.x, centre.y + half.y ),
new Vector2( centre.x - half.x, centre.y + half.y )
}
: ArchFootprint.Ellipse( centre - half, centre + half, sides ).ToArray();
var turned = MathF.Abs( yaw % 360f ) < 0.01f ? corners : ArchFootprint.Turned( corners, centre, yaw ).ToArray();
return turned.Select( point => new Vector3( point.x, point.y, height ) ).ToArray();
}
static void Spans( ArchMesh canvas, ArchPillarPart part, ArchKit kit, float seat, float height, ArchBrush brush ) {
var rotation = Rotation.FromYaw( part.Yaw );
var top = seat + height;
var springing = MathF.Max( seat + 8f, top - part.SpanBand );
var half = part.Half;
var lap = ArchLap.Bite( kit );
foreach ( var run in Runs( part ) ) {
var inset = (run.AlongX ? half.x : half.y) - lap;
Connect( canvas, part,
Point( part, rotation, run.FromX, run.FromY ), Point( part, rotation, run.ToX, run.ToY ),
run.AlongX, springing, top, inset, brush );
}
foreach ( var (ix, iy) in Carried( part ) ) {
var centre = Point( part, rotation, ix, iy );
canvas.Box(
new Vector3( centre.x - half.x, centre.y - half.y, ArchLap.Bury( kit, springing, 1f ) ),
new Vector3( centre.x + half.x, centre.y + half.y, top ),
brush );
}
}
static Vector2 Point( ArchPillarPart part, Rotation rotation, int ix, int iy ) {
var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
var world = rotation * local;
return new Vector2( part.Origin.x + world.x, part.Origin.y + world.y );
}
static void Connect( ArchMesh canvas, ArchPillarPart part, Vector2 from, Vector2 to, bool alongX, float springing, float top, float inset, ArchBrush brush ) {
ArchSpanGen.Build( canvas, new ArchSpanRun {
From = from,
To = to,
Springing = springing,
Top = top,
Thickness = part.SpanThickness( alongX ),
InsetFrom = inset,
InsetTo = inset,
Segments = part.ArchSegments,
Ring = part.ArchRing,
Form = part.Span
}, brush );
}
static void Pilaster( ArchMesh canvas, ArchPillarPart part, ArchRoom room, float height, ArchBrush shaft, ArchBrush cap, ArchKit kit ) {
var wall = room.Walls.Find( candidate => candidate.Id == part.WallId ) ?? room.Walls.FirstOrDefault();
if ( wall is null ) {
return;
}
var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
var half = thickness * 0.5f;
var halfWidth = part.PilasterWidth * 0.5f;
var offset = Math.Clamp( part.WallOffset, halfWidth, Math.Max( halfWidth, wall.Length - halfWidth ) );
var direction = wall.Direction;
var normal = wall.Normal;
var centre = wall.PointAt( offset );
Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, false );
if ( part.PilasterBothFaces ) {
Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, true );
}
}
static void Face(
ArchMesh canvas,
Vector2 centre,
Vector2 direction,
Vector2 normal,
float halfWidth,
float wallHalf,
ArchPillarPart part,
float height,
ArchBrush shaft,
ArchBrush cap,
bool inner ) {
// Exterior is -normal; flip both normal and run direction together to preserve winding
var sign = inner ? 1f : -1f;
var run = direction * sign;
var near = wallHalf * sign;
var far = (wallHalf + part.PilasterProtrusion) * sign;
var bottom = part.BaseHeight;
var plinth = part.Plinth ? Math.Max( 0f, part.PlinthHeight ) : 0f;
var capital = part.Capital ? Math.Max( 0f, part.CapitalHeight ) : 0f;
Slab( canvas, centre, run, normal, halfWidth, near, far, bottom + plinth, bottom + height - capital, shaft );
if ( plinth > 0.05f ) {
Slab( canvas, centre, run, normal, halfWidth + part.PlinthOversize, near, far + part.PlinthOversize * sign, bottom, bottom + plinth, cap );
}
if ( capital > 0.05f ) {
Slab( canvas, centre, run, normal, halfWidth + part.CapitalOversize, near, far + part.CapitalOversize * sign, bottom + height - capital, bottom + height, cap );
}
}
static void Slab(
ArchMesh canvas,
Vector2 centre,
Vector2 direction,
Vector2 normal,
float halfWidth,
float near,
float far,
float bottom,
float top,
ArchBrush brush ) {
if ( top - bottom < 0.05f ) {
return;
}
var a = centre - direction * halfWidth + normal * near;
var b = centre + direction * halfWidth + normal * near;
var c = centre + direction * halfWidth + normal * far;
var d = centre - direction * halfWidth + normal * far;
var lower = new List<Vector3>
{
new( a.x, a.y, bottom ),
new( b.x, b.y, bottom ),
new( c.x, c.y, bottom ),
new( d.x, d.y, bottom )
};
var upper = new List<Vector3>();
foreach ( var point in lower ) {
upper.Add( point.WithZ( top ) );
}
canvas.Prism( lower, upper, brush );
}
}