Editor-side roof generation utilities. Implements deck patch, lit patch and many routines to compute roof facet planes, decompose roof decks into patches, determine pierced volumes for lights/dormers, and emit mesh geometry (rings, slabs, dormers, glazing, etc.) into an ArchMesh.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Only axis-aligned rectangles - the one thing ArchCarve's grid holds. A Hip has none:
// its faces are 45° triangles/trapezoids at every hip and valley.
public readonly struct ArchDeckPatch
{
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
public ArchCarvePlane Plane { get; init; }
public List<Vector2> Footprint() => ArchFootprint.Rect( Min, Max );
}
// Everything is the same carve on the patch's own plane - no pitch branches, ArchCarve takes planes.
// The kerb is NOT an ArchRun: that carries one Height.
public static partial class ArchRoofGen
{
// The hip slope a footprint stands on, as the plane it IS. Read off the SAME bounds and slope ArchRoofGen.Deck
// measures a hip with, so the patch, the seat, the ghost and the carve cannot disagree. The nearest eave to the
// point names the face, so a dormer straddling a hip line takes the slope it mostly sits on.
public static ArchCarvePlane Facet( ArchRoofPart roof, Vector2 at )
{
ArchFootprint.Bounds( roof.Outline(), out var min, out var max );
var gradient = ArchRoofPlane.Slope( roof );
var eaves = new[]
{
(Reach: at.y - min.y, Plane: ArchCarvePlane.Through( new Vector2( 0f, min.y ), roof.BaseHeight, new Vector2( 0f, gradient ) )),
(Reach: max.y - at.y, Plane: ArchCarvePlane.Through( new Vector2( 0f, max.y ), roof.BaseHeight, new Vector2( 0f, -gradient ) )),
(Reach: at.x - min.x, Plane: ArchCarvePlane.Through( new Vector2( min.x, 0f ), roof.BaseHeight, new Vector2( gradient, 0f ) )),
(Reach: max.x - at.x, Plane: ArchCarvePlane.Through( new Vector2( max.x, 0f ), roof.BaseHeight, new Vector2( -gradient, 0f ) ))
};
return eaves.OrderBy( eave => eave.Reach ).First().Plane;
}
// A faceted deck has no patches, so what stands on one is ONE piece on the slope it landed on, never clipped -
// the hip's own faces do the clipping, in the carve. Only a form Carries admits comes through here.
public static List<ArchLitPatch> LitFacets( ArchRoofPart roof )
{
var lit = new List<ArchLitPatch>();
foreach ( var light in roof.Lights )
{
if ( !ArchRoofPlane.Carries( roof, light.Form ) || light.Size.x <= 4f || light.Size.y <= 4f || !ArchLayerGate.On( light ) )
{
continue;
}
lit.Add( new ArchLitPatch
{
Light = light,
Min = light.Min,
Max = light.Max,
OpenMin = new Vector2( 1f, 1f ),
OpenMax = new Vector2( 1f, 1f ),
Plane = Facet( roof, (light.Min + light.Max) * 0.5f )
} );
}
return lit;
}
// Mirrors ArchRoofDeck.Gable's decomposition, per-wing spans included, so carved and uncarved decks land
// identically.
public static List<ArchDeckPatch> Patches( ArchRoofPart roof, IReadOnlyList<List<Vector2>> rafters, Vector2 min, Vector2 max )
{
var patches = new List<ArchDeckPatch>();
var acrossY = roof.RidgeAlongX;
var acrossMin = acrossY ? min.y : min.x;
var acrossMax = acrossY ? max.y : max.x;
foreach ( var cell in ArchFootprint.Cells( rafters, null ) )
{
if ( roof.Style != RoofStyle.Gable )
{
patches.Add( Patch( cell.Min, cell.Max, Plane( roof, min, max ) ) );
continue;
}
// Patches must break on the ridge - it is where the two planes meet - and the ridge belongs to the
// wing the cell stands in.
var centre = (cell.Min + cell.Max) * 0.5f;
var (footing, head) = ArchRoofPlane.AcrossSpan( acrossY, rafters, centre, acrossMin, acrossMax );
var ridge = (footing + head) * 0.5f;
var low = acrossY ? cell.Min.y : cell.Min.x;
var high = acrossY ? cell.Max.y : cell.Max.x;
if ( low < ridge - ArchCarve.Grain && high > ridge + ArchCarve.Grain )
{
patches.Add( Patch( cell.Min, Across( cell.Max, acrossY, ridge ), Slope( roof, footing, head, true ) ) );
patches.Add( Patch( Across( cell.Min, acrossY, ridge ), cell.Max, Slope( roof, footing, head, false ) ) );
continue;
}
patches.Add( Patch( cell.Min, cell.Max, Slope( roof, footing, head, high <= ridge + ArchCarve.Grain ) ) );
}
return patches;
}
// Read off the same RiseFor the deck is lofted from - carve and emitter can't disagree.
public static ArchCarvePlane Plane( ArchRoofPart roof, Vector2 min, Vector2 max )
{
if ( roof.Style != RoofStyle.Shed )
{
return ArchCarvePlane.Level( roof.BaseHeight );
}
var acrossY = roof.RidgeAlongX;
var span = MathF.Max( 1f, acrossY ? max.y - min.y : max.x - min.x );
var rise = ArchRoofPlane.Rise( roof, span );
var gradient = rise / span * (roof.Reversed ? -1f : 1f);
var datum = roof.Reversed ? roof.BaseHeight + rise : roof.BaseHeight;
return ArchCarvePlane.Through( min, datum, acrossY ? new Vector2( 0f, gradient ) : new Vector2( gradient, 0f ) );
}
// AddGableFace measures off the NEARER eave: low side climbs from acrossMin, high from acrossMax.
static ArchCarvePlane Slope( ArchRoofPart roof, float acrossMin, float acrossMax, bool low )
{
var gradient = ArchRoofPlane.Rise( roof, 1f ) * (low ? 1f : -1f);
var eave = low ? acrossMin : acrossMax;
var acrossY = roof.RidgeAlongX;
return ArchCarvePlane.Through(
acrossY ? new Vector2( 0f, eave ) : new Vector2( eave, 0f ),
roof.BaseHeight,
acrossY ? new Vector2( 0f, gradient ) : new Vector2( gradient, 0f ) );
}
static ArchDeckPatch Patch( Vector2 min, Vector2 max, ArchCarvePlane plane )
{
return new ArchDeckPatch { Min = min, Max = max, Plane = plane };
}
static Vector2 Across( Vector2 point, bool acrossY, float at )
{
return acrossY ? new Vector2( point.x, at ) : new Vector2( at, point.y );
}
// Carved only when pierced, so untouched roofs keep the geometry they were built with - and a tank or a stack
// stands on a deck that was never opened.
static void Deck(
ArchMesh canvas,
ArchRoofPart roof,
ArchBuilding building,
List<List<Vector2>> rafters,
Vector2 deckMin,
Vector2 deckMax,
float thickness,
ArchPlan plan,
ArchKit kit,
ArchStyle style,
ArchPalette[] chain,
ArchBrush deck )
{
var patches = Patches( roof, rafters, deckMin, deckMax );
var standing = Lit( roof, patches );
var cuts = Piercing( roof, building, plan, kit, patches, thickness );
if ( !standing.Any( piece => piece.Light.Form.Pierces() ) && cuts.Count == 0 )
{
Unpierced( canvas, roof, building, rafters, deckMin, deckMax, thickness, kit, deck );
}
else
{
var shape = Pierced( patches, thickness, standing, cuts );
var reveal = style.Brush( ArchSurface.Reveal, chain );
foreach ( var face in shape.Faces )
{
canvas.Polygon( face.Points, face.Side == ArchCarveSide.Jamb ? reveal : deck );
}
}
foreach ( var piece in standing )
{
using ( canvas.Part( piece.Light.Form.Piece() ) )
{
Light( canvas, piece, thickness, kit, style, chain );
}
}
}
static void Unpierced(
ArchMesh canvas,
ArchRoofPart roof,
ArchBuilding building,
List<List<Vector2>> rafters,
Vector2 min,
Vector2 max,
float thickness,
ArchKit kit,
ArchBrush deck )
{
switch ( roof.Style )
{
case RoofStyle.Shed:
Shed( canvas, roof, rafters, min, max, thickness, deck );
return;
case RoofStyle.Gable:
Gable( canvas, roof, building, kit, rafters, min, max, thickness, deck );
return;
default:
ArchFloorGen.Solid( canvas, rafters, null, roof.BaseHeight, roof.BaseHeight + thickness, deck );
return;
}
}
// A light across the ridge is TWO pieces - a ridge lantern. A side meeting its sibling
// is internal: no kerb there, or a spine runs down the glass.
public readonly struct ArchLitPatch
{
public ArchRoofLightPart Light { get; init; }
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
public Vector2 OpenMin { get; init; }
public Vector2 OpenMax { get; init; }
public ArchCarvePlane Plane { get; init; }
public List<Vector2> Footprint() => ArchFootprint.Rect( Min, Max );
// Grown only on open sides; an internal edge grows nothing (zero-width ring).
public List<Vector2> Grown( float by )
{
return ArchFootprint.Rect( Min - OpenMin * by, Max + OpenMax * by );
}
}
// The intersection with the covered patches IS the answer - nothing is refused for straddling.
public static List<ArchLitPatch> Lit( ArchRoofPart roof, IReadOnlyList<ArchDeckPatch> patches )
{
var lit = new List<ArchLitPatch>();
foreach ( var light in roof.Lights.Where( entry => entry.Size.x > 4f && entry.Size.y > 4f && ArchLayerGate.On( entry ) ) )
{
var pieces = new List<(Vector2 Min, Vector2 Max, ArchCarvePlane Plane)>();
foreach ( var patch in patches )
{
var min = Vector2.Max( light.Min, patch.Min );
var max = Vector2.Min( light.Max, patch.Max );
if ( max.x - min.x < 4f || max.y - min.y < 4f )
{
continue;
}
pieces.Add( (min, max, patch.Plane) );
}
if ( pieces.Count == 0 )
{
Log.Info( $"Architecture: {roof.Name}/{light.Name} falls entirely off the deck, so nothing was cut. Drag it over the roof." );
continue;
}
foreach ( var piece in pieces )
{
lit.Add( new ArchLitPatch
{
Light = light,
Min = piece.Min,
Max = piece.Max,
OpenMin = Open( piece.Min, piece.Max, pieces, false ),
OpenMax = Open( piece.Min, piece.Max, pieces, true ),
Plane = piece.Plane
} );
}
}
return lit;
}
static Vector2 Open( Vector2 min, Vector2 max, IReadOnlyList<(Vector2 Min, Vector2 Max, ArchCarvePlane Plane)> pieces, bool upper )
{
var open = new Vector2( 1f, 1f );
foreach ( var other in pieces )
{
if ( other.Min == min && other.Max == max )
{
continue;
}
if ( Meets( upper ? max.x : min.x, upper ? other.Min.x : other.Max.x, min.y, max.y, other.Min.y, other.Max.y ) )
{
open.x = 0f;
}
if ( Meets( upper ? max.y : min.y, upper ? other.Min.y : other.Max.y, min.x, max.x, other.Min.x, other.Max.x ) )
{
open.y = 0f;
}
}
return open;
}
static bool Meets( float edge, float against, float from, float to, float otherFrom, float otherTo )
{
return MathF.Abs( edge - against ) < ArchCarve.Grain
&& MathF.Min( to, otherTo ) - MathF.Max( from, otherFrom ) > ArchCarve.Grain;
}
public static ArchCarveShape Pierced(
IReadOnlyList<ArchDeckPatch> patches,
float thickness,
IReadOnlyList<ArchLitPatch> lights,
IReadOnlyList<ArchCarveVolume> cuts = null )
{
var carve = new ArchCarve();
foreach ( var patch in patches )
{
carve.Plus( ArchCarveVolume.Raked( patch.Footprint(), patch.Plane, thickness ) );
}
// Grown past both slab faces, so a raked light cuts square instead of leaving a wedge. Only a form that
// opens the deck takes a bite out of it - a tank stands on a slab that was never cut.
foreach ( var light in lights.Where( piece => piece.Light.Form.Pierces() ) )
{
carve.Less( ArchCarveVolume.Raked( light.Footprint(), light.Plane.Raised( -1f ), thickness + 2f ) );
}
// A cut keeps its own band - one stairwell can open a roof and leave the floor alone.
foreach ( var volume in cuts ?? Array.Empty<ArchCarveVolume>() )
{
carve.Less( volume );
}
return carve.Resolve();
}
// Roofs are opened from ABOVE: a rooftop landing is just a cut whose head clears the deck.
static List<ArchCarveVolume> Piercing(
ArchRoofPart roof,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
IReadOnlyList<ArchDeckPatch> patches,
float thickness )
{
var low = float.MaxValue;
var high = float.MinValue;
foreach ( var patch in patches )
{
foreach ( var corner in patch.Footprint() )
{
low = MathF.Min( low, patch.Plane.At( corner ) );
high = MathF.Max( high, patch.Plane.At( corner ) + thickness );
}
}
return Piercing( roof, building, plan, kit, low, high );
}
// Asked of the PLAN, the way a slab asks: a shaft is world space, so the roof a cut comes out of need
// not be the building the cut was filed in. Reading the host's own list left a bool dropped over the
// neighbour opening its floors and leaving its roof standing.
static List<ArchCarveVolume> Piercing( ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, float low, float high )
{
if ( building is null || high <= low )
{
return new List<ArchCarveVolume>();
}
return ArchCut.Volumes( plan, roof.Level, kit, low - ArchContact.Bite( kit ), high, building.Id, ArchCutAffects.Roofs ).ToList();
}
// Hips pierced face by face: their triangles are exactly what the carve grid cannot hold. A dormer's hole comes
// through the same pass rather than a second one, because a hole in a hip is a hole in a hip whoever asked for it.
public static List<List<Vector3>> Pierced(
IReadOnlyList<List<Vector3>> faces,
ArchRoofPart roof,
ArchBuilding building,
ArchKit kit,
float thickness,
ArchPlan plan,
IReadOnlyList<ArchLitPatch> standing = null )
{
var low = float.MaxValue;
var high = float.MinValue;
foreach ( var point in faces.SelectMany( face => face ) )
{
low = MathF.Min( low, point.z );
high = MathF.Max( high, point.z );
}
var cuts = Piercing( roof, building, plan, kit, low, high + thickness ).ToList();
foreach ( var piece in standing ?? Array.Empty<ArchLitPatch>() )
{
if ( piece.Light.Form.Pierces() )
{
cuts.Add( ArchCarveVolume.Raked( piece.Footprint(), piece.Plane.Raised( -1f ), thickness + 2f ) );
}
}
return ArchFaceCarve.Surround( faces, cuts, thickness, ArchContact.Bite( kit ) );
}
// One answer for the stack a light or a plant item stands in, read by the generator, the extent, the MCP
// report and the tests: a kerbed light is an upstand with the glazing straight on it, a monitor puts a coping
// between the two, and a stack or a tank is a standing section with neither. Every question about the form is
// asked of ArchRoofLightKinds, so an authored Glazed on a chimney cannot put a pane on a flue.
public readonly struct ArchRoofLightStack
{
public float Upstand { get; init; }
public float Coping { get; init; }
public float Head { get; init; }
public float Standing { get; init; }
// The pitched lid a dormer takes where a monitor takes a coping: its own ridge over its own span.
public float Ridge { get; init; }
public float Top { get; init; }
}
public static ArchRoofLightStack Stack( ArchRoofLightPart light, ArchKit kit )
{
var form = light.Form;
var rise = MathF.Max( 1f, light.Curb );
var upstand = form.Kerbed() ? rise : 0f;
var coping = form.Coped() ? MathF.Max( 1f, kit.WallCapHeight ) : 0f;
var head = upstand + coping;
var standing = form.Stands() ? rise : 0f;
var lid = (form.Glazes() && light.Glazed && !form.Fronted()) || form.Lidded() ? MathF.Max( 1f, kit.FrameDepth ) : 0f;
// Off the authored footprint, never the clipped piece, so the ghost, the report and the generator quote one
// number - and off the kit's own pitch, so a dormer reads as part of the house rather than a lean-to on it.
var ridge = form.Roofed() ? ArchPitch.Slope( kit.RoofPitch ) * MathF.Max( 1f, MathF.Min( light.Size.x, light.Size.y ) ) * 0.5f : 0f;
return new ArchRoofLightStack
{
Upstand = upstand,
Coping = coping,
Head = head,
Standing = standing,
Ridge = ridge,
Top = head + standing + lid + ridge
};
}
static void Light( ArchMesh canvas, ArchLitPatch piece, float thickness, ArchKit kit, ArchStyle style, ArchPalette[] chain )
{
var light = piece.Light;
var form = light.Form;
var skin = new[] { light.Palette }.Concat( chain ).ToArray();
var surface = piece.Plane.Raised( thickness );
var stack = Stack( light, kit );
var width = MathF.Max( 1f, light.CurbWidth );
// First, because a dormer's cheeks stand LEVEL out of the slope where a kerb's ring rakes with it.
if ( form.Roofed() )
{
Dormer( canvas, piece, stack, width, thickness, kit, style, skin );
return;
}
if ( form.Kerbed() )
{
Upstand( canvas, piece, stack, width, surface, kit, style, skin );
}
if ( form.Stands() )
{
Stood( canvas, piece, stack, width, Seat( piece, piece.Grown( width ), thickness ), kit, style, skin );
return;
}
if ( form.Lidded() )
{
Lid( canvas, piece, width, surface.Raised( stack.Head ), kit, style, skin );
return;
}
if ( form.Glazes() && light.Glazed )
{
Glazing( canvas, piece, stack, surface, kit, style, skin );
}
}
static void Upstand( ArchMesh canvas, ArchLitPatch piece, ArchRoofLightStack stack, float width, ArchCarvePlane surface, ArchKit kit, ArchStyle style, ArchPalette[] skin )
{
var bite = ArchContact.Bite( kit );
// Bitten in, or the kerb's underside is coplanar with the roof all round the light.
Ring(
canvas,
piece.Grown( width ),
piece.Footprint(),
surface.Raised( -bite ),
bite + stack.Upstand,
style.Brush( piece.Light.Form.Clad() ? ArchSurface.WallExterior : ArchSurface.RoofEdge, skin ) );
// A housing tall enough to read as a wall is finished the way a parapet is - by its coping.
if ( stack.Coping <= 0f )
{
return;
}
var oversail = ArchContact.Proud( kit );
Ring( canvas, piece.Grown( width + oversail ), piece.Grown( -oversail ), surface.Raised( stack.Upstand ),
stack.Coping, style.Brush( ArchSurface.WallCap, skin ) );
}
static void Glazing( ArchMesh canvas, ArchLitPatch piece, ArchRoofLightStack stack, ArchCarvePlane surface, ArchKit kit, ArchStyle style, ArchPalette[] skin )
{
var face = MathF.Max( 1f, kit.FrameFace );
var depth = MathF.Max( 1f, kit.FrameDepth );
var head = surface.Raised( stack.Head );
var frame = style.Brush( ArchSurface.WindowFrame, skin );
// Inset only where there's a kerb, so a ridge lantern's panes meet over the ridge.
var glazing = piece.Grown( -face );
Ring( canvas, piece.Footprint(), glazing, head, depth, frame );
Slab( canvas, glazing, head, MathF.Max( 0.4f, kit.GlazingBar ), style.Brush( ArchSurface.Glass, skin ) );
Bars( canvas, glazing, head, piece.Light.PaneSpan, face, depth, frame );
}
// A DORMER IS A BOX BREAKING OUT OF A SLOPE, and every piece of it is a shape the roof already emits: the deck's
// own carve for the hole, a LEVEL ring for the cheeks, two raked slabs for the lid, a triangle for each gable end
// and a framed pane in the face it fronts. Its ridge runs DOWNHILL, so the gable it shows stands over the window
// and its back buries into the slope instead of standing proud of it.
static void Dormer( ArchMesh canvas, ArchLitPatch piece, ArchRoofLightStack stack, float width, float thickness, ArchKit kit, ArchStyle style, ArchPalette[] skin )
{
var outer = piece.Grown( width );
var seat = Seat( piece, outer, thickness );
var cheek = MathF.Max( 4f, stack.Upstand );
var bite = ArchContact.Bite( kit );
// Level, not raked: the uphill cheek buries in the deck and the downhill one stands its full height, which
// is what makes the box read as breaking out rather than lying on the slope.
Ring( canvas, outer, piece.Footprint(), ArchCarvePlane.Level( seat - bite ), bite + cheek,
style.Brush( ArchSurface.WallExterior, skin ) );
ArchFootprint.Bounds( outer, out var min, out var max );
var downhill = Downhill( piece, max - min );
var alongX = MathF.Abs( downhill.x ) > 0.5f;
var plate = seat + cheek;
var head = plate + stack.Ridge;
var oversail = ArchContact.Proud( kit );
var lid = MathF.Max( 1f, kit.RoofThickness );
var deck = style.Brush( ArchSurface.Roof, skin );
// The ridge runs down the slope, so the span the lid pitches over is the OTHER axis.
var eaves = ArchFootprint.Rect( min - new Vector2( oversail, oversail ), max + new Vector2( oversail, oversail ) );
ArchFootprint.Bounds( eaves, out var eaveMin, out var eaveMax );
var acrossMin = alongX ? eaveMin.y : eaveMin.x;
var acrossMax = alongX ? eaveMax.y : eaveMax.x;
var ridge = (acrossMin + acrossMax) * 0.5f;
var gradient = ridge - acrossMin > 0.5f ? stack.Ridge / (ridge - acrossMin) : 0f;
foreach ( var low in new[] { true, false } )
{
var from = low ? acrossMin : ridge;
var to = low ? ridge : acrossMax;
var eave = low ? acrossMin : acrossMax;
var fall = alongX ? new Vector2( 0f, low ? gradient : -gradient ) : new Vector2( low ? gradient : -gradient, 0f );
var origin = alongX ? new Vector2( eaveMin.x, eave ) : new Vector2( eave, eaveMin.y );
Slab( canvas,
alongX
? ArchFootprint.Rect( new Vector2( eaveMin.x, from ), new Vector2( eaveMax.x, to ) )
: ArchFootprint.Rect( new Vector2( from, eaveMin.y ), new Vector2( to, eaveMax.y ) ),
ArchCarvePlane.Through( origin, plate, fall ), lid, deck );
}
DormerGables( canvas, min, max, alongX, plate, head, thickness, style.Brush( ArchSurface.WallExterior, skin ) );
if ( piece.Light.Glazed )
{
DormerWindow( canvas, min, max, downhill, alongX, seat, plate, kit, style, skin );
}
}
// Both ends: the one it fronts closes the gable over the window, and the one behind it closes the box against
// the slope it came out of. Bare, either would show the underside of the lid from inside the roof.
static void DormerGables( ArchMesh canvas, Vector2 min, Vector2 max, bool alongX, float plate, float head, float thickness, ArchBrush brush )
{
var span = alongX ? new Vector2( min.y, max.y ) : new Vector2( min.x, max.x );
var mid = (span.x + span.y) * 0.5f;
foreach ( var end in alongX ? new[] { min.x, max.x } : new[] { min.y, max.y } )
{
var inward = MathF.Abs( end - (alongX ? min.x : min.y) ) < 0.01f ? thickness : -thickness;
ArchSlab.Face( canvas, new List<Vector3>
{
DormerCorner( alongX, end, span.x, plate ),
DormerCorner( alongX, end, span.y, plate ),
DormerCorner( alongX, end, mid, head )
}, DormerAxis( alongX ) * inward, brush );
}
}
// The window in the cheek it fronts: a frame round the hole and a pane behind it, both boxes on the vertical
// face, because a height field cannot describe a plane that stands up. A cheek is short, so the sash fills it
// between its own bottom rail and the plate rather than sitting on an authored sill.
static void DormerWindow( ArchMesh canvas, Vector2 min, Vector2 max, Vector2 downhill, bool alongX, float seat, float plate, ArchKit kit, ArchStyle style, ArchPalette[] skin )
{
var face = MathF.Max( 1f, kit.FrameFace );
var depth = MathF.Max( 1f, kit.FrameDepth );
var sill = seat + face * 2f;
var lintel = plate - face;
if ( lintel - sill < face * 2f )
{
return;
}
var outward = alongX ? (downhill.x > 0f ? max.x : min.x) : (downhill.y > 0f ? max.y : min.y);
var inward = outward + (alongX ? (downhill.x > 0f ? -depth : depth) : (downhill.y > 0f ? -depth : depth));
var span = alongX ? new Vector2( min.y + face, max.y - face ) : new Vector2( min.x + face, max.x - face );
if ( span.y - span.x < face * 2f )
{
return;
}
var frame = style.Brush( ArchSurface.WindowFrame, skin );
DormerBoard( canvas, alongX, outward, inward, new Vector2( span.x, span.x + face ), sill, lintel + face, frame );
DormerBoard( canvas, alongX, outward, inward, new Vector2( span.y - face, span.y ), sill, lintel + face, frame );
DormerBoard( canvas, alongX, outward, inward, span, sill, sill + face, frame );
DormerBoard( canvas, alongX, outward, inward, span, lintel, lintel + face, frame );
DormerBoard( canvas, alongX, inward, inward + (outward < inward ? -1f : 1f) * MathF.Max( 0.4f, kit.GlazingBar ),
new Vector2( span.x + face, span.y - face ), sill + face, lintel,
style.Brush( ArchSurface.Glass, skin ) );
}
static void DormerBoard( ArchMesh canvas, bool alongX, float outward, float inward, Vector2 span, float from, float to, ArchBrush brush )
{
var low = alongX
? new Vector3( MathF.Min( outward, inward ), span.x, from )
: new Vector3( span.x, MathF.Min( outward, inward ), from );
var high = alongX
? new Vector3( MathF.Max( outward, inward ), span.y, to )
: new Vector3( span.y, MathF.Max( outward, inward ), to );
canvas.Box( low, high, brush );
}
// Which way the slope falls, which is the way a dormer faces. Read off the patch's own plane, so nothing here
// knows what a roof style is; a level patch has no downhill and falls back to the footprint's longer axis.
static Vector2 Downhill( ArchLitPatch piece, Vector2 size )
{
var fall = piece.Plane.Fall;
if ( fall.Length > 0.0001f )
{
return -fall.Normal;
}
return size.x >= size.y ? new Vector2( 1f, 0f ) : new Vector2( 0f, 1f );
}
static Vector3 DormerCorner( bool alongX, float along, float across, float height )
{
return alongX ? new Vector3( along, across, height ) : new Vector3( across, along, height );
}
static Vector3 DormerAxis( bool alongX ) => alongX ? Vector3.Forward : Vector3.Left;
// A lid is the pane's own slab in plant metal, oversailing the kerb the way a coping does - a hatch is a
// kerbed light you climb out of rather than look through.
static void Lid( ArchMesh canvas, ArchLitPatch piece, float width, ArchCarvePlane head, ArchKit kit, ArchStyle style, ArchPalette[] skin )
{
var oversail = ArchContact.Proud( kit );
Slab( canvas, piece.Grown( width + oversail ), head, MathF.Max( 1f, kit.FrameDepth ), style.Brush( ArchSurface.Plant, skin ) );
}
// Stood through ArchPillarGen, so a cowl is a capital and a plinth is a plinth and nothing here knows how to
// build a cylinder.
static void Stood( ArchMesh canvas, ArchLitPatch piece, ArchRoofLightStack stack, float width, float seat, ArchKit kit, ArchStyle style, ArchPalette[] skin )
{
var section = piece.Light.Form.Pillar( (piece.Min + piece.Max) * 0.5f, piece.Max - piece.Min, seat, stack.Standing, width, kit );
var plant = style.Brush( ArchSurface.Plant, skin );
ArchAsks.StandPillar( canvas, section, plant, plant );
}
// A section beds into the rake rather than floating on it: the LOWEST deck corner under everything it puts on
// the roof, its plinth included, so the uphill side buries instead of the downhill side hanging clear.
static float Seat( ArchLitPatch piece, IReadOnlyList<Vector2> footprint, float thickness )
{
var lowest = float.MaxValue;
foreach ( var corner in footprint )
{
lowest = MathF.Min( lowest, piece.Plane.At( corner ) );
}
return lowest + thickness;
}
// Bars ride the pane's own plane, so a raked lantern's grid rakes with it and no case branches on pitch.
static void Bars( ArchMesh canvas, IReadOnlyList<Vector2> glazing, ArchCarvePlane head, float span, float face, float depth, ArchBrush brush )
{
if ( span < face * 3f )
{
return;
}
ArchFootprint.Bounds( glazing, out var min, out var max );
var half = face * 0.5f;
foreach ( var offset in ArchDivide.AtMost( max.x - min.x, span ).Inner )
{
var at = min.x + offset;
Slab( canvas, ArchFootprint.Rect( new Vector2( at - half, min.y ), new Vector2( at + half, max.y ) ), head, depth, brush );
}
foreach ( var offset in ArchDivide.AtMost( max.y - min.y, span ).Inner )
{
var at = min.y + offset;
Slab( canvas, ArchFootprint.Rect( new Vector2( min.x, at - half ), new Vector2( max.x, at + half ) ), head, depth, brush );
}
}
// Inner loop grown past both faces, so the ring is cut clean through on a rake.
static void Ring( ArchMesh canvas, IReadOnlyList<Vector2> outer, IReadOnlyList<Vector2> inner, ArchCarvePlane floor, float thickness, ArchBrush brush )
{
var shape = new ArchCarve()
.Plus( ArchCarveVolume.Raked( outer, floor, thickness ) )
.Less( ArchCarveVolume.Raked( inner, floor.Raised( -1f ), thickness + 2f ) )
.Resolve();
Paint( canvas, shape, brush );
}
static void Slab( ArchMesh canvas, IReadOnlyList<Vector2> footprint, ArchCarvePlane floor, float thickness, ArchBrush brush )
{
Paint( canvas, new ArchCarve().Plus( ArchCarveVolume.Raked( footprint, floor, thickness ) ).Resolve(), brush );
}
static void Paint( ArchMesh canvas, ArchCarveShape shape, ArchBrush brush )
{
// One shape = one solid, so its corners weld; kerb, frame and pane are separate scopes.
using var welding = canvas.Welding();
foreach ( var face in shape.Faces )
{
canvas.Polygon( face.Points, brush );
}
}
}