Editor/Tool/ArchLayersDockPanel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using static Editor.BaseItemWidget;
namespace Sunless.Architecture;
[Dock( "Editor", "Plan Layers", "layers" )]
public sealed class ArchLayersDockPanel : Widget {
public const string DockName = "Plan Layers";
readonly ArchLayerStack stack;
readonly Layout header;
readonly Label breadcrumb;
readonly List<(IArchDockToggle Toggle, IconButton Button)> contributed = new();
ArchLayerTree projected = new();
HashSet<int> built = new();
bool pickedInStack;
bool trackedActive;
bool trackedGeometry;
ArchPlan trackedPlan;
int trackedRevision;
int trackedDrafts;
int trackedAssemblies;
object trackedPicked;
public ArchLayersDockPanel( Widget parent ) : base( parent ) {
Layout = Layout.Column();
Layout.Margin = 4;
Layout.Spacing = 4;
header = Layout.AddRow();
header.Spacing = 2;
breadcrumb = new Label( "Nothing picked" );
breadcrumb.WordWrap = true;
breadcrumb.MinimumHeight = 16;
Layout.Add( breadcrumb );
stack = new ArchLayerStack( this );
stack.ItemClicked += OnItemClicked;
Layout.Add( stack, 1 );
BuildHeader();
}
public static void Open() {
EditorWindow.DockManager.SetDockState( DockName, true );
EditorWindow.DockManager.RaiseDock( DockName );
}
public ArchLayerTree Projected => projected;
[EditorEvent.Frame]
void OnFrame() {
var tool = ArchTool.Active;
var plan = tool?.Plan;
var picked = tool?.Picked?.Item;
var drafts = tool?.Drafts.Count( draft => draft.Standing ) ?? 0;
var assemblies = plan?.Assemblies.Count ?? 0;
var planChanged = (tool is not null) != trackedActive
|| ArchTool.ShowGeometry != trackedGeometry
|| !ReferenceEquals( plan, trackedPlan )
|| (tool?.Revision ?? 0) != trackedRevision
|| drafts != trackedDrafts
|| assemblies != trackedAssemblies;
trackedActive = tool is not null;
trackedGeometry = ArchTool.ShowGeometry;
trackedPlan = plan;
trackedRevision = tool?.Revision ?? 0;
trackedDrafts = drafts;
trackedAssemblies = assemblies;
if ( planChanged ) {
Rebuild();
}
SyncContributed();
if ( tool?.Flashing == true ) {
stack.Update();
}
var inStack = pickedInStack;
pickedInStack = false;
SyncFaces( tool, inStack );
if ( ReferenceEquals( picked, trackedPicked ) ) {
return;
}
trackedPicked = picked;
if ( inStack ) {
UpdateBreadcrumb( projected.Find( picked ) );
return;
}
SyncSelection();
}
// The tool owns the pick — the stack writes it only on a stack click, never reads it back.
void SyncFaces( ArchTool tool, bool inStack ) {
if ( tool is null ) {
return;
}
if ( inStack ) {
tool.SelectFaces( Wanted() );
return;
}
if ( Same( tool.DebugFaces, Wanted() ) ) {
return;
}
Reveal( tool );
}
List<ArchFaceDetail> Wanted() {
var picked = stack.SelectedItems.ToList();
return picked
.OfType<ArchFacePiece>()
.SelectMany( piece => piece.Faces )
.Concat( picked.OfType<ArchFaceGroup>().SelectMany( group => group.Every ) )
.Concat( picked.OfType<ArchFaceDetail>() )
.Distinct()
.ToList();
}
static bool Same( IReadOnlyList<ArchFaceDetail> left, IReadOnlyList<ArchFaceDetail> right ) {
return left.Count == right.Count && left.All( right.Contains );
}
bool Reveal( ArchTool tool ) {
if ( !ArchTool.ShowGeometry ) {
return false;
}
if ( tool.DebugFaces.Count == 0 ) {
stack.UnselectAll();
return false;
}
foreach ( var face in tool.DebugFaces ) {
if ( projected.Find( face.LayerId ) is not { } node ) {
continue;
}
for ( var ancestor = node; ancestor is not null; ancestor = ancestor.Parent ) {
stack.Open( projected.StableKey( ancestor ) );
}
stack.Open( (face.LayerId, "geometry") );
foreach ( var piece in tool.BuiltFaces( face.LayerId ).Where( piece => piece.Faces.Contains( face ) ) ) {
stack.Open( piece );
foreach ( var group in Holding( piece.Groups, face ) ) {
stack.Open( group );
}
}
}
stack.SelectItems( tool.DebugFaces, false );
stack.ScrollTo( tool.DebugFaces[^1] );
return true;
}
static IEnumerable<ArchFaceGroup> Holding( IEnumerable<ArchFaceGroup> groups, ArchFaceDetail face ) {
foreach ( var group in groups.Where( group => group.Holds( face ) ) ) {
yield return group;
foreach ( var child in Holding( group.Children, face ) ) {
yield return child;
}
}
}
void BuildHeader() {
header.Clear( true );
header.Add( Tool( "add", "Add a layer inside the insertion target", AddMenu ) );
header.Add( Tool( "create_new_folder", "Group the selected layer - a group is the scope an operation inside it may reach", GroupPicked ) );
header.Add( Tool( "folder_off", "Dissolve the selected group; its members keep their own homes", UngroupPicked ) );
header.Add( Tool( "visibility", "Show every layer again", () => ArchTool.Active?.ShowEveryLayer() ) );
var isolate = new IconButton( "filter_center_focus" ) {
ToolTip = "Fold away the groups the current edit is not happening inside",
IconSize = 15,
FixedSize = 22,
IsToggle = true,
IsActive = ArchTool.IsolateGroups
};
isolate.OnToggled = value => {
ArchTool.IsolateGroups = value;
stack.Update();
};
header.Add( isolate );
var geometry = new IconButton( "widgets" ) {
ToolTip = "Show what each layer BUILT - its pieces and their faces, for picking one and copying its debug info",
IconSize = 15,
FixedSize = 22,
IsToggle = true,
IsActive = ArchTool.ShowGeometry
};
geometry.OnToggled = value => {
ArchTool.ShowGeometry = value;
if ( !value ) {
ArchTool.Active?.SelectFaces( null );
}
Rebuild();
};
header.Add( geometry );
contributed.Clear();
foreach ( var toggle in ArchDockToggles.Contributed() ) {
var button = Contributed( toggle );
contributed.Add( (toggle, button) );
header.Add( button );
}
header.AddStretchCell();
}
static IconButton Tool( string icon, string tooltip, Action clicked ) {
var button = new IconButton( icon ) { ToolTip = tooltip, IconSize = 15, FixedSize = 22 };
button.OnClick = () => clicked();
return button;
}
void SyncContributed() {
var standing = ArchDockToggles.Contributed();
if ( standing.Length != contributed.Count ) {
BuildHeader();
return;
}
foreach ( var (toggle, button) in contributed ) {
var held = ArchDockToggles.Held( toggle );
if ( button.IsActive == held ) {
continue;
}
button.IsActive = held;
button.Update();
}
}
static IconButton Contributed( IArchDockToggle toggle ) {
var button = new IconButton( toggle.Icon ) {
ToolTip = toggle.ToolTip,
IconSize = 15,
FixedSize = 22,
IsToggle = true,
IsActive = ArchDockToggles.Held( toggle )
};
button.OnToggled = value => ArchDockToggles.Press( toggle, value );
return button;
}
void Rebuild() {
projected = ArchTool.Active?.LayerTree ?? new ArchLayerTree();
built = ArchTool.Active is { } tool ? tool.BuiltMeshes.Keys.ToHashSet() : new HashSet<int>();
stack.UnselectAll();
stack.Clear();
foreach ( var group in projected.Domains ) {
var domain = new ArchDomainRow( group.Name, group.Domain );
stack.AddItem( domain );
foreach ( var child in group.Children ) {
domain.AddItem( RowFor( child ) );
}
foreach ( var draft in RootDrafts( group.Domain ) ) {
domain.AddItem( new ArchDraftRow( draft ) );
}
}
trackedPicked = null;
}
ArchLayerRow RowFor( ArchLayerNode node ) {
var row = new ArchLayerRow( node, projected.StableKey( node ) );
foreach ( var child in node.Children ) {
row.AddItem( RowFor( child ) );
}
foreach ( var draft in DraftsUnder( node.Ref?.ItemId ?? 0 ) ) {
row.AddItem( new ArchDraftRow( draft ) );
}
foreach ( var piece in ArchTool.Active?.PiecesOf( node.Ref?.ItemId ?? 0 ) ?? Array.Empty<ArchBuiltPiece>() ) {
row.AddItem( new ArchPieceRow( piece ) );
}
if ( ArchTool.ShowGeometry && node.Ref is { } layer && built.Contains( layer.ItemId ) ) {
row.AddItem( new ArchGeometryHeadRow( layer.ItemId ) );
}
return row;
}
static IEnumerable<ArchDraft> DraftsUnder( int parentId ) {
return ArchTool.Active?.Drafts
.Where( draft => draft.Active && draft.Standing && (draft.Parent?.ItemId ?? 0) == parentId )
?? Enumerable.Empty<ArchDraft>();
}
static IEnumerable<ArchDraft> RootDrafts( ArchLayerDomain domain ) {
var kinds = ArchKinds.Load();
return ArchTool.Active?.Drafts
.Where( draft => draft.Active && draft.Standing
&& (draft.Parent is null || draft.Parent.Value.ItemId == 0)
&& kinds.Domain( draft.Kind ) == domain )
?? Enumerable.Empty<ArchDraft>();
}
void OnItemClicked( object obj ) {
pickedInStack = true;
var modifiers = Editor.Application.KeyboardModifiers;
var extending = modifiers.HasFlag( KeyboardModifiers.Shift ) || modifiers.HasFlag( KeyboardModifiers.Ctrl );
if ( !extending && obj is not (ArchGeometryFaceRow or ArchGeometryGroupRow or ArchGeometryPieceRow) ) {
stack.UnselectAll();
ArchTool.Active?.SelectFaces( null );
}
if ( obj is ArchLayerRow row ) {
ApplySelection( row.Node );
}
}
void ApplySelection( ArchLayerNode node ) {
if ( ArchTool.Active is not { } tool ) {
return;
}
if ( node.Payload is ArchLayerReference reference ) {
if ( projected.Find( reference.TargetId ) is { } target ) {
tool.SelectLayer( target );
}
return;
}
if ( node.Payload is null ) {
if ( node.Kind == ArchKind.Story && node.Building is { } storyBuilding ) {
tool.Level = node.Floor;
tool.ActiveBuildingId = storyBuilding.Id;
}
return;
}
tool.SelectLayer( node );
}
void SyncSelection() {
var node = projected.Find( ArchTool.Active?.Picked?.Item );
UpdateBreadcrumb( node );
if ( ArchTool.Active?.DebugFaces.Count > 0 ) {
return;
}
stack.UnselectAll();
if ( node is not null ) {
stack.SelectItem( projected.StableKey( node ) );
}
}
void UpdateBreadcrumb( ArchLayerNode selected ) {
var tool = ArchTool.Active;
if ( tool is null ) {
breadcrumb.Text = "No Architecture tool active";
return;
}
var target = tool.InsertionTarget is { } insertion && projected.Find( insertion.ItemId ) is { } targetNode
? targetNode.DisplayName
: "—";
breadcrumb.Text = selected is null
? $"Nothing picked · into {target}"
: $"{projected.Breadcrumb( selected )} · into {target}";
}
void AddMenu() {
if ( ArchTool.Active is not { } tool ) {
return;
}
var kinds = ArchKinds.Load();
var parent = tool.InsertionTarget ?? tool.SelectedLayer;
var menu = new ContextMenu( this );
foreach ( var kind in Offered( kinds, parent ) ) {
var captured = kind;
menu.AddOption( kinds.Label( captured ), kinds.Glyph( captured ),
() => tool.BeginChild( parent ?? default, captured ) );
}
menu.OpenAtCursor();
}
static IReadOnlyList<ArchKind> Offered( ArchKinds kinds, ArchLayerRef? parent ) {
var offered = parent is { } layer
? ArchLayerRules.ValidChildren( layer.Kind )
: kinds.RootKinds();
return offered.Where( kind => kinds.Subtool( kind ).Length > 0 ).ToList();
}
void GroupPicked() {
if ( ArchTool.Active is not { } tool ) {
return;
}
var members = stack.SelectedItems
.Select( item => projected.Find( item ) )
.Where( node => node?.Ref is not null )
.Select( node => node.Ref.Value.ItemId )
.ToList();
if ( members.Count == 0 && tool.SelectedLayer is { } layer ) {
members.Add( layer.ItemId );
}
tool.GroupLayers( members );
}
void UngroupPicked() {
if ( ArchTool.Active is not { } tool ) {
return;
}
var group = tool.Picked?.Item as ArchSiteAssembly
?? ArchLayerGroups.Holding( tool.Plan, tool.SelectedLayer?.ItemId ?? 0 );
tool.UngroupLayer( group );
}
}
// The stack itself. Rows carry their own controls, so a press has to say whether it landed on the
// eye, the lock or the row before the tree decides it was a selection.
sealed class ArchLayerStack : TreeView {
public const float ControlWidth = 22f;
public ArchLayerStack( Widget parent ) : base( parent ) {
MultiSelect = true;
}
// A badge press on a shift-picked run of rows applies to all of them.
public List<ArchLayerRow> Picked( ArchLayerRow clicked ) {
var rows = Selection
.Select( entry => ResolveNode( entry, false ) )
.OfType<ArchLayerRow>()
.Where( row => row.Node.Ref is not null )
.ToList();
return rows.Count > 1 && rows.Contains( clicked ) ? rows : new List<ArchLayerRow> { clicked };
}
protected override bool OnItemPressed( VirtualWidget item, MouseEvent e ) {
if ( item.Object is ArchPieceRow piece && ArchTool.Active is { } owner ) {
return Pressed( piece, owner, item, e );
}
if ( item.Object is not ArchLayerRow row || row.Node.Ref is not { } layer || ArchTool.Active is not { } tool ) {
return base.OnItemPressed( item, e );
}
var local = e.LocalPosition.x - item.Rect.Left;
var fromRight = item.Rect.Width - local;
if ( fromRight <= ControlWidth ) {
ArchLayerBulk.Lock( tool, Picked( row ), !row.Node.Locked, row.Node.Name );
return false;
}
if ( fromRight <= ControlWidth * 2f ) {
ArchLayerBulk.Hide( tool, Picked( row ), !tool.LayerHiddenSelf( layer.ItemId ) );
Update();
return false;
}
if ( fromRight <= ControlWidth * 3f && row.Node.Payload is IArchCollides ) {
var menu = new ContextMenu( this );
row.Collisions( menu, tool );
menu.OpenAtCursor();
return false;
}
if ( fromRight <= ControlWidth * 4f && row.Node.Ref is not null ) {
var menu = new ContextMenu( this );
row.Shadows( menu, tool );
menu.OpenAtCursor();
return false;
}
return base.OnItemPressed( item, e );
}
bool Pressed( ArchPieceRow row, ArchTool tool, VirtualWidget item, MouseEvent e ) {
var fromRight = item.Rect.Width - (e.LocalPosition.x - item.Rect.Left);
if ( fromRight <= ControlWidth ) {
return false;
}
if ( fromRight <= ControlWidth * 2f ) {
tool.TogglePieceHidden( row.Built.Owner, row.Built.Piece );
Update();
return false;
}
if ( fromRight <= ControlWidth * 3f ) {
var menu = new ContextMenu( this );
row.Collisions( menu, tool );
menu.OpenAtCursor();
return false;
}
if ( fromRight <= ControlWidth * 4f ) {
var menu = new ContextMenu( this );
row.Shadows( menu, tool );
menu.OpenAtCursor();
return false;
}
return base.OnItemPressed( item, e );
}
}
// EVERY BULK EDIT PAYS FOR ONE COMMIT. A commit snapshots the plan, writes it to disk and rebuilds the scene, and
// the tool puts that rebuild two orders of magnitude above the edit itself - so sixteen rows done one at a time is
// sixteen freezes rather than one edit. Each row still gets Affect'd, through the list form: the single-id one
// clears what it holds, so calling it per row would flash the last row alone.
static class ArchLayerBulk {
public static string Across( int rows ) => rows > 1 ? $" · {rows} layers" : "";
static List<int> Ids( IEnumerable<ArchLayerRow> rows ) => rows.Select( row => row.ItemId ).ToList();
static string Named( string verb, IReadOnlyList<ArchLayerRow> rows, string name ) {
return rows.Count > 1 ? $"{verb} {rows.Count} layers" : $"{verb} {name}";
}
public static void Lock( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, bool locked, string name ) {
foreach ( var row in rows ) {
tool.SetLayerLocked( row.Node.Ref.Value, locked );
}
tool.Affect( Ids( rows ) );
tool.Bank( Named( locked ? "Lock" : "Unlock", rows, name ) );
}
public static void Hide( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, bool hidden ) {
foreach ( var row in rows ) {
tool.SetLayerHidden( row.ItemId, hidden );
}
tool.ApplyVisibility();
}
public static void Enable( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, bool enabled, string name ) {
foreach ( var row in rows ) {
tool.SetLayerEnabled( row.Node.Ref.Value, enabled );
}
tool.Affect( Ids( rows ) );
tool.Touch( Named( enabled ? "Enable" : "Disable", rows, name ) );
}
public static void Leave( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, string name ) {
foreach ( var row in rows ) {
ArchLayerGroups.Leave( tool.Plan, row.ItemId );
}
tool.Affect( Ids( rows ) );
tool.Touch( rows.Count > 1 ? $"Take {rows.Count} layers out of their groups" : $"Take {name} out of its group" );
}
public static void Affect( ArchTool tool, IReadOnlyList<ArchLayerRow> rows ) {
tool.Affect( Ids( rows ) );
}
}
// One row per authored layer. Value is the stable identity - the payload itself for authored rows,
// a synthetic story key for virtual ones - so the tree keeps open and selected state across rebuilds.
sealed class ArchLayerRow : TreeNode {
public ArchLayerNode Node { get; }
public ArchLayerRow( ArchLayerNode node, object key ) : base( key ) {
Node = node;
Height = 22;
// Must use the raw name, not DisplayName — SeparateIdentifier would destroy the authored casing.
Name = node.Payload is IArchNamed named ? named.Name : node.DisplayName;
}
// Locked layers refuse rename — the name is part of every generated path the lock protects.
public override bool CanEdit => !Node.Locked && Node.Payload is IArchNamed;
public override void OnRename( VirtualWidget item, string text, List<TreeNode> selection = null ) {
if ( ArchTool.Active is not { } tool || string.IsNullOrWhiteSpace( text ) ) {
return;
}
if ( !ArchKinds.Load().TryRename( Node.Payload, text ) ) {
return;
}
tool.Affect( ItemId );
tool.Touch( $"Rename {text}" );
}
bool IsGroup => Node.Payload is ArchSiteAssembly;
public int ItemId => Node.Ref?.ItemId ?? 0;
public override void OnPaint( VirtualWidget item ) {
var tool = ArchTool.Active;
var kinds = ArchKinds.Load();
var rect = item.Rect;
PaintSelection( item );
if ( IsGroup ) {
Paint.ClearPen();
Paint.SetBrush( Theme.Blue.WithAlpha( 0.12f ) );
Paint.DrawRect( rect, 2f );
}
var flash = tool?.Flash( ItemId ) ?? 0f;
if ( flash > 0f ) {
Paint.ClearPen();
Paint.SetBrush( Theme.Green.WithAlpha( 0.45f * flash ) );
Paint.DrawRect( rect, 2f );
}
if ( tool?.InsertionTarget is { } target && target.ItemId == ItemId ) {
Paint.ClearPen();
Paint.SetBrush( Theme.Primary );
Paint.DrawRect( new Rect( rect.Left, rect.Top, 3f, rect.Height ), 0 );
}
var muted = tool?.Muted( Node ) == true;
var hidden = tool?.LayerHidden( ItemId ) == true;
var alpha = muted || hidden ? 0.28f : Node.Enabled ? 1f : 0.45f;
Paint.SetPen( Theme.Text.WithAlpha( alpha ) );
Paint.DrawIcon( rect.Shrink( 4, 0, 0, 0 ), kinds.Glyph( Node.Kind ), 15, TextFlag.LeftCenter );
var controls = ArchLayerStack.ControlWidth * 5f;
var body = rect.Shrink( 24, 0, controls + 4f, 0 );
Paint.DrawText( body, Node.DisplayName, TextFlag.LeftCenter );
Paint.SetPen( Theme.Text.WithAlpha( 0.3f * alpha ) );
Paint.DrawText( body, Node.Stage.ToString().ToLowerInvariant(), TextFlag.RightCenter );
Collision( rect );
Shadow( rect );
Palette( rect, alpha );
var own = tool?.LayerHiddenSelf( ItemId ) == true;
Paint.SetPen( Theme.Text.WithAlpha( own ? 0.75f : 0.3f ) );
Paint.DrawIcon( Control( rect, 1 ), hidden ? "visibility_off" : "visibility", 14, TextFlag.Center );
Paint.SetPen( Node.Locked ? Theme.Yellow : Theme.Text.WithAlpha( 0.3f ) );
Paint.DrawIcon( Control( rect, 0 ), Node.Locked ? "lock" : "lock_open", 14, TextFlag.Center );
}
void Palette( Rect rect, float alpha ) {
if ( !ArchLayerPalettePicker.HasOverride( Node ) ) {
return;
}
Paint.SetPen( Theme.Primary.WithAlpha( alpha ) );
Paint.DrawIcon( Control( rect, 4 ), "palette", 14, TextFlag.Center );
}
void Shadow( Rect rect ) {
if ( Node.Ref is null || ArchTool.Active is not { } tool ) {
return;
}
var casts = ArchShadows.Showing( tool.Plan, tool.Kit, Node, out var own );
Paint.SetPen( !own
? Theme.Text.WithAlpha( 0.3f )
: casts ? Theme.Blue : Theme.Yellow );
Paint.DrawIcon( Control( rect, 3 ), ArchShadows.Glyph( casts ), 14, TextFlag.Center );
}
void Collision( Rect rect ) {
if ( Node.Payload is not IArchCollides ) {
return;
}
var mode = ArchCollision.Showing( Node, ArchTool.Active?.Kit, out var own );
Paint.SetPen( !own
? Theme.Text.WithAlpha( 0.3f )
: mode == ArchCollisionMode.Complex ? Theme.Yellow : Theme.Blue );
Paint.DrawIcon( Control( rect, 2 ), ArchCollision.Glyph( mode ), 14, TextFlag.Center );
}
static Rect Control( Rect row, int fromRight ) {
var width = ArchLayerStack.ControlWidth;
return new Rect( row.Right - width * (fromRight + 1), row.Top, width, row.Height );
}
public override string GetTooltip() {
var problems = ArchTool.Active?.LayerTree.Problems( Node );
return problems is { Count: > 0 }
? problems[0]
: $"{ArchKindsAsked.Label( Node.Kind )} · {Node.Stage} · id {ItemId}"
+ (ArchLayerPalettePicker.HasOverride( Node ) ? " · palette override" : "");
}
public override void OnActivated() {
if ( ArchTool.Active is { } tool && Node.Payload is not null ) {
tool.SelectLayer( Node );
tool.Frame();
}
}
public override bool OnDragStart() {
if ( Node.Payload is null ) {
return false;
}
var drag = new Drag( TreeView );
drag.Data.Object = Node;
drag.Execute();
return true;
}
public override DropAction OnDragDrop( ItemDragEvent e ) {
if ( e.Data.Object is not ArchLayerNode moving || moving.Ref is not { } layer || ArchTool.Active is not { } tool ) {
return DropAction.Ignore;
}
if ( ReferenceEquals( moving, Node ) || Descends( Node, moving ) ) {
return DropAction.Ignore;
}
var beside = e.DropEdge.HasFlag( ItemEdge.Top ) || e.DropEdge.HasFlag( ItemEdge.Bottom );
if ( beside ) {
if ( !e.IsDrop ) {
return DropAction.Move;
}
if ( tool.LayerTree.Reorder( tool.Plan, layer, ItemId, e.DropEdge.HasFlag( ItemEdge.Bottom ) ) ) {
tool.Affect( layer.ItemId );
tool.Commit( $"Reorder {moving.Name}" );
}
return DropAction.Move;
}
if ( !IsGroup && !ArchLayerRules.CanParent( Node.Kind, layer.Kind ).Allowed ) {
return DropAction.Ignore;
}
if ( e.IsDrop && tool.LayerTree.Reparent( tool.Plan, layer, ItemId ) ) {
tool.Affect( layer.ItemId );
tool.Commit( $"Move {moving.Name} into {Node.Name}" );
}
return DropAction.Move;
}
static bool Descends( ArchLayerNode node, ArchLayerNode ancestor ) {
for ( var current = node; current is not null; current = current.Parent ) {
if ( ReferenceEquals( current, ancestor ) ) {
return true;
}
}
return false;
}
List<ArchLayerRow> Picked() => (TreeView as ArchLayerStack)?.Picked( this ) ?? new List<ArchLayerRow> { this };
static string Across( int rows ) => ArchLayerBulk.Across( rows );
public void Collisions( ContextMenu menu, ArchTool tool ) {
var picked = Picked().Where( row => row.Node.Payload is IArchCollides ).ToList();
if ( picked.Count == 0 ) {
return;
}
var showing = ArchCollision.Showing( Node, tool.Kit, out _ );
var collisions = menu.AddMenu( $"Collisions{Across( picked.Count )}", ArchCollision.Glyph( showing ) );
var following = ArchCollision.Above( Node ) is { } parent
? $"Follow the layer above — {ArchCollision.Describe( parent )}"
: $"Follow the kit — {ArchCollision.Describe( tool.Kit?.Physics ?? ArchCollisionMode.Solids )}";
collisions.AddOption( following, "settings", () => Collide( tool, picked, null ) );
collisions.AddSeparator();
foreach ( var mode in new[] { ArchCollisionMode.Solids, ArchCollisionMode.Convex, ArchCollisionMode.Complex, ArchCollisionMode.None } ) {
var captured = mode;
collisions.AddOption( ArchCollision.Describe( captured ), ArchCollision.Glyph( captured ),
() => Collide( tool, picked, captured ) );
}
}
void Collide( ArchTool tool, List<ArchLayerRow> rows, ArchCollisionMode? mode ) {
foreach ( var row in rows ) {
if ( row.Node.Payload is not IArchCollides host ) {
continue;
}
host.Collision = mode;
}
ArchLayerBulk.Affect( tool, rows );
tool.Touch( rows.Count > 1 ? $"Collisions on {rows.Count} layers" : $"Collisions {Node.Name}" );
}
public void Shadows( ContextMenu menu, ArchTool tool ) {
if ( Node.Ref is null ) {
return;
}
var picked = Picked();
var showing = ArchShadows.Showing( tool.Plan, tool.Kit, Node, out _ );
var shadows = menu.AddMenu( $"Shadows{Across( picked.Count )}", ArchShadows.Glyph( showing ) );
shadows.AddOption( ArchShadows.Following( tool.Plan, tool.Kit, Node ), "settings", () => Cast( tool, picked, null ) );
shadows.AddSeparator();
foreach ( var casts in new[] { true, false } ) {
var captured = casts;
shadows.AddOption( ArchShadows.Describe( captured ), ArchShadows.Glyph( captured ),
() => Cast( tool, picked, captured ) );
}
}
void Cast( ArchTool tool, List<ArchLayerRow> rows, bool? casts ) {
foreach ( var row in rows ) {
ArchParts.Record( tool.Plan, row.ItemId, "" ).Shadows = casts;
}
tool.Plan.Parts.RemoveAll( record => record is { Collision: null, Shadows: null } );
ArchLayerBulk.Affect( tool, rows );
tool.Touch( rows.Count > 1 ? $"Shadows on {rows.Count} layers" : $"Shadows {Node.Name}" );
}
void Hollow( ArchTool tool, ArchLayerRef layer, IEnumerable<ArchRoom> rooms ) {
var stripped = 0;
foreach ( var room in rooms.ToList() ) {
stripped += ArchFacade.Empty( room );
}
tool.Affect( layer.ItemId );
tool.Commit( $"Convert {Node.Name} to Facade" );
Log.Info( $"Architecture: {Node.Name} is a facade - {stripped} interior setting{(stripped == 1 ? "" : "s")} stripped." );
}
void Keep( ArchTool tool, ArchBuilding building ) {
ArchNameDialog.Ask( TreeView, "Save Building",
$"Saves {building.Name} and every layer standing in it, ready to stamp again from the Building tool's Saved mode.",
building.Name, name => {
if ( ArchLayerAssets.SaveBuilding( tool.Plan, building, name ) is not { } path ) {
Log.Warning( $"Architecture: could not save '{name}'." );
return;
}
Log.Info( $"Architecture: saved {name} to Assets/{path}." );
} );
}
void Transform( ContextMenu menu, ArchTool tool ) {
var transform = menu.AddMenu( "Transform", "open_with" );
if ( Node.Payload is ArchBuilding building ) {
transform.AddOption( "Move", "open_with", () => { tool.SelectLayer( Node ); tool.BeginCarry( building ); } );
} else {
Handle( transform, tool, "Move", "open_with", ArchHandleMode.Move );
}
Handle( transform, tool, "Rotate", "rotate_right", ArchHandleMode.Turn );
Handle( transform, tool, "Resize", "aspect_ratio", ArchHandleMode.Scale );
}
void Handle( Menu menu, ArchTool tool, string label, string glyph, ArchHandleMode mode ) {
menu.AddOption( label, glyph, () => {
tool.SelectLayer( Node );
ArchShapeHandles.Mode = mode;
} );
}
public override bool OnContextMenu() {
if ( ArchTool.Active is not { } tool || Node.Payload is null || Node.Ref is not { } layer ) {
return false;
}
var menu = new ContextMenu( TreeView );
var picked = Picked();
var one = picked.Count == 1;
if ( one ) {
foreach ( var extension in ArchAddons.Load().Contributed<IArchLayerMenu>() ) {
extension.Populate( menu, tool, Node );
}
}
if ( one ) {
menu.AddOption( "Set as insertion target", "playlist_add", () => tool.InsertionTarget = layer );
}
var enabling = !Node.Enabled;
var locking = !Node.Locked;
menu.AddOption( (enabling ? "Enable generation" : "Disable generation") + Across( picked.Count ),
enabling ? "toggle_on" : "toggle_off",
() => ArchLayerBulk.Enable( tool, picked, enabling, Node.Name ) );
menu.AddOption( (locking ? "Lock" : "Unlock") + Across( picked.Count ),
locking ? "lock" : "lock_open",
() => ArchLayerBulk.Lock( tool, picked, locking, Node.Name ) );
if ( one && ArchKinds.Load().Transforms( Node.Kind ) ) {
Transform( menu, tool );
}
Collisions( menu, tool );
Shadows( menu, tool );
if ( one && ArchLayerPalettePicker.CanOpen( tool, Node ) ) {
menu.AddOption( "Apply Palette", "palette", () => ArchLayerPalettePicker.Show( TreeView, tool, Node ) );
}
if ( one && Node.Payload is ArchBuilding building ) {
menu.AddOption( "Toggle Building Layout", "architecture", () => ArchBuildingWindow.Toggle( tool, building ) );
menu.AddOption( "Save", "inventory_2", () => Keep( tool, building ) );
menu.AddOption( "Convert to Facade", "filter_1", () => Hollow( tool, layer, building.Rooms ) );
}
if ( one && Node.Payload is ArchRoom room ) {
menu.AddOption( "Convert to Facade", "filter_1", () => Hollow( tool, layer, new[] { room } ) );
}
if ( one && Node.Payload is ArchCutPart { ResolvedDamage: ArchDamageKind.Masonry } cut ) {
menu.AddOption( "Bake to Target", "fit_screen", () => {
if ( !ArchDamage.BakeToTarget( tool.Plan, tool.EnsureKit(), cut ) ) {
Log.Warning( "Architecture: damage zone does not overlap a wall corner or pillar." );
return;
}
tool.Affect( layer.ItemId );
tool.Commit( "Bake Damage to Target" );
} );
}
menu.AddSeparator();
if ( one && CanEdit ) {
menu.AddOption( "Rename", "edit", () => {
TreeView.SelectItem( Node.Payload );
TreeView.BeginRename();
} );
}
if ( one && IsGroup ) {
menu.AddOption( "Ungroup", "folder_off", () => tool.UngroupLayer( (ArchSiteAssembly)Node.Payload ) );
} else if ( !IsGroup ) {
menu.AddOption( $"Group{Across( picked.Count )}", "create_new_folder",
() => tool.GroupLayers( picked.Select( row => row.ItemId ).ToList() ) );
var held = picked.Where( row => ArchLayerGroups.Holding( tool.Plan, row.ItemId ) is not null ).ToList();
if ( held.Count > 0 ) {
menu.AddOption( $"Take out of its group{Across( held.Count )}", "output",
() => ArchLayerBulk.Leave( tool, held, Node.Name ) );
}
}
menu.AddSeparator();
if ( one ) {
menu.AddOption( "Frame", "filter_center_focus", () => { tool.SelectLayer( Node ); tool.Frame(); } );
}
menu.AddOption( $"Delete{Across( picked.Count )}", "delete",
() => tool.DeleteLayers( picked.Select( row => row.Node ).ToList() ) );
menu.OpenAtCursor();
return true;
}
}
sealed class ArchDraftRow : TreeNode {
public ArchDraft Draft { get; }
public ArchDraftRow( ArchDraft draft ) : base( draft ) {
Draft = draft;
Height = 22;
}
public override void OnPaint( VirtualWidget item ) {
var kinds = ArchKinds.Load();
Paint.SetPen( Color.Cyan );
Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), kinds.Glyph( Draft.Kind ), 15, TextFlag.LeftCenter );
Paint.DrawText( item.Rect.Shrink( 24, 0, 0, 0 ), $"{Draft.Name} — placing", TextFlag.LeftCenter );
}
}
sealed class ArchDomainRow : TreeNode {
readonly string label;
readonly string glyph;
public ArchDomainRow( string label, ArchLayerDomain domain ) : base( domain ) {
this.label = label;
glyph = domain switch {
ArchLayerDomain.Buildings => "domain",
ArchLayerDomain.Connections => "link",
_ => "route"
};
Height = 24;
}
public override void OnPaint( VirtualWidget item ) {
Paint.SetPen( Theme.Text.WithAlpha( 0.85f ) );
Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), glyph, 15, TextFlag.LeftCenter );
Paint.DrawText( item.Rect.Shrink( 24, 0, 0, 0 ), label.ToUpperInvariant(), TextFlag.LeftCenter );
}
public override DropAction OnDragDrop( ItemDragEvent e ) {
if ( e.Data.Object is not ArchLayerNode moving || moving.Ref is not { } layer || ArchTool.Active is not { } tool ) {
return DropAction.Ignore;
}
if ( ArchLayerGroups.Holding( tool.Plan, layer.ItemId ) is null ) {
return DropAction.Ignore;
}
if ( e.IsDrop ) {
ArchLayerGroups.Leave( tool.Plan, layer.ItemId );
tool.Affect( layer.ItemId );
tool.Commit( $"Take {moving.Name} out of its group" );
}
return DropAction.Move;
}
}