Editor/Tool/ArchEditGhost.cs
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.Mode switch {
ArchZoneMode.Damage => $"damage zone — {cut.Name}",
ArchZoneMode.Extrude => $"extrude 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 );
var shell = ArchHandles.Shell( building );
if ( shell is { Count: >= 3 } ) {
ArchGhost.Prism( shell, lift, head );
} else {
ArchGhost.Volume( min, max, lift, head );
}
// The ticks follow the ring the widgets edit, or a turned shell previews inside a world-axis box it does
// not touch - a second set of brackets standing off the building beside the one the overlay already drew.
ArchGhost.Brackets( shell is { Count: >= 3 } ? shell : ArchFootprint.Rect( 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;
}
}