Editor/Tool/ArchSubtool.cs
using System;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public abstract class ArchSubtool : EditorTool {
ToolSidebarWidget sidebar;
protected ArchSubtool( ArchTool owner ) {
Owner = owner;
}
protected ArchTool Owner { get; }
static readonly ArchViewAxis[] PlanOnly = { ArchViewAxis.Top };
ArchViewAxis served = (ArchViewAxis)(-1);
protected bool Dragging { get; private set; }
protected Vector2 DragStart { get; private set; }
protected Vector2 DragCurrent { get; private set; }
protected float DragStartHeight { get; private set; }
protected float DragHeight { get; private set; }
protected ArchRoofPart DragDeck { get; private set; }
protected ArchCursor Pointer { get; private set; }
protected virtual bool UsesDrag => true;
protected virtual bool Snapped => ArchGridService.Snapping;
public virtual ArchSnapReach SnapReach( float authored ) => ArchWallSnap.Reach( authored );
public virtual ArchViewAxis[] Works => PlanOnly;
public bool Serves( ArchViewAxis axis ) => Works.Contains( axis );
public virtual bool Placing => Manager?.IsCurrentViewFocused == true;
public virtual ArchSurface[] Surfaces => Array.Empty<ArchSurface>();
public string Icon => ArchIcons.Get( ArchIcons.SubtoolSlug( this ), EditorTypeLibrary.GetType( GetType() )?.Icon ?? "category" );
bool Gesturing => Dragging || Run().Active;
public override void OnUpdate() {
activeDraft?.Show( Gesturing );
if ( Manager?.IsCurrentViewFocused != true ) {
return;
}
var axis = Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis;
if ( axis != served ) {
served = axis;
Refresh();
}
if ( Serves( axis ) && Adjusting() ) {
return;
}
if ( TakesFreeClick ) {
using ( ArchGhost.Begin() ) {
DrawFreeHover();
}
if ( Gizmo.WasLeftMousePressed ) {
OnFreeClick();
}
return;
}
if ( !Serves( axis ) || !Owner.Cursor( out var cursor ) ) {
Dragging &= !Gizmo.WasLeftMouseReleased;
if ( !Dragging ) {
Owner.StepOff();
}
return;
}
var point = Snapped ? cursor.Plan : cursor.Free;
Pointer = cursor;
DragCurrent = point;
DragHeight = cursor.Height;
if ( !UsesDrag ) {
using ( ArchGhost.Begin() ) {
DrawHover( point );
}
if ( Gizmo.WasLeftMousePressed ) {
Owner.EnsureTarget();
OnClick( point );
}
return;
}
if ( Gizmo.WasLeftMousePressed ) {
DragStart = point;
DragStartHeight = cursor.Height;
DragDeck = cursor.OnDeck;
Dragging = true;
// Lock the work plane to where the gesture began.
Owner.StandOn( cursor.Height, cursor.OnDeck );
return;
}
using ( ArchGhost.Begin() ) {
if ( Dragging ) {
DrawPreview();
} else {
DrawHover( point );
}
}
if ( !Gizmo.WasLeftMouseReleased || !Dragging ) {
return;
}
Dragging = false;
drawn = null;
Owner.StepOff();
Owner.EnsureTarget();
OnDrag( DragStart, point );
}
protected virtual bool Adjusts => false;
protected virtual bool Adjusting() => Adjust();
bool adjusting;
object drawn;
// Suppresses handle display on the just-placed shape until another gesture completes.
protected void Drew( object placed ) => drawn = placed;
internal void Forget() => drawn = null;
bool Adjust() {
// Reset on mouse-up (spare the release frame so the commit below fires).
if ( !Gizmo.IsLeftMouseDown && !Gizmo.WasLeftMouseReleased ) {
adjusting = false;
}
if ( !Adjusts || Owner.Picked is not { } picked || !ArchShapeHandles.ShowsHandles( picked.Item, drawn ) ) {
return false;
}
using ( ArchGhost.Begin() ) {
if ( ArchHandles.Draw( Owner, picked ) ) {
adjusting = true;
}
if ( adjusting ) {
ArchEditGhost.Draw( Owner, picked );
}
}
if ( !ArchShapeHandles.CapturesPlacement( ArchShapeHandles.HandlePressed, adjusting ) ) {
return false;
}
if ( Gizmo.WasLeftMouseReleased ) {
adjusting = false;
Owner.CommitEdited( $"Edit {picked.Describe()}", picked );
}
return true;
}
protected virtual void OnDrag( Vector2 from, Vector2 to ) { }
protected virtual void OnClick( Vector2 point ) { }
protected virtual bool TakesFreeClick => false;
protected virtual void OnFreeClick() { }
protected virtual void DrawFreeHover() { }
protected virtual void DrawHover( Vector2 point ) {
ArchGhost.Cursor( point, DragHeight, Owner.Kit.GridSize );
}
protected virtual void DrawPreview() {
ArchGhost.Cursor( DragCurrent, DragHeight, Owner.Kit.GridSize );
Gizmo.Draw.Line(
new Vector3( DragStart.x, DragStart.y, DragStartHeight ),
new Vector3( DragCurrent.x, DragCurrent.y, DragHeight ) );
}
protected void DrawRectPreview() {
ArchGhost.Plate( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), Owner.LevelHeight );
}
protected static Vector2 Min( Vector2 a, Vector2 b ) => new( MathF.Min( a.x, b.x ), MathF.Min( a.y, b.y ) );
protected static Vector2 Max( Vector2 a, Vector2 b ) => new( MathF.Max( a.x, b.x ), MathF.Max( a.y, b.y ) );
public override Widget CreateToolSidebar() {
lent = null;
sidebar = new ToolSidebarWidget();
Populate();
return sidebar;
}
public override Widget CreateToolFooter() => new ArchRunBar( Run, FinishRun, CancelRun );
protected virtual ArchRunState Run() => default;
protected virtual void FinishRun() { }
protected virtual void CancelRun() { }
protected virtual string Title() => "Options";
protected virtual string Shortcut() => null;
protected string Scope( string key ) => ArchSidebarState.Scope( this, key );
protected virtual ArchKind? DraftKind => null;
ArchDraft activeDraft;
protected ArchLayerRef? PlacementParent() {
if ( Owner.InsertionTarget is { } target ) {
return target;
}
return Owner.LayerTree.Find( Owner.ActiveBuildingId )?.Ref;
}
public override void OnEnabled() {
base.OnEnabled();
LetGo();
BeginDraft();
}
// Deselect anything this tool can't author, so its handles don't capture placement drags.
void LetGo() {
if ( Keeps || Owner.Picked?.Item is not { } item || Hosts( item ) ) {
return;
}
Owner.Picked = null;
Forget();
}
protected virtual bool Keeps => false;
protected void BeginDraft() {
if ( DraftKind is { } kind ) {
activeDraft ??= Owner.BeginPlacement( kind, PlacementParent(), null, null, this );
}
}
public override void OnDisabled() {
Owner.StepOff();
CancelDraft();
base.OnDisabled();
}
protected void CancelDraft() {
if ( activeDraft is not null ) {
Owner.CancelPlacement( activeDraft );
activeDraft = null;
}
}
protected void FinishDraft() => FinishDraft( 0 );
protected void FinishDraft( int itemId ) {
if ( activeDraft is not null ) {
Owner.FinishPlacement( activeDraft, itemId );
activeDraft = null;
}
}
protected virtual string Advice() => null;
protected virtual void BuildOptions( ToolSidebarWidget panel ) { }
public virtual bool Turns() => false;
public virtual bool Adopts => false;
public virtual bool Hosts( object payload ) {
if ( !Adopts || payload is null ) {
return false;
}
return ArchKinds.Load().Claiming( payload )?.Kind is { } claimed && ReferenceEquals( Authoring( claimed ), this );
}
public void BuildAdopted( ToolSidebarWidget panel, ArchSelection picked, Action refresh ) {
lent = refresh;
Adopt( picked );
BuildOptions( panel );
}
Action lent;
protected bool Lent => lent is not null;
protected virtual void Adopt( ArchSelection picked ) { }
public ArchSubtool Authoring( ArchKind kind ) {
var wanted = ArchKindsAsked.Subtool( kind );
if ( wanted.Length == 0 ) {
return null;
}
return Owner.Tools.OfType<ArchSubtool>().FirstOrDefault( tool => tool.GetType().Name == wanted );
}
protected void Refresh() {
if ( lent is not null ) {
lent();
return;
}
if ( !sidebar.IsValid() ) {
return;
}
sidebar.Layout.Clear( true );
Populate();
}
public void RefreshSidebar() => Refresh();
void Populate() {
var serves = Serves( Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis );
ArchSidebarLayout.Header( sidebar, Title(), Icon, Shortcut(), serves ? Advice() : Refusal(), Guide() );
if ( !serves ) {
sidebar.Layout.AddStretchCell();
return;
}
ArchSidebarLayout.Editing( sidebar, EditedLayer(), Edits() ? "New" : "Clear", StopEditing );
BuildOptions( sidebar );
sidebar.Layout.AddStretchCell();
}
bool Edits() => Owner.Picked?.Item is { } item && Hosts( item );
string EditedLayer() {
if ( Lent || Owner.Picked?.Item is not { } item ) {
return null;
}
var name = item is IArchNamed { Name.Length: > 0 } named
? named.Name
: ArchKinds.Load().Claiming( item )?.Label;
if ( string.IsNullOrWhiteSpace( name ) ) {
return null;
}
return Edits() ? $"Editing {name}" : $"Selected {name}";
}
void StopEditing() {
Owner.Picked = null;
Forget();
Refresh();
}
Action Guide() {
if ( ArchGuides.For( this ) is not { } written ) {
return null;
}
return () => ArchGuideWindow.Open( written );
}
string Refusal() {
var views = string.Join( ", ", Works.Select( axis => axis == ArchViewAxis.Top ? "the plan" : $"the {axis} elevation" ) );
return $"Nothing to place from here — this tool works in {views}.";
}
protected static Widget Wrapped( string text ) => ArchPartUi.Wrapped( text );
}
public static class ArchPaletteUi {
public static Widget Row( Widget parent, ArchPalette palette, ArchSurface surface, Action changed ) {
var holder = new Widget( parent );
holder.Layout = Layout.Row();
holder.Layout.Spacing = 4;
var label = new Label( surface.ToString() );
label.MinimumWidth = 92;
holder.Layout.Add( label );
palette.TryGet( surface, out var path );
var value = new Label( string.IsNullOrWhiteSpace( path ) ? "(inherited)" : System.IO.Path.GetFileNameWithoutExtension( path ) );
value.MinimumWidth = 110;
holder.Layout.Add( value );
holder.Layout.Add( new Button( "", "image" ) {
Clicked = () => {
var picker = AssetPicker.Create( parent, AssetType.Material );
picker.Window.Title = $"Material for {surface}";
picker.OnAssetPicked = assets => {
var asset = assets.FirstOrDefault();
if ( asset is null ) return;
palette.Set( surface, asset.Path );
ArchStyle.InvalidateCache();
changed?.Invoke();
};
picker.Show();
}
} );
var scale = palette.ScaleFor( surface );
var density = new LineEdit( scale > 0f ? scale.ToString( "0.####" ) : "" ) {
PlaceholderText = ArchMesh.TexelScale.ToString( "0.####" ),
MaximumWidth = 64,
ToolTip = "Units per texel for this role. Blank inherits."
};
density.TextEdited += text => {
palette.SetScale( surface, float.TryParse( text, out var parsed ) ? parsed : 0f );
ArchStyle.InvalidateCache();
changed?.Invoke();
};
holder.Layout.Add( density );
holder.Layout.Add( new Button( "", "close" ) {
Clicked = () => {
palette.Set( surface, null );
density.Text = "";
ArchStyle.InvalidateCache();
changed?.Invoke();
}
} );
holder.Layout.AddStretchCell();
return holder;
}
}