Editor generator for roof geometry. It builds roof regions, decks, soffits, fascias, parapets, interior ceilings and related mesh parts into an ArchMesh using plan, kit and style data.
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();
// A storey above stops the overhang ring at the wall line, so only a ceiling is left.
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 );
// Raked gable ends and a shed's high side shed nothing, so they carry no fascia or soffit.
bool Bare( Vector2 point, Vector2 outward ) => FittingAbuts( point, outward ) || !Sheds( roof, shapeStyle, outward );
// Only a TALLER roof takes the slot away. A deck arriving level with the plate covers the closure
// instead, so it laps into that - held clear of it the way a board is, the plate is left open along
// the whole mouth and you look into the roof from inside the passage.
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 );
// The soffit bands the plate line, so it is notched the way the eave loop is: left whole it only had
// the whole-edge break to fall back on, and a link taking 192 in out of a 448 in eave took the boards
// off all of it.
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 );
}
}
// The eave assembly, one row: the boards, the channel hung under them and the pipes off its outlets are
// the thing an author reaches for, and each was a separate hunt down the roof.
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 );
}
}
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;
}
// LAPPED INTO the deck, not held off it. Level with the soffit, the ceiling's top and the deck's
// underside are one plane the eave line runs through - a coplanar pair on a non-manifold edge. Held
// short of it instead, the slot shows at the mouth of a link. So the top buries in the deck and the
// soffit stays exactly where it was, which is what the cornice under it is measured from.
var head = roof.BaseHeight + ArchContact.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();
// Handed over, not doubled: where a room carries its own ceiling this slab steps aside, so the two
// never stand an inch apart with a coplanar pair down every wall between them.
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 ) );
}
}
}
// A vault or a loft floor is the roof's own shape and keeps the whole region; anything else gives way
// to the rooms that carry a ceiling themselves.
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;
}
}
// A ceiling trims the wells it is actually under. Handed every cutout the building owns, a small
// deck in one corner - a walkway's - ran a band of trim round a stairwell hundreds of inches away.
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;
}
// Only a FULL storey; partial overlaps are left to the abutment clip.
static bool BuiltOver( ArchRoofPart roof, ArchBuilding building, IReadOnlyList<Vector2> outline )
{
return ArchRegion.Covers( ArchRegion.Storey( building, roof.Level + 1 ), outline );
}
// A higher eave buries what runs into it, so its footprint clips the overhang.
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();
}
// What a FITTING cannot hang in: a deck standing LEVEL with this eave is in the way even though the
// eave itself carries on - a link's deck meets the house at its own eave height, and the gutter ran
// straight over the passage and hung inside it under the ceiling. Only a HIGHER roof buries the eave,
// which is a house's own roof over its own wall. Filed on one of the two houses it joins, a link
// stands in the eave of both, so the neighbours count and the plan is how they are seen.
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();
}
// Everything hanging on this eave stops short of the same footprints - fascia, soffit, closure and
// channel - or a board runs on past where the channel gave up. Padded off the wall each of these
// stands on: an outline is a wall CENTRELINE, so a run to it dies half a thickness inside the wall
// it meets instead of stopping clear of its face.
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 + ArchContact.Bite( kit ) );
}
static bool Buried( List<List<Vector2>> abutments, Vector2 point )
{
return abutments.Any( loop => ArchFloorGen.Contains( loop, point ) );
}
// The same split GutterRuns makes - a board and a channel must agree on the eave.
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;
}
}