Editor/Carve/ArchCutPart.cs

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.

File AccessNetworking
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() );
}