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.
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;
}
}
}