Editor-side data model for an architectural cut part. Defines enums for cut profiles, enclosure, heads, damage kinds and patterns, flags for affected systems, and two classes: ArchCutSegment represents one leg/segment with geometry and helpers, ArchCutPart aggregates segments and many settings for how a cut affects architecture and rendering.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum CutProfile
{
Box,
Steps,
Passage,
// A free footprint - the leg carries its own loop instead of a Start/End rectangle.
Poly
}
// A stairwell past the solid it started in must carry its own walls up.
public enum CutEnclosure
{
None,
Walls
}
// Housing is the little walled box with its own roof; coping is the band round the mouth, the same finish a
// platform's own outside edge wears.
public enum CutHead
{
None,
Cap,
Coping,
Housing
}
public enum ArchDamageKind
{
None,
Masonry,
SurfaceSpall,
MissingPanels,
DisplacedPanels,
MixedPanels
}
public enum ArchMasonryPattern
{
ExposedEdge,
WornCorner,
BrickPier
}
[Flags]
public enum ArchCutAffects
{
None = 0,
Floors = 1 << 0,
Ceilings = 1 << 1,
Foundations = 1 << 2,
Walls = 1 << 3,
WallFittings = 1 << 4,
Roofs = 1 << 5,
Platforms = 1 << 6,
Trims = 1 << 7,
Gutters = 1 << 8,
// The units already hung in a wall - a window, a door, a garage. Only ones the hole covers WHOLE: a cut
// across one jamb leaves the window standing, because a window is a thing and not a length of wall.
Windows = 1 << 9,
Beams = 1 << 10,
// The road-borne hosts: a tunnel's swept lining, and the road's own strips. A cut over one is world-space
// like every other, so joining two roads re-parameterises the run without moving the hole.
Lining = 1 << 11,
Roadway = 1 << 12,
Pillars = 1 << 13,
// A flight's treads and its landings. What a cut leaves is a new EDGE up the side of the steps, and an edge
// on a stair is a drop, so it is railed - which is what makes a lift shaft standing in the well between two
// flights one dragged box rather than a hand-built assembly.
Stairs = 1 << 14,
All = Floors | Ceilings | Foundations | Walls | WallFittings | Roofs | Platforms | Trims | Gutters | Windows | Beams | Lining | Roadway | Pillars | Stairs
}
// A stairwell is a chain of these - straight is one, an L is two.
public sealed class ArchCutSegment
{
public Vector2 Start { get; set; }
public Vector2 End { get; set; }
public float Width { get; set; } = 48f;
public float BaseHeight { get; set; }
public float TopHeight { get; set; } = 128f;
// A free footprint - when set, the leg IS this loop and Start/End only name it.
public List<Vector2> Loop { get; set; }
public bool HasLoop => Loop is { Count: >= 3 };
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 List<Vector2> Outline()
{
if ( HasLoop )
{
return Loop;
}
var axes = Axes;
var across = axes.Across * (MathF.Max( 1f, Width ) * 0.5f);
return new List<Vector2> { Start - across, End - across, End + across, Start + across };
}
public ArchCutSegment Copy()
{
return new ArchCutSegment
{
Start = Start,
End = End,
Width = Width,
BaseHeight = BaseHeight,
TopHeight = TopHeight,
Loop = Loop?.ToList()
};
}
}
// Nothing here says what it cuts into - generators ask which cuts cover them.
public sealed class ArchCutPart : IArchRamped, IArchPainted, IArchNamed
{
public int Id { get; set; }
public string Name { get; set; } = "Cut";
// The layer whose effect this bite IS - a recessed balcony's loggia. Deleting the owner takes the cut,
// the way deleting a flight takes its stairwell. Zero was authored by hand and answers to nobody.
public int OwnerId { get; set; }
public int Level { get; set; }
public CutProfile Profile { get; set; } = CutProfile.Box;
public ArchCutAffects Affects { get; set; } = ArchCutAffects.All;
public List<ArchCutSegment> Segments { get; set; } = new();
public float StepRise { get; set; }
public float StepGoing { get; set; }
// A raked FLOOR rather than a raked top: nothing is taken out at the head and the whole band goes at the
// foot, so what is left under it comes out as a ramp you can walk. Same three numbers a platform's rake has,
// answered by the same ArchRamp.
public bool Ramp { get; set; }
public float RampFall { get; set; }
public float RampYaw { get; set; }
// The rise belongs to the chain, not to a height typed per leg.
public float Rise { get; set; }
// Off, a leg keeps the angle it was dragged on - a shaft at any angle between two buildings.
public bool SnapAngle { get; set; } = true;
public CutEnclosure Enclosure { get; set; }
public float WallThickness { get; set; } = 8f;
public bool Edge { get; set; }
public float EdgeWidth { get; set; }
// A drop this cut opens across a stair wears the flight's balustrade; off, the edge stays bare so a
// split flight can carry a pillar line instead.
public bool GuardsOpenedEdges { get; set; } = true;
// How FAR it wanders is the kit's BreakJitter and how it wanders is this cut's own id - there is no amount,
// no frequency and no seed to author here.
public bool BreakEdges { get; set; }
public ArchDamageKind Damage { get; set; }
public float DamageAmount { get; set; } = 0.55f;
public float DamageCellWidth { get; set; } = 24f;
public float DamageCellLength { get; set; } = 48f;
public float DamageDepth { get; set; } = 4f;
public ArchMasonryPattern MasonryPattern { get; set; }
public bool MasonryCarves { get; set; }
public float DamageDrop { get; set; } = 4f;
public float DamageTilt { get; set; } = 12f;
public ArchDamageKind ResolvedDamage => Damage != ArchDamageKind.None
? Damage
: Name?.StartsWith( "Damage", StringComparison.OrdinalIgnoreCase ) == true ? ArchDamageKind.Masonry : ArchDamageKind.None;
public bool IsDamage => ResolvedDamage != ArchDamageKind.None;
public CutHead Head { get; set; }
public float HeadHeight { get; set; } = 96f;
public bool HeadRoof { get; set; } = true;
// The band's own numbers, not the housing's - a coping is four inches tall where a housing is eight foot.
public float CopingHeight { get; set; } = 4f;
public float CopingOversail { get; set; } = 1.5f;
public ArchPalette Palette { get; set; } = new();
public bool HasContent => Segments.Any( segment => segment.HasLoop || segment.Length > 1f );
// A cut that only carves reports none - deleting it leaves nothing to prune.
public bool Builds => Enclosure != CutEnclosure.None || Head != CutHead.None;
public IEnumerable<ArchCutSegment> Legs() => Segments.Where( segment => segment.HasLoop || segment.Length > 1f );
public IEnumerable<List<Vector2>> Outlines() => Legs().Select( segment => segment.Outline() );
}