Editor builder for architectural output. Walks a plan, resolves connections, and invokes drawer/filer components to produce parts and root transforms for buildings, roads, rooms and other architectural elements.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchArchitectureBuild
{
public IReadOnlyList<ArchBuiltPart> Parts { get; init; } = Array.Empty<ArchBuiltPart>();
public IReadOnlyDictionary<string, Transform> RootTransforms { get; init; } = new Dictionary<string, Transform>();
// Object-path segments of the scopes this build did not touch. Their parts are still standing and still
// correct, so nothing here describes them - the cache remembers what they are, and the prune must not read
// their absence from this list as a deletion.
public IReadOnlyList<string> Standing { get; init; } = Array.Empty<string>();
}
// The story of one rebuild: resolve the plan's connections, then walk every building and every road, asking
// the drawer for each thing it stands on to fill a canvas. Nothing here names a generator - a role nobody answers
// simply builds nothing.
public sealed class ArchArchitectureBuilder
{
readonly ArchGenerationServices services;
readonly ArchOutputService output;
readonly Dictionary<string, Transform> rootTransforms = new();
readonly bool worldSpace;
ArchDrawers drawers;
ArchAnswers answers;
ArchBuildGate gate;
public ArchArchitectureBuilder( ArchPlan plan, ArchKit kit, Scene scene = null, ArchGenerationServices services = null )
{
this.services = services ?? new ArchGenerationServices( plan, kit, scene );
output = new ArchOutputService( this.services.Plan );
drawers = ArchDrawers.Load();
answers = ArchAnswers.Load();
worldSpace = scene is null;
}
// The generation gate for the whole layer system: a disabled layer draws nothing.
static bool On( object payload ) => ArchLayerGate.On( payload );
// Draws with a hand-picked table instead of the written-out one, which is the only way to stand in front of
// the tool as it looks with one role unanswered.
public ArchArchitectureBuilder Drawing( ArchDrawers with )
{
drawers = with;
return this;
}
// The same, for the questions the builder asks between drawings - what a building's grade is, what storeys it
// stands, whether a room is a link.
public ArchArchitectureBuilder Answering( ArchAnswers with )
{
answers = with;
return this;
}
// A scope nothing could have touched is not generated at all - the parts it made last build are still standing
// and still correct, so the cheapest thing the generator can do about them is nothing.
public ArchArchitectureBuilder Through( ArchBuildGate through )
{
gate = through;
return this;
}
// The object-path segment of each scope left standing, which is the name the scene and the cache agree on -
// never the plan id, because a junction's node name parses as a road id and would be filed under it.
readonly List<string> skipped = new();
bool Standing( int itemId, string segment )
{
if ( gate is null || !gate.Skips( itemId ) )
{
return false;
}
skipped.Add( segment );
return true;
}
public ArchArchitectureBuilder WithConnections()
{
services.ResolveConnections();
return this;
}
public ArchArchitectureBuilder WithBuildings()
{
foreach ( var building in services.Plan.Buildings )
{
if ( On( building ) && !Standing( building.Id, ArchNames.Building( building ) ) )
{
BuildBuilding( building );
}
}
return this;
}
public ArchArchitectureBuilder WithRoads()
{
if ( !services.ConnectionsResolved )
{
WithConnections();
}
foreach ( var road in services.Plan.Roads() )
{
if ( !On( road ) || Standing( road.Id, ArchNames.Road( road ) ) )
{
continue;
}
File( ArchNames.Road( road ), road, ArchFiles.Road );
DrawShafts( ArchNames.Road( road ), road.Cuts, road.Palette );
}
foreach ( var junction in services.Connections.Junctions )
{
File( junction.Name, junction.Subject, ArchFiles.Junction );
}
return this;
}
// The contact pass is NOT run here any more: it decides what survives by comparing coplanar neighbours, so
// whether it has to run at all is a question about which parts moved - and that is ArchBuildCache's.
public ArchArchitectureBuild Create()
{
var transforms = new Dictionary<string, Transform>( rootTransforms );
// A part that put its whole body into named pieces files no mesh of its own, so the node those pieces
// hang under takes its frame from here or they are drawn in the wrong space.
foreach ( var frame in output.Frames )
{
transforms[frame.Key] = frame.Value;
}
return new ArchArchitectureBuild
{
Parts = output.Create(),
RootTransforms = transforms,
Standing = skipped.ToList()
};
}
// The one place a generator is reached. A role nothing answers draws nothing at all, rather than filing an
// empty node the cache would read as a part that vanished.
void Draw( string path, Transform nodeTransform, Transform space, object subject, bool welded, string role, params object[] hosts )
{
if ( drawers.For( role ) is not IArchDrawer drawer )
{
return;
}
output.Draw( path, nodeTransform, space, subject, welded, ( canvas, source, part ) => drawer.Draw( new ArchDrawing
{
Canvas = canvas,
Subject = source,
Part = part,
Services = services,
Hosts = hosts
} ) );
}
// The same, for a generator that opens more than one part out of a single layer and so files its own.
void File( string path, object subject, string role, params object[] hosts )
{
if ( drawers.Filing( role ) is not IArchFiler filer )
{
return;
}
filer.File( new ArchFiling
{
Output = output,
Path = path,
Subject = subject,
Services = services,
Hosts = hosts
} );
}
void BuildBuilding( ArchBuilding building )
{
var root = ArchNames.Building( building );
var lift = answers.Lift( services.Plan, building, services.Kit );
rootTransforms[root] = new Transform( Vector3.Up * lift );
// The group holding it first, then the unit's own answer over the top - the same scope order everything else
// in the stack reads.
using var held = output.Within( ArchLayerGroups.Holding( services.Plan, building.Id )?.Collision );
using var collision = output.Within( building.Collision );
DrawStoreys( root, building );
foreach ( var room in building.Rooms )
{
if ( On( room ) )
{
DrawRoom( root, room, building );
}
}
DrawRoofs( root, building );
DrawDownpipes( root, building );
DrawPipeRuns( root, building );
DrawBrackets( root, building );
DrawLadders( root, building );
DrawBalconies( root, building );
DrawExteriorStairs( root, building );
DrawFences( root, building, lift );
DrawShafts( root, building.Cuts, building.Palette, BuildingSpace( building ) );
DrawPlatforms( root, building, lift );
}
void DrawRoom( string root, ArchRoom room, ArchBuilding building )
{
var roomPath = $"{root}/{ArchNames.Room( room )}";
using var collision = output.Within( room.Collision );
DrawOverhang( roomPath, room, building );
DrawShell( roomPath, room, building );
DrawPillars( roomPath, room, building );
DrawSpans( roomPath, room, building );
DrawBeams( roomPath, room, building );
DrawStairs( roomPath, room, building );
DrawTrims( roomPath, room, building );
DrawPorches( roomPath, room, building );
DrawApproaches( roomPath, room, building );
}
void DrawStoreys( string root, ArchBuilding building )
{
foreach ( var storey in answers.Storeys( building, services.Kit, services.Style ) )
{
var floor = $"{root}/{storey.Name}";
Draw( $"{floor}/Slab", Transform.Zero, BuildingSpace( building ), storey.Subject, false, ArchDraws.Storey, building );
Draw( $"{floor}/Foundation", Transform.Zero, BuildingSpace( building ), storey.Subject, false, ArchDraws.Foundation, building );
}
}
// Only a rising link draws its own walls - they rake between the mouths, which no vertical wall
// generator can say. A flat link's passage walls are ordinary walls, so they take the ordinary path
// and everything an opening wears comes with them.
void DrawShell( string roomPath, ArchRoom room, ArchBuilding building )
{
if ( answers.Rising( room ) )
{
DrawRakedShell( roomPath, room, building );
return;
}
foreach ( var wall in room.Walls.Where( wall => !ArchWallJoins.CoveredBy( building, room, wall ) && On( wall ) ) )
{
var frame = new Transform( new Vector3( wall.Start.x, wall.Start.y, room.BaseHeight ), Rotation.FromYaw( ArchWallJoins.AngleDegrees( wall ) ) );
Draw( $"{roomPath}/{ArchNames.Wall( wall )}", frame, WallSpace( building, frame ), wall, true, ArchDraws.Wall, room, building );
}
if ( answers.IsWalkway( room ) )
{
DrawWalkwayBoards( roomPath, room, building );
}
}
void DrawRakedShell( string roomPath, ArchRoom room, ArchBuilding building )
{
Draw( $"{roomPath}/WalkwayShell", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.WalkwayRising, building );
}
// The link's own boards, drawn apart from its walls so they die square at the mouth corners.
void DrawWalkwayBoards( string roomPath, ArchRoom room, ArchBuilding building )
{
Draw( $"{roomPath}/WalkwayBoards", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.WalkwayBoards, building );
}
// The channel hangs on the eave that feeds it, so it is filed beside the fascia and soffit of ITS roof rather
// than in one heap at the foot of the house - a building with two roofs had one Gutters node for both.
void DrawRoofs( string root, ArchBuilding building )
{
foreach ( var roof in building.Roofs )
{
if ( !On( roof ) )
{
continue;
}
var runs = new List<ArchRunPath>();
var roofPath = $"{root}/{ArchNames.Roof( roof )}";
Draw( roofPath, Transform.Zero, BuildingSpace( building ), roof, false, ArchDraws.Roof,
building, building.GuttersEnabled ? runs : null );
DrawGutterChannel( roofPath, runs, building );
DrawRoofWalls( roofPath, roof, building );
}
}
// A parapet that varies, a party wall between two rooftops: an ordinary wall through the ordinary generator, so
// everything a wall wears comes with it. The host room and its joins are ArchRoofWalls' single answer - there is
// no room on a deck, and a second stand-in made here would disagree with the ghost about the seat.
void DrawRoofWalls( string roofPath, ArchRoofPart roof, ArchBuilding building )
{
if ( roof.Walls.Count == 0 )
{
return;
}
var deck = ArchRoofWalls.Deck( roof, services.Kit );
foreach ( var wall in roof.Walls )
{
if ( !On( wall ) )
{
continue;
}
var frame = new Transform( new Vector3( wall.Start.x, wall.Start.y, deck.Seat ), Rotation.FromYaw( ArchWallJoins.AngleDegrees( wall ) ) );
Draw( $"{roofPath}/{ArchNames.Wall( wall )}", frame, WallSpace( building, frame ), wall, true, ArchDraws.Wall,
deck.Room, building, deck.Connections );
}
}
void DrawGutterChannel( string roofPath, List<ArchRunPath> runs, ArchBuilding building )
{
if ( runs.Count == 0 )
{
return;
}
File( roofPath, runs, ArchFiles.GutterChannel, building, BuildingSpace( building ) );
}
void DrawDownpipes( string root, ArchBuilding building )
{
foreach ( var pipe in building.Downpipes )
{
if ( !On( pipe ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Downpipe( pipe )}", Transform.Zero, BuildingSpace( building ), pipe, false,
ArchDraws.Downpipe, building );
}
}
// The hangers go in AFTER the runs, because a bracket volume reads the lane paths the runs resolve - and
// both resolve through their own shape rather than off anything the other one built, so the order here is
// only the order the rows read in, never a dependency.
void DrawPipeRuns( string root, ArchBuilding building )
{
foreach ( var run in building.Pipes )
{
if ( !On( run ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Pipe( run )}", Transform.Zero, BuildingSpace( building ), run, false,
ArchDraws.Pipe, building );
}
}
void DrawBrackets( string root, ArchBuilding building )
{
foreach ( var bracket in building.Brackets )
{
if ( !On( bracket ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Bracket( bracket )}", Transform.Zero, BuildingSpace( building ), bracket, false,
ArchDraws.Bracket, building );
}
}
void DrawLadders( string root, ArchBuilding building )
{
foreach ( var ladder in building.Ladders )
{
if ( !On( ladder ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Ladder( ladder )}", Transform.Zero, BuildingSpace( building ), ladder, false,
ArchDraws.Ladder, building );
}
}
void DrawBalconies( string root, ArchBuilding building )
{
foreach ( var balcony in building.Balconies )
{
if ( !On( balcony ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Balcony( balcony )}", Transform.Zero, BuildingSpace( building ), balcony, false,
ArchDraws.Balcony, building );
}
}
void DrawExteriorStairs( string root, ArchBuilding building )
{
foreach ( var flight in building.ExteriorStairs )
{
if ( !On( flight ) )
{
continue;
}
Draw( $"{root}/{ArchNames.ExteriorStair( flight )}", Transform.Zero, BuildingSpace( building ), flight, false,
ArchDraws.ExteriorStair, building );
}
}
void DrawFences( string root, ArchBuilding building, float lift )
{
foreach ( var fence in building.Fences )
{
if ( !On( fence ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Fence( fence )}", new Transform( Vector3.Up * -lift ), Transform.Zero, fence, true,
ArchDraws.Fence, building );
}
}
// A cut that merely carves is the absence in what it passed through - no geometry, nothing to delete. What it
// DOES build is its enclosure and its head, and those read the same whether it was filed on a building or on a
// road: a service chamber off a bore is a shaft with walls round it.
void DrawShafts( string root, IReadOnlyList<ArchCutPart> cuts, ArchPalette owner, Transform space = default )
{
foreach ( var cut in cuts.Where( entry => entry.Builds ) )
{
if ( !On( cut ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Cut( cut )}", Transform.Zero, space, cut, false, ArchDraws.Cut, owner );
}
}
// On grade, like a fence: the lift belongs to the floor, not a platform poured beside it.
void DrawPlatforms( string root, ArchBuilding building, float lift )
{
foreach ( var platform in building.Platforms )
{
if ( !On( platform ) )
{
continue;
}
Draw( $"{root}/{ArchNames.Platform( platform )}", new Transform( Vector3.Up * -lift ), Transform.Zero, platform, false,
ArchDraws.Platform, building );
}
}
void DrawOverhang( string roomPath, ArchRoom room, ArchBuilding building )
{
Draw( $"{roomPath}/Overhang", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.Overhang, building );
}
void DrawPillars( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var pillar in room.Pillars )
{
if ( !On( pillar ) )
{
continue;
}
Draw( $"{roomPath}/{ArchNames.Pillar( pillar )}", Transform.Zero, BuildingSpace( building ), pillar, false,
ArchDraws.Pillar, room, building );
}
}
void DrawSpans( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var span in room.PierSpans )
{
if ( !On( span ) )
{
continue;
}
Draw( $"{roomPath}/{ArchNames.Span( span )}", Transform.Zero, BuildingSpace( building ), span, false,
ArchDraws.Span, room, building );
}
}
void DrawBeams( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var beam in room.Beams )
{
if ( !On( beam ) )
{
continue;
}
Draw( $"{roomPath}/{ArchNames.Beam( beam )}", Transform.Zero, BuildingSpace( building ), beam, false,
ArchDraws.Beam, room, building );
}
}
void DrawStairs( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var stair in room.Stairs )
{
if ( !On( stair ) )
{
continue;
}
Draw( $"{roomPath}/{ArchNames.Stair( stair )}", Transform.Zero, BuildingSpace( building ), stair, false,
ArchDraws.Stair, room, building );
}
}
void DrawTrims( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var trim in room.Trims )
{
if ( !On( trim ) )
{
continue;
}
Draw( $"{roomPath}/{ArchNames.Trim( trim )}", Transform.Zero, BuildingSpace( building ), trim, false,
ArchDraws.Trim, room, building );
}
}
void DrawPorches( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var porch in room.Porches )
{
if ( !On( porch ) )
{
continue;
}
var porchPath = $"{roomPath}/{ArchNames.Porch( porch )}";
Draw( porchPath, Transform.Zero, BuildingSpace( building ), porch, false, ArchDraws.Porch, room, building );
DrawPorchParts( porchPath, porch, room, building );
}
}
// A porch is a HOST, so what stands on its deck is drawn under it - its own object, its own row, its own
// eye and its own collision - exactly as a room's flights, columns and runs are drawn under the room.
void DrawPorchParts( string porchPath, ArchPorchPart porch, ArchRoom room, ArchBuilding building )
{
foreach ( var stair in porch.Stairs )
{
if ( !On( stair ) )
{
continue;
}
Draw( $"{porchPath}/{ArchNames.Stair( stair )}", Transform.Zero, BuildingSpace( building ), stair, false,
ArchDraws.Stair, room, building );
}
foreach ( var pillar in porch.Pillars )
{
if ( !On( pillar ) )
{
continue;
}
Draw( $"{porchPath}/{ArchNames.Pillar( pillar )}", Transform.Zero, BuildingSpace( building ), pillar, false,
ArchDraws.Pillar, room, building );
}
foreach ( var trim in porch.Trims )
{
if ( !On( trim ) )
{
continue;
}
Draw( $"{porchPath}/{ArchNames.Trim( trim )}", Transform.Zero, BuildingSpace( building ), trim, false,
ArchDraws.Trim, room, building );
}
}
void DrawApproaches( string roomPath, ArchRoom room, ArchBuilding building )
{
foreach ( var approach in room.Approaches )
{
if ( !On( approach ) )
{
continue;
}
Draw( $"{roomPath}/{ArchNames.Approach( approach )}", Transform.Zero, BuildingSpace( building ), approach, false,
ArchDraws.Approach, room, building );
}
}
Transform BuildingSpace( ArchBuilding building )
{
return worldSpace ? new Transform( Vector3.Up * answers.Lift( services.Plan, building, services.Kit ) ) : Transform.Zero;
}
Transform WallSpace( ArchBuilding building, Transform frame )
{
if ( !worldSpace )
{
return frame;
}
return new Transform( Vector3.Up * answers.Lift( services.Plan, building, services.Kit ) + frame.Position, frame.Rotation );
}
}