Editor overlay rendering code for the architecture tool. Draws building plans, storeys, rooms, roads and preview fixtures using Gizmo drawing calls and helper ArchGhost/ArchPreviews utilities, handles picking and visibility per layer and level.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// Overlay draws ONE building, ONE storey (`Shown( floor )`), ONE room at full detail - and resolves nothing in the draw path.
// Roads are the map's, not the building's, so every centreline draws whatever house is up.
public partial class ArchTool
{
void DrawPlan()
{
using ( Gizmo.Scope( "arch-plan" ) )
{
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2f;
// Faces are world space and are being looked at on purpose, so they draw whatever storey,
// building or view mode is up.
DrawDebugFaces();
// Edge-on, plan lines are footprints from the side; the section says the same building the only way it can be said from here.
if ( InElevation )
{
DrawSection();
return;
}
if ( EditedBuilding() is { } building && !LayerHidden( building.Id ) )
{
DrawNeighbours( building );
if ( Placing )
{
DrawWorkPlane( building );
}
DrawStoreyBelow( building );
DrawBuilding( building );
Identify( building );
}
foreach ( var road in Plan.Roads() )
{
if ( LayerHidden( road.Id ) )
{
continue;
}
DrawRoad( road );
}
}
}
// The pick comes first: clicking a house moves you onto it, and the target picker follows rather than leads.
public ArchBuilding EditedBuilding()
{
return Picked?.Building ?? ActiveBuilding();
}
// Corner ticks, not a second outline - a box round the building would read as a wall.
void Identify( ArchBuilding building )
{
if ( !Bounds( building, out var min, out var max ) )
{
return;
}
var height = LevelHeight + ArchAsks.Lift( Plan, building, Kit );
Gizmo.Draw.LineThickness = 3f;
Gizmo.Draw.Color = ArchGhost.Accent;
ArchGhost.Brackets( min, max, height );
Gizmo.Draw.LineThickness = 2f;
ArchGhost.Tag( new Vector3( min.x, max.y, height ), $"{building.Name} · L{Level}", ArchGhost.Accent );
}
// A box round each, never its plan - a neighbour's wall lines read as the edited ones. Under the cursor it names itself.
void DrawNeighbours( ArchBuilding edited )
{
if ( Plan.Buildings.Count < 2 )
{
return;
}
var found = GroundPoint( out var point );
Gizmo.Draw.LineThickness = 1f;
foreach ( var building in Plan.Buildings )
{
if ( building == edited || LayerHidden( building.Id ) || !Bounds( building, out var min, out var max ) )
{
continue;
}
var over = found && point.x >= min.x && point.y >= min.y && point.x <= max.x && point.y <= max.y;
Gizmo.Draw.Color = ArchGhost.Line.WithAlpha( over ? 0.55f : 0.12f );
DrawRect( min, max, LevelHeight );
if ( over )
{
ArchGhost.Tag( new Vector3( min.x, max.y, LevelHeight ), building.Name, ArchGhost.Line );
}
}
Gizmo.Draw.LineThickness = 2f;
}
// Every storey: a marker that blinked with the level would read as the building being deleted.
bool Bounds( ArchBuilding building, out Vector2 min, out Vector2 max )
{
var low = new Vector2( float.MaxValue, float.MaxValue );
var high = new Vector2( float.MinValue, float.MinValue );
var found = false;
foreach ( var room in building.Rooms )
{
foreach ( var corner in Outline( room ) )
{
low = Vector2.Min( low, corner );
high = Vector2.Max( high, corner );
found = true;
}
}
min = low;
max = high;
return found && high.x - low.x > 1f && high.y - low.y > 1f;
}
// Unconditional - it is the one thing that says which floor you are on.
void DrawStoreyBelow( ArchBuilding building )
{
if ( Level <= 0 )
{
return;
}
var height = LevelHeight + ArchAsks.Lift( Plan, building, Kit );
Gizmo.Draw.LineThickness = 3f;
Gizmo.Draw.Color = ArchGhost.Accent.WithAlpha( 0.45f );
foreach ( var room in building.Rooms )
{
if ( room.Floor != Level - 1 || LayerHidden( room.Id ) )
{
continue;
}
var loop = Outline( room );
if ( loop.Count >= 3 )
{
ArchGhost.Ring( loop, height );
}
}
Gizmo.Draw.LineThickness = 2f;
}
void DrawBuilding( ArchBuilding building )
{
var inside = HoveredRoom( building );
var lift = ArchAsks.Lift( Plan, building, Kit );
var previews = ArchPreviews.Load();
foreach ( var room in building.Rooms )
{
DrawRoom( room, inside, lift, building, previews );
}
foreach ( var cutout in building.Cutouts )
{
var sourceId = cutout.OwnerId != 0 ? cutout.OwnerId : cutout.Id;
if ( !Shown( cutout.Level, sourceId ) )
{
continue;
}
Gizmo.Draw.Color = Color.Red.WithAlpha( 0.8f );
ArchGhost.Ring( cutout.Outline(), FloorOf( cutout.Level ) + lift );
}
foreach ( var cut in building.Cuts )
{
if ( Shown( cut.Level, cut.Id ) )
{
DrawCut( cut, lift );
}
}
foreach ( var roof in building.Roofs )
{
if ( !Shown( roof.Level, roof.Id ) )
{
continue;
}
var selected = Picked?.Targets( roof ) == true;
Gizmo.Draw.Color = (selected ? Color.Yellow : Color.Orange).WithAlpha( 0.6f );
DrawLoop( roof.Outline(), roof.BaseHeight + lift );
}
foreach ( var fence in building.Fences )
{
if ( LayerHidden( fence.Id ) )
{
continue;
}
var picked = Picked?.Targets( fence ) == true;
Gizmo.Draw.Color = picked ? Color.Yellow : ArchGhost.Line.WithAlpha( 0.8f );
ArchGhost.Curve( Curve( fence ) );
// Post resolution is the expensive half; an unpicked run reads fine as its own line.
if ( !picked )
{
continue;
}
if ( fence.Barrier.Style == BarrierStyle.Retaining )
{
ArchGhost.Retaining( Retaining( fence ) );
continue;
}
ArchGhost.Barrier( Barrier( fence ), fence.Barrier );
}
foreach ( var pipe in building.Downpipes )
{
if ( LayerHidden( pipe.Id ) )
{
continue;
}
var picked = Picked?.Targets( pipe ) == true;
Gizmo.Draw.Color = picked ? Color.Yellow : Color.Cyan.WithAlpha( 0.7f );
Gizmo.Draw.Line(
new Vector3( pipe.Outlet.x, pipe.Outlet.y, pipe.TopHeight + lift ),
new Vector3( pipe.Wall.x, pipe.Wall.y, pipe.BaseHeight + lift ) );
if ( picked )
{
Gizmo.Draw.LineSphere( new Vector3( pipe.Outlet.x, pipe.Outlet.y, pipe.TopHeight + lift ), 6f );
}
}
foreach ( var run in building.Pipes )
{
DrawPipeRun( run, building, lift, previews );
}
foreach ( var bracket in building.Brackets )
{
DrawBrackets( bracket, building, lift, previews );
}
}
// The corridor always, the LANES only on the picked one. A run's lanes are where the crossing actually
// shows, and drawing every bundle in the plan at once buries the one being worked on.
void DrawPipeRun( ArchPipePart run, ArchBuilding building, float lift, ArchPreviews previews )
{
if ( LayerHidden( run.Id ) )
{
return;
}
previews.Draw( ArchKind.Pipe,
new ArchPreviewContext( run, null, building, lift, Picked?.Targets( run ) == true, this ) );
}
void DrawBrackets( ArchPipeBracketPart bracket, ArchBuilding building, float lift, ArchPreviews previews )
{
if ( LayerHidden( bracket.Id ) )
{
return;
}
previews.Draw( ArchKind.Bracket,
new ArchPreviewContext( bracket, null, building, lift, Picked?.Targets( bracket ) == true, this ) );
}
// A cut builds nothing of its own, so this outline is the only thing standing where it is - which is why it is
// drawn for a road's cuts as well as a building's, and why a road cut left undrawn read as a cut that never
// landed. Two rings and no posts between them read as two unrelated shapes at unrelated heights, so a prism
// draws the corners too, and the selected one gets its extents called out.
void DrawCut( ArchCutPart cut, float lift )
{
var picked = Picked?.Targets( cut ) == true;
Gizmo.Draw.Color = (picked ? Color.Yellow : Color.Red).WithAlpha( 0.7f );
foreach ( var segment in cut.Segments.Where( leg => leg.HasLoop || leg.Length > 1f ) )
{
var loop = segment.Outline();
var band = ArchCut.WorldBand( segment, lift );
ArchGhost.Prism( loop, band.x, band.y );
if ( !picked )
{
continue;
}
ArchFootprint.Bounds( loop, out var low, out var high );
ArchGhost.Brackets( low, high, band.y );
ArchGhost.Note( new Vector3( high.x, high.y, band.y ),
$"{cut.Name} — {high.x - low.x:0} x {high.y - low.y:0}, {segment.TopHeight - segment.BaseHeight:0} deep" );
}
}
// Contained, not RoomOnLevel: the fallback would read every real room as somebody else's.
ArchRoom HoveredRoom( ArchBuilding building )
{
return GroundPoint( out var point ) ? Contained( building, point ) : null;
}
// A road has no storey, so it draws on every level; centreline only until detailed - the rest is a full walk of the curve per frame.
void DrawRoad( ArchRoadPart road )
{
foreach ( var cut in road.Cuts )
{
if ( !LayerHidden( cut.Id ) )
{
DrawCut( cut, 0f );
}
}
ArchPreviews.Load().Draw( ArchKind.Road,
new ArchPreviewContext( road, null, null, 0f, Picked?.Targets( road ) == true, this ) );
}
// Only the live storey draws; the one below is DrawStoreyBelow's bare outline.
bool Shown( int floor ) => floor == Level;
// The eye is presentation, and so is the overlay: a hidden layer stops being drawn here too, or its
// outline is the only thing left of something you asked not to see.
bool Shown( int floor, int itemId ) => OverlayShows( floor, Level, itemId, LayerHidden );
public static bool OverlayShows( int floor, int level, int itemId, Func<int, bool> hidden )
{
return floor == level && !(hidden?.Invoke( itemId ) ?? false);
}
// Ruled past the footprint to leave room to start a wing, and reach for the hover that brings it up.
const int GridMargin = 3;
// An upper floor needs the grid at its own height, or it is placed against thin air.
// Grid is per-building, over the house's own footprint, while the cursor is over it - whole-plan would be thousands of lines a frame.
void DrawWorkPlane( ArchBuilding building )
{
if ( !WorkBounds( building, out var min, out var max ) || !Over( min, max ) )
{
return;
}
var grid = MathF.Max( 8f, Kit.GridSize );
var height = LevelHeight + ArchAsks.Lift( Plan, building, Kit );
Gizmo.Draw.LineThickness = 1f;
Gizmo.Draw.Color = ArchGhost.Line.WithAlpha( 0.12f );
for ( var x = min.x; x <= max.x + 0.01f; x += grid )
{
Gizmo.Draw.Line( new Vector3( x, min.y, height ), new Vector3( x, max.y, height ) );
}
for ( var y = min.y; y <= max.y + 0.01f; y += grid )
{
Gizmo.Draw.Line( new Vector3( min.x, y, height ), new Vector3( max.x, y, height ) );
}
Gizmo.Draw.LineThickness = 2f;
Gizmo.Draw.Color = ArchGhost.Line.WithAlpha( 0.4f );
DrawRect( min, max, height );
}
bool Over( Vector2 min, Vector2 max )
{
return GroundPoint( out var point )
&& point.x >= min.x && point.y >= min.y
&& point.x <= max.x && point.y <= max.y;
}
// Covers this storey AND the one below - upper-floor work sights against the footprint underneath. Room BOUNDS, so the courtyard between wings stays in the house.
bool WorkBounds( ArchBuilding building, out Vector2 min, out Vector2 max )
{
min = default;
max = default;
var low = new Vector2( float.MaxValue, float.MaxValue );
var high = new Vector2( float.MinValue, float.MinValue );
var found = false;
foreach ( var room in building.Rooms )
{
if ( room.Floor != Level && room.Floor != Level - 1 )
{
continue;
}
foreach ( var corner in Outline( room ) )
{
low = Vector2.Min( low, corner );
high = Vector2.Max( high, corner );
found = true;
}
}
if ( !found )
{
return false;
}
var grid = MathF.Max( 8f, Kit.GridSize );
var margin = grid * GridMargin;
min = new Vector2(
MathF.Floor( (low.x - margin) / grid ) * grid,
MathF.Floor( (low.y - margin) / grid ) * grid );
max = new Vector2(
MathF.Ceiling( (high.x + margin) / grid ) * grid,
MathF.Ceiling( (high.y + margin) / grid ) * grid );
return true;
}
// Only the edited room draws fittings; every other room on the storey is just its wall line.
bool Editing( ArchRoom room, ArchRoom inside )
{
return room == inside || room.Id == ActiveRoomId || Picked?.Targets( room ) == true || Picked?.Room == room;
}
void DrawRoom( ArchRoom room, ArchRoom inside, float lift, ArchBuilding building, ArchPreviews previews )
{
if ( !Shown( room.Floor, room.Id ) )
{
return;
}
var loop = Outline( room );
var floor = room.BaseHeight + lift;
// Hover and target are often the same room, so each needs its own cue.
if ( room == inside && loop.Count >= 3 )
{
Gizmo.Draw.LineThickness = 4f;
Gizmo.Draw.Color = ArchGhost.Line;
ArchGhost.Ring( loop, floor );
Gizmo.Draw.LineThickness = 2f;
}
if ( !Editing( room, inside ) )
{
Gizmo.Draw.Color = Color.White.WithAlpha( 0.3f );
DrawWalls( room, lift );
return;
}
var picked = Picked?.Targets( room ) == true;
// Picking a room in the dock must light it up, or you cannot tell which one you chose.
if ( loop.Count >= 3 && (picked || room.Id == ActiveRoomId) )
{
Gizmo.Draw.Color = picked ? Color.Yellow.WithAlpha( 0.85f ) : ArchGhost.Accent.WithAlpha( 0.45f );
ArchGhost.Prism( loop, floor, floor + ArchFloorGen.WallHeight( room, Kit ) );
}
foreach ( var wall in room.Walls )
{
if ( LayerHidden( wall.Id ) )
{
continue;
}
Gizmo.Draw.Color = Picked?.Targets( wall ) == true ? Color.Yellow : Color.Cyan;
Gizmo.Draw.Line(
new Vector3( wall.Start.x, wall.Start.y, room.BaseHeight + lift ),
new Vector3( wall.End.x, wall.End.y, room.BaseHeight + lift ) );
foreach ( var opening in wall.Openings )
{
if ( LayerHidden( opening.Id ) || LayerHidden( opening.OwnerId ) )
{
continue;
}
var left = wall.PointAt( opening.Left );
var right = wall.PointAt( opening.Right );
var z = room.BaseHeight + lift + opening.SillHeight;
Gizmo.Draw.Color = Picked?.Targets( opening ) == true ? Color.Yellow : Color.Green;
Gizmo.Draw.Line( new Vector3( left.x, left.y, z ), new Vector3( right.x, right.y, z ) );
}
}
foreach ( var pillar in room.Pillars )
{
if ( LayerHidden( pillar.Id ) )
{
continue;
}
previews.Draw( ArchKind.Pillar,
new ArchPreviewContext( pillar, room, building, lift, Picked?.Targets( pillar ) == true ) );
}
foreach ( var stair in room.Stairs )
{
if ( LayerHidden( stair.Id ) )
{
continue;
}
Gizmo.Draw.Color = Picked?.Targets( stair ) == true ? Color.Yellow : Color.Magenta.WithAlpha( 0.6f );
Gizmo.Draw.LineSphere( new Vector3( stair.Origin.x, stair.Origin.y, stair.BaseHeight + lift ), 10f );
previews.Draw( ArchKind.Stair,
new ArchPreviewContext( stair, room, building, lift, Picked?.Targets( stair ) == true, this ) );
}
foreach ( var trim in room.Trims )
{
if ( LayerHidden( trim.Id ) )
{
continue;
}
Gizmo.Draw.Color = Picked?.Targets( trim ) == true ? Color.Yellow : Color.Blue.WithAlpha( 0.7f );
for ( var index = 0; index < trim.Path.Count - 1; index++ )
{
Gizmo.Draw.Line( trim.Path[index] + Vector3.Up * lift, trim.Path[index + 1] + Vector3.Up * lift );
}
}
var kinds = ArchKinds.Load();
foreach ( var porch in room.Porches )
{
if ( LayerHidden( porch.Id ) )
{
continue;
}
Gizmo.Draw.Color = Picked?.Targets( porch ) == true ? Color.Yellow : ArchGhost.Line;
var manifest = kinds.Claiming( porch );
foreach ( var outline in manifest?.Loops( porch ) ?? Array.Empty<List<Vector2>>() )
{
ArchGhost.Ring( outline, manifest.Height( porch, porch.BaseHeight ) + lift );
}
}
}
void DrawWalls( ArchRoom room, float lift )
{
foreach ( var wall in room.Walls )
{
if ( LayerHidden( wall.Id ) )
{
continue;
}
Gizmo.Draw.Line(
new Vector3( wall.Start.x, wall.Start.y, room.BaseHeight + lift ),
new Vector3( wall.End.x, wall.End.y, room.BaseHeight + lift ) );
}
}
public static void DrawRect( Vector2 min, Vector2 max, float height )
{
DrawLoop( ArchFootprint.Rect( min, max ), height );
}
public static void DrawLoop( IReadOnlyList<Vector2> loop, float height )
{
for ( var index = 0; index < loop.Count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
Gizmo.Draw.Line( new Vector3( from.x, from.y, height ), new Vector3( to.x, to.y, height ) );
}
}
}