Editor/Roof/ArchRoofDeck.cs
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 ( !patch.Plane.Raised( thickness ).Crosses( 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.
// Met on the FLATTENED ray, because a section is authored flat and the deck the author can see rides its
// building's grade lift - crossed against the authored height, the plane met is a lift below the one being
// pointed at, and at a shallow angle that is feet of error across the plan.
public static bool Struck( ArchPlan plan, int level, ArchKit kit, Ray ray, out Vector3 hit, out ArchRoofPart on ) {
hit = default;
on = null;
var answers = ArchAnswers.Load();
var nearest = float.MaxValue;
foreach ( var (building, roof) in ArchRoofLight.Owned( plan, level ) ) {
var flattened = answers.Flattened( ray, plan, building, kit );
if ( !Struck( roof, kit, flattened, out var struck ) ) {
continue;
}
var reach = Vector3.Dot( struck - flattened.Position, flattened.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 ( !ArchCarvePlane.Level( roof.BaseHeight + ArchRoofPlane.Thickness( roof, kit ) ).Crosses( ray, out _, out var eave ) ) {
return false;
}
var at = new Vector2( eave.x, eave.y );
if ( !ArchCarvePlane.Level( Seat( roof, kit, at ) ).Crosses( ray, out _, out hit ) ) {
return false;
}
return ArchFootprint.Encloses( new[] { (IReadOnlyList<Vector2>)roof.Outline() }, new Vector2( hit.x, hit.y ) );
}
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);
}
}
}
}