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.
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 );
}
}
}
}