Editor-side roof deck generator utilities. Computes roof deck heights, ray intersections, and builds mesh faces for different roof styles (gable, shed, sawtooth, cross-gable), and generates closures and end walls for rendering/export in the editor.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchRoofGen
{
// What a roof light is cut out of and what roof plant stands ON, so neither ever takes an authored z: the
// deck's own SURFACE under a point, read off the patch the generator lofts there rather than a second formula,
// which is why a plant item follows the pitch the moment it changes. A skeleton deck has no patches and falls
// back to its eave datum.
public static float Seat( ArchRoofPart roof, ArchKit kit, Vector2 at )
{
var thickness = ArchRoofPlane.Thickness( roof, kit );
if ( !ArchRoofPlane.Planar( roof ) )
{
return ArchRoofPlane.At( roof, at ) + thickness;
}
var rafters = ArchFootprint.Grow(
new List<List<Vector2>> { ArchFootprint.Wind( roof.Outline() ) },
ArchRoofPlane.Overhang( roof ) + ArchRoofPlane.Drip( roof, kit ) );
ArchFootprint.Bounds( rafters.SelectMany( loop => loop ).ToList(), out var min, out var max );
foreach ( var patch in Patches( roof, rafters, min, max ) )
{
if ( Covers( patch, at ) )
{
return patch.Plane.At( at ) + thickness;
}
}
return ArchRoofPlane.At( roof, at ) + thickness;
}
// Where a light or a plant item seats on that deck. Everything it puts on the roof is its footprint GROWN by
// its own reach, and the seat is the LOWEST corner of that: taken over the bare hole instead, a plinth or a
// flashing hangs off the downhill side by its own oversail, and taken over the highest corner it floats.
public static float Seat( ArchRoofPart roof, ArchRoofLightPart light, ArchKit kit )
{
return Seat( roof, kit, light.Min, light.Max, light.CurbWidth );
}
public static float Seat( ArchRoofPart roof, ArchKit kit, Vector2 min, Vector2 max, float reach )
{
var grown = new Vector2( MathF.Max( 1f, reach ), MathF.Max( 1f, reach ) );
return Seat( roof, kit, ArchFootprint.Rect( min - grown, max + grown ) );
}
static float Seat( ArchRoofPart roof, ArchKit kit, IReadOnlyList<Vector2> footprint )
{
var lowest = float.MaxValue;
foreach ( var corner in footprint )
{
lowest = MathF.Min( lowest, Seat( roof, kit, corner ) );
}
return lowest;
}
// WHERE A RAY MEETS THE DECK, so a gesture can start on a roof from any angle rather than only from straight
// overhead - off-axis, a ray taken down to the storey's own plane crosses it out past the building and the drag
// lands in the yard behind the house. Exact on a patch: a raked plane and a ray are both linear, so the crossing
// solves in closed form. A skeleton deck has no patches and is met on its eave plane, then read again at where
// that landed - two passes, because a hip is a cone and one plane cannot be its surface.
public static bool Struck( ArchRoofPart roof, ArchKit kit, Ray ray, out Vector3 hit )
{
hit = default;
if ( roof is null )
{
return false;
}
var thickness = ArchRoofPlane.Thickness( roof, kit );
if ( !ArchRoofPlane.Planar( roof ) )
{
return Skeleton( roof, kit, ray, out hit );
}
var rafters = ArchFootprint.Grow(
new List<List<Vector2>> { ArchFootprint.Wind( roof.Outline() ) },
ArchRoofPlane.Overhang( roof ) + ArchRoofPlane.Drip( roof, kit ) );
ArchFootprint.Bounds( rafters.SelectMany( loop => loop ).ToList(), out var min, out var max );
var nearest = float.MaxValue;
foreach ( var patch in Patches( roof, rafters, min, max ) )
{
if ( !Crosses( patch.Plane.Raised( thickness ), ray, out var reach, out var struck )
|| reach >= nearest
|| !Covers( patch, new Vector2( struck.x, struck.y ) ) )
{
continue;
}
nearest = reach;
hit = struck;
}
return nearest < float.MaxValue;
}
// The nearest deck on the storey a ray meets at all, so the work plane can offer it as somewhere to place.
public static bool Struck( ArchPlan plan, int level, ArchKit kit, Ray ray, out Vector3 hit, out ArchRoofPart on )
{
hit = default;
on = null;
var nearest = float.MaxValue;
foreach ( var roof in ArchRoofLight.Sections( plan, level ) )
{
if ( !Struck( roof, kit, ray, out var struck ) )
{
continue;
}
var reach = Vector3.Dot( struck - ray.Position, ray.Forward );
if ( reach >= nearest )
{
continue;
}
nearest = reach;
hit = struck;
on = roof;
}
return on is not null;
}
static bool Skeleton( ArchRoofPart roof, ArchKit kit, Ray ray, out Vector3 hit )
{
hit = default;
if ( !Crosses( ArchCarvePlane.Level( roof.BaseHeight + ArchRoofPlane.Thickness( roof, kit ) ), ray, out _, out var eave ) )
{
return false;
}
var at = new Vector2( eave.x, eave.y );
if ( !Crosses( ArchCarvePlane.Level( Seat( roof, kit, at ) ), ray, out _, out hit ) )
{
return false;
}
return ArchFootprint.Encloses( new[] { (IReadOnlyList<Vector2>)roof.Outline() }, new Vector2( hit.x, hit.y ) );
}
static bool Crosses( ArchCarvePlane plane, Ray ray, out float reach, out Vector3 hit )
{
reach = 0f;
hit = default;
var closing = ray.Forward.z - Vector2.Dot( plane.Fall, new Vector2( ray.Forward.x, ray.Forward.y ) );
if ( MathF.Abs( closing ) < 0.0001f )
{
return false;
}
reach = (plane.At( new Vector2( ray.Position.x, ray.Position.y ) ) - ray.Position.z) / closing;
if ( reach < 0f )
{
return false;
}
hit = ray.Position + ray.Forward * reach;
return true;
}
static bool Covers( ArchDeckPatch patch, Vector2 at )
{
return at.x >= patch.Min.x - ArchCarve.Grain && at.x <= patch.Max.x + ArchCarve.Grain
&& at.y >= patch.Min.y - ArchCarve.Grain && at.y <= patch.Max.y + ArchCarve.Grain;
}
// One plane, but only the CELLS the region covers: a single min/max quad roofs an L's notch too.
static void Shed( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> region, Vector2 min, Vector2 max, float thickness, ArchBrush brush )
{
var plane = Plane( roof, min, max );
var faces = new List<List<Vector3>>();
foreach ( var cell in ArchFootprint.Cells( region, null ) )
{
var corners = new[] { cell.Min, new Vector2( cell.Max.x, cell.Min.y ), cell.Max, new Vector2( cell.Min.x, cell.Max.y ) };
faces.Add( corners.Select( corner => new Vector3( corner.x, corner.y, plane.At( corner ) ) ).ToList() );
}
JoinedSlab( canvas, faces, Vector3.Up * thickness, brush );
}
static void Gable(
ArchMesh canvas,
ArchRoofPart roof,
ArchBuilding building,
ArchKit kit,
IReadOnlyList<List<Vector2>> region,
Vector2 min,
Vector2 max,
float thickness,
ArchBrush brush )
{
var junctions = ArchCrossGableService.Resolve( building, kit );
var hosted = junctions.Where( junction => ReferenceEquals( junction.Host, roof ) ).ToList();
var branched = junctions.Where( junction => ReferenceEquals( junction.Branch, roof ) ).ToList();
if ( Rectangular( region ) && (hosted.Count > 0 || branched.Count > 0) )
{
CrossGable( canvas, roof, min, max, thickness, brush, hosted, branched );
return;
}
var acrossMin = roof.RidgeAlongX ? min.y : min.x;
var acrossMax = roof.RidgeAlongX ? max.y : max.x;
var faces = new List<List<Vector3>>();
// Each cell eaves off the wing it stands in, so an L's narrow leg gets its own ridge at its own centre
// rather than floating a pitch-times-inset above its walls.
foreach ( var cell in ArchFootprint.Cells( region, null ) )
{
var centre = (cell.Min + cell.Max) * 0.5f;
var (footing, head) = ArchRoofPlane.AcrossSpan( roof.RidgeAlongX, region, centre, acrossMin, acrossMax );
var mid = (footing + head) * 0.5f;
if ( roof.RidgeAlongX && cell.Min.y < mid - ArchCarve.Grain && cell.Max.y > mid + ArchCarve.Grain )
{
AddGableFace( faces, roof, new ArchBox { Min = cell.Min, Max = new Vector2( cell.Max.x, mid ) }, footing, head );
AddGableFace( faces, roof, new ArchBox { Min = new Vector2( cell.Min.x, mid ), Max = cell.Max }, footing, head );
continue;
}
if ( !roof.RidgeAlongX && cell.Min.x < mid - ArchCarve.Grain && cell.Max.x > mid + ArchCarve.Grain )
{
AddGableFace( faces, roof, new ArchBox { Min = cell.Min, Max = new Vector2( mid, cell.Max.y ) }, footing, head );
AddGableFace( faces, roof, new ArchBox { Min = new Vector2( mid, cell.Min.y ), Max = cell.Max }, footing, head );
continue;
}
AddGableFace( faces, roof, cell, footing, head );
}
JoinedSlab( canvas, faces, Vector3.Up * thickness, brush );
}
static void CrossGable(
ArchMesh canvas,
ArchRoofPart roof,
Vector2 min,
Vector2 max,
float thickness,
ArchBrush brush,
List<ArchCrossGableJunction> hosted,
List<ArchCrossGableJunction> branched )
{
var alongMin = roof.RidgeAlongX ? min.x : min.y;
var alongMax = roof.RidgeAlongX ? max.x : max.y;
var acrossMin = roof.RidgeAlongX ? min.y : min.x;
var acrossMax = roof.RidgeAlongX ? max.y : max.x;
var ridge = (acrossMin + acrossMax) * 0.5f;
var slope = ArchRoofPlane.Slope( roof );
var ridgeHeight = roof.BaseHeight + slope * (acrossMax - acrossMin) * 0.5f;
var faces = new List<List<Vector3>>();
var valleys = new List<(Vector3 From, Vector3 To)>();
var branchMinimum = branched.FirstOrDefault( junction => junction.BranchAtMinimum );
var branchMaximum = branched.FirstOrDefault( junction => !junction.BranchAtMinimum );
var builtAlongMin = branchMinimum?.EaveAcross ?? alongMin;
var builtAlongMax = branchMaximum?.EaveAcross ?? alongMax;
AddCrossGableSlope( roof, faces, hosted.Where( junction => junction.HostAtMinimum ).ToList(),
builtAlongMin, builtAlongMax, acrossMin, ridge, acrossMin, ridgeHeight, slope, true );
AddCrossGableSlope( roof, faces, hosted.Where( junction => !junction.HostAtMinimum ).ToList(),
builtAlongMin, builtAlongMax, ridge, acrossMax, acrossMax, ridgeHeight, slope, false );
foreach ( var junction in branched )
{
var eaveHeight = roof.BaseHeight;
var left = junction.BranchPoint( junction.EaveAcross, junction.AlongMin, eaveHeight );
var centre = junction.BranchPoint( junction.EaveAcross, junction.AlongMid, ridgeHeight );
var apex = junction.BranchPoint( junction.ApexAcross, junction.AlongMid, ridgeHeight );
var right = junction.BranchPoint( junction.EaveAcross, junction.AlongMax, eaveHeight );
faces.Add( new List<Vector3> { left, centre, apex } );
faces.Add( new List<Vector3> { centre, right, apex } );
valleys.Add( (left, apex) );
valleys.Add( (apex, right) );
}
foreach ( var junction in hosted )
{
var left = junction.HostPoint( junction.AlongMin, junction.EaveAcross, roof.BaseHeight );
var right = junction.HostPoint( junction.AlongMax, junction.EaveAcross, roof.BaseHeight );
var apexHeight = roof.BaseHeight + slope * MathF.Abs( junction.ApexAcross - junction.EaveAcross );
var apex = junction.HostPoint( junction.AlongMid, junction.ApexAcross, apexHeight );
valleys.Add( (left, apex) );
valleys.Add( (apex, right) );
}
JoinedSlab( canvas, faces, Vector3.Up * thickness, brush, valleys );
}
static void AddCrossGableSlope(
ArchRoofPart roof,
List<List<Vector3>> faces,
List<ArchCrossGableJunction> junctions,
float alongMin,
float alongMax,
float acrossMin,
float acrossMax,
float eave,
float ridgeHeight,
float slope,
bool minimum )
{
var breaks = new List<float> { alongMin, alongMax };
foreach ( var junction in junctions )
{
breaks.Add( Math.Clamp( junction.AlongMin, alongMin, alongMax ) );
breaks.Add( Math.Clamp( junction.AlongMid, alongMin, alongMax ) );
breaks.Add( Math.Clamp( junction.AlongMax, alongMin, alongMax ) );
}
breaks = breaks.Distinct().OrderBy( value => value ).ToList();
for ( var index = 0; index < breaks.Count - 1; index++ )
{
var from = breaks[index];
var to = breaks[index + 1];
if ( to - from < 0.05f )
{
continue;
}
var fromAcross = BoundaryAt( junctions, from, eave, minimum );
var toAcross = BoundaryAt( junctions, to, eave, minimum );
var fromHeight = roof.BaseHeight + slope * MathF.Abs( fromAcross - eave );
var toHeight = roof.BaseHeight + slope * MathF.Abs( toAcross - eave );
faces.Add( new List<Vector3>
{
RoofPoint( roof, from, fromAcross, fromHeight ),
RoofPoint( roof, to, toAcross, toHeight ),
RoofPoint( roof, to, minimum ? acrossMax : acrossMin, ridgeHeight ),
RoofPoint( roof, from, minimum ? acrossMax : acrossMin, ridgeHeight )
} );
}
}
static float BoundaryAt( List<ArchCrossGableJunction> junctions, float along, float eave, bool minimum )
{
var boundary = eave;
foreach ( var junction in junctions.Where( junction => along >= junction.AlongMin && along <= junction.AlongMax ) )
{
var candidate = junction.HostBoundaryAt( along );
boundary = minimum ? MathF.Max( boundary, candidate ) : MathF.Min( boundary, candidate );
}
return boundary;
}
static Vector3 RoofPoint( ArchRoofPart roof, float along, float across, float height )
{
return roof.RidgeAlongX
? new Vector3( along, across, height )
: new Vector3( across, along, height );
}
static void AddGableFace( List<List<Vector3>> faces, ArchRoofPart roof, ArchBox cell, float footing, float head )
{
float Height( Vector2 point )
{
var across = roof.RidgeAlongX ? point.y : point.x;
var distance = MathF.Min( across - footing, head - across );
return roof.BaseHeight + ArchRoofPlane.Rise( roof, MathF.Max( 0f, distance ) );
}
var a = cell.Min;
var b = new Vector2( cell.Max.x, cell.Min.y );
var c = cell.Max;
var d = new Vector2( cell.Min.x, cell.Max.y );
faces.Add( new List<Vector3>
{
new( a.x, a.y, Height( a ) ),
new( b.x, b.y, Height( b ) ),
new( c.x, c.y, Height( c ) ),
new( d.x, d.y, Height( d ) )
} );
}
// Bays step across the bounds, but each bay only decks and glazes the CELLS the region covers - run whole,
// the bays and the glass march straight across an L's notch.
static void Sawtooth( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> region, Vector2 min, Vector2 max, float thickness, ArchBrush deck, ArchBrush glazing )
{
var bays = Math.Max( 1, roof.SawtoothBays );
var alongX = !roof.RidgeAlongX;
var total = alongX ? max.x - min.x : max.y - min.y;
var step = total / bays;
var rise = ArchRoofPlane.Rise( roof, step );
var cells = ArchFootprint.Cells( region, null );
for ( var bay = 0; bay < bays; bay++ )
{
var from = (alongX ? min.x : min.y) + bay * step;
var to = from + step;
foreach ( var cell in cells )
{
var begin = MathF.Max( from, alongX ? cell.Min.x : cell.Min.y );
var end = MathF.Min( to, alongX ? cell.Max.x : cell.Max.y );
if ( end - begin < 0.05f )
{
continue;
}
var low = roof.BaseHeight + ArchRoofPlane.Rise( roof, begin - from );
var high = roof.BaseHeight + ArchRoofPlane.Rise( roof, end - from );
var acrossLow = alongX ? cell.Min.y : cell.Min.x;
var acrossHigh = alongX ? cell.Max.y : cell.Max.x;
var glazed = end >= to - 0.05f;
if ( alongX )
{
Sloped( canvas,
new Vector3( begin, acrossLow, low ),
new Vector3( end, acrossLow, high ),
new Vector3( end, acrossHigh, high ),
new Vector3( begin, acrossHigh, low ),
thickness, deck );
if ( glazed )
{
canvas.Box(
new Vector3( to - thickness, acrossLow, roof.BaseHeight ),
new Vector3( to, acrossHigh, roof.BaseHeight + rise ),
glazing );
}
continue;
}
Sloped( canvas,
new Vector3( acrossLow, begin, low ),
new Vector3( acrossHigh, begin, low ),
new Vector3( acrossHigh, end, high ),
new Vector3( acrossLow, end, high ),
thickness, deck );
if ( glazed )
{
canvas.Box(
new Vector3( acrossLow, to - thickness, roof.BaseHeight ),
new Vector3( acrossHigh, to, roof.BaseHeight + rise ),
glazing );
}
}
}
}
static void Sloped( ArchMesh canvas, Vector3 a, Vector3 b, Vector3 c, Vector3 d, float thickness, ArchBrush brush )
{
ArchSlab.Face( canvas, new List<Vector3> { a, b, c, d }, Vector3.Up * thickness, brush );
}
// Seals the slot the deck's underside leaves at plate height; reuses the fascia's bare-edge test.
static void Closure( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> plates, ArchKit kit, ArchBrush brush, float reach, Func<Vector2, Vector2, bool> bare, ArchPlan plan, int hostId )
{
// Bays meet the wall line at their own heights; one band cannot seal them.
if ( roof.Style == RoofStyle.Sawtooth )
{
return;
}
var thickness = MathF.Max( 2f, kit.WallThickness );
// Measured to the band's outer face, or the top corner stands proud of the deck.
var lift = ArchRoofPlane.Slope( roof ) * (reach - thickness * 0.5f);
if ( lift < 0.5f )
{
return;
}
foreach ( var loop in plates )
{
Band( canvas, loop, thickness * -0.5f, thickness, roof.BaseHeight, roof.BaseHeight + lift, brush, bare, kit, plan, roof.Level, hostId, ArchCutAffects.Trims );
}
}
static void EndWalls(
ArchMesh canvas,
ArchRoofPart roof,
Vector2 deckMin,
Vector2 deckMax,
IReadOnlyList<List<Vector2>> region,
ArchKit kit,
ArchBrush brush,
IReadOnlyList<List<Vector2>> abutments,
IReadOnlyList<ArchCrossGableJunction> junctions )
{
if ( roof.Style != RoofStyle.Gable && roof.Style != RoofStyle.Shed )
{
return;
}
var alongX = roof.RidgeAlongX;
var half = MathF.Max( 1f, kit.WallThickness ) * 0.5f;
var low = roof.BaseHeight;
var gable = roof.Style == RoofStyle.Gable;
var deckFrom = alongX ? deckMin.y : deckMin.x;
var deckTo = alongX ? deckMax.y : deckMax.x;
var full = deckTo - deckFrom;
if ( full < 1f )
{
return;
}
var shedRise = ArchRoofPlane.Rise( roof, full );
float ShedUnder( float at ) => low + shedRise * (roof.Reversed ? deckTo - at : at - deckFrom) / full;
// Every region edge closes up to the deck over it, so an L's junction gets its gablet and a shed's raked
// flank follows its own outline rather than the bounding box. An eave edge rises nothing and is skipped.
foreach ( var loop in region )
{
for ( var index = 0; index < loop.Count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
var span = to - from;
if ( span.Length < 1f )
{
continue;
}
var crossesRidge = alongX ? MathF.Abs( span.x ) < 0.5f : MathF.Abs( span.y ) < 0.5f;
if ( gable && !crossesRidge )
{
continue;
}
var outward = ArchRegion.Outward( from, to );
if ( gable && ArchCrossGableService.JoinsBranchEnd( roof, from, to, junctions ) )
{
continue;
}
// This edge's own wing decides the profile - the box put an L's narrow ridge over its notch.
var probe = (from + to) * 0.5f - outward * MathF.Max( 1f, half );
var (footing, head) = gable
? ArchRoofPlane.AcrossSpan( alongX, region, probe, deckFrom, deckTo )
: (deckFrom, deckTo);
var mid = (footing + head) * 0.5f;
var halfSpan = MathF.Max( 0.5f, (head - footing) * 0.5f );
var gableRise = ArchRoofPlane.Rise( roof, halfSpan );
float Under( float at ) => gable
? low + gableRise * MathF.Max( 0f, 1f - MathF.Abs( at - mid ) / halfSpan )
: ShedUnder( at );
foreach ( var visible in VisibleEndSegments( from, to, outward, abutments ) )
{
var inset = outward * -half;
var fromAcross = alongX ? visible.From.y : visible.From.x;
var toAcross = alongX ? visible.To.y : visible.To.x;
var topFrom = Under( fromAcross );
var topTo = Under( toAcross );
if ( MathF.Max( topFrom, topTo ) - low < 1f )
{
continue;
}
var face = new List<Vector3>
{
new( visible.From.x + inset.x, visible.From.y + inset.y, low ),
new( visible.To.x + inset.x, visible.To.y + inset.y, low ),
new( visible.To.x + inset.x, visible.To.y + inset.y, topTo )
};
if ( gable && MathF.Min( fromAcross, toAcross ) < mid && MathF.Max( fromAcross, toAcross ) > mid )
{
var peak = alongX
? new Vector2( visible.From.x, mid )
: new Vector2( mid, visible.From.y );
face.Add( new Vector3( peak.x + inset.x, peak.y + inset.y, low + gableRise ) );
}
face.Add( new Vector3( visible.From.x + inset.x, visible.From.y + inset.y, topFrom ) );
ArchSlab.Face( canvas, face, new Vector3( outward.x, outward.y, 0f ) * (half * 2f), brush );
}
}
}
}
static IEnumerable<(Vector2 From, Vector2 To)> VisibleEndSegments(
Vector2 from,
Vector2 to,
Vector2 outward,
IReadOnlyList<List<Vector2>> abutments )
{
var span = to - from;
var lengthSquared = span.LengthSquared;
if ( lengthSquared < 0.01f )
{
yield break;
}
var breaks = new List<float> { 0f, 1f };
foreach ( var point in abutments.SelectMany( loop => loop ) )
{
var along = Vector2.Dot( point - from, span ) / lengthSquared;
if ( along > 0.001f && along < 0.999f )
{
breaks.Add( along );
}
}
breaks = breaks.Distinct().OrderBy( value => value ).ToList();
for ( var index = 0; index < breaks.Count - 1; index++ )
{
var start = Vector2.Lerp( from, to, breaks[index] );
var end = Vector2.Lerp( from, to, breaks[index + 1] );
var middle = (start + end) * 0.5f + outward * ArchProbe.Step;
if ( !abutments.Any( loop => ArchFloorGen.Contains( loop, middle ) ) )
{
yield return (start, end);
}
}
}
}