Data class representing an architectural pillar part used in the editor. It stores placement, dimensions, counts, footing/plinth/capital options, span/arch parameters and exposes computed properties and helpers like TotalCount, SpanBand, Curved, SpanThickness, Half, Reach, Seat and a Section method that returns a copy with only the placement/type/section fields.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum PillarPlacement
{
Freestanding,
Grid,
Pilaster
}
// APPEND-ONLY: a plan on disk names these by ordinal.
public enum PillarSpan
{
None,
Beam,
Arch,
// Not a shortened arch - a cove filling the corner at each end, with nothing between them, which is how a
// concrete pier reads as flaring into the slab it carries.
Haunch
}
// A pier carrying anything real has one - founded, not just resting on the ground.
public enum PillarFooting
{
None,
Square,
Bevelled,
Stepped
}
public sealed class ArchPillarPart : IArchCollides, IArchPainted, IArchNamed
{
// Overrides the kit for THIS part alone: one wall that needs its exact holes, one tower that needs none.
public ArchCollisionMode? Collision { get; set; }
public int Id { get; set; }
public string Name { get; set; } = "Pillars";
// Retyping replaces the whole column, like a bridge type.
public string Type { get; set; } = "";
public PillarPlacement Placement { get; set; } = PillarPlacement.Grid;
public Vector2 Origin { get; set; }
public float Yaw { get; set; }
public float BaseHeight { get; set; }
public float Height { get; set; }
// A rectangle, not a uniformly scaled lathe profile - piers need width AND depth.
public float Width { get; set; } = 20f;
public float Depth { get; set; } = 20f;
// Four or fewer is the rectangle; more is the n-gon inscribed in Width x Depth, so a column dragged as
// a circle is round and every course - footing, plinth, capital - is round with it.
public int Sides { get; set; }
public Vector2 Spacing { get; set; } = new( 192f, 192f );
public int CountX { get; set; } = 3;
public int CountY { get; set; } = 2;
public PillarFooting Footing { get; set; } = PillarFooting.None;
// Buried below the base so a footing on moving ground doesn't float.
public float FootingSpread { get; set; } = 8f;
public float FootingHeight { get; set; } = 10f;
public float FootingBuried { get; set; } = 4f;
public bool Plinth { get; set; }
public float PlinthHeight { get; set; } = 8f;
public float PlinthOversize { get; set; } = 3f;
public bool Capital { get; set; }
public float CapitalHeight { get; set; } = 8f;
public float CapitalOversize { get; set; } = 3f;
public PillarSpan Span { get; set; } = PillarSpan.None;
public bool SpanAlongX { get; set; } = true;
public bool SpanAlongY { get; set; }
// Without it a two-way beam is a floor frame, not an arcade.
public bool PerimeterOnly { get; set; }
public float SpanDepth { get; set; } = 14f;
public float SpanHeight { get; set; } = 18f;
public float ArchRise { get; set; } = 48f;
public float ArchRing { get; set; } = 10f;
public int ArchSegments { get; set; } = 10;
public int WallId { get; set; }
public float WallOffset { get; set; }
public float PilasterWidth { get; set; } = 20f;
public float PilasterProtrusion { get; set; } = 6f;
public bool PilasterBothFaces { get; set; }
public ArchPalette Palette { get; set; } = new();
public int TotalCount => Placement switch
{
PillarPlacement.Grid => System.Math.Max( 1, CountX ) * System.Math.Max( 1, CountY ),
_ => 1
};
// Measured down from the column top. An arch spends its band on the ring as well; a cove's band IS its radius.
public float SpanBand => Span switch
{
PillarSpan.Arch => System.MathF.Max( 8f, ArchRise + System.MathF.Max( 2f, ArchRing ) ),
PillarSpan.Haunch => System.MathF.Max( 2f, ArchRise ),
_ => System.MathF.Max( 1f, SpanHeight )
};
public bool Curved => Span is PillarSpan.Arch or PillarSpan.Haunch;
// An arch is cut through its pier, so its ring follows the pier's own section rather than a thinner authored
// one - but SHY of it, never level: two solids sharing a face plane down both sides flicker, and the hairline
// that costs is a fraction of a texel. An authored depth already proud of the pier is taken as authored.
public float SpanThickness( bool alongX )
{
var authored = System.MathF.Max( 1f, SpanDepth );
var across = alongX ? System.MathF.Max( 1f, Depth ) : System.MathF.Max( 1f, Width );
return Curved ? System.MathF.Max( authored, System.MathF.Max( 1f, across - ArchContact.Shy ) ) : authored;
}
public Vector2 Half => new( System.MathF.Max( 1f, Width ) * 0.5f, System.MathF.Max( 1f, Depth ) * 0.5f );
public float FootingReach => Footing == PillarFooting.None ? 0f : System.MathF.Max( 0.5f, FootingSpread );
float Dressing => System.MathF.Max( Plinth ? PlinthOversize : 0f, Capital ? CapitalOversize : 0f );
// The widest thing on the column - what a span trims back to.
public float Reach => System.MathF.Max( Half.x, Half.y ) + Dressing;
// At its base, pad included - what sets a column in from an edge.
public float Seat => System.MathF.Max( Half.x, Half.y ) + System.MathF.Max( Dressing, FootingReach );
// What a type hands a placement - listed once so no field is left off either.
public ArchPillarPart Section()
{
return new ArchPillarPart
{
Type = Type,
Placement = Placement,
Width = Width,
Depth = Depth,
Sides = Sides,
Footing = Footing,
FootingSpread = FootingSpread,
FootingHeight = FootingHeight,
FootingBuried = FootingBuried,
Plinth = Plinth,
PlinthHeight = PlinthHeight,
PlinthOversize = PlinthOversize,
Capital = Capital,
CapitalHeight = CapitalHeight,
CapitalOversize = CapitalOversize,
Span = Span,
SpanAlongX = SpanAlongX,
SpanAlongY = SpanAlongY,
PerimeterOnly = PerimeterOnly,
SpanDepth = SpanDepth,
SpanHeight = SpanHeight,
ArchRise = ArchRise,
ArchRing = ArchRing,
ArchSegments = ArchSegments,
Spacing = Spacing,
PilasterWidth = PilasterWidth,
PilasterProtrusion = PilasterProtrusion,
PilasterBothFaces = PilasterBothFaces,
Palette = Palette
};
}
}