Editor/Output/ArchArchitectureBuilder.cs
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>();
public IReadOnlyList<string> Standing { get; init; } = Array.Empty<string>();
}
public sealed class ArchArchitectureBuilder {
readonly ArchBuildContext context;
readonly ArchOutput output;
readonly Dictionary<string, Transform> rootTransforms = new();
ArchDrawers drawers;
ArchAnswers answers;
ArchBuildGate gate;
public ArchArchitectureBuilder( ArchPlan plan, ArchKit kit, Scene scene = null, ArchBuildContext context = null ) {
this.context = context ?? new ArchBuildContext( plan, kit, scene );
output = new ArchOutput( this.context.Plan );
drawers = ArchDrawers.Load();
answers = ArchAnswers.Load();
}
static bool On( object payload ) => ArchLayerGate.On( payload );
public ArchArchitectureBuilder Drawing( ArchDrawers with ) {
drawers = with;
return this;
}
public ArchArchitectureBuilder Answering( ArchAnswers with ) {
answers = with;
return this;
}
public ArchArchitectureBuilder Through( ArchBuildGate through ) {
gate = through;
return this;
}
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() {
context.ResolveConnections();
return this;
}
public ArchArchitectureBuilder WithBuildings() {
foreach ( var building in context.Plan.Buildings ) {
if ( On( building ) && !Standing( building.Id, ArchNames.Building( building ) ) ) {
BuildBuilding( building );
}
}
return this;
}
public ArchArchitectureBuilder WithRoads() {
if ( !context.ConnectionsResolved ) {
WithConnections();
}
foreach ( var road in context.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, Transform.Zero );
}
foreach ( var junction in context.Connections.Junctions ) {
File( junction.Name, junction.Subject, ArchFiles.Junction );
}
return this;
}
public ArchArchitectureBuilder WithUnits() {
foreach ( var unit in context.Plan.Units ) {
if ( unit is ArchBuilding or ArchRoadPart || !On( unit ) ) {
continue;
}
var root = ArchNames.Unit( unit );
if ( Standing( unit.Id, root ) ) {
continue;
}
using var collision = output.Within( unit.Collision );
using var shadows = output.WithinShadows( unit.Id );
Draw( root, Transform.Zero, Transform.Zero, unit, false, ArchDraws.Unit( unit.Kind ) );
File( root, unit, ArchFiles.Unit( unit.Kind ) );
DrawShafts( root, unit.Cuts, unit.Palette, Transform.Zero );
}
return this;
}
public ArchArchitectureBuild Create() {
var transforms = new Dictionary<string, Transform>( rootTransforms );
foreach ( var frame in output.Frames ) {
transforms[frame.Key] = frame.Value;
}
return new ArchArchitectureBuild {
Parts = output.Create(),
RootTransforms = transforms,
Standing = skipped.ToList()
};
}
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;
}
var from = ArchBuildProfile.Now;
output.Draw( path, nodeTransform, space, subject, welded, ( canvas, source, part ) => drawer.Draw( new ArchDrawing {
Canvas = canvas,
Subject = source,
Part = part,
Services = context,
Hosts = hosts
} ) );
ArchBuildProfile.Ran( role, from );
}
void File( string path, object subject, string role, params object[] hosts ) {
if ( drawers.Filing( role ) is not IArchFiler filer ) {
return;
}
var from = ArchBuildProfile.Now;
filer.File( new ArchFiling {
Output = output,
Path = path,
Subject = subject,
Services = context,
Hosts = hosts
} );
ArchBuildProfile.Ran( role, from );
}
void BuildBuilding( ArchBuilding building ) {
var root = ArchNames.Building( building );
var lift = answers.Lift( context.Plan, building, context.Kit );
rootTransforms[root] = new Transform( Vector3.Up * lift );
var holding = ArchLayerGroups.Holding( context.Plan, building.Id );
using var held = output.Within( holding?.Collision );
using var heldShadows = output.WithinShadows( holding?.Id ?? 0 );
using var collision = output.Within( building.Collision );
using var shadows = output.WithinShadows( building.Id );
DrawStoreys( root, building );
foreach ( var room in building.Rooms ) {
var roomPath = $"{root}/{ArchNames.Room( room )}";
if ( On( room ) && !Standing( room.Id, roomPath ) ) {
DrawRoom( roomPath, 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 roomPath, ArchRoom room, ArchBuilding building ) {
using var collision = output.Within( room.Collision );
using var shadows = output.WithinShadows( room.Id );
DrawOverhang( roomPath, room, building );
DrawShell( roomPath, room, building );
DrawCornice( roomPath, room, building );
DrawPillars( roomPath, room, building );
DrawSpans( roomPath, room, building );
DrawBeams( roomPath, room, building );
DrawCabinets( 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, context.Kit, context.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 );
}
}
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 );
}
void DrawWalkwayBoards( string roomPath, ArchRoom room, ArchBuilding building ) {
Draw( $"{roomPath}/WalkwayBoards", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.WalkwayBoards, building );
}
void DrawRoofs( string root, ArchBuilding building ) {
foreach ( var roof in building.Roofs ) {
var roofPath = $"{root}/{ArchNames.Roof( roof )}";
if ( !On( roof ) || Standing( roof.Id, roofPath ) ) {
continue;
}
var runs = new List<ArchRunPath>();
Draw( roofPath, Transform.Zero, BuildingSpace( building ), roof, false, ArchDraws.Roof,
building, building.GuttersEnabled ? runs : null );
DrawGutterChannel( roofPath, runs, building );
DrawRoofWalls( roofPath, roof, building );
}
}
void DrawRoofWalls( string roofPath, ArchRoofPart roof, ArchBuilding building ) {
if ( roof.Walls.Count == 0 ) {
return;
}
var deck = ArchRoofWalls.Deck( roof, context.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 );
}
}
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 );
}
}
// `space` must be explicitly named — default(Transform) has zero scale and maps all UVs to one texel
void DrawShafts( string root, IReadOnlyList<ArchCutPart> cuts, ArchPalette owner, Transform space ) {
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 );
}
}
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 );
}
// The storey's entablature is ONE ring round its own loop, so it hangs off the storey rather than off each wall.
void DrawCornice( string roomPath, ArchRoom room, ArchBuilding building ) {
Draw( $"{roomPath}/{ArchPieces.Cornice}", Transform.Zero, BuildingSpace( building ), room, false, ArchDraws.Cornice, building );
}
void DrawPillars( string roomPath, ArchRoom room, ArchBuilding building ) {
foreach ( var pillar in room.Pillars ) {
if ( !On( pillar ) || !ArchLayerGate.Owned( pillar.OwnerId ) ) {
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 DrawCabinets( string roomPath, ArchRoom room, ArchBuilding building ) {
foreach ( var cabinets in room.Cabinets ) {
if ( !On( cabinets ) ) {
continue;
}
Draw( $"{roomPath}/{ArchNames.Cabinets( cabinets )}", Transform.Zero, BuildingSpace( building ), cabinets, false,
ArchDraws.Cabinet, 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 );
}
}
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 );
}
}
// The node's actual world space — UVs and contact pass both need this
Transform BuildingSpace( ArchBuilding building ) {
return new Transform( Vector3.Up * answers.Lift( context.Plan, building, context.Kit ) );
}
Transform WallSpace( ArchBuilding building, Transform frame ) {
return new Transform( Vector3.Up * answers.Lift( context.Plan, building, context.Kit ) + frame.Position, frame.Rotation );
}
}