Editor/Roof/ArchRoofPart.cs

Editor-side data model for roof, roof-light, ladder and downpipe parts used by an architecture editor. Defines enums and plain POCO classes that store geometry, style flags, palettes and helper computed properties, plus a Duplicate/Reshape API for roof parts.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public enum RoofStyle
{
	Flat,
	Shed,
	Gable,
	Hip,
	Sawtooth
}

// A monitor is the same carve standing tall enough to be a housing: clad sides, a coping, a lid on top. Roof
// plant is the same carve called differently again - so append only, because a form inserted mid-list renumbers
// every authored light after it. What each one actually builds is ArchRoofLightKinds, never a test out here.
public enum RoofLightForm
{
	Kerbed,
	Monitor,
	Chimney,
	Vent,
	Tank,
	Hatch,
	Dormer
}

public sealed class ArchRoofPart : 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; } = "Roof";
	// Which storey it roofs - a height alone can't say.
	public int Level { get; set; }
	public RoofStyle Style { get; set; } = RoofStyle.Gable;
	public Vector2 Min { get; set; }
	public Vector2 Max { get; set; }
	public float BaseHeight { get; set; } = 128f;
	public float Pitch { get; set; } = 30f;
	public float Overhang { get; set; } = 16f;
	public float Thickness { get; set; } = 6f;
	public int SawtoothBays { get; set; } = 4;
	public bool RidgeAlongX { get; set; } = true;
	// Sends the fall the other way - a porch roof on the house's low side.
	public bool Reversed { get; set; }
	public bool Gutters { get; set; } = true;
	public bool Fascia { get; set; } = true;
	public bool Soffit { get; set; } = true;
	public bool EndWalls { get; set; } = true;
	public bool RidgeCap { get; set; } = true;
	// Rafters and purlins left showing under the deck.
	public bool Frame { get; set; }
	public float FrameSpacing { get; set; }
	public bool Ceiling { get; set; }
	public bool LoftFloor { get; set; }
	public bool VaultedCeiling { get; set; }
	public float VaultRise { get; set; } = 48f;
	// A walkway deck: the cornice runs only along the run, never across the mouths.
	public bool Walkway { get; set; }
	public bool Cornice { get; set; } = true;
	public bool Parapet { get; set; }
	public float ParapetHeight { get; set; } = 18f;
	// Edge protection, standing on the parapet where there is one and on the deck where there is not.
	public bool Guardrail { get; set; }
	public float GuardHeight { get; set; } = 42f;
	// Placed, not derived; left off Duplicate - a light is flat-deck only.
	public List<ArchRoofLightPart> Lights { get; set; } = new();
	// Walls standing ON the deck: a parapet that varies edge by edge, a party wall between two rooftops. Left off
	// Duplicate for the reason Lights is - a split hands the new section a fresh deck, and the walls that were
	// authored stay on the section they were drawn against.
	public List<ArchWall> Walls { get; set; } = new();
	public List<Vector2> Footprint { get; set; } = new();
	public ArchPalette Palette { get; set; } = new();

	public Vector2 Size => Max - Min;

	public bool HasFootprint => Footprint.Count >= 4;

	public List<Vector2> Outline() => ArchFootprint.OrRect( Footprint, Min, Max );

	public void Reshape( List<Vector2> outline )
	{
		(Footprint, Min, Max) = ArchFootprint.Authored( outline );
	}

	public ArchRoofPart Duplicate( int id, List<Vector2> outline )
	{
		var (loop, min, max) = ArchFootprint.Authored( outline );

		return new ArchRoofPart
		{
			Id = id,
			Name = Name,
			Level = Level,
			Style = Style,
			Min = min,
			Max = max,
			BaseHeight = BaseHeight,
			Pitch = Pitch,
			Overhang = Overhang,
			Thickness = Thickness,
			SawtoothBays = SawtoothBays,
			RidgeAlongX = ArchBuild.Ridged( RidgeRun.Auto, min, max ),
			Reversed = Reversed,
			Gutters = Gutters,
			Fascia = Fascia,
			Soffit = Soffit,
			EndWalls = EndWalls,
			RidgeCap = RidgeCap,
			Frame = Frame,
			FrameSpacing = FrameSpacing,
			Ceiling = Ceiling,
			LoftFloor = LoftFloor,
			VaultedCeiling = VaultedCeiling,
			VaultRise = VaultRise,
			Walkway = Walkway,
			Cornice = Cornice,
			Parapet = Parapet,
			ParapetHeight = ParapetHeight,
			Guardrail = Guardrail,
			GuardHeight = GuardHeight,
			Footprint = loop,
			Palette = Palette
		};
	}
}

// The hole is a carve - the kerb lining is the reveal faces, not a second box.
public sealed class ArchRoofLightPart : IArchNamed
{
	public int Id { get; set; }
	public string Name { get; set; } = "RoofLight";
	public RoofLightForm Form { get; set; } = RoofLightForm.Kerbed;
	public Vector2 Min { get; set; }
	public Vector2 Max { get; set; }
	// Flush panes pond water and read as a painted rectangle.
	public float Curb { get; set; } = 8f;
	public float CurbWidth { get; set; } = 6f;
	public bool Glazed { get; set; } = true;
	// 0 leaves one sheet; else a bar wherever a pane would otherwise run wider than this.
	public float PaneSpan { get; set; }
	public ArchPalette Palette { get; set; } = new();

	public bool IsMonitor => Form == RoofLightForm.Monitor;

	public Vector2 Size => Max - Min;

	public List<Vector2> Outline() => ArchFootprint.Rect( Min, Max );
}

public sealed class ArchLadderPart : IArchNamed
{
	public int Id { get; set; }
	public string Name { get; set; } = "Ladder";
	// Anchors, not a wall id - it outlives the wall re-drawn under it.
	public Vector2 Anchor { get; set; }
	public Vector2 Outward { get; set; } = new( 1f, 0f );
	public float BaseHeight { get; set; }
	public float TopHeight { get; set; } = 128f;
	public float Width { get; set; } = 18f;
	public float Standoff { get; set; } = 7f;
	public float RungSpacing { get; set; } = 12f;
	public bool Cage { get; set; }
	public float CageFrom { get; set; } = 84f;
	public bool Return { get; set; } = true;
	public ArchPalette Palette { get; set; } = new();

	public float Rise => MathF.Max( 1f, TopHeight - BaseHeight );

	public Vector2 Facing => Outward.Length < 0.01f ? new Vector2( 1f, 0f ) : Outward.Normal;

	public Vector2 Along
	{
		get
		{
			var facing = Facing;

			return new Vector2( -facing.y, facing.x );
		}
	}
}

// A conduit is a downpipe with a different profile and a bracket rhythm - no eave to tap, no hopper, no swan neck.
public enum PipeRun
{
	Rainwater,
	Conduit
}

public sealed class ArchDownpipePart : IArchPainted, IArchNamed
{
	public int Id { get; set; }
	public string Name { get; set; } = "Downpipe";
	public Vector2 Outlet { get; set; }
	public Vector2 Wall { get; set; }
	public float TopHeight { get; set; }
	public float BaseHeight { get; set; }
	public string Profile { get; set; } = "downpipe";
	public bool Shoe { get; set; } = true;
	public PipeRun Run { get; set; } = PipeRun.Rainwater;
	public ArchPalette Palette { get; set; } = new();

	public bool IsConduit => Run == PipeRun.Conduit;
}