Editor/Pillar/ArchPillarPart.cs

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.

File Access
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
		};
	}
}