Editor data classes for stair pieces and a floor cutout used by the architecture editor. Defines enums for stair properties, ArchStairLeg for legacy leg data, ArchStairPart which stores stair configuration, derived properties and a ReverseChain method, and ArchFloorCutout which stores a hole/loop footprint and utility methods.
using System.Text.Json.Serialization;
namespace Sunless.Architecture;
public enum StairTurn
{
None,
Left,
Right,
Back
}
public enum StairSupport
{
ClosedString,
OpenString
}
public enum StairUnder
{
Open,
Filled,
Cupboard
}
public enum StairGuard
{
None,
Left,
Right,
Both
}
// A flight that carries its own walls up the well, so it reads as a stair core rather than a rake
// standing in the open. This is what the Stairwell shaft's enclosure used to stand.
public enum StairWrap
{
None,
Walls
}
// What a stair was drawn as before the core: a chain of world-space boxes that had to OVERLAP, where the square
// two of them shared was the landing. Kept only so a plan authored that way still opens - ArchPlan.Normalize reads
// it once through ArchStairLanes.FromLegs and clears it. Nothing else in the tool may touch it.
public sealed class ArchStairLeg
{
public Vector2 Start { get; set; }
public Vector2 End { get; set; }
public float Width { get; set; }
public bool Landing { get; set; }
public Vector2 Span => End - Start;
public float Length => Span.Length;
public float Yaw => MathF.Atan2( Span.y, Span.x ).RadianToDegree();
public ArchStairAxes Axes => new() { Origin = Start, Yaw = Yaw };
}
public sealed class ArchStairPart : 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; } = "Stair";
public float BaseHeight { get; set; }
// The shaft, and every flight standing in it. Both are the stair - there is nothing else to author.
public ArchStairCore Core { get; set; } = new();
public List<ArchStairLane> Lanes { get; set; } = new();
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
public List<ArchStairLeg> Legs { get; set; }
// The climb, from before it belonged to the shaft. It keeps the OLD json name, which is the whole point:
// ArchPlan.Normalize lifts it onto the core and clears it, and nothing else may read it.
[JsonPropertyName( "TotalRise" )]
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
public float StoredRise { get; set; }
// The width a fresh lane takes, and the fallback for one authored at nothing. A lane keeps its own.
public float Width { get; set; } = 48f;
public float StepRise { get; set; } = 8f;
public float StepGoing { get; set; } = 12f;
public float TreadThickness { get; set; } = 3f;
public float TreadNosing { get; set; } = 1f;
public StairSupport Support { get; set; } = StairSupport.ClosedString;
public StairUnder Under { get; set; } = StairUnder.Open;
public StairGuard Guard { get; set; } = StairGuard.Both;
// What a guarded side stands, per side, so a flight can carry a full balustrade down one flank and a bare
// rake on newels down the other without a railing row being authored on every step to say so.
public StairRailing LeftRailing { get; set; } = StairRailing.Balustrade;
public StairRailing RightRailing { get; set; } = StairRailing.Balustrade;
// Whether a step with no railing rows of its own gets one from Guard and WallRail at all. Off, NOTHING is
// derived: only the railings actually placed on a step stand, which is what makes each of them a thing you
// put somewhere rather than a side effect of a setting.
public bool AutoRailings { get; set; } = true;
public bool WellGuard { get; set; } = true;
// The handrail bolted to the plaster on a side that runs against a wall - the side a balustrade never stands on.
public bool WallRail { get; set; } = true;
public StairWrap Wrap { get; set; }
public float WrapThickness { get; set; } = 8f;
// Zero takes the kit's.
public float BalusterSpacing { get; set; }
// Authored, not derived - it's what a handrail needs to turn through.
public float WellGap { get; set; } = 4f;
public bool TopLanding { get; set; } = true;
public float TopLandingDepth { get; set; } = 48f;
public string CupboardOpening { get; set; } = "door";
public ArchPalette Palette { get; set; } = new();
public bool Risers => Support == StairSupport.ClosedString;
// The climb the stair actually stands: the core's, unless the flights pinned to more than it holds - a box
// follows what is inside it, so a flight dragged to a height climbs to that height.
[JsonIgnore]
public float TotalRise => ArchStairLanes.Climb( Core, Lanes );
[JsonIgnore]
public int StepCount => ArchStairLanes.Steps( TotalRise, StepRise, Core?.Length ?? 0f, StepGoing );
[JsonIgnore]
public Vector2 Origin => Core?.Origin ?? Vector2.Zero;
[JsonIgnore]
public float Yaw => Core?.Yaw ?? 0f;
[JsonIgnore]
public float HeadHeight => BaseHeight + TotalRise;
// The last flight becomes the first and every walk turns round, so the climb flows the other way.
public void ReverseChain()
{
Lanes.Reverse();
foreach ( var lane in Lanes )
{
lane.Walk = lane.Reversed;
}
}
}
// The loop is what's tested, so a turned flight cuts its own shape.
public sealed class ArchFloorCutout
{
public int Id { get; set; }
public string Name { get; set; } = "Stairwell";
public int Level { get; set; }
// Deleting the piercer takes its holes - no shaft left by a deleted stair.
public int OwnerId { get; set; }
public Vector2 Min { get; set; }
public Vector2 Max { get; set; }
public List<Vector2> Loop { get; set; } = new();
// Derived from the cut that opened it, never authored: a stored stairwell is not a ruin.
[JsonIgnore]
public ArchCarveBreak Break { get; set; }
public bool HasLoop => Loop.Count >= 3;
// The one answer to what this hole's footprint is: its authored loop when it has one, otherwise the
// rect its bounds describe. Slabs, planks, foundations and the overlay all read this, so a circle
// cut and a box cut are the same question to every consumer - the way ArchCutSegment.Outline
// answers for a cut's own legs.
public List<Vector2> Outline()
{
return HasLoop ? Loop : ArchFootprint.Rect( Min, Max );
}
public void Reshape( IEnumerable<Vector2> loop )
{
Loop = loop.ToList();
Min = new Vector2( Loop.Min( point => point.x ), Loop.Min( point => point.y ) );
Max = new Vector2( Loop.Max( point => point.x ), Loop.Max( point => point.y ) );
}
public bool Contains( Vector2 point ) => ArchFootprint.Contains( Outline(), point );
}