Editor/Pillar/ArchPillarPart.cs
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,
Haunch
}
public enum PillarFooting {
None,
Square,
Bevelled,
Stepped
}
public sealed class ArchPillarPart : IArchCollides, IArchPainted, IArchNamed {
public ArchCollisionMode? Collision { get; set; }
public int Id { get; set; }
// Zero = hand-authored; nonzero = owned by an extrude zone etc.
public int OwnerId { get; set; }
public string Name { get; set; } = "Pillars";
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; }
// True = hand-authored seat; false = re-reads terrain/deck on each build
public bool Founded { get; set; }
public float Width { get; set; } = 20f;
public float Depth { get; set; } = 20f;
// ≤4 = rectangle; >4 = n-gon inscribed in Width×Depth
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 PillarFooting CapitalShape { get; set; } = PillarFooting.Square;
public PillarSpan Span { get; set; } = PillarSpan.None;
public bool SpanAlongX { get; set; } = true;
public bool SpanAlongY { get; set; }
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;
// Arch follows pier section minus a shy offset to avoid coplanar flicker
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 - ArchLap.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 );
public float Reach => System.MathF.Max( Half.x, Half.y ) + Dressing;
public float Seat => System.MathF.Max( Half.x, Half.y ) + System.MathF.Max( Dressing, FootingReach );
// Per-axis reach including all courses — for flush snap against a face
public Vector2 Seated => Spanned( Half + Vector2.One * System.MathF.Max( Dressing, FootingReach ) );
// Top course reach — for flush snap under a soffit
public Vector2 Heading => Spanned( Half + Vector2.One * (Capital ? System.MathF.Max( 0f, CapitalOversize ) : 0f) );
Vector2 Spanned( Vector2 reach ) {
if ( Span == PillarSpan.None || Placement != PillarPlacement.Grid ) {
return reach;
}
return new Vector2(
System.MathF.Max( reach.x, SpanAlongY ? SpanThickness( false ) * 0.5f : 0f ),
System.MathF.Max( reach.y, SpanAlongX ? SpanThickness( true ) * 0.5f : 0f ) );
}
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,
CapitalShape = CapitalShape,
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
};
}
}