Editor/Roof/ArchRoofGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchRoofGen {
public static void Build( ArchMesh canvas, ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, List<ArchRunPath> gutterRuns ) {
foreach ( var region in new ArchRoofRegionService( building, roof ).Resolve() ) {
BuildRegion( canvas, roof, building, plan, kit, style, gutterRuns, region );
}
}
static void BuildRegion( ArchMesh canvas, ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, List<ArchRunPath> gutterRuns, ArchRoofRegion region ) {
var outline = region.Outer;
var shapeStyle = roof.Style;
var chain = new[] { roof.Palette, building.Palette };
var deck = style.Brush( ArchSurface.Roof, chain );
var edge = style.Brush( ArchSurface.RoofEdge, chain );
var kinds = ArchKinds.Load();
if ( BuiltOver( roof, building, outline ) ) {
Interior( canvas, roof, region.Loops, building, plan, kit, style, chain, kinds );
return;
}
var abutments = Abutments( roof, building, kinds );
var junctions = ArchCrossGableService.Resolve( building, kit, kinds );
var fittingAbutments = FittingAbutments( roof, building, plan, kit, junctions, kinds );
var overhang = ArchRoofPlane.Overhang( roof );
var thickness = ArchRoofPlane.Thickness( roof, kit );
bool Abuts( Vector2 point, Vector2 outward ) => Buried( abutments, point + outward * ArchProbe.Step );
bool FittingAbuts( Vector2 point, Vector2 outward ) => Buried( fittingAbutments, point + outward * ArchProbe.Step );
bool Bare( Vector2 point, Vector2 outward ) => FittingAbuts( point, outward ) || !Sheds( roof, shapeStyle, outward );
// Only a taller roof removes the slot — a level deck laps into closure instead
bool Slotless( Vector2 point, Vector2 outward ) => Abuts( point, outward ) || !Sheds( roof, shapeStyle, outward );
var drip = ArchRoofPlane.Drip( roof, kit );
var rafters = ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, overhang + drip ), abutments );
var eaves = ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, overhang ), fittingAbutments );
// Notched like eaves so partial abutments don't strip the whole run
var soffits = ArchFootprint.Subtract( region.Loops, fittingAbutments );
var plates = ArchFootprint.Subtract( region.Loops, abutments );
if ( rafters.Count == 0 ) {
return;
}
Bounds( rafters, out var deckMin, out var deckMax );
switch ( shapeStyle ) {
case RoofStyle.Flat:
using ( canvas.Part( ArchPieces.Deck ) ) {
Deck( canvas, roof, building, rafters, deckMin, deckMax, thickness, plan, kit, style, chain, deck );
}
if ( roof.RidgeCap ) {
using ( canvas.Part( ArchPieces.Capping ) ) {
Capping( canvas, Abutted( rafters, Abuts, roof.BaseHeight ), thickness, kit, edge, plan, roof.Level, building.Id );
}
}
break;
case RoofStyle.Hip:
var shape = ArchRoofSkeleton.Shape( rafters, Abuts, roof.BaseHeight, ArchRoofPlane.Slope( roof ) );
var faceted = LitFacets( roof );
using ( canvas.Part( ArchPieces.Deck ) ) {
JoinedSlab( canvas, Pierced( shape.Faces, roof, building, kit, thickness, plan, faceted ), Vector3.Up * thickness, deck );
}
if ( roof.RidgeCap ) {
using ( canvas.Part( ArchPieces.Capping ) ) {
Capping( canvas, shape.Creases, thickness, kit, edge, plan, roof.Level, building.Id );
Capping( canvas, shape.Flashing, thickness, kit, edge, plan, roof.Level, building.Id );
}
}
foreach ( var piece in faceted ) {
using ( canvas.Part( piece.Light.Form.Piece() ) ) {
Light( canvas, piece, thickness, kit, style, chain );
}
}
break;
case RoofStyle.Shed:
case RoofStyle.Gable:
using ( canvas.Part( ArchPieces.Deck ) ) {
Deck( canvas, roof, building, rafters, deckMin, deckMax, thickness, plan, kit, style, chain, deck );
}
break;
case RoofStyle.Sawtooth:
using ( canvas.Part( ArchPieces.Deck ) ) {
Sawtooth( canvas, roof, rafters, deckMin, deckMax, thickness, deck, edge );
}
break;
}
if ( roof.Frame ) {
using ( canvas.Part( ArchPieces.Frame ) ) {
Frame( canvas, roof, rafters, kit, style.Brush( ArchSurface.RoofFrame, chain ) );
}
}
if ( roof.EndWalls ) {
var walls = style.Brush( ArchSurface.WallExterior, chain );
using ( canvas.Part( ArchPieces.EndWalls ) ) {
EndWalls( canvas, roof, deckMin, deckMax, region.Loops, kit, walls, abutments, junctions );
Closure( canvas, roof, plates, kit, walls, overhang + drip, Slotless, plan, building.Id );
}
}
if ( roof.Soffit && overhang > 0.5f ) {
using ( canvas.Part( ArchPieces.Gutters ) )
using ( canvas.Part( ArchPieces.Soffit ) ) {
Soffit( canvas, roof, soffits, kit, style.Brush( ArchSurface.Soffit, chain ), Bare, plan, building.Id );
}
}
if ( roof.Fascia ) {
using ( canvas.Part( ArchPieces.Gutters ) )
using ( canvas.Part( ArchPieces.Fascia ) ) {
Fascia( canvas, roof, eaves, kit, edge, Bare, plan, building.Id );
}
}
if ( roof.Parapet ) {
using ( canvas.Part( ArchPieces.Parapet ) ) {
Parapet( canvas, roof, outline, kit, style.Brush( ArchSurface.WallExterior, chain ), style.Brush( ArchSurface.WallCap, chain ), Abuts, plan, building.Id );
}
}
using ( canvas.Part( ArchPieces.Cornice ) ) {
Crown( canvas, roof, outline, kit, style, chain, Abuts, plan, building.Id );
}
if ( roof.Guardrail ) {
using ( canvas.Part( ArchPieces.Guardrail ) ) {
Guardrail( canvas, roof, outline, kit, style, chain, Abuts );
}
}
Interior( canvas, roof, region.Loops, building, plan, kit, style, chain, kinds );
if ( gutterRuns is not null ) {
foreach ( var run in GutterRuns( roof, building, kit, plan, region ) ) {
gutterRuns.AddRange( ArchCut.Runs( plan, kit, roof.Level, building.Id, run, ArchCutAffects.Gutters ) );
}
}
}
static void Interior( ArchMesh canvas, ArchRoofPart roof, List<List<Vector2>> region, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchPalette[] chain, ArchKinds kinds ) {
if ( !roof.Ceiling && !roof.VaultedCeiling ) {
return;
}
if ( roof.VaultedCeiling ) {
using ( canvas.Part( ArchPieces.Ceiling ) ) {
ArchVaultGen.Build( canvas, roof, kit, style );
}
return;
}
// Ceiling top laps into the deck to avoid a coplanar pair at the eave line
var head = roof.BaseHeight + ArchLap.Bite( kit );
var ceiling = roof.BaseHeight - kit.FloorThickness;
var surface = roof.LoftFloor ? ArchSurface.Floor : ArchSurface.Ceiling;
var (bitten, recesses) = ArchFloorGen.Bites( plan, roof.Level + 1, kit, ceiling, head, building.Id, ArchCutAffects.Roofs, roof.Id );
var wells = ArchCut.Stored( building, roof.Level + 1 )
.Concat( bitten )
.Where( cutout => Reaches( region, cutout ) )
.ToList();
var handed = wells.Concat( HandedOver( roof, building ) ).ToList();
using ( canvas.Part( ArchPieces.Ceiling ) ) {
ArchFloorGen.Solid( canvas, region, handed, ceiling, head, style.Brush( surface, chain ), recesses );
ArchFloorGen.WellTrim( canvas, wells, ceiling, head, kit, style.Brush( ArchSurface.Trim, chain ),
ArchWellEdges.Trimmed( building, kit, wells, head ),
building.Rooms
.SelectMany( room => plan.Filed( ArchKind.Wall, room, kinds ) )
.OfType<ArchWall>()
.ToList(), region );
}
using ( canvas.Part( ArchPieces.Ceiling ) )
using ( canvas.Part( ArchPieces.Cornice ) ) {
foreach ( var outline in region ) {
if ( roof.Walkway ) {
WalkwayCornice( canvas, roof, wells, kit, ceiling, style.Brush( ArchSurface.Trim, chain ) );
break;
}
ArchCornice.Around( canvas, outline, wells, kit, ceiling, style.Brush( ArchSurface.Trim, chain ) );
}
}
}
static IEnumerable<ArchFloorCutout> HandedOver( ArchRoofPart roof, ArchBuilding building ) {
if ( ArchFloorGen.RoofKeepsCeiling( roof ) ) {
yield break;
}
foreach ( var room in building.Rooms.Where( room => room.Floor == roof.Level && room.HasCeiling ) ) {
var footprint = ArchFloorGen.Footprint( room );
if ( footprint.Count < 3 ) {
continue;
}
var handed = new ArchFloorCutout { Level = roof.Level, Name = room.Name };
handed.Reshape( footprint );
yield return handed;
}
}
static bool Reaches( IReadOnlyList<List<Vector2>> region, ArchFloorCutout cutout ) {
foreach ( var loop in region ) {
ArchFootprint.Bounds( loop, out var min, out var max );
if ( ArchFootprint.Overlaps(
new ArchBox { Min = cutout.Min, Max = cutout.Max },
new ArchBox { Min = min, Max = max } ) ) {
return true;
}
}
return false;
}
static bool BuiltOver( ArchRoofPart roof, ArchBuilding building, IReadOnlyList<Vector2> outline ) {
return ArchRegion.Covers( ArchRegion.Storey( building, roof.Level + 1 ), outline );
}
static List<List<Vector2>> Abutments( ArchRoofPart roof, ArchBuilding building, ArchKinds kinds ) {
return ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds )
.OfType<ArchRoofPart>()
.Where( other => !ReferenceEquals( other, roof ) && other.BaseHeight > roof.BaseHeight + 1f )
.Select( other => ArchFootprint.Wind( other.Outline() ) )
.ToList();
}
// Level-or-higher decks block fittings even though the eave itself continues
static List<List<Vector2>> Blocking( ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKinds kinds ) {
// A ghosted roof stands on a building the plan does not hold, so its neighbours are that building's alone.
var standing = plan is not null
? plan.AllRoofs( kinds )
: ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds ).OfType<ArchRoofPart>();
return standing
.Where( other => !ReferenceEquals( other, roof ) && other.BaseHeight > roof.BaseHeight - 1f )
.Select( other => ArchFootprint.Wind( other.Outline() ) )
.ToList();
}
// Padded off centreline by half-thickness so fittings stop at the wall face, not behind it
static List<List<Vector2>> FittingAbutments(
ArchRoofPart roof,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
IReadOnlyList<ArchCrossGableJunction> junctions,
ArchKinds kinds ) {
return ArchFootprint.Grow(
Blocking( roof, building, plan, kinds )
.Concat( ArchCrossGableService.FittingAbutments( roof, kit, junctions ) )
.ToList(),
kit.WallThickness * 0.5f + ArchLap.Bite( kit ) );
}
static bool Buried( List<List<Vector2>> abutments, Vector2 point ) {
return abutments.Any( loop => ArchFloorGen.Contains( loop, point ) );
}
static bool Sheds( ArchRoofPart roof, RoofStyle style, Vector2 outward ) {
if ( style == RoofStyle.Gable ) {
return MathF.Abs( roof.RidgeAlongX ? outward.y : outward.x ) > 0.5f;
}
if ( style != RoofStyle.Shed ) {
return true;
}
var fall = roof.Reversed ? 1f : -1f;
return (roof.RidgeAlongX ? outward.y * fall : outward.x * fall) > 0.5f;
}
static bool Rectangular( ArchRoofRegion region ) {
return Rectangular( region.Loops );
}
static bool Rectangular( IReadOnlyList<List<Vector2>> loops ) {
return loops.Count == 1 && loops[0].Count == 4;
}
static void Bounds( List<List<Vector2>> loops, out Vector2 min, out Vector2 max ) {
ArchFootprint.Bounds( loops.SelectMany( loop => loop ).ToList(), out min, out max );
}
static Vector3 Plan( bool alongX, float span, float across, float height ) {
return alongX ? new Vector3( across, span, height ) : new Vector3( span, across, height );
}
static Vector3 AcrossAxis( bool alongX ) {
return alongX ? new Vector3( 1f, 0f, 0f ) : new Vector3( 0f, 1f, 0f );
}
static Vector3 SpanAxis( bool alongX ) {
return alongX ? new Vector3( 0f, 1f, 0f ) : new Vector3( 1f, 0f, 0f );
}
static void JoinedSlab(
ArchMesh canvas,
IReadOnlyList<List<Vector3>> faces,
Vector3 lift,
ArchBrush brush,
IReadOnlyList<(Vector3 From, Vector3 To)> openEdges = null ) {
if ( lift.IsNearZeroLength ) {
return;
}
var boundaries = new Dictionary<(long, long, long, long, long, long), (Vector3 From, Vector3 To, int Count)>();
var openings = openEdges?
.Select( edge => EdgeKey( edge.From, edge.To ) )
.ToHashSet() ?? new HashSet<(long, long, long, long, long, long)>();
using ( canvas.Welding() ) {
foreach ( var face in faces ) {
var corners = ArchSlab.Cleaned( face );
if ( corners.Count < 3 ) {
continue;
}
if ( Vector3.Dot( ArchMesh.Newell( corners ), lift ) < 0f ) {
corners.Reverse();
}
canvas.Polygon( corners, brush, true );
canvas.Polygon( corners.Select( point => point + lift ).ToList(), brush );
for ( var index = 0; index < corners.Count; index++ ) {
var from = corners[index];
var to = corners[(index + 1) % corners.Count];
var key = EdgeKey( from, to );
if ( boundaries.TryGetValue( key, out var boundary ) ) {
boundaries[key] = (boundary.From, boundary.To, boundary.Count + 1);
continue;
}
boundaries[key] = (from, to, 1);
}
}
foreach ( var boundary in boundaries.Values.Where( boundary => boundary.Count == 1 ) ) {
if ( openings.Contains( EdgeKey( boundary.From, boundary.To ) ) ) {
continue;
}
canvas.Quad( boundary.From, boundary.To, boundary.To + lift, boundary.From + lift, brush );
}
}
}
static (long, long, long, long, long, long) EdgeKey( Vector3 first, Vector3 second ) {
var a = PointKey( first );
var b = PointKey( second );
if ( ComesAfter( a, b ) ) {
(a, b) = (b, a);
}
return (a.X, a.Y, a.Z, b.X, b.Y, b.Z);
}
static (long X, long Y, long Z) PointKey( Vector3 point ) {
var size = ArchGridService.FinestSize;
return (
(long)MathF.Round( point.x / size ),
(long)MathF.Round( point.y / size ),
(long)MathF.Round( point.z / size ));
}
static bool ComesAfter( (long X, long Y, long Z) first, (long X, long Y, long Z) second ) {
if ( first.X != second.X ) {
return first.X > second.X;
}
if ( first.Y != second.Y ) {
return first.Y > second.Y;
}
return first.Z > second.Z;
}
}