Editor-side portion of the ArchTool, managing editor-only state for layer visibility, grouping, flashing recent edits, locking, and selection. It tracks hidden layers/pieces, which group is being edited (for isolate mode), records affected items to drive a temporary flash, and provides operations to toggle visibility/lock, group/ungroup layers, begin adding a child, and select a layer.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// The stack's editor-only state, kept off the plan: which rows are muted by the eye, which layers
// the last edit reached, and whether the groups it did not reach fold away while you work.
public partial class ArchTool
{
public const float FlashSeconds = 1.6f;
const string IsolateKey = "arch.layers.isolate";
const string GeometryKey = "arch.layers.geometry";
readonly HashSet<int> hiddenLayers = new();
// A piece has no id of its own, so the eye holds it by the layer that built it and where it hangs on that layer.
readonly HashSet<string> hiddenPieces = new();
readonly HashSet<int> affected = new();
RealTimeSince affectedAt = FlashSeconds;
// Set by whoever knows what was touched, consumed by the commit that follows it. Without the
// latch, a commit would overwrite a fresh placement's flash with whatever was picked before it.
bool affectedNamed;
// A preference, not plan semantics: it survives restart and changes no JSON.
public static bool IsolateGroups
{
get => EditorCookie.Get( IsolateKey, true );
set => EditorCookie.Set( IsolateKey, value );
}
// Whether the stack shows what each layer BUILT under it. Debugging geometry is a mode, not the
// authoring surface, so it folds away entirely rather than sitting under every row forever.
public static bool ShowGeometry
{
get => EditorCookie.Get( GeometryKey, true );
set => EditorCookie.Set( GeometryKey, value );
}
// What the last edit reached, and how recently - the stack fades a row from this.
public float Flash( int itemId )
{
if ( !affected.Contains( itemId ) )
{
return 0f;
}
return Math.Clamp( 1f - affectedAt / FlashSeconds, 0f, 1f );
}
public bool Flashing => affectedAt < FlashSeconds && affected.Count > 0;
// Everything the edit could have changed: the layer, its subtree, its linked consumers and the
// group it stands in - so the flash names the whole thing the user actually affected.
public void Affect( int itemId )
{
if ( itemId == 0 )
{
return;
}
affected.Clear();
affectedAt = 0f;
affectedNamed = true;
foreach ( var id in LayerTree.DirtyClosure( itemId ) )
{
affected.Add( id );
}
if ( ArchLayerGroups.Holding( Plan, itemId ) is { } group )
{
affected.Add( group.Id );
foreach ( var member in group.Children )
{
affected.Add( member );
}
}
EditedGroupId = ArchLayerGroups.Holding( Plan, itemId )?.Id ?? 0;
}
bool ConsumeAffected()
{
var named = affectedNamed;
affectedNamed = false;
return named;
}
// The group the work is happening in. Isolate folds every other one away until it changes.
public int EditedGroupId { get; set; }
// A group is muted while isolating when the edit is not happening inside it. Ungrouped layers
// are never muted - there is no scope to be outside of.
public bool Muted( ArchLayerNode node )
{
if ( !IsolateGroups || EditedGroupId == 0 || node?.Ref is not { } layer )
{
return false;
}
if ( layer.ItemId == EditedGroupId )
{
return false;
}
var holding = ArchLayerGroups.Holding( Plan, layer.ItemId )
?? ArchLayerGroups.Holding( Plan, Root( node )?.Ref?.ItemId ?? 0 );
return holding is not null && holding.Id != EditedGroupId;
}
static ArchLayerNode Root( ArchLayerNode node )
{
while ( node?.Parent is { } parent )
{
node = parent;
}
return node;
}
public bool LayerHidden( int itemId ) => hiddenLayers.Contains( itemId );
public bool PieceHidden( int owner, string piece ) => hiddenPieces.Contains( ArchParts.Key( owner, piece ) );
// The eye is presentation only - the layer still generates, it just stops being drawn here.
public void ToggleLayerHidden( int itemId )
{
if ( !hiddenLayers.Add( itemId ) )
{
hiddenLayers.Remove( itemId );
}
ApplyVisibility();
}
public void TogglePieceHidden( int owner, string piece )
{
var key = ArchParts.Key( owner, piece );
if ( !hiddenPieces.Add( key ) )
{
hiddenPieces.Remove( key );
}
ApplyVisibility();
}
public void ShowEveryLayer()
{
hiddenLayers.Clear();
hiddenPieces.Clear();
ApplyVisibility();
}
public void ToggleLayerLocked( ArchLayerRef layer )
{
if ( LayerTree.Find( layer.ItemId ) is not { } node || node.Payload is null )
{
return;
}
var record = LayerTree.Record( Plan, node );
record.Locked = !record.Locked;
Commit( record.Locked ? $"Lock {node.Name}" : $"Unlock {node.Name}" );
}
// Gathers the picked layer and every other selected one into a new folder. A group is a scope,
// so what it holds is what an operation inside it may reach.
public ArchSiteAssembly GroupLayers( IEnumerable<int> members, string name = null )
{
var wanted = members.Where( id => id != 0 ).Distinct().ToList();
if ( wanted.Count == 0 )
{
return null;
}
var group = ArchLayerGroups.Create( Plan, name ?? $"Group {Plan.Assemblies.Count + 1}", ArchAssemblyKind.Group, wanted );
Affect( group.Id );
Commit( "Group Layers" );
return group;
}
public void UngroupLayer( ArchSiteAssembly group )
{
if ( group is null )
{
return;
}
ArchLayerGroups.Dissolve( Plan, group );
if ( EditedGroupId == group.Id )
{
EditedGroupId = 0;
}
Commit( "Ungroup Layers" );
}
// The tree chooses a layer KIND to add; the shelf owns the gesture that authors one, so adding a
// child sets the parent and raises the tool that knows how to draw it.
public void BeginChild( ArchLayerRef parent, ArchKind kind )
{
InsertionTarget = parent;
if ( ArchKindsAsked.Subtool( kind ) is { Length: > 0 } subtool )
{
EditorToolManager.SetSubTool( subtool );
}
}
// One entry point for both the tree and the viewport: select the layer, then show its handles.
public void SelectLayer( ArchLayerNode node )
{
if ( node?.Payload is null )
{
return;
}
Select( new ArchSelection { Item = node.Payload, Room = node.Room, Building = node.Building } );
if ( CurrentTool is not ArchSelectSubtool )
{
EditorToolManager.SetSubTool( nameof( ArchSelectSubtool ) );
}
}
}