Editor/Tool/ArchToolLayers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
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();
readonly HashSet<string> hiddenPieces = new();
readonly HashSet<int> affected = new();
RealTimeSince affectedAt = FlashSeconds;
bool affectedNamed;
public static bool IsolateGroups {
get => EditorCookie.Get( IsolateKey, true );
set => EditorCookie.Set( IsolateKey, value );
}
public static bool ShowGeometry {
get => EditorCookie.Get( GeometryKey, true );
set => EditorCookie.Set( GeometryKey, value );
}
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;
public void Affect( int itemId ) => Affect( new[] { itemId } );
// Clears before filling — bulk callers must pass all ids at once
public void Affect( IReadOnlyList<int> itemIds ) {
var wanted = itemIds?.Where( id => id != 0 ).ToList();
if ( wanted is not { Count: > 0 } ) {
return;
}
affected.Clear();
affectedAt = 0f;
affectedNamed = true;
foreach ( var itemId in wanted ) {
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, wanted[^1] )?.Id ?? 0;
}
bool ConsumeAffected() {
var named = affectedNamed;
affectedNamed = false;
return named;
}
public int EditedGroupId { get; set; }
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;
}
// Walks up — a group has no scene object, so hiding propagates through the tree
public bool LayerHidden( int itemId ) {
if ( hiddenLayers.Count == 0 ) {
return false;
}
if ( hiddenLayers.Contains( itemId ) ) {
return true;
}
for ( var node = LayerTree.Find( itemId )?.Parent; node is not null; node = node.Parent ) {
if ( node.Ref is { } layer && hiddenLayers.Contains( layer.ItemId ) ) {
return true;
}
}
return false;
}
public bool LayerHiddenSelf( int itemId ) => hiddenLayers.Contains( itemId );
public bool PieceHidden( int owner, string piece ) => hiddenPieces.Contains( ArchParts.Key( owner, piece ) );
public void ToggleLayerHidden( int itemId ) {
SetLayerHidden( itemId, !LayerHiddenSelf( itemId ) );
ApplyVisibility();
}
// No walk — caller must call ApplyVisibility once after bulk changes
public void SetLayerHidden( int itemId, bool hidden ) {
if ( hidden ) {
hiddenLayers.Add( itemId );
} else {
hiddenLayers.Remove( itemId );
}
}
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 locked = !node.Locked;
SetLayerLocked( layer, locked );
// Banked, not committed — a lock moves no vertex
Bank( locked ? $"Lock {node.Name}" : $"Unlock {node.Name}" );
}
// No commit — bulk callers set every row then commit once
public void SetLayerLocked( ArchLayerRef layer, bool locked ) {
if ( LayerTree.Find( layer.ItemId ) is not { } node || node.Payload is null ) {
return;
}
LayerTree.Record( Plan, node ).Locked = locked;
}
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 );
Touch( "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" );
}
public void BeginChild( ArchLayerRef parent, ArchKind kind ) {
InsertionTarget = parent;
if ( ArchKindsAsked.Subtool( kind ) is { Length: > 0 } subtool ) {
EditorToolManager.SetSubTool( subtool );
}
}
public ArchBuilding CarryRequest { get; set; }
public void BeginCarry( ArchBuilding building ) {
CarryRequest = building;
EditorToolManager.SetSubTool( nameof( ArchSelectSubtool ) );
}
public void SelectLayer( ArchLayerNode node ) {
if ( node?.Payload is null ) {
return;
}
Select( new ArchSelection { Item = node.Payload, Room = node.Room, Building = node.Building } );
// The standing tool keeps focus if it can host this pick
if ( CurrentTool is ArchSubtool standing && (standing is ArchSelectSubtool || standing.Hosts( node.Payload )) ) {
return;
}
EditorToolManager.SetSubTool( nameof( ArchSelectSubtool ) );
}
}