Editor/Tool/Subtools/ArchSelectSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Select" ), Icon( "ads_click" ), Group( "01" )]
public sealed class ArchSelectSubtool( ArchTool owner ) : ArchSubtool( owner ) {
bool editing;
bool geometry;
IReadOnlyList<ArchFaceDetail> faces = Array.Empty<ArchFaceDetail>();
int faceIndex;
// Latched — scene view claims ctrl+click, so modifier alone isn't reliable
bool adding;
IReadOnlyList<ArchHit> stack = Array.Empty<ArchHit>();
int stackIndex;
bool stackOpen;
protected override bool UsesDrag => false;
protected override bool Keeps => true;
public override ArchViewAxis[] Works => new[] { ArchViewAxis.Top, ArchViewAxis.Front, ArchViewAxis.Side };
protected override string Title() => "Selection";
protected override string Advice() {
if ( Carrying ) {
return $"Carrying {carried.Name} — click where it should stand. R turns it a quarter; Cancel puts it back.";
}
if ( geometry ) {
return Owner.DebugFaces.Count == 1
? $"{Owner.DebugFaces[0].Label} — the stack has opened to it; Copy Debug Info hands it over."
: "Click a surface to pick the FACE under the cursor. The Plan Layers stack opens to it, and Copy Debug Info says what it collides with.";
}
if ( Owner.Picked is { Item: ArchBuilding } picked ) {
return $"{picked.Describe()} — R turns it a quarter; the turn ring swings it to any angle.";
}
return Owner.Picked?.Describe() ?? "Click an authored part, or Alt+click to list every part under the cursor. Repeat clicks cycle.";
}
ArchBuilding carried;
ArchAssetGhost carriedGhost;
int carriedQuarters;
public bool Carrying => carried is not null && carriedGhost is { Shapes.Count: > 0 };
public override bool Placing => false;
public void Carry( ArchBuilding building ) {
carried = building;
carriedGhost = ArchBuildingStamp.Ghost( building );
carriedQuarters = 0;
}
public override void OnUpdate() {
if ( Owner.CarryRequest is { } asked ) {
Owner.CarryRequest = null;
Carry( asked );
}
base.OnUpdate();
}
protected override ArchRunState Run() {
return Carrying ? new ArchRunState( true, $"Carrying {carried.Name}" ) : default;
}
protected override void FinishRun() => Carry( null );
protected override void CancelRun() => Carry( null );
public override bool Turns() {
if ( !Carrying ) {
return false;
}
carriedQuarters = (carriedQuarters + 1) % 4;
return true;
}
protected override void DrawHover( Vector2 point ) {
base.DrawHover( point );
if ( !Carrying ) {
return;
}
var map = Landing( point );
foreach ( var shape in carriedGhost.Shapes ) {
ArchGhost.Prism( shape.Loop.Select( corner => map.Of( corner ) ).ToList(), shape.Bottom, shape.Top );
}
var span = map.Swapped( carriedGhost.Max - carriedGhost.Min );
ArchGhost.Note( new Vector3( point.x + span.x * 0.5f, point.y + span.y * 0.5f, Owner.LevelHeight ),
$"{carried.Name} — {span.x:0} x {span.y:0}" );
}
ArchRemap Landing( Vector2 point ) => ArchBuildingStamp.Carried( carriedGhost, point, carriedQuarters * 90f );
void PutDown( Vector2 point ) {
var moved = carried;
var turn = carriedQuarters * 90f;
ArchCarry.Unit( moved, Landing( point ), Owner.Plan );
moved.Facing = (moved.Facing + turn) % 360f;
Carry( null );
Owner.Commit( $"Move {moved.Name}" );
}
protected override bool Adjusting() {
if ( Carrying ) {
return false;
}
if ( Owner.Picked is not { } picked ) {
editing = false;
return false;
}
using ( ArchGhost.Begin() ) {
if ( ArchHandles.Draw( Owner, picked ) ) {
editing = true;
}
if ( editing ) {
ArchEditGhost.Draw( Owner, picked );
}
}
if ( !editing ) {
return ArchShapeHandles.HandlePressed;
}
if ( !Gizmo.WasLeftMouseReleased ) {
return true;
}
editing = false;
if ( picked.Item is ArchStairPart stair ) {
ArchPiercers.Pierce( Owner.Plan, picked.Building, picked.Room, stair, Owner.Kit );
}
Owner.Commit( $"Edit {picked.Describe()}" );
return true;
}
protected override void OnClick( Vector2 point ) {
if ( Carrying ) {
PutDown( point );
return;
}
if ( Gizmo.Pressed.Any || editing ) {
return;
}
if ( Owner.InElevation ) {
stack = Array.Empty<ArchHit>();
stackOpen = false;
Owner.Select( PickInSection() );
return;
}
// Free pointer — no snap, pick what's actually under the cursor
var found = ArchPick.HitsAt( Owner.LayerTree, Owner.Kit, Owner.Level, Pointer.Free );
var repeat = stack.Count > 0 && found.Count == stack.Count
&& found.Select( hit => hit.Layer ).SequenceEqual( stack.Select( hit => hit.Layer ) );
stack = found;
stackOpen = global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt );
stackIndex = repeat ? (stackIndex + 1) % stack.Count : 0;
PickFromStack();
}
protected override bool TakesFreeClick => geometry;
protected override void OnFreeClick() {
var hits = Owner.FacesUnder( Gizmo.CurrentRay );
var adds = Adding;
if ( hits.Count == 0 ) {
faces = Array.Empty<ArchFaceDetail>();
if ( !adds ) {
Owner.SelectFaces( null );
}
Refresh();
return;
}
var repeat = faces.Count == hits.Count && faces.All( hits.Contains );
faces = hits;
// Adding picks the next unpicked face in the stack
if ( adds ) {
var next = hits.FirstOrDefault( face => !Owner.DebugFaces.Contains( face ) );
faceIndex = hits.IndexOf( next ?? hits[0] );
Take( next ?? hits[0], true );
return;
}
faceIndex = repeat ? (faceIndex + 1) % hits.Count : 0;
Take( hits[faceIndex], false );
}
// Application not Gizmo — Gizmo.Active is null outside a gizmo scope
static bool Adds => global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl )
|| global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift );
bool Adding => adding || Adds;
void Take( ArchFaceDetail face, bool adding ) {
var picked = Owner.DebugFaces.ToList();
if ( !adding ) {
picked.Clear();
picked.Add( face );
} else if ( !picked.Remove( face ) ) {
picked.Add( face );
}
Owner.SelectFaces( picked );
ArchTool.ShowGeometry = true;
ArchLayersDockPanel.Open();
Refresh();
}
void PickFromStack() {
if ( stack.Count == 0 ) {
Owner.Select( null );
return;
}
var hit = stack[Math.Min( stackIndex, stack.Count - 1 )];
var node = Owner.LayerTree.Find( hit.Layer.ItemId );
if ( node is null ) {
Owner.Select( null );
return;
}
Owner.Select( new ArchSelection { Item = node.Payload, Room = node.Room, Building = node.Building } );
}
ArchSelection PickInSection() {
if ( Owner.EditedBuilding() is not { } building || !Owner.Cursor( out var cursor ) ) {
return null;
}
var cut = Owner.Section( building );
if ( cut.At( cut.Plane.Across( cursor.Plan ), cursor.Height ) is not { } piece ) {
return null;
}
var room = building.Rooms.FirstOrDefault( candidate => candidate.Floor == piece.Floor
&& (ReferenceEquals( candidate, piece.Item )
|| candidate.Walls.Any( wall => ReferenceEquals( wall, piece.Item ) || wall.Openings.Any( opening => ReferenceEquals( opening, piece.Item ) ) )) );
return new ArchSelection { Item = piece.Item, Room = room, Building = building };
}
protected override void BuildOptions( ToolSidebarWidget panel ) {
ArchSidebarSection.Show( panel, Scope( "pick" ), "Pick", group => {
using ( var grid = ArchIconGrid.In( group ) ) {
grid.Pick( "Elements — the authored parts: buildings, rooms, walls, the things you drew", "select_element", "ads_click",
!geometry, () => { geometry = false; Owner.SelectFaces( null ); Refresh(); } );
grid.Pick( "Geometry — the built FACE under the cursor, for debugging a z-fight or a buried fitting", "select_face", "crop_square",
geometry, () => { geometry = true; Refresh(); } );
}
if ( geometry ) {
group.Add( ArchPartUi.Check( "Add each click to the pick (or hold Ctrl / Shift)", adding,
value => adding = value ) );
}
} );
if ( geometry ) {
BuildFaceOptions( panel );
return;
}
if ( stackOpen && stack.Count > 0 ) {
var group = panel.AddGroup( "Objects under cursor" );
for ( var index = 0; index < stack.Count; index++ ) {
var captured = index;
var hit = stack[index];
var node = Owner.LayerTree.Find( hit.Layer.ItemId );
var name = node is null ? hit.Layer.Kind.ToString() : Owner.LayerTree.Breadcrumb( node );
group.Add( new Button( $"{name} · {hit.Channel}" ) {
Clicked = () => {
stackIndex = captured;
PickFromStack();
Refresh();
}
} );
}
}
if ( Owner.Picked is not { Item: not null } picked ) {
return;
}
if ( Owner.LayerTree.Find( picked.Item ) is { } layer && Authoring( layer.Kind ) is { Adopts: true } authoring && authoring != this ) {
authoring.BuildAdopted( panel, picked, Refresh );
return;
}
ArchPartUi.Sheet( panel, Owner, picked, Refresh );
}
void BuildFaceOptions( ToolSidebarWidget panel ) {
if ( faces.Count > 0 ) {
var under = panel.AddGroup( "Under the cursor" );
for ( var index = 0; index < faces.Count; index++ ) {
var captured = index;
var face = faces[index];
var picked = Owner.DebugFaces.Contains( face );
under.Add( new Button( $"{(picked ? "✓ " : "")}{face.Piece.Split( '/' )[^1]} · {face.Role} · {face.Plane}" ) {
Clicked = () => {
faceIndex = captured;
Take( face, Adding );
}
} );
}
}
if ( Owner.DebugFaces.Count == 0 ) {
panel.AddGroup( "Face" ).Add( Wrapped( "Nothing picked. Click any built surface — ceilings and soffits included. Ctrl+click adds, clicking again drops." ) );
return;
}
var group = panel.AddGroup( $"{Owner.DebugFaces.Count} picked" );
foreach ( var face in Owner.DebugFaces.Take( 6 ) ) {
group.Add( Wrapped( $"{face.Piece.Split( '/' )[^1]} · {face.Label}" ) );
}
var fought = Owner.DebugFaces
.SelectMany( Owner.Fighting )
.Distinct()
.Where( face => !Owner.DebugFaces.Contains( face ) )
.ToList();
if ( fought.Count > 0 ) {
group.Add( new Button( $"Add the {fought.Count} face(s) it z-fights", "layers" ) {
Clicked = () => {
Owner.SelectFaces( Owner.DebugFaces.Concat( fought ).ToList() );
Refresh();
}
} );
}
group.Add( new Button.Primary( "Copy Briefing", "smart_toy" ) { Clicked = () => Log.Info( Owner.CopyFaceDebug( true ) ) } );
group.Add( new Button( "Copy Debug Info Plain", "content_copy" ) { Clicked = () => Log.Info( Owner.CopyFaceDebug() ) } );
}
}