Editor/Tool/ArchSelection.cs

Editor-side selection helper for the architecture tool. It stores the selected Item plus its Room and Building, computes lift/standing, describes the selection as a string, enumerates relevant walls, and removes the selected item from an ArchPlan by dispatching removal logic for many architecture part types.

Reflection
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// Kept out of ArchTool so the tool file stays about the tool.
public sealed class ArchSelection
{
	public object Item { get; init; }
	public ArchRoom Room { get; init; }
	public ArchBuilding Building { get; init; }

	public bool Targets( object candidate ) => ReferenceEquals( Item, candidate );

	public ArchSelection Following( object replacement )
	{
		return new ArchSelection
		{
			Item = replacement,
			Room = Room,
			Building = Building
		};
	}

	// How high off the plan the picked thing actually STANDS. Everything under a building rides that building's
	// grade lift, so the overlay, the handles and the edit ghost all ask HERE or they draw one shape at three
	// heights - and a ghost a whole plinth below its own geometry read as the shape dropping the instant any
	// widget was grabbed. A platform is the exception the generator itself makes: it is drawn back down by the
	// same lift, because a plinth is poured on the ground and not on the pad the building stands on.
	public float Lift( ArchPlan plan, ArchKit kit )
	{
		if ( plan is null || Item is ArchPlatformPart )
		{
			return 0f;
		}

		return Standing( plan ) is { } building ? ArchAsks.Lift( plan, building, kit ) : 0f;
	}

	ArchBuilding Standing( ArchPlan plan ) => Item switch
	{
		ArchBuilding own => own,
		ArchCutPart cut => plan.OwnerOf( cut ),
		ArchRoom room => plan.OwnerOf( room ),
		_ => Building ?? (Room is null ? null : plan.OwnerOf( Room ))
	};

	public ArchPalette Palette => (Item as IArchPainted)?.Palette;

	public string Describe() => Item switch
	{
		ArchBuilding building => $"Building {building.Name}",
		ArchWall wall => $"Wall {wall.Id}",
		ArchOpening opening => $"{opening.Preset} {opening.Id}",
		ArchWallModPart modifier => $"{modifier.Kind} {modifier.Name}",
		ArchRoofPart roof => $"Roof {roof.Name}",
		ArchStairPart stair => $"Stair {stair.Name}",
		ArchTrimPart trim => $"Trim {trim.Name}",
		ArchPillarPart pillar => $"Pillars {pillar.Name}",
		ArchBeamPart beam => $"{beam.Members} {beam.Name}",
		ArchDownpipePart pipe => $"Downpipe {pipe.Name}",
		ArchBalconyPart balcony => $"Balcony {balcony.Name}",
		ArchExteriorStairPart flight => $"Exterior Stair {flight.Name}",
		ArchPorchPart porch => $"Porch {porch.Name}",
		ArchFencePart fence => $"Fence {fence.Name}",
		ArchApproachPart approach => approach.IsSpline ? $"Spline Driveway {approach.Name}" : $"{approach.Kind} {approach.Name}",
		ArchRoadPart road => $"Road {road.Name}",
		ArchRoadCrossing crossing => $"{crossing.Kind} {crossing.Name}",
		ArchBridgePart bridge => $"Bridge {bridge.Name}",
		ArchTunnelPart tunnel => $"Tunnel {tunnel.Name}",
		ArchRoom room => $"Room {room.Name}",
		ArchCutPart cut => cut.IsDamage ? $"{cut.ResolvedDamage} {cut.Name}" : $"{cut.Profile} {cut.Name}",
		ArchPlatformPart platform => $"{(platform.Ramp ? "Ramp" : "Platform")} {platform.Name}",
		ArchSiteAssembly assembly => $"{assembly.Kind} {assembly.Name}",
		_ => "Nothing"
	};

	// The room's own walls where there is one, and every wall in the plan where there is not - an opening or a sign
	// on a parapet has no room to be looked up through.
	IEnumerable<ArchWall> Walls( ArchPlan plan )
	{
		if ( Room is not null )
		{
			return Room.Walls;
		}

		return plan?.AllWalls() ?? Enumerable.Empty<ArchWall>();
	}

	// A porch is a host, so a flight, a column or a run may be filed on its deck instead of in the room.
	void Unfile<T>( ArchPlan plan, Func<ArchPorchPart, List<T>> held, T part )
	{
		var porches = Room is not null
			? Room.Porches
			: plan?.AllRooms().SelectMany( room => room.Porches ) ?? Enumerable.Empty<ArchPorchPart>();

		foreach ( var porch in porches )
		{
			if ( held( porch ).Remove( part ) )
			{
				return;
			}
		}
	}

	public void RemoveFrom( ArchPlan plan )
	{
		ArchAffectors.Close( plan, Item );

		switch ( Item )
		{
			// Room first, then the decks: a wall is filed on whichever it stands on, and only one holds it.
			case ArchWall wall:
				Room?.Walls.Remove( wall );
				plan.HostOf<ArchRoofPart>( wall )?.Walls.Remove( wall );
				break;

			case ArchOpening opening:
				foreach ( var wall in Walls( plan ) )
				{
					wall.Openings.Remove( opening );
				}
				break;

			case ArchWallModPart modifier:
				foreach ( var wall in Walls( plan ) )
				{
					wall.Modifiers.Remove( modifier );
				}
				break;

			case ArchRoofPart roof:
				Building?.Roofs.Remove( roof );
				break;

			// Plan-wide, through the affector's own close: a flight walking through a party wall opened
			// the slabs and the wall of the building it arrived in, and those holes are only ever its.
			case ArchStairPart stair:
				plan.Unfile( stair );
				break;

			case ArchTrimPart trim:
				if ( Room?.Trims.Remove( trim ) != true )
				{
					Unfile( plan, porch => porch.Trims, trim );
				}
				break;

			case ArchPillarPart pillar:
				if ( Room?.Pillars.Remove( pillar ) != true )
				{
					Unfile( plan, porch => porch.Pillars, pillar );
				}
				break;

			case ArchBeamPart beam:
				(Room ?? plan.RoomStanding( beam ))?.Beams.Remove( beam );
				break;

			case ArchSpanPart span:
				(Room ?? plan.RoomStanding( span ))?.PierSpans.Remove( span );
				break;

			case ArchApproachPart approach:
				Room?.Approaches.Remove( approach );
				break;

			case ArchDownpipePart pipe:
				Building?.Downpipes.Remove( pipe );
				break;

			case ArchPipePart run:
				Building?.Pipes.Remove( run );
				break;

			case ArchPipeBracketPart bracket:
				Building?.Brackets.Remove( bracket );
				break;

			// Plan-wide, through the affector's own close: a loggia's bite and a flight's archways are only
			// ever theirs, and the sweep only collects what an owner has actually left behind.
			case ArchBalconyPart balcony:
				Building?.Balconies.Remove( balcony );
				ArchBalconies.Remove( plan, Building, balcony );
				break;

			case ArchExteriorStairPart flight:
				plan.Unfile( flight );
				break;

			case ArchPorchPart porch:
				plan.Unfile( porch );
				break;

			case ArchFencePart fence:
				Building?.Fences.Remove( fence );
				break;

			case ArchRoadPart road:
				plan.Units.Remove( road );
				break;

			case ArchRoadCrossing crossing:
				foreach ( var road in plan.Roads() )
				{
					road.Crossings.Remove( crossing );
				}
				break;

			case ArchBridgePart bridge:
				foreach ( var road in plan.Roads() )
				{
					road.Bridges.Remove( bridge );
				}
				break;

			case ArchTunnelPart tunnel:
				foreach ( var road in plan.Roads() )
				{
					road.Tunnels.Remove( tunnel );
				}
				break;

			case ArchRoom room when room.Spans:
				Building?.Rooms.Remove( room );
				break;

			case ArchRoom room:
				Building?.Rooms.Remove( room );
				break;

			case ArchBuilding building:
				plan.Units.Remove( building );
				break;

			// The carved slabs come back whole on their own, being derived - the walls it opened do not,
			// so those close through the affector's own close.
			case ArchCutPart cut:
				// Whatever holds it, building or road - a cut deleted out of the wrong list stays in the plan.
				plan?.CutsHolding( cut )?.Remove( cut );

				if ( plan is not null )
				{
					ArchCutAffector.Remove( plan, cut );
				}
				break;

			case ArchPlatformPart platform:
				Building?.Platforms.Remove( platform );
				break;

			// Dissolving a group takes the scope, never its members - they keep their own homes.
			case ArchSiteAssembly assembly:
				ArchLayerGroups.Dissolve( plan, assembly );
				break;
		}
	}
}