Editor-side geometry generator for pillars and pilasters. Computes pillar heights, column positions and runs, builds mesh geometry (shafts, caps, spans, pilasters), handles carving when other geometry overlaps, and emits polygons/prisms to an ArchMesh.
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
{
// One answer to how tall a pillar stands: its authored height where it has one, else up to the lowest thing
// standing over it. The generator, the ghost, the handle and the extents report all resolve the same way, so
// a part that climbs past its room reads the same to the shaft that builds it and the box that selects it.
//
// Level with that soffit rather than lapped into it: the contact pass deletes a face its coplanar neighbour
// covers whole, so a bite would only bury a surviving face inside the slab.
public static float Height( ArchPillarPart part, ArchRoom room, ArchKit kit, ArchPlan plan = null )
{
if ( part.Height > 0f )
{
return part.Height;
}
if ( room is null )
{
return kit.WallHeight;
}
return MathF.Max( 8f, ArchPillarSoffit.Over( plan, kit, room, part ) - part.BaseHeight );
}
// Computed once, so the spandrel and the run landing on it cannot disagree.
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 height = Height( part, room, kit, plan );
if ( part.Placement == PillarPlacement.Pilaster )
{
Pilaster( canvas, part, room, height, shaft, cap, kit );
return;
}
// A span bears ON the columns, so the shaft stops at the springing.
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)>();
// Only a column a run springs off gives up its top to the span.
foreach ( var column in Columns( part ) )
{
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, height, cap );
}
}
}
// A section standing where something else already decided its seat - roof plant on a deck plane, not a column
// in a room, so there is no wall height to fall back on and its own is the only answer.
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 )
{
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, part.BaseHeight ), 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, part.BaseHeight ), ix, iy ) );
}
}
return columns;
}
readonly record struct Course( float Bottom, float Top, Vector2 Lower, Vector2 Upper, bool Dressed );
// What a Subtract standing over this column takes out of it, in the band the column actually occupies. Answered
// through ArchCut like every other host's, so ArchLayerOrder decides whether a cut authored before the column
// reaches it at all.
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 );
}
// A column nothing carves stays ONE welded hull, which is what keeps a colonnade walkable and what stops a
// stacked box sealing a face pair at every step.
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();
// ONE shape for the whole column, not one per course: the hull over every ring closes the inset at a plinth
// or a capital step, which nothing can stand in anyway, and a colonnade stays walkable because each column
// is its own hull rather than all of them being one mesh.
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 );
}
// A column a cut reaches has no hull to be, so it goes through the same carve algebra a platform does - one
// prism per course, so a bite's cheeks share the shaft's own edges. A course that TAPERS comes out straight
// sided here: the prism algebra has no taper, and the only tapering course is a footing, which is 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 );
}
}
}
// Every corner the column has, bottom ring to top ring, which is what its hull is taken over.
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 )
{
var spread = section + new Vector2( part.CapitalOversize, part.CapitalOversize );
courses.Add( new Course( shaftTop, top, spread, spread, true ) );
}
return courses;
}
static void Footing( ArchPillarPart part, List<Course> courses, Vector2 section, float baseHeight )
{
if ( part.Footing == PillarFooting.None )
{
return;
}
var spread = part.FootingReach;
var depth = MathF.Max( 1f, part.FootingHeight );
var buried = MathF.Max( 0f, part.FootingBuried );
var foot = baseHeight - buried;
var wide = section + new Vector2( spread, spread );
switch ( part.Footing )
{
case PillarFooting.Bevelled:
// A third stays square under the taper, or the pad reads as a cone.
var shelf = foot + depth * 0.34f;
courses.Add( new Course( foot, shelf, wide, wide, true ) );
courses.Add( 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 );
courses.Add( new Course( foot, middle, wide, wide, true ) );
courses.Add( new Course( middle, foot + depth, upper, upper, true ) );
break;
default:
courses.Add( 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 );
}
}
// Equal sections continue instead - a plain pillar stays six faces.
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;
}
// The two wind opposite ways - the wrong one leaves a hole in the half-edge mesh.
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 );
}
}
// The section, whatever number of sides it has: the rectangle by hand so a plain pier keeps its exact
// corners, and anything rounder through ArchFootprint's own ellipse so a bool's circle and a column's
// circle are the same loop. Turned about its own centre - a yaw that only marched the grid and left every
// section square was a turn handle that did nothing at all to a lone column.
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();
}
// Seated so its top finishes on the column top, not proud of the capital.
static void Spans( ArchMesh canvas, ArchPillarPart part, ArchKit kit, float height, ArchBrush brush )
{
var rotation = Rotation.FromYaw( part.Yaw );
var top = part.BaseHeight + height;
var springing = MathF.Max( part.BaseHeight + 8f, top - part.SpanBand );
var half = part.Half;
var lap = ArchContact.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 );
}
// Laps a bite down into the shaft; flush leaves cap and block in one plane.
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, ArchContact.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 );
}
// Inset is the pier's half-section less a bite, so the run runs INTO the spandrel. The solid itself comes off
// ArchSpanGen, which is what makes an arcade and a hand-picked arch the same geometry.
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 )
{
var sign = inner ? -1f : 1f;
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, direction, normal, halfWidth, near, far, bottom + plinth, bottom + height - capital, shaft );
if ( plinth > 0.05f )
{
Slab( canvas, centre, direction, normal, halfWidth + part.PlinthOversize, near, far + part.PlinthOversize * sign, bottom, bottom + plinth, cap );
}
if ( capital > 0.05f )
{
Slab( canvas, centre, direction, 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 );
}
}