Editor/Geometry/ArchRoofPlane.cs

Utility static class for roof geometry. Computes properties of ArchRoofPart like whether it is ridged, planar or faceted, slope, rise, thickness, overhang, drip, surface height at a point, across-span of contiguous cells, and roof peak height.

File AccessNative Interop
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// What a deck IS, read off the section's own authored fields. The module that owns the kind lofts real patches over
// this and answers a finer seat, but a section drawn on a plan has a nominal surface whether or not it is installed,
// and the tool, the report and the elevation all need to draw it.
public static class ArchRoofPlane
{
	public static bool Ridged( RoofStyle style ) => style is RoofStyle.Gable or RoofStyle.Shed or RoofStyle.Sawtooth;

	public static bool Planar( ArchRoofPart roof ) => roof.Style is RoofStyle.Flat or RoofStyle.Shed or RoofStyle.Gable;

	// A deck whose slopes ARE planes but whose faces the carve grid cannot hold: every hip and valley runs at 45°.
	public static bool Faceted( ArchRoofPart roof ) => roof.Style == RoofStyle.Hip;

	// WHICH SECTIONS CARRY WHICH FORMS, asked wherever a placement is offered or refused. A light, a housing and
	// plant all want the axis-aligned patches the carve grid holds, because their kerb, frame, pane and plinth are
	// rings grown off a rectangle sitting on one. A dormer wants nothing but a seat and a fall, and a hip has both -
	// each of its slopes is a plane rising off one eave - so it goes on a hip through the face carve instead.
	public static bool Carries( ArchRoofPart roof, RoofLightForm form ) => Planar( roof ) || (form.Roofed() && Faceted( roof ));

	public static float Slope( ArchRoofPart roof ) => ArchPitch.Slope( roof.Pitch );

	public static float Rise( ArchRoofPart roof, float span ) => Slope( roof ) * span;

	public static float Thickness( ArchRoofPart roof, ArchKit kit ) => roof.Thickness > 0f ? roof.Thickness : kit.RoofThickness;

	// A parapet IS the wall carried up past the deck, so the deck must not oversail it.
	public static float Overhang( ArchRoofPart roof ) => roof.Parapet ? 0f : roof.Overhang;

	// Drip edge: runoff clears the fascia and buries its top face.
	public static float Drip( ArchRoofPart roof, ArchKit kit ) => roof.Fascia ? ArchProfiles.FasciaDepth( kit ) + 1.5f : 0f;

	// The nominal surface over any point, evaluated on the OUTLINE the deck is built over. A shed and a sawtooth
	// are single planes and rightly read the bounds; a hip is the skeleton's own arrival and a gable eaves off
	// the wing the point stands in - the bounding box put both a pitch-times-inset above the walls of an L's leg.
	public static float At( ArchRoofPart roof, Vector2 at )
	{
		var outline = ArchFootprint.Wind( roof.Outline() );

		ArchFootprint.Bounds( outline, out var min, out var max );

		var acrossY = roof.RidgeAlongX;
		var across = acrossY ? at.y : at.x;
		var low = acrossY ? min.y : min.x;
		var high = acrossY ? max.y : max.x;

		switch ( roof.Style )
		{
			case RoofStyle.Flat:
				return roof.BaseHeight;

			case RoofStyle.Shed:
			{
				var span = MathF.Max( 1f, high - low );
				var reach = roof.Reversed ? high - across : across - low;

				return roof.BaseHeight + Rise( roof, Math.Clamp( reach, 0f, span ) );
			}

			case RoofStyle.Sawtooth:
			{
				var alongX = !roof.RidgeAlongX;
				var from = alongX ? min.x : min.y;
				var total = MathF.Max( 1f, (alongX ? max.x : max.y) - from );
				var step = total / Math.Max( 1, roof.SawtoothBays );
				var into = ((alongX ? at.x : at.y) - from) % step;

				return roof.BaseHeight + Rise( roof, Math.Clamp( into, 0f, step ) );
			}

			case RoofStyle.Hip:
				return roof.BaseHeight + Rise( roof, MathF.Max( 0f, ArchRoofSkeleton.Arrival( new[] { outline }, at ) ) );

			default:
			{
				var (footing, head) = AcrossSpan( acrossY, new[] { (IReadOnlyList<Vector2>)outline }, at, low, high );

				return roof.BaseHeight + Rise( roof, MathF.Max( 0f, MathF.Min( across - footing, head - across ) ) );
			}
		}
	}

	// The across extent of the loops at this station: the contiguous stack of cells the point stands in, so a
	// gable over an L eaves off its own wing. One resolution for the deck, the patches and every seat query.
	public static (float Low, float High) AcrossSpan( bool acrossY, IEnumerable<IReadOnlyList<Vector2>> loops, Vector2 at, float low, float high )
	{
		var along = acrossY ? at.x : at.y;
		var across = acrossY ? at.y : at.x;

		var slices = ArchFootprint.Cells( loops, null )
			.Where( cell => along >= (acrossY ? cell.Min.x : cell.Min.y) - 0.01f
				&& along <= (acrossY ? cell.Max.x : cell.Max.y) + 0.01f )
			.Select( cell => acrossY ? (cell.Min.y, cell.Max.y) : (cell.Min.x, cell.Max.x) )
			.OrderBy( slice => slice.Item1 )
			.ToList();

		if ( slices.Count == 0 )
		{
			return (low, high);
		}

		var footing = slices[0].Item1;
		var head = slices[0].Item2;

		foreach ( var (from, to) in slices.Skip( 1 ) )
		{
			if ( from > head + 0.05f )
			{
				if ( across <= head + 0.05f )
				{
					break;
				}

				footing = from;
			}

			head = MathF.Max( head, to );
		}

		return (footing, head);
	}

	// How high the deck ever gets, so a cut told to come out the top of a roof knows how far up that is.
	public static float Peak( ArchRoofPart roof )
	{
		var outline = ArchFootprint.Wind( roof.Outline() );

		ArchFootprint.Bounds( outline, out var min, out var max );

		var alongX = !roof.RidgeAlongX;
		var bay = (alongX ? max.x - min.x : max.y - min.y) / Math.Max( 1, roof.SawtoothBays );

		switch ( roof.Style )
		{
			case RoofStyle.Shed:
				return At( roof, roof.Reversed ? min : max );

			case RoofStyle.Sawtooth:
				return At( roof, alongX
					? new Vector2( min.x + bay * 0.999f, min.y )
					: new Vector2( min.x, min.y + bay * 0.999f ) );

			case RoofStyle.Hip:
				return roof.BaseHeight + Rise( roof, ArchRoofSkeleton.Depth( new[] { outline } ) );

			default:
			{
				// The tallest ridge is the widest wing's, and the box centre may stand in an L's notch.
				var reach = ArchFootprint.Cells( new[] { outline }, null )
					.Select( cell => (cell.Min + cell.Max) * 0.5f )
					.Select( centre =>
					{
						var (footing, head) = AcrossSpan( roof.RidgeAlongX, new[] { (IReadOnlyList<Vector2>)outline }, centre, min.y, max.y );

						return (head - footing) * 0.5f;
					} )
					.DefaultIfEmpty( 0f )
					.Max();

				return roof.BaseHeight + Rise( roof, reach );
			}
		}
	}
}