Editor/Tool/ArchToolOverlay.cs

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.

File Access
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 ) );
		}
	}
}