Editor/Roof/ArchRoofLightGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
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 );
}
public static partial class ArchRoofGen {
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;
}
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;
}
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;
}
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;
}
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 ) );
}
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 );
}
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;
}
}
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 );
public List<Vector2> Grown( float by ) {
return ArchFootprint.Rect( Min - OpenMin * by, Max + OpenMax * by );
}
}
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 ) );
}
foreach ( var light in lights.Where( piece => piece.Light.Form.Pierces() ) ) {
carve.Less( ArchCarveVolume.Raked( light.Footprint(), light.Plane.Raised( -1f ), thickness + 2f ) );
}
foreach ( var volume in cuts ?? Array.Empty<ArchCarveVolume>() ) {
carve.Less( volume );
}
return carve.Resolve();
}
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 );
}
// Cuts are plan-wide — a shaft from a neighbouring building can open this roof
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 - ArchLap.Bite( kit ), high, building.Id, ArchCutAffects.Roofs ).ToList();
}
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, ArchLap.Bite( kit ) );
}
public readonly struct ArchRoofLightStack {
public float Upstand { get; init; }
public float Coping { get; init; }
public float Head { get; init; }
public float Standing { get; init; }
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;
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 );
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 = ArchLap.Bite( kit );
// Bitten into the deck so underside clears the roof plane
Ring(
canvas,
piece.Grown( width ),
piece.Footprint(),
surface.Raised( -bite ),
bite + stack.Upstand,
style.Brush( piece.Light.Form.Clad() ? ArchSurface.WallExterior : ArchSurface.RoofEdge, skin ) );
if ( stack.Coping <= 0f ) {
return;
}
var oversail = ArchLap.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 );
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 );
}
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 = ArchLap.Bite( kit );
// Level cheeks — uphill buries, downhill stands full height
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 = ArchLap.Proud( kit );
var lid = MathF.Max( 1f, kit.RoofThickness );
var deck = style.Brush( ArchSurface.Roof, skin );
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 );
}
}
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 );
}
}
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 );
}
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;
static void Lid( ArchMesh canvas, ArchLitPatch piece, float width, ArchCarvePlane head, ArchKit kit, ArchStyle style, ArchPalette[] skin ) {
var oversail = ArchLap.Proud( kit );
Slab( canvas, piece.Grown( width + oversail ), head, MathF.Max( 1f, kit.FrameDepth ), style.Brush( ArchSurface.Plant, skin ) );
}
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 );
}
// Lowest deck corner so the uphill side buries rather than the downhill hangs 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;
}
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 );
}
}
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 ) {
using var welding = canvas.Welding();
foreach ( var face in shape.Faces ) {
canvas.Polygon( face.Points, brush );
}
}
}