Editor/Tool/ArchEditGhost.cs

Editor utility that draws preview "ghost" geometry for architecture editing tools. It chooses how to render rooms, buildings, groups, platforms, roofs, and cuts by calling ArchGhost and related systems to draw prisms, solids, plates, brackets and labels at calculated world heights.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Editing is placing. A widget drag re-runs the same resolution the placement drag ran, so it has
// to preview like one too - the wireframe of what is already standing says nothing about the shape
// you are pulling it into.
//
// Every height here is a WORLD height, through ArchSelection.Lift, the same answer the overlay and the
// handles take. Drawn at the plan's own numbers instead, the ghost stood a grade lift below the geometry
// it was previewing, so grabbing any widget on a building with a plinth looked like the shape dropping.
public static class ArchEditGhost
{
	public static void Draw( ArchTool tool, ArchSelection picked )
	{
		var lift = picked?.Lift( tool.Plan, tool.Kit ) ?? 0f;
		var kind = tool.LayerTree.Find( picked?.Item )?.Kind
			?? ArchKinds.Load().Claiming( picked?.Item )?.Kind
			?? default;

		if ( ArchPreviews.Load().Draw( kind,
			new ArchPreviewContext( picked?.Item, picked?.Room, picked?.Building, lift, true, tool ) ) )
		{
			return;
		}

		switch ( picked?.Item )
		{
			case ArchRoom room:
				Room( tool, room, lift );
				break;

			case ArchBuilding building:
				Blueprint( building, lift );
				break;

			case ArchSiteAssembly group:
				Group( tool, group );
				break;

			case ArchPlatformPart platform:
				Solid( platform.Outline(), platform.GradeHeight, ArchRamp.Deck( platform ),
					$"{(platform.Ramp ? "ramp" : "platform")} — {platform.Name}" );
				break;

			case ArchRoofPart roof:
				Solid( roof.Outline(), roof.BaseHeight + lift, roof.BaseHeight + lift + 4f, $"roof — {roof.Name}" );
				break;

			// Every leg, because a chain winds and the one being pulled is not always the one you drew.
			case ArchCutPart cut:
				foreach ( var leg in cut.Legs() )
				{
					var band = ArchCut.WorldBand( leg, lift );
					var label = cut.IsDamage
						? $"damage zone — {cut.Name}"
						: $"{(cut.Ramp ? "ramp cut" : "cut")} — {cut.Name}";

					Void( leg.Outline(), ArchCut.Floor( cut, leg ).Raised( lift ), band, label );
				}
				break;

		}
	}

	static void Room( ArchTool tool, ArchRoom room, float lift )
	{
		var loop = ArchFloorGen.Footprint( room );

		if ( loop.Count < 3 )
		{
			return;
		}

		var floor = room.BaseHeight + lift;
		var height = room.WallHeight > 0f ? room.WallHeight : tool.Kit.WallHeight;

		ArchGhost.Prism( loop, floor, floor + height );
		ArchGhost.Plate( Low( loop ), High( loop ), floor, 8 );
		Measure( loop, floor + height, $"room — {room.Name}" );
	}

	// The ghost stands on the same shell ring the widgets edit: a box around an L previews a rectangle
	// growing while the commit reshapes the L.
	static void Blueprint( ArchBuilding building, float lift )
	{
		if ( !ArchHandles.Bounds( building, out var min, out var max ) )
		{
			return;
		}

		var top = building.Rooms.Select( room => room.BaseHeight + room.WallHeight ).DefaultIfEmpty( 0f ).Max() + lift;
		var head = MathF.Max( top, lift + 8f );

		if ( ArchHandles.Shell( building ) is { Count: >= 3 } shell )
		{
			ArchGhost.Prism( shell, lift, head );
		}
		else
		{
			ArchGhost.Volume( min, max, lift, head );
		}

		ArchGhost.Brackets( min, max, head );
		ArchGhost.Note( new Vector3( max.x, max.y, head ), $"{building.Name} — {max.x - min.x:0} x {max.y - min.y:0}" );
	}

	// A group previews as the box it holds, so moving one reads as moving the whole scope.
	static void Group( ArchTool tool, ArchSiteAssembly group )
	{
		var loops = tool.Plan.Buildings
			.Where( building => group.Children.Contains( building.Id ) )
			.SelectMany( building => building.Rooms )
			.Concat( tool.Plan.AllRooms().Where( room => group.Children.Contains( room.Id ) ) )
			.Select( ArchFloorGen.Footprint )
			.Where( loop => loop.Count >= 3 )
			.ToList();

		if ( loops.Count == 0 )
		{
			return;
		}

		var min = loops.Select( loop => Low( loop ) ).Aggregate( Vector2.Min );
		var max = loops.Select( loop => High( loop ) ).Aggregate( Vector2.Max );

		ArchGhost.Brackets( min, max, tool.LevelHeight );
		ArchGhost.Plate( min, max, tool.LevelHeight, 16 );
		ArchGhost.Note( new Vector3( max.x, max.y, tool.LevelHeight ), $"{group.Name} — {group.Children.Count} layers" );
	}

	static void Solid( List<Vector2> loop, float bottom, float top, string note )
	{
		Solid( loop, bottom, ArchCarvePlane.Level( top ), note );
	}

	// A hole is drawn by the FLOOR it leaves, so a ramped bite previews as the wedge of material it takes rather
	// than the box its band describes - and the note still reads at the head, where the widget is.
	static void Void( List<Vector2> loop, ArchCarvePlane floor, Vector2 band, string note )
	{
		if ( loop.Count < 3 )
		{
			return;
		}

		ArchGhost.Solid( loop, floor, ArchCarvePlane.Level( band.y ) );
		Measure( loop, band.y, note );
	}

	// The top as a PLANE, so a ramp being dragged shows the wedge its generator carves rather than the box its
	// bounds describe - the frame still measures at the head, which is where the note can be read.
	static void Solid( List<Vector2> loop, float bottom, ArchCarvePlane top, string note )
	{
		if ( loop.Count < 3 )
		{
			return;
		}

		ArchGhost.Solid( loop, bottom, top );
		Measure( loop, loop.Max( corner => top.At( corner ) ), note );
	}

	static void Measure( IReadOnlyList<Vector2> loop, float height, string note )
	{
		ArchFootprint.Bounds( loop, out var min, out var max );

		ArchGhost.Brackets( min, max, height );
		ArchGhost.Note( new Vector3( max.x, max.y, height ), $"{note} — {max.x - min.x:0} x {max.y - min.y:0}" );
	}

	static Vector2 Low( IReadOnlyList<Vector2> loop )
	{
		ArchFootprint.Bounds( loop, out var min, out _ );

		return min;
	}

	static Vector2 High( IReadOnlyList<Vector2> loop )
	{
		ArchFootprint.Bounds( loop, out _, out var max );

		return max;
	}
}