Editor/Tool/ArchToolLayers.cs

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.

File AccessNetworking
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 ) );
		}
	}
}