Editor/Wall/ArchWall.cs

Editor-side data model for architectural walls and related subtypes. Defines enums for cladding and band seating, classes for bands, trims, pilaster runs, opening runs, an interface for wall treatments, utility methods for copying and ordering treatments, and the ArchWall class that stores wall geometry, treatments, palette and helper properties (Length, Direction, Normal, PointAt).

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

namespace Sunless.Architecture;

public enum WallCladding
{
	None,
	Weatherboard,
	Profiled
}

// A wall carries a LIST of them - a real elevation has several fields.
public enum BandSeat
{
	Centred,
	// A cap finishes a field rather than dividing two, so it sits ON it.
	Above
}

public sealed class ArchWallBand
{
	public float Height { get; set; }
	// Relative - a preset naming an absolute height caps only walls that tall.
	public bool AtHead { get; set; }
	// The role the field BELOW this band takes.
	public ArchSurface Field { get; set; } = ArchSurface.WallBase;
	public bool Course { get; set; } = true;
	public BandSeat Seat { get; set; } = BandSeat.Centred;
	// 0 falls back to the kit's own band section.
	public float CourseHeight { get; set; }
	public float CourseDepth { get; set; }

	public float At( float wallHeight ) => AtHead ? wallHeight : Height;

	public ArchWallBand Copy()
	{
		return new ArchWallBand
		{
			Height = Height,
			AtHead = AtHead,
			Field = Field,
			Course = Course,
			Seat = Seat,
			CourseHeight = CourseHeight,
			CourseDepth = CourseDepth
		};
	}
}

// Marks no change of field, so it can sit anywhere.
public sealed class ArchWallTrim
{
	public float Height { get; set; } = 96f;
	// Relative - one treatment is worn by walls of several heights.
	public bool AtHead { get; set; }
	public float Rise { get; set; } = 3f;
	public float Depth { get; set; } = 1.5f;
	public float Bevel { get; set; } = 0.75f;
	public ArchSurface Field { get; set; } = ArchSurface.Trim;
	public bool Interior { get; set; }

	public float At( float wallHeight ) => AtHead ? wallHeight - Rise : Height;

	public ArchWallTrim Copy()
	{
		return new ArchWallTrim
		{
			Height = Height,
			AtHead = AtHead,
			Rise = Rise,
			Depth = Depth,
			Bevel = Bevel,
			Field = Field,
			Interior = Interior
		};
	}
}

// A run of expressed strips - one part, not one per strip.
public sealed class ArchPilasterRun
{
	public float Bay { get; set; }
	// Wins over Bay when set.
	public int Count { get; set; }
	public float Width { get; set; } = 12f;
	public float Depth { get; set; } = 3f;
	// So a pilaster can die under an eave rather than into it.
	public float Headroom { get; set; }
	// So a column line can stand on a plinth rather than run into the paving.
	public float Foot { get; set; }
	// End strips straddle the corner so two elevations wrap one column.
	public bool OnCorners { get; set; } = true;

	public bool Stands => (Bay > 1f || Count > 0) && Width > 0.1f && Depth > 0.05f;

	public ArchPilasterRun Copy()
	{
		return new ArchPilasterRun
		{
			Bay = Bay,
			Count = Count,
			Width = Width,
			Depth = Depth,
			Headroom = Headroom,
			Foot = Foot,
			OnCorners = OnCorners
		};
	}
}

// A rhythm the whole wall wears, standing real openings rather than deriving holes - a rhythm's window has
// to wear the frame, sash and sill a dragged one wears, and only an authored unit does.
public sealed class ArchOpeningRun
{
	public string Preset { get; set; } = "";
	public float Bay { get; set; }
	// Wins over Bay when set.
	public int Count { get; set; }
	// 0 takes the preset's own.
	public float Width { get; set; }
	public float Height { get; set; }
	public float SillHeight { get; set; }

	public bool Stands => !string.IsNullOrWhiteSpace( Preset ) && (Bay > 1f || Count > 0);

	public ArchOpeningRun Copy()
	{
		return new ArchOpeningRun
		{
			Preset = Preset,
			Bay = Bay,
			Count = Count,
			Width = Width,
			Height = Height,
			SillHeight = SillHeight
		};
	}
}

// One list of fields, so a setting can't go missing between wall and preset.
public interface IArchWallTreatment
{
	float Height { get; set; }
	float Thickness { get; set; }
	bool Exterior { get; set; }
	bool SingleSided { get; set; }
	bool Baseboard { get; set; }
	bool Cap { get; set; }
	float CapHeight { get; set; }
	float CapOverhang { get; set; }
	WallCladding Cladding { get; set; }
	bool Wainscot { get; set; }
	List<ArchWallBand> Bands { get; set; }
	List<ArchWallTrim> Trims { get; set; }
	ArchPilasterRun Pilasters { get; set; }
	ArchOpeningRun OpeningRun { get; set; }
}

public static class ArchWallTreatments
{
	// Copied, not shared - two walls holding one list is one wall's band moving both.
	public static void Dress( IArchWallTreatment source, IArchWallTreatment target )
	{
		target.Height = source.Height;
		target.Thickness = source.Thickness;
		target.Exterior = source.Exterior;
		target.SingleSided = source.SingleSided;
		target.Baseboard = source.Baseboard;
		target.Cap = source.Cap;
		target.CapHeight = source.CapHeight;
		target.CapOverhang = source.CapOverhang;
		target.Cladding = source.Cladding;
		target.Wainscot = source.Wainscot;
		target.Bands = source.Bands.Select( band => band.Copy() ).ToList();
		target.Trims = source.Trims.Select( trim => trim.Copy() ).ToList();
		target.Pilasters = source.Pilasters.Copy();
		target.OpeningRun = source.OpeningRun.Copy();
	}

	// The order depends on the wall height, so every caller names it.
	public static IEnumerable<ArchWallBand> Ordered( IEnumerable<ArchWallBand> bands, float wallHeight )
	{
		return (bands ?? Enumerable.Empty<ArchWallBand>())
			.Where( band => band.At( wallHeight ) > 0.05f )
			.OrderBy( band => band.At( wallHeight ) );
	}

	// Low edge first; one clearing the plate belongs to the elevation above.
	public static IEnumerable<ArchWallTrim> Standing( IEnumerable<ArchWallTrim> trims, float wallHeight )
	{
		return (trims ?? Enumerable.Empty<ArchWallTrim>())
			.Where( trim => trim.Rise > 0.05f && trim.Depth > 0.05f )
			.Where( trim => trim.At( wallHeight ) >= 0f && trim.At( wallHeight ) + trim.Rise <= wallHeight + 0.05f )
			.OrderBy( trim => trim.At( wallHeight ) );
	}
}

public sealed class ArchWall : IArchWallTreatment, IArchPainted
{
	public int Id { get; set; }
	public Vector2 Start { get; set; }
	public Vector2 End { get; set; }
	public float Height { get; set; }
	public float Thickness { get; set; }
	public bool Exterior { get; set; } = true;
	// WHETHER the far face exists, where Exterior only decides which SKIN each face takes. A silhouette
	// filling the space between the viewer and the landmark is never entered and never seen from behind,
	// so half its triangles are paying for nothing.
	public bool SingleSided { get; set; }
	public bool Baseboard { get; set; } = true;
	public bool Cap { get; set; }
	// 0 falls back to the kit's own coping section.
	public float CapHeight { get; set; }
	// How far the coping oversails each face - a parapet reads as brick because it stands proud of both.
	public float CapOverhang { get; set; }
	public WallCladding Cladding { get; set; }
	public bool Wainscot { get; set; }
	// Rides the SAME z split the openings make - a banded elevation costs no second wall.
	public List<ArchWallBand> Bands { get; set; } = new();
	// Proud strips at heights of their own, marking nothing.
	public List<ArchWallTrim> Trims { get; set; } = new();
	public ArchPilasterRun Pilasters { get; set; } = new();
	public ArchOpeningRun OpeningRun { get; set; } = new();
	public ArchPalette Palette { get; set; } = new();
	public List<ArchOpening> Openings { get; set; } = new();
	// Placed work at a station on this run - a pilaster where the rhythm above puts none, a recess.
	public List<ArchWallModPart> Modifiers { get; set; } = new();
	public float Length => (End - Start).Length;

	public Vector2 Direction => Length < 0.001f ? new Vector2( 1f, 0f ) : (End - Start) / Length;

	public Vector2 Normal
	{
		get
		{
			var direction = Direction;
			return new Vector2( -direction.y, direction.x );
		}
	}

	public Vector2 PointAt( float distance ) => Start + Direction * distance;
}