s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (11 total matches found)
subzerostudios.subhierarchy / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "SubHierarchy" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "subhierarchy" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "subzerostudios" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "subzerostudios.subhierarchy" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "29" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-09-14T18:56:09.5154493Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.118.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.118.0")]
subzerostudios.subhierarchy / Editor/HierarchyColorPainter.cs
Editor library
using System.Collections;
using System.Reflection;

namespace SubHierarchy;

/// <summary>
/// Tints Hierarchy rows for GameObjects that have a <see cref="HierarchyColor"/> component.
/// Hooks the built-in Hierarchy TreeView paint (GameObjectNode is internal).
/// </summary>
public static class HierarchyColorPainter
{
	const float TintAlpha = 0.38f;
	const BindingFlags LayoutFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	static TreeView _hookedTree;
	static PropertyInfo _itemLayoutsProperty;
	static FieldInfo _itemLayoutsField;
	static bool _layoutsLookupDone;

	[Event( "editor.created" )]
	static void OnEditorCreated( object _ ) => EnsureHooked();

	[EditorEvent.Frame]
	static void OnFrame() => EnsureHooked();

	public static void EnsureHooked()
	{
		var tree = SceneTreeWidget.Current?.TreeView;
		if ( tree is null || !tree.IsValid() )
		{
			_hookedTree = null;
			return;
		}

		// Always re-apply: Hierarchy BuildUI can replace TreeView or reset OnPaintOverride.
		if ( !ReferenceEquals( _hookedTree, tree ) )
			_hookedTree = tree;

		tree.OnPaintOverride = PaintHierarchyBackground;
	}

	static bool PaintHierarchyBackground()
	{
		var tree = _hookedTree;
		if ( tree is null || !tree.IsValid() )
			return false;

		Paint.ClearPen();
		Paint.SetBrush( Theme.ControlBackground );
		Paint.DrawRect( tree.LocalRect, Theme.ControlRadius );

		PaintColoredRows( tree );
		return false;
	}

	static void PaintColoredRows( TreeView tree )
	{
		var layouts = GetItemLayouts( tree );
		if ( layouts is not null )
		{
			foreach ( var entry in layouts )
			{
				if ( entry is not VirtualWidget item )
					continue;

				if ( !TryResolveGameObject( item.Object, out var go ) )
					continue;

				var marker = go.Components.Get<HierarchyColor>( true );
				if ( marker is null || !marker.IsValid() )
					continue;

				// ItemLayouts rects are already in paint / scrolled local space.
				PaintRow( tree, item.Rect, marker.Tint );
			}

			return;
		}

		// Fallback: TryGetItemRect is content-space — convert using scrollbar value.
		PaintColoredRowsFallback( tree );
	}

	static void PaintColoredRowsFallback( TreeView tree )
	{
		var scene = SceneEditorSession.Active?.Scene;
		if ( scene is null || !scene.IsValid() )
			return;

		var scroll = tree.VerticalScrollbar?.Value ?? 0f;
		var originY = tree.LocalRect.Top + scroll - tree.Margin.Top;

		foreach ( var go in scene.GetAllObjects( true ) )
		{
			if ( !go.IsValid() || go is Scene )
				continue;

			var marker = go.Components.Get<HierarchyColor>( true );
			if ( marker is null || !marker.IsValid() )
				continue;

			if ( !tree.TryGetItemRect( go, out var contentRect ) )
				continue;

			var rect = contentRect;
			rect.Top -= originY;
			PaintRow( tree, rect, marker.Tint );
		}
	}

	static void PaintRow( TreeView tree, Rect rect, Color color )
	{
		rect.Left = 0;
		rect.Right = tree.Width;

		Paint.ClearPen();
		Paint.SetBrush( color.WithAlpha( TintAlpha ) );
		Paint.DrawRect( rect );
	}

	static bool TryResolveGameObject( object obj, out GameObject go )
	{
		go = null;

		if ( obj is TreeNode node )
			go = node.Value as GameObject;
		else
			go = obj as GameObject;

		return go.IsValid() && go is not Scene;
	}

	/// <summary>
	/// Visible row layouts live on BaseItemWidget as a protected member.
	/// Those rects match what Hierarchy paints; TryGetItemRect is content-space (scroll Y) and misaligns.
	/// </summary>
	static IEnumerable GetItemLayouts( TreeView tree )
	{
		if ( !_layoutsLookupDone )
		{
			_layoutsLookupDone = true;
			var type = typeof( BaseItemWidget );
			_itemLayoutsProperty = type.GetProperty( "ItemLayouts", LayoutFlags );
			_itemLayoutsField = type.GetField( "ItemLayouts", LayoutFlags );
		}

		object value = null;
		if ( _itemLayoutsProperty is not null )
			value = _itemLayoutsProperty.GetValue( tree );
		else if ( _itemLayoutsField is not null )
			value = _itemLayoutsField.GetValue( tree );

		return value as IEnumerable;
	}
}
subzerostudios.subhierarchy / Editor/SubHierarchyDock.cs
Editor library
namespace SubHierarchy;

/// <summary>
/// Separate dock with Hierarchy helpers: collapse/expand, hide/show, row colors.
/// </summary>
[Dock( "Editor", "Sub Hierarchy", "account_tree" )]
public sealed class SubHierarchyDock : Widget
{
	public const string DockTitle = "Sub Hierarchy";
	const string CustomColorsCookie = "SubHierarchy.CustomColors";
	const int MaxCustomColors = 24;
	const float ColorMatchEpsilon = 0.02f;

	static readonly Color[] Presets =
	[
		new Color( 0.85f, 0.25f, 0.25f ), // red
		new Color( 0.90f, 0.50f, 0.15f ), // orange
		new Color( 0.90f, 0.80f, 0.20f ), // yellow
		new Color( 0.55f, 0.85f, 0.20f ), // lime
		new Color( 0.30f, 0.75f, 0.35f ), // green
		new Color( 0.15f, 0.70f, 0.65f ), // teal
		new Color( 0.20f, 0.75f, 0.90f ), // cyan
		new Color( 0.25f, 0.55f, 0.95f ), // blue
		new Color( 0.40f, 0.35f, 0.90f ), // indigo
		new Color( 0.65f, 0.35f, 0.90f ), // purple
		new Color( 0.90f, 0.35f, 0.70f ), // magenta
		new Color( 0.90f, 0.45f, 0.55f ), // pink
		new Color( 0.70f, 0.50f, 0.30f ), // brown
	];

	Color _draftColor = new Color( 0.25f, 0.55f, 0.95f );
	ColorSwatchButton _swatch;
	bool _colorChildrenRecursively;
	Layout _content;
	ColorSwatchPalette _palette;
	List<Color> _customColors = new();

	public SubHierarchyDock( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();

		var scroller = Layout.Add( new ScrollArea( this ), 1 );
		scroller.Canvas = new Widget( scroller )
		{
			HorizontalSizeMode = SizeMode.Flexible,
			VerticalSizeMode = SizeMode.CanGrow
		};
		scroller.Canvas.Layout = Layout.Column();
		scroller.Canvas.Layout.Margin = 8;
		scroller.Canvas.Layout.Spacing = 8;
		_content = scroller.Canvas.Layout;

		LoadCustomColors();
		BuildUi();
		HierarchyColorPainter.EnsureHooked();
	}

	public static void Open()
	{
		EditorWindow.DockManager.SetDockState( DockTitle, true );
		EditorWindow.DockManager.RaiseDock( DockTitle );
	}

	void BuildUi()
	{
		_content.Clear( true );

		AddSectionLabel( "Tree" );
		AddButtonRow(
			( "Collapse All", "unfold_less", HierarchyTreeActions.CollapseAll ),
			( "Expand All", "unfold_more", HierarchyTreeActions.ExpandAll )
		);

		AddSectionLabel( "Visibility" );
		AddButtonRow(
			( "Hide All", "visibility_off", HierarchyTreeActions.HideAll ),
			( "Show All", "visibility", HierarchyTreeActions.ShowAll )
		);
		AddButtonRow(
			( "Isolate Selection", "filter_center_focus", HierarchyTreeActions.IsolateSelection )
		);

		AddSectionLabel( "Hierarchy Color" );
		AddColorControls();
		RebuildColorSwatches();

		AddSectionLabel( "Reorganize" );
		AddButtonRow(
			( "Sort Alphabetically", "sort_by_alpha", HierarchyTreeActions.ReorganizeAlphabetically ),
			( "Sort by Color", "palette", HierarchyTreeActions.ReorganizeByColor )
		);

		_content.AddStretchCell( 1 );
	}

	void AddSectionLabel( string text )
	{
		var label = _content.Add( new Label( text ) );
		label.Color = Theme.TextControl.WithAlpha( 0.85f );
	}

	void AddButtonRow( params (string Title, string Icon, Action Action)[] buttons )
	{
		var row = _content.AddRow();
		row.Spacing = 6;

		foreach ( var button in buttons )
		{
			var btn = row.Add( new Button( button.Title, button.Icon ), 1 );
			btn.Clicked = button.Action;
		}
	}

	void AddColorControls()
	{
		var row = _content.AddRow();
		row.Spacing = 6;
		row.Alignment = TextFlag.LeftCenter;

		_swatch = row.Add( new ColorSwatchButton( () => _draftColor, OnCustomColorPicked )
		{
			RememberOnClose = true,
			OnRemember = RememberCustomColor
		} );
		_swatch.ToolTip = "Pick a color and apply it to the selection";
		_swatch.FixedWidth = Theme.RowHeight * 2;
		_swatch.FixedHeight = Theme.RowHeight;

		var clear = row.Add( new Button( "Clear", "format_color_reset" ), 1 );
		clear.ToolTip = "Remove Hierarchy Color from selection";
		clear.Clicked = () => HierarchyTreeActions.ClearColorFromSelection( _colorChildrenRecursively );

		var clearAll = row.Add( new Button( "Clear All", "delete_sweep" ), 1 );
		clearAll.ToolTip = "Remove Hierarchy Color from every object in the scene";
		clearAll.Clicked = HierarchyTreeActions.ClearAllColors;

		var recursiveRow = _content.AddRow();
		recursiveRow.Spacing = 6;
		recursiveRow.Alignment = TextFlag.LeftCenter;

		var recursive = recursiveRow.Add( new Checkbox( "Color children recursively" ) );
		recursive.State = _colorChildrenRecursively ? CheckState.On : CheckState.Off;
		recursive.ToolTip = "When applying or clearing, include all descendants of the selection";
		recursive.Clicked = () => _colorChildrenRecursively = recursive.State == CheckState.On;

		_palette = _content.Add( new ColorSwatchPalette( OnCustomColorPicked, OnColorSwatchRemoved ) );
		_palette.HorizontalSizeMode = SizeMode.Flexible;
	}

	void RebuildColorSwatches()
	{
		if ( _palette is null || !_palette.IsValid() )
			return;

		var entries = new List<(Color Color, string ToolTip, bool Removable)>();
		foreach ( var preset in Presets )
			entries.Add( (preset, "Left-click: apply", false) );

		foreach ( var custom in _customColors )
			entries.Add( (custom, "Left-click: apply. Right-click: remove from palette and all objects.", true) );

		_palette.SetColors( entries );
	}

	void OnCustomColorPicked( Color color )
	{
		_draftColor = color.WithAlpha( 1f );
		_swatch?.Update();
		HierarchyTreeActions.ApplyColorToSelection( _draftColor, _colorChildrenRecursively );
	}

	void OnColorSwatchRemoved( Color color )
	{
		color = color.WithAlpha( 1f );

		// Built-in presets stay in the palette permanently.
		if ( Presets.Any( p => ColorsMatch( p, color ) ) )
			return;

		var removedCustom = _customColors.RemoveAll( c => ColorsMatch( c, color ) ) > 0;
		if ( !removedCustom )
			return;

		HierarchyTreeActions.ClearColorsMatching( color, ColorMatchEpsilon );
		SaveCustomColors();

		if ( ColorsMatch( _draftColor, color ) )
			_draftColor = Presets[0];

		_swatch?.Update();
		RebuildColorSwatches();
	}

	void RememberCustomColor( Color color )
	{
		color = color.WithAlpha( 1f );

		if ( Presets.Any( p => ColorsMatch( p, color ) ) )
			return;

		if ( _customColors.Any( c => ColorsMatch( c, color ) ) )
			return;

		_customColors.Add( color );
		while ( _customColors.Count > MaxCustomColors )
			_customColors.RemoveAt( 0 );

		SaveCustomColors();
		RebuildColorSwatches();
	}

	void LoadCustomColors()
	{
		_customColors = EditorCookie.Get( CustomColorsCookie, new List<Color>() ) ?? new List<Color>();
	}

	void SaveCustomColors()
	{
		EditorCookie.Set( CustomColorsCookie, _customColors );
	}

	static bool ColorsMatch( Color a, Color b )
	{
		return MathF.Abs( a.r - b.r ) < ColorMatchEpsilon
			&& MathF.Abs( a.g - b.g ) < ColorMatchEpsilon
			&& MathF.Abs( a.b - b.b ) < ColorMatchEpsilon;
	}

	/// <summary>
	/// Wraps color swatches to the available width, with at most 10 per row.
	/// </summary>
	sealed class ColorSwatchPalette : Widget
	{
		const int MaxPerRow = 10;
		const float Spacing = 4f;

		readonly Action<Color> _onPick;
		readonly Action<Color> _onRemove;
		readonly List<(Color Color, string ToolTip, bool Removable)> _entries = new();
		int _builtPerRow = -1;

		public ColorSwatchPalette( Action<Color> onPick, Action<Color> onRemove ) : base( null )
		{
			_onPick = onPick;
			_onRemove = onRemove;
			HorizontalSizeMode = SizeMode.Flexible;
			VerticalSizeMode = SizeMode.CanGrow;
			Layout = Layout.Column();
			Layout.Spacing = Spacing;
		}

		public void SetColors( IEnumerable<(Color Color, string ToolTip, bool Removable)> entries )
		{
			_entries.Clear();
			_entries.AddRange( entries );
			_builtPerRow = -1;
			Rebuild();
		}

		protected override void DoLayout()
		{
			base.DoLayout();

			var perRow = CalcPerRow();
			if ( perRow != _builtPerRow )
				Rebuild();
		}

		int CalcPerRow()
		{
			var cell = Theme.RowHeight + Spacing;
			if ( cell <= 0f || Width <= 1f )
				return MaxPerRow;

			var fit = Math.Max( 1, (int)( (Width + Spacing) / cell ) );
			return Math.Min( MaxPerRow, fit );
		}

		void Rebuild()
		{
			var perRow = CalcPerRow();
			_builtPerRow = perRow;

			Layout.Clear( true );
			Layout.Spacing = Spacing;

			Layout row = null;
			var countInRow = 0;

			foreach ( var entry in _entries )
			{
				if ( row is null || countInRow >= perRow )
				{
					row = Layout.AddRow();
					row.Spacing = Spacing;
					row.Alignment = TextFlag.LeftCenter;
					countInRow = 0;
				}

				var color = entry.Color;
				var removable = entry.Removable;
				var swatch = row.Add( new ColorSwatchButton(
					() => color,
					c => _onPick( c ),
					removable ? () => _onRemove( color ) : null ) );
				swatch.FixedWidth = Theme.RowHeight;
				swatch.FixedHeight = Theme.RowHeight;
				swatch.ToolTip = entry.ToolTip;
				swatch.OpenPickerOnClick = false;
				countInRow++;
			}
		}
	}

	/// <summary>Simple color swatch; left-click opens the engine color picker (optional).</summary>
	sealed class ColorSwatchButton : Widget
	{
		readonly Func<Color> _getColor;
		readonly Action<Color> _setColor;
		readonly Action _onRemove;

		public bool OpenPickerOnClick { get; set; } = true;
		public bool RememberOnClose { get; set; }
		public Action<Color> OnRemember { get; set; }

		public ColorSwatchButton( Func<Color> getColor, Action<Color> setColor, Action onRemove = null ) : base( null )
		{
			_getColor = getColor;
			_setColor = setColor;
			_onRemove = onRemove;
			Cursor = CursorShape.Finger;
			MouseLeftPress = OnClicked;
			MouseRightPress = OnRightClicked;
		}

		void OnClicked()
		{
			if ( !OpenPickerOnClick )
			{
				_setColor( _getColor() );
				return;
			}

			var popup = new ColorPopup( null );
			popup.FixedWidth = 233;
			popup.Layout = Layout.Column();
			popup.Position = Editor.Application.CursorPosition;

			var colorPicker = popup.Layout.Add( new ColorPicker( popup, _getColor() ), 1 );
			colorPicker.OnChildValuesChanged += _ =>
			{
				_setColor( colorPicker.Value );
				Update();
			};

			if ( RememberOnClose )
			{
				popup.Closed = () => OnRemember?.Invoke( colorPicker.Value );
			}

			popup.Show();
			popup.ConstrainToScreen();
		}

		void OnRightClicked()
		{
			_onRemove?.Invoke();
		}

		protected override void OnPaint()
		{
			var color = _getColor();
			const float radius = 3f;
			const float border = 1.25f;

			Paint.Antialiasing = true;

			// Solid white rounded plate first so color can't bleed past the corners.
			Paint.ClearPen();
			Paint.SetBrush( Color.White );
			Paint.DrawRect( LocalRect, radius );

			var inner = LocalRect.Shrink( border );
			var innerRadius = Math.Max( radius - border * 0.5f, 0f );
			Paint.SetBrush( color );
			Paint.DrawRect( inner, innerRadius );
		}
	}

	/// <summary>Popup that notifies when closed so custom colors can be saved once.</summary>
	sealed class ColorPopup : PopupWidget
	{
		public Action Closed { get; set; }

		public ColorPopup( Widget parent ) : base( parent )
		{
		}

		protected override void OnClosed()
		{
			Closed?.Invoke();
			base.OnClosed();
		}
	}
}

/// <summary>Menu + View registration for Sub Hierarchy.</summary>
public static class SubHierarchyMenus
{
	[Menu( "Editor", "Sub Hierarchy/Open", "account_tree" )]
	public static void Open() => SubHierarchyDock.Open();

	[Event( "tools.editorwindow.postcreateview" )]
	static void AddViewMenu( Menu menu )
	{
		menu.AddSeparator();
		menu.AddOption( "Sub Hierarchy", "account_tree", () => SubHierarchyDock.Open() );
	}

	[Event( "editor.created" )]
	static void OnEditorCreated( object _ ) => HierarchyColorPainter.EnsureHooked();
}
subzerostudios.subhierarchy / Code/Assembly.cs
Game library
global using Sandbox;
global using System;
global using System.Collections.Generic;
global using System.Linq;
subzerostudios.subhierarchy / UnitTests/UnitTest.cs
UnitTest library
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
subzerostudios.subhierarchy / Assembly.cs
Game library
global using Sandbox;
global using System;
global using System.Collections.Generic;
global using System.Linq;
subzerostudios.subhierarchy / UnitTests/LibraryTest.cs
UnitTest library
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
subzerostudios.subhierarchy / Editor/HierarchyTreeActions.cs
Editor library
namespace SubHierarchy;

/// <summary>
/// Actions that drive the built-in Hierarchy / scene visibility.
/// </summary>
public static class HierarchyTreeActions
{
	const string HiddenTag = "hidden";

	public static TreeView GetHierarchyTree()
	{
		return SceneTreeWidget.Current?.TreeView;
	}

	public static Scene GetActiveScene()
	{
		return SceneEditorSession.Active?.Scene;
	}

	/// <summary>Collapse every open group; keep the scene root expanded.</summary>
	public static void CollapseAll()
	{
		var tree = GetHierarchyTree();
		var scene = GetActiveScene();
		if ( tree is null || scene is null || !scene.IsValid() )
			return;

		tree.Close( scene, recursive: true );
		tree.Open( scene );
		tree.Update();
	}

	/// <summary>Expand every group under the scene root.</summary>
	public static void ExpandAll()
	{
		var tree = GetHierarchyTree();
		var scene = GetActiveScene();
		if ( tree is null || scene is null || !scene.IsValid() )
			return;

		tree.Open( scene, recursive: true );
		tree.Update();
	}

	/// <summary>Hide every GameObject in the scene (same <c>hidden</c> tag as H / U).</summary>
	public static void HideAll()
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session?.Scene is not { } scene || !scene.IsValid() )
			return;

		var gos = scene.GetAllObjects( true )
			.Where( go => go.IsValid() && go is not Scene && !go.Tags.Has( HiddenTag ) )
			.ToArray();

		if ( gos.Length == 0 )
			return;

		using ( session.UndoScope( "Hide All Object(s)" )
			.WithGameObjectChanges( gos, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in gos )
				go.Tags.Add( HiddenTag );
		}
	}

	/// <summary>Show every GameObject that has the <c>hidden</c> tag.</summary>
	public static void ShowAll()
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session?.Scene is not { } scene || !scene.IsValid() )
			return;

		var gos = scene.GetAllObjects( true )
			.Where( go => go.IsValid() && go.Tags.Has( HiddenTag ) )
			.ToArray();

		if ( gos.Length == 0 )
			return;

		using ( session.UndoScope( "Unhide All Object(s)" )
			.WithGameObjectChanges( gos, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in gos )
				go.Tags.Remove( HiddenTag );
		}
	}

	/// <summary>Hide everything except the selection, its ancestors, and its descendants.</summary>
	public static void IsolateSelection()
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session?.Scene is not { } scene || !scene.IsValid() )
			return;

		var selected = EditorScene.Selection
			.OfType<GameObject>()
			.Where( go => go.IsValid() && go is not Scene )
			.ToArray();
		if ( selected.Length == 0 )
			return;

		var visible = new HashSet<GameObject>();
		foreach ( var s in selected )
		{
			// Descendants first — CollectSelfAndDescendants skips if already present.
			CollectSelfAndDescendants( s, visible );

			for ( var go = s.Parent; go is not null and not Scene; go = go.Parent )
				visible.Add( go );
		}

		var changedObjects = scene.GetAllObjects( true )
			.Where( go => go.IsValid() && go is not Scene )
			.Where( go => go.Tags.Has( HiddenTag ) == visible.Contains( go ) )
			.ToArray();
		if ( changedObjects.Length == 0 )
			return;

		using ( session.UndoScope( "Isolate Selection" )
			.WithGameObjectChanges( changedObjects, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in changedObjects )
			{
				if ( visible.Contains( go ) ) go.Tags.Remove( HiddenTag );
				else go.Tags.Add( HiddenTag );
			}
		}
	}

	/// <summary>Apply a HierarchyColor to the current selection.</summary>
	public static void ApplyColorToSelection( Color color, bool includeChildren = false )
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session is null )
			return;

		var gos = CollectSelection( includeChildren );
		if ( gos.Length == 0 )
			return;

		using ( session.UndoScope( "Set Hierarchy Color" )
			.WithGameObjectChanges( gos, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in gos )
			{
				var marker = go.Components.GetOrCreate<HierarchyColor>();
				marker.Tint = color;
			}
		}

		GetHierarchyTree()?.Update();
	}

	/// <summary>Remove HierarchyColor from the current selection.</summary>
	public static void ClearColorFromSelection( bool includeChildren = false )
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session is null )
			return;

		var gos = CollectSelection( includeChildren )
			.Where( go => go.Components.Get<HierarchyColor>( true ) is not null )
			.ToArray();

		if ( gos.Length == 0 )
			return;

		using ( session.UndoScope( "Clear Hierarchy Color" )
			.WithGameObjectChanges( gos, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in gos )
			{
				var marker = go.Components.Get<HierarchyColor>( true );
				marker?.Destroy();
			}
		}

		GetHierarchyTree()?.Update();
	}

	/// <summary>Remove HierarchyColor from every object in the active scene.</summary>
	public static void ClearAllColors()
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session?.Scene is not { } scene || !scene.IsValid() )
			return;

		var gos = scene.GetAllObjects( true )
			.Where( go => go.IsValid() && go is not Scene && go.Components.Get<HierarchyColor>( true ) is not null )
			.ToArray();

		if ( gos.Length == 0 )
			return;

		using ( session.UndoScope( "Clear All Hierarchy Colors" )
			.WithGameObjectChanges( gos, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in gos )
			{
				var marker = go.Components.Get<HierarchyColor>( true );
				marker?.Destroy();
			}
		}

		GetHierarchyTree()?.Update();
	}

	/// <summary>Remove HierarchyColor from every object whose tint matches <paramref name="color"/>.</summary>
	public static void ClearColorsMatching( Color color, float epsilon = 0.02f )
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session?.Scene is not { } scene || !scene.IsValid() )
			return;

		var gos = scene.GetAllObjects( true )
			.Where( go => go.IsValid() && go is not Scene )
			.Where( go =>
			{
				var marker = go.Components.Get<HierarchyColor>( true );
				return marker is not null && marker.IsValid() && ColorsMatch( marker.Tint, color, epsilon );
			} )
			.ToArray();

		if ( gos.Length == 0 )
			return;

		using ( session.UndoScope( "Clear Matching Hierarchy Colors" )
			.WithGameObjectChanges( gos, GameObjectUndoFlags.All )
			.Push() )
		{
			foreach ( var go in gos )
			{
				var marker = go.Components.Get<HierarchyColor>( true );
				marker?.Destroy();
			}
		}

		GetHierarchyTree()?.Update();
	}

	static bool ColorsMatch( Color a, Color b, float epsilon )
	{
		return MathF.Abs( a.r - b.r ) < epsilon
			&& MathF.Abs( a.g - b.g ) < epsilon
			&& MathF.Abs( a.b - b.b ) < epsilon;
	}

	static GameObject[] CollectSelection( bool includeChildren )
	{
		var selected = EditorScene.Selection
			.OfType<GameObject>()
			.Where( go => go.IsValid() && go is not Scene )
			.ToArray();

		if ( !includeChildren || selected.Length == 0 )
			return selected;

		var set = new HashSet<GameObject>();
		foreach ( var root in selected )
			CollectSelfAndDescendants( root, set );

		return set.ToArray();
	}

	static void CollectSelfAndDescendants( GameObject go, HashSet<GameObject> set )
	{
		if ( !go.IsValid() || go is Scene || !set.Add( go ) )
			return;

		foreach ( var child in go.Children )
			CollectSelfAndDescendants( child, set );
	}

	/// <summary>Sort every parent's children A–Z by name (recursive).</summary>
	public static void ReorganizeAlphabetically()
	{
		ReorganizeHierarchy(
			"Reorganize Hierarchy Alphabetically",
			children => children
				.OrderBy( go => go.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase )
				.ThenBy( go => go.Id )
				.ToList() );
	}

	/// <summary>Sort every parent's children by HierarchyColor hue, then name (recursive).</summary>
	public static void ReorganizeByColor()
	{
		ReorganizeHierarchy(
			"Reorganize Hierarchy By Color",
			children => children
				.OrderBy( GetColorSortKey )
				.ThenBy( go => go.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase )
				.ThenBy( go => go.Id )
				.ToList() );
	}

	static void ReorganizeHierarchy( string undoName, Func<List<GameObject>, List<GameObject>> sorter )
	{
		using var scope = SceneEditorSession.Scope();
		var session = SceneEditorSession.Active;
		if ( session?.Scene is not { } scene || !scene.IsValid() )
			return;

		var affected = scene.GetAllObjects( true )
			.Where( go => go.IsValid() && go is not Scene )
			.ToArray();

		if ( affected.Length == 0 )
			return;

		using ( session.UndoScope( undoName )
			.WithGameObjectChanges( affected, GameObjectUndoFlags.All )
			.Push() )
		{
			SortChildrenRecursive( scene, sorter );
		}

		GetHierarchyTree()?.Update();
	}

	static void SortChildrenRecursive( GameObject parent, Func<List<GameObject>, List<GameObject>> sorter )
	{
		if ( !parent.IsValid() )
			return;

		SortChildrenOf( parent, sorter );

		foreach ( var child in parent.Children.ToArray() )
			SortChildrenRecursive( child, sorter );
	}

	static void SortChildrenOf( GameObject parent, Func<List<GameObject>, List<GameObject>> sorter )
	{
		var children = parent.Children
			.Where( go => go.IsValid() && ShouldReorder( go ) )
			.ToList();

		if ( children.Count < 2 )
			return;

		var sorted = sorter( children );
		var alreadyOrdered = true;
		for ( var i = 0; i < sorted.Count; i++ )
		{
			if ( !ReferenceEquals( children[i], sorted[i] ) )
			{
				alreadyOrdered = false;
				break;
			}
		}

		if ( alreadyOrdered )
			return;

		for ( var i = 0; i < sorted.Count; i++ )
		{
			var desired = sorted[i];
			var live = parent.Children.Where( ShouldReorder ).ToList();
			if ( i >= live.Count )
				break;

			if ( ReferenceEquals( live[i], desired ) )
				continue;

			if ( i == 0 )
				live[0].AddSibling( desired, before: true );
			else
				sorted[i - 1].AddSibling( desired, before: false );
		}
	}

	static bool ShouldReorder( GameObject go )
	{
		if ( !go.IsValid() || go is Scene )
			return false;

		// Leave engine-generated hierarchy nodes alone.
		if ( go.Flags.Contains( GameObjectFlags.Bone ) )
			return false;
		if ( go.Flags.Contains( GameObjectFlags.Attachment ) )
			return false;
		if ( go.Flags.Contains( GameObjectFlags.PhysicsBone ) )
			return false;
		if ( go.Flags.Contains( GameObjectFlags.ProceduralBone ) )
			return false;

		return true;
	}

	/// <summary>Colored objects by hue; uncolored last.</summary>
	static float GetColorSortKey( GameObject go )
	{
		var marker = go.Components.Get<HierarchyColor>( true );
		if ( marker is null || !marker.IsValid() )
			return float.MaxValue;

		var hsv = marker.Tint.ToHsv();
		return hsv.Hue;
	}
}
subzerostudios.subhierarchy / HierarchyColor.cs
Game library
using Sandbox;

namespace SubHierarchy;

/// <summary>
/// Marks a GameObject with a Hierarchy row tint color (editor organization).
/// </summary>
[Title( "Hierarchy Color" )]
[Category( "Editor" )]
[Icon( "palette" )]
public sealed class HierarchyColor : Component
{
	/// <summary>Background tint drawn behind the object name in the Hierarchy.</summary>
	[Property]
	public Color Tint { get; set; } = new Color( 0.2f, 0.55f, 0.95f, 1f );
}
subzerostudios.subhierarchy / Code/HierarchyColor.cs
Game library
using Sandbox;

namespace SubHierarchy;

/// <summary>
/// Marks a GameObject with a Hierarchy row tint color (editor organization).
/// </summary>
[Title( "Hierarchy Color" )]
[Category( "Editor" )]
[Icon( "palette" )]
public sealed class HierarchyColor : Component
{
	/// <summary>Background tint drawn behind the object name in the Hierarchy.</summary>
	[Property]
	public Color Tint { get; set; } = new Color( 0.2f, 0.55f, 0.95f, 1f );
}
subzerostudios.subhierarchy / Editor/Assembly.cs
Editor library
global using Sandbox;
global using Editor;
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using SubHierarchy;
Debug: View Raw JSON Response
{
    "TotalCount": 11,
    "Files": [
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"SubHierarchy\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"subhierarchy\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"subzerostudios\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"subzerostudios.subhierarchy\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"29\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-09-14T18:56:09.5154493Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.118.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.118.0\")]"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Editor/HierarchyColorPainter.cs",
            "FileName": "HierarchyColorPainter.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "using System.Collections;\r\nusing System.Reflection;\r\n\r\nnamespace SubHierarchy;\r\n\r\n/// <summary>\r\n/// Tints Hierarchy rows for GameObjects that have a <see cref=\"HierarchyColor\"/> component.\r\n/// Hooks the built-in Hierarchy TreeView paint (GameObjectNode is internal).\r\n/// </summary>\r\npublic static class HierarchyColorPainter\r\n{\r\n\tconst float TintAlpha = 0.38f;\r\n\tconst BindingFlags LayoutFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;\r\n\r\n\tstatic TreeView _hookedTree;\r\n\tstatic PropertyInfo _itemLayoutsProperty;\r\n\tstatic FieldInfo _itemLayoutsField;\r\n\tstatic bool _layoutsLookupDone;\r\n\r\n\t[Event( \"editor.created\" )]\r\n\tstatic void OnEditorCreated( object _ ) => EnsureHooked();\r\n\r\n\t[EditorEvent.Frame]\r\n\tstatic void OnFrame() => EnsureHooked();\r\n\r\n\tpublic static void EnsureHooked()\r\n\t{\r\n\t\tvar tree = SceneTreeWidget.Current?.TreeView;\r\n\t\tif ( tree is null || !tree.IsValid() )\r\n\t\t{\r\n\t\t\t_hookedTree = null;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Always re-apply: Hierarchy BuildUI can replace TreeView or reset OnPaintOverride.\r\n\t\tif ( !ReferenceEquals( _hookedTree, tree ) )\r\n\t\t\t_hookedTree = tree;\r\n\r\n\t\ttree.OnPaintOverride = PaintHierarchyBackground;\r\n\t}\r\n\r\n\tstatic bool PaintHierarchyBackground()\r\n\t{\r\n\t\tvar tree = _hookedTree;\r\n\t\tif ( tree is null || !tree.IsValid() )\r\n\t\t\treturn false;\r\n\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Theme.ControlBackground );\r\n\t\tPaint.DrawRect( tree.LocalRect, Theme.ControlRadius );\r\n\r\n\t\tPaintColoredRows( tree );\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic void PaintColoredRows( TreeView tree )\r\n\t{\r\n\t\tvar layouts = GetItemLayouts( tree );\r\n\t\tif ( layouts is not null )\r\n\t\t{\r\n\t\t\tforeach ( var entry in layouts )\r\n\t\t\t{\r\n\t\t\t\tif ( entry is not VirtualWidget item )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tif ( !TryResolveGameObject( item.Object, out var go ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\t\t\tif ( marker is null || !marker.IsValid() )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// ItemLayouts rects are already in paint / scrolled local space.\r\n\t\t\t\tPaintRow( tree, item.Rect, marker.Tint );\r\n\t\t\t}\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Fallback: TryGetItemRect is content-space \u2014 convert using scrollbar value.\r\n\t\tPaintColoredRowsFallback( tree );\r\n\t}\r\n\r\n\tstatic void PaintColoredRowsFallback( TreeView tree )\r\n\t{\r\n\t\tvar scene = SceneEditorSession.Active?.Scene;\r\n\t\tif ( scene is null || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar scroll = tree.VerticalScrollbar?.Value ?? 0f;\r\n\t\tvar originY = tree.LocalRect.Top + scroll - tree.Margin.Top;\r\n\r\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\r\n\t\t{\r\n\t\t\tif ( !go.IsValid() || go is Scene )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\t\tif ( marker is null || !marker.IsValid() )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( !tree.TryGetItemRect( go, out var contentRect ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar rect = contentRect;\r\n\t\t\trect.Top -= originY;\r\n\t\t\tPaintRow( tree, rect, marker.Tint );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void PaintRow( TreeView tree, Rect rect, Color color )\r\n\t{\r\n\t\trect.Left = 0;\r\n\t\trect.Right = tree.Width;\r\n\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( color.WithAlpha( TintAlpha ) );\r\n\t\tPaint.DrawRect( rect );\r\n\t}\r\n\r\n\tstatic bool TryResolveGameObject( object obj, out GameObject go )\r\n\t{\r\n\t\tgo = null;\r\n\r\n\t\tif ( obj is TreeNode node )\r\n\t\t\tgo = node.Value as GameObject;\r\n\t\telse\r\n\t\t\tgo = obj as GameObject;\r\n\r\n\t\treturn go.IsValid() && go is not Scene;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Visible row layouts live on BaseItemWidget as a protected member.\r\n\t/// Those rects match what Hierarchy paints; TryGetItemRect is content-space (scroll Y) and misaligns.\r\n\t/// </summary>\r\n\tstatic IEnumerable GetItemLayouts( TreeView tree )\r\n\t{\r\n\t\tif ( !_layoutsLookupDone )\r\n\t\t{\r\n\t\t\t_layoutsLookupDone = true;\r\n\t\t\tvar type = typeof( BaseItemWidget );\r\n\t\t\t_itemLayoutsProperty = type.GetProperty( \"ItemLayouts\", LayoutFlags );\r\n\t\t\t_itemLayoutsField = type.GetField( \"ItemLayouts\", LayoutFlags );\r\n\t\t}\r\n\r\n\t\tobject value = null;\r\n\t\tif ( _itemLayoutsProperty is not null )\r\n\t\t\tvalue = _itemLayoutsProperty.GetValue( tree );\r\n\t\telse if ( _itemLayoutsField is not null )\r\n\t\t\tvalue = _itemLayoutsField.GetValue( tree );\r\n\r\n\t\treturn value as IEnumerable;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Editor/SubHierarchyDock.cs",
            "FileName": "SubHierarchyDock.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "namespace SubHierarchy;\r\n\r\n/// <summary>\r\n/// Separate dock with Hierarchy helpers: collapse/expand, hide/show, row colors.\r\n/// </summary>\r\n[Dock( \"Editor\", \"Sub Hierarchy\", \"account_tree\" )]\r\npublic sealed class SubHierarchyDock : Widget\r\n{\r\n\tpublic const string DockTitle = \"Sub Hierarchy\";\r\n\tconst string CustomColorsCookie = \"SubHierarchy.CustomColors\";\r\n\tconst int MaxCustomColors = 24;\r\n\tconst float ColorMatchEpsilon = 0.02f;\r\n\r\n\tstatic readonly Color[] Presets =\r\n\t[\r\n\t\tnew Color( 0.85f, 0.25f, 0.25f ), // red\r\n\t\tnew Color( 0.90f, 0.50f, 0.15f ), // orange\r\n\t\tnew Color( 0.90f, 0.80f, 0.20f ), // yellow\r\n\t\tnew Color( 0.55f, 0.85f, 0.20f ), // lime\r\n\t\tnew Color( 0.30f, 0.75f, 0.35f ), // green\r\n\t\tnew Color( 0.15f, 0.70f, 0.65f ), // teal\r\n\t\tnew Color( 0.20f, 0.75f, 0.90f ), // cyan\r\n\t\tnew Color( 0.25f, 0.55f, 0.95f ), // blue\r\n\t\tnew Color( 0.40f, 0.35f, 0.90f ), // indigo\r\n\t\tnew Color( 0.65f, 0.35f, 0.90f ), // purple\r\n\t\tnew Color( 0.90f, 0.35f, 0.70f ), // magenta\r\n\t\tnew Color( 0.90f, 0.45f, 0.55f ), // pink\r\n\t\tnew Color( 0.70f, 0.50f, 0.30f ), // brown\r\n\t];\r\n\r\n\tColor _draftColor = new Color( 0.25f, 0.55f, 0.95f );\r\n\tColorSwatchButton _swatch;\r\n\tbool _colorChildrenRecursively;\r\n\tLayout _content;\r\n\tColorSwatchPalette _palette;\r\n\tList<Color> _customColors = new();\r\n\r\n\tpublic SubHierarchyDock( Widget parent ) : base( parent )\r\n\t{\r\n\t\tLayout = Layout.Column();\r\n\r\n\t\tvar scroller = Layout.Add( new ScrollArea( this ), 1 );\r\n\t\tscroller.Canvas = new Widget( scroller )\r\n\t\t{\r\n\t\t\tHorizontalSizeMode = SizeMode.Flexible,\r\n\t\t\tVerticalSizeMode = SizeMode.CanGrow\r\n\t\t};\r\n\t\tscroller.Canvas.Layout = Layout.Column();\r\n\t\tscroller.Canvas.Layout.Margin = 8;\r\n\t\tscroller.Canvas.Layout.Spacing = 8;\r\n\t\t_content = scroller.Canvas.Layout;\r\n\r\n\t\tLoadCustomColors();\r\n\t\tBuildUi();\r\n\t\tHierarchyColorPainter.EnsureHooked();\r\n\t}\r\n\r\n\tpublic static void Open()\r\n\t{\r\n\t\tEditorWindow.DockManager.SetDockState( DockTitle, true );\r\n\t\tEditorWindow.DockManager.RaiseDock( DockTitle );\r\n\t}\r\n\r\n\tvoid BuildUi()\r\n\t{\r\n\t\t_content.Clear( true );\r\n\r\n\t\tAddSectionLabel( \"Tree\" );\r\n\t\tAddButtonRow(\r\n\t\t\t( \"Collapse All\", \"unfold_less\", HierarchyTreeActions.CollapseAll ),\r\n\t\t\t( \"Expand All\", \"unfold_more\", HierarchyTreeActions.ExpandAll )\r\n\t\t);\r\n\r\n\t\tAddSectionLabel( \"Visibility\" );\r\n\t\tAddButtonRow(\r\n\t\t\t( \"Hide All\", \"visibility_off\", HierarchyTreeActions.HideAll ),\r\n\t\t\t( \"Show All\", \"visibility\", HierarchyTreeActions.ShowAll )\r\n\t\t);\r\n\t\tAddButtonRow(\r\n\t\t\t( \"Isolate Selection\", \"filter_center_focus\", HierarchyTreeActions.IsolateSelection )\r\n\t\t);\r\n\r\n\t\tAddSectionLabel( \"Hierarchy Color\" );\r\n\t\tAddColorControls();\r\n\t\tRebuildColorSwatches();\r\n\r\n\t\tAddSectionLabel( \"Reorganize\" );\r\n\t\tAddButtonRow(\r\n\t\t\t( \"Sort Alphabetically\", \"sort_by_alpha\", HierarchyTreeActions.ReorganizeAlphabetically ),\r\n\t\t\t( \"Sort by Color\", \"palette\", HierarchyTreeActions.ReorganizeByColor )\r\n\t\t);\r\n\r\n\t\t_content.AddStretchCell( 1 );\r\n\t}\r\n\r\n\tvoid AddSectionLabel( string text )\r\n\t{\r\n\t\tvar label = _content.Add( new Label( text ) );\r\n\t\tlabel.Color = Theme.TextControl.WithAlpha( 0.85f );\r\n\t}\r\n\r\n\tvoid AddButtonRow( params (string Title, string Icon, Action Action)[] buttons )\r\n\t{\r\n\t\tvar row = _content.AddRow();\r\n\t\trow.Spacing = 6;\r\n\r\n\t\tforeach ( var button in buttons )\r\n\t\t{\r\n\t\t\tvar btn = row.Add( new Button( button.Title, button.Icon ), 1 );\r\n\t\t\tbtn.Clicked = button.Action;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid AddColorControls()\r\n\t{\r\n\t\tvar row = _content.AddRow();\r\n\t\trow.Spacing = 6;\r\n\t\trow.Alignment = TextFlag.LeftCenter;\r\n\r\n\t\t_swatch = row.Add( new ColorSwatchButton( () => _draftColor, OnCustomColorPicked )\r\n\t\t{\r\n\t\t\tRememberOnClose = true,\r\n\t\t\tOnRemember = RememberCustomColor\r\n\t\t} );\r\n\t\t_swatch.ToolTip = \"Pick a color and apply it to the selection\";\r\n\t\t_swatch.FixedWidth = Theme.RowHeight * 2;\r\n\t\t_swatch.FixedHeight = Theme.RowHeight;\r\n\r\n\t\tvar clear = row.Add( new Button( \"Clear\", \"format_color_reset\" ), 1 );\r\n\t\tclear.ToolTip = \"Remove Hierarchy Color from selection\";\r\n\t\tclear.Clicked = () => HierarchyTreeActions.ClearColorFromSelection( _colorChildrenRecursively );\r\n\r\n\t\tvar clearAll = row.Add( new Button( \"Clear All\", \"delete_sweep\" ), 1 );\r\n\t\tclearAll.ToolTip = \"Remove Hierarchy Color from every object in the scene\";\r\n\t\tclearAll.Clicked = HierarchyTreeActions.ClearAllColors;\r\n\r\n\t\tvar recursiveRow = _content.AddRow();\r\n\t\trecursiveRow.Spacing = 6;\r\n\t\trecursiveRow.Alignment = TextFlag.LeftCenter;\r\n\r\n\t\tvar recursive = recursiveRow.Add( new Checkbox( \"Color children recursively\" ) );\r\n\t\trecursive.State = _colorChildrenRecursively ? CheckState.On : CheckState.Off;\r\n\t\trecursive.ToolTip = \"When applying or clearing, include all descendants of the selection\";\r\n\t\trecursive.Clicked = () => _colorChildrenRecursively = recursive.State == CheckState.On;\r\n\r\n\t\t_palette = _content.Add( new ColorSwatchPalette( OnCustomColorPicked, OnColorSwatchRemoved ) );\r\n\t\t_palette.HorizontalSizeMode = SizeMode.Flexible;\r\n\t}\r\n\r\n\tvoid RebuildColorSwatches()\r\n\t{\r\n\t\tif ( _palette is null || !_palette.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar entries = new List<(Color Color, string ToolTip, bool Removable)>();\r\n\t\tforeach ( var preset in Presets )\r\n\t\t\tentries.Add( (preset, \"Left-click: apply\", false) );\r\n\r\n\t\tforeach ( var custom in _customColors )\r\n\t\t\tentries.Add( (custom, \"Left-click: apply. Right-click: remove from palette and all objects.\", true) );\r\n\r\n\t\t_palette.SetColors( entries );\r\n\t}\r\n\r\n\tvoid OnCustomColorPicked( Color color )\r\n\t{\r\n\t\t_draftColor = color.WithAlpha( 1f );\r\n\t\t_swatch?.Update();\r\n\t\tHierarchyTreeActions.ApplyColorToSelection( _draftColor, _colorChildrenRecursively );\r\n\t}\r\n\r\n\tvoid OnColorSwatchRemoved( Color color )\r\n\t{\r\n\t\tcolor = color.WithAlpha( 1f );\r\n\r\n\t\t// Built-in presets stay in the palette permanently.\r\n\t\tif ( Presets.Any( p => ColorsMatch( p, color ) ) )\r\n\t\t\treturn;\r\n\r\n\t\tvar removedCustom = _customColors.RemoveAll( c => ColorsMatch( c, color ) ) > 0;\r\n\t\tif ( !removedCustom )\r\n\t\t\treturn;\r\n\r\n\t\tHierarchyTreeActions.ClearColorsMatching( color, ColorMatchEpsilon );\r\n\t\tSaveCustomColors();\r\n\r\n\t\tif ( ColorsMatch( _draftColor, color ) )\r\n\t\t\t_draftColor = Presets[0];\r\n\r\n\t\t_swatch?.Update();\r\n\t\tRebuildColorSwatches();\r\n\t}\r\n\r\n\tvoid RememberCustomColor( Color color )\r\n\t{\r\n\t\tcolor = color.WithAlpha( 1f );\r\n\r\n\t\tif ( Presets.Any( p => ColorsMatch( p, color ) ) )\r\n\t\t\treturn;\r\n\r\n\t\tif ( _customColors.Any( c => ColorsMatch( c, color ) ) )\r\n\t\t\treturn;\r\n\r\n\t\t_customColors.Add( color );\r\n\t\twhile ( _customColors.Count > MaxCustomColors )\r\n\t\t\t_customColors.RemoveAt( 0 );\r\n\r\n\t\tSaveCustomColors();\r\n\t\tRebuildColorSwatches();\r\n\t}\r\n\r\n\tvoid LoadCustomColors()\r\n\t{\r\n\t\t_customColors = EditorCookie.Get( CustomColorsCookie, new List<Color>() ) ?? new List<Color>();\r\n\t}\r\n\r\n\tvoid SaveCustomColors()\r\n\t{\r\n\t\tEditorCookie.Set( CustomColorsCookie, _customColors );\r\n\t}\r\n\r\n\tstatic bool ColorsMatch( Color a, Color b )\r\n\t{\r\n\t\treturn MathF.Abs( a.r - b.r ) < ColorMatchEpsilon\r\n\t\t\t&& MathF.Abs( a.g - b.g ) < ColorMatchEpsilon\r\n\t\t\t&& MathF.Abs( a.b - b.b ) < ColorMatchEpsilon;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Wraps color swatches to the available width, with at most 10 per row.\r\n\t/// </summary>\r\n\tsealed class ColorSwatchPalette : Widget\r\n\t{\r\n\t\tconst int MaxPerRow = 10;\r\n\t\tconst float Spacing = 4f;\r\n\r\n\t\treadonly Action<Color> _onPick;\r\n\t\treadonly Action<Color> _onRemove;\r\n\t\treadonly List<(Color Color, string ToolTip, bool Removable)> _entries = new();\r\n\t\tint _builtPerRow = -1;\r\n\r\n\t\tpublic ColorSwatchPalette( Action<Color> onPick, Action<Color> onRemove ) : base( null )\r\n\t\t{\r\n\t\t\t_onPick = onPick;\r\n\t\t\t_onRemove = onRemove;\r\n\t\t\tHorizontalSizeMode = SizeMode.Flexible;\r\n\t\t\tVerticalSizeMode = SizeMode.CanGrow;\r\n\t\t\tLayout = Layout.Column();\r\n\t\t\tLayout.Spacing = Spacing;\r\n\t\t}\r\n\r\n\t\tpublic void SetColors( IEnumerable<(Color Color, string ToolTip, bool Removable)> entries )\r\n\t\t{\r\n\t\t\t_entries.Clear();\r\n\t\t\t_entries.AddRange( entries );\r\n\t\t\t_builtPerRow = -1;\r\n\t\t\tRebuild();\r\n\t\t}\r\n\r\n\t\tprotected override void DoLayout()\r\n\t\t{\r\n\t\t\tbase.DoLayout();\r\n\r\n\t\t\tvar perRow = CalcPerRow();\r\n\t\t\tif ( perRow != _builtPerRow )\r\n\t\t\t\tRebuild();\r\n\t\t}\r\n\r\n\t\tint CalcPerRow()\r\n\t\t{\r\n\t\t\tvar cell = Theme.RowHeight + Spacing;\r\n\t\t\tif ( cell <= 0f || Width <= 1f )\r\n\t\t\t\treturn MaxPerRow;\r\n\r\n\t\t\tvar fit = Math.Max( 1, (int)( (Width + Spacing) / cell ) );\r\n\t\t\treturn Math.Min( MaxPerRow, fit );\r\n\t\t}\r\n\r\n\t\tvoid Rebuild()\r\n\t\t{\r\n\t\t\tvar perRow = CalcPerRow();\r\n\t\t\t_builtPerRow = perRow;\r\n\r\n\t\t\tLayout.Clear( true );\r\n\t\t\tLayout.Spacing = Spacing;\r\n\r\n\t\t\tLayout row = null;\r\n\t\t\tvar countInRow = 0;\r\n\r\n\t\t\tforeach ( var entry in _entries )\r\n\t\t\t{\r\n\t\t\t\tif ( row is null || countInRow >= perRow )\r\n\t\t\t\t{\r\n\t\t\t\t\trow = Layout.AddRow();\r\n\t\t\t\t\trow.Spacing = Spacing;\r\n\t\t\t\t\trow.Alignment = TextFlag.LeftCenter;\r\n\t\t\t\t\tcountInRow = 0;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar color = entry.Color;\r\n\t\t\t\tvar removable = entry.Removable;\r\n\t\t\t\tvar swatch = row.Add( new ColorSwatchButton(\r\n\t\t\t\t\t() => color,\r\n\t\t\t\t\tc => _onPick( c ),\r\n\t\t\t\t\tremovable ? () => _onRemove( color ) : null ) );\r\n\t\t\t\tswatch.FixedWidth = Theme.RowHeight;\r\n\t\t\t\tswatch.FixedHeight = Theme.RowHeight;\r\n\t\t\t\tswatch.ToolTip = entry.ToolTip;\r\n\t\t\t\tswatch.OpenPickerOnClick = false;\r\n\t\t\t\tcountInRow++;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Simple color swatch; left-click opens the engine color picker (optional).</summary>\r\n\tsealed class ColorSwatchButton : Widget\r\n\t{\r\n\t\treadonly Func<Color> _getColor;\r\n\t\treadonly Action<Color> _setColor;\r\n\t\treadonly Action _onRemove;\r\n\r\n\t\tpublic bool OpenPickerOnClick { get; set; } = true;\r\n\t\tpublic bool RememberOnClose { get; set; }\r\n\t\tpublic Action<Color> OnRemember { get; set; }\r\n\r\n\t\tpublic ColorSwatchButton( Func<Color> getColor, Action<Color> setColor, Action onRemove = null ) : base( null )\r\n\t\t{\r\n\t\t\t_getColor = getColor;\r\n\t\t\t_setColor = setColor;\r\n\t\t\t_onRemove = onRemove;\r\n\t\t\tCursor = CursorShape.Finger;\r\n\t\t\tMouseLeftPress = OnClicked;\r\n\t\t\tMouseRightPress = OnRightClicked;\r\n\t\t}\r\n\r\n\t\tvoid OnClicked()\r\n\t\t{\r\n\t\t\tif ( !OpenPickerOnClick )\r\n\t\t\t{\r\n\t\t\t\t_setColor( _getColor() );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tvar popup = new ColorPopup( null );\r\n\t\t\tpopup.FixedWidth = 233;\r\n\t\t\tpopup.Layout = Layout.Column();\r\n\t\t\tpopup.Position = Editor.Application.CursorPosition;\r\n\r\n\t\t\tvar colorPicker = popup.Layout.Add( new ColorPicker( popup, _getColor() ), 1 );\r\n\t\t\tcolorPicker.OnChildValuesChanged += _ =>\r\n\t\t\t{\r\n\t\t\t\t_setColor( colorPicker.Value );\r\n\t\t\t\tUpdate();\r\n\t\t\t};\r\n\r\n\t\t\tif ( RememberOnClose )\r\n\t\t\t{\r\n\t\t\t\tpopup.Closed = () => OnRemember?.Invoke( colorPicker.Value );\r\n\t\t\t}\r\n\r\n\t\t\tpopup.Show();\r\n\t\t\tpopup.ConstrainToScreen();\r\n\t\t}\r\n\r\n\t\tvoid OnRightClicked()\r\n\t\t{\r\n\t\t\t_onRemove?.Invoke();\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tvar color = _getColor();\r\n\t\t\tconst float radius = 3f;\r\n\t\t\tconst float border = 1.25f;\r\n\r\n\t\t\tPaint.Antialiasing = true;\r\n\r\n\t\t\t// Solid white rounded plate first so color can't bleed past the corners.\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tPaint.SetBrush( Color.White );\r\n\t\t\tPaint.DrawRect( LocalRect, radius );\r\n\r\n\t\t\tvar inner = LocalRect.Shrink( border );\r\n\t\t\tvar innerRadius = Math.Max( radius - border * 0.5f, 0f );\r\n\t\t\tPaint.SetBrush( color );\r\n\t\t\tPaint.DrawRect( inner, innerRadius );\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Popup that notifies when closed so custom colors can be saved once.</summary>\r\n\tsealed class ColorPopup : PopupWidget\r\n\t{\r\n\t\tpublic Action Closed { get; set; }\r\n\r\n\t\tpublic ColorPopup( Widget parent ) : base( parent )\r\n\t\t{\r\n\t\t}\r\n\r\n\t\tprotected override void OnClosed()\r\n\t\t{\r\n\t\t\tClosed?.Invoke();\r\n\t\t\tbase.OnClosed();\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// <summary>Menu + View registration for Sub Hierarchy.</summary>\r\npublic static class SubHierarchyMenus\r\n{\r\n\t[Menu( \"Editor\", \"Sub Hierarchy/Open\", \"account_tree\" )]\r\n\tpublic static void Open() => SubHierarchyDock.Open();\r\n\r\n\t[Event( \"tools.editorwindow.postcreateview\" )]\r\n\tstatic void AddViewMenu( Menu menu )\r\n\t{\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \"Sub Hierarchy\", \"account_tree\", () => SubHierarchyDock.Open() );\r\n\t}\r\n\r\n\t[Event( \"editor.created\" )]\r\n\tstatic void OnEditorCreated( object _ ) => HierarchyColorPainter.EnsureHooked();\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Code/Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "global using Sandbox;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "UnitTests/UnitTest.cs",
            "FileName": "UnitTest.cs",
            "PackageType": "library",
            "CodeKind": "UnitTest",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "global using Microsoft.VisualStudio.TestTools.UnitTesting;\r\n\r\n[TestClass]\r\npublic class TestInit\r\n{\r\n\tpublic static Sandbox.TestAppSystem AppSystem;\r\n\r\n\t[AssemblyInitialize]\r\n\tpublic static void AssemblyInitialize( TestContext context )\r\n\t{\r\n\t\tAppSystem = new Sandbox.TestAppSystem();\r\n\t\tAppSystem.Init();\r\n\t}\r\n\r\n\t[AssemblyCleanup]\r\n\tpublic static void AssemblyCleanup()\r\n\t{\r\n\t\tAppSystem.Shutdown();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "global using Sandbox;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "UnitTests/LibraryTest.cs",
            "FileName": "LibraryTest.cs",
            "PackageType": "library",
            "CodeKind": "UnitTest",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\n[TestClass]\r\npublic partial class LibraryTests\r\n{\r\n\t[TestMethod]\r\n\tpublic void SceneTest()\r\n\t{\r\n\t\tvar scene = new Scene();\r\n\t\tusing ( scene.Push() )\r\n\t\t{\r\n\t\t\tvar go = new GameObject();\r\n\r\n\t\t\tAssert.AreEqual( 1, scene.Directory.GameObjectCount );\r\n\t\t}\r\n\t}\r\n\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Editor/HierarchyTreeActions.cs",
            "FileName": "HierarchyTreeActions.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "namespace SubHierarchy;\r\n\r\n/// <summary>\r\n/// Actions that drive the built-in Hierarchy / scene visibility.\r\n/// </summary>\r\npublic static class HierarchyTreeActions\r\n{\r\n\tconst string HiddenTag = \"hidden\";\r\n\r\n\tpublic static TreeView GetHierarchyTree()\r\n\t{\r\n\t\treturn SceneTreeWidget.Current?.TreeView;\r\n\t}\r\n\r\n\tpublic static Scene GetActiveScene()\r\n\t{\r\n\t\treturn SceneEditorSession.Active?.Scene;\r\n\t}\r\n\r\n\t/// <summary>Collapse every open group; keep the scene root expanded.</summary>\r\n\tpublic static void CollapseAll()\r\n\t{\r\n\t\tvar tree = GetHierarchyTree();\r\n\t\tvar scene = GetActiveScene();\r\n\t\tif ( tree is null || scene is null || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\ttree.Close( scene, recursive: true );\r\n\t\ttree.Open( scene );\r\n\t\ttree.Update();\r\n\t}\r\n\r\n\t/// <summary>Expand every group under the scene root.</summary>\r\n\tpublic static void ExpandAll()\r\n\t{\r\n\t\tvar tree = GetHierarchyTree();\r\n\t\tvar scene = GetActiveScene();\r\n\t\tif ( tree is null || scene is null || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\ttree.Open( scene, recursive: true );\r\n\t\ttree.Update();\r\n\t}\r\n\r\n\t/// <summary>Hide every GameObject in the scene (same <c>hidden</c> tag as H / U).</summary>\r\n\tpublic static void HideAll()\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session?.Scene is not { } scene || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar gos = scene.GetAllObjects( true )\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene && !go.Tags.Has( HiddenTag ) )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( gos.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Hide All Object(s)\" )\r\n\t\t\t.WithGameObjectChanges( gos, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in gos )\r\n\t\t\t\tgo.Tags.Add( HiddenTag );\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Show every GameObject that has the <c>hidden</c> tag.</summary>\r\n\tpublic static void ShowAll()\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session?.Scene is not { } scene || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar gos = scene.GetAllObjects( true )\r\n\t\t\t.Where( go => go.IsValid() && go.Tags.Has( HiddenTag ) )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( gos.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Unhide All Object(s)\" )\r\n\t\t\t.WithGameObjectChanges( gos, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in gos )\r\n\t\t\t\tgo.Tags.Remove( HiddenTag );\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Hide everything except the selection, its ancestors, and its descendants.</summary>\r\n\tpublic static void IsolateSelection()\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session?.Scene is not { } scene || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar selected = EditorScene.Selection\r\n\t\t\t.OfType<GameObject>()\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene )\r\n\t\t\t.ToArray();\r\n\t\tif ( selected.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar visible = new HashSet<GameObject>();\r\n\t\tforeach ( var s in selected )\r\n\t\t{\r\n\t\t\t// Descendants first \u2014 CollectSelfAndDescendants skips if already present.\r\n\t\t\tCollectSelfAndDescendants( s, visible );\r\n\r\n\t\t\tfor ( var go = s.Parent; go is not null and not Scene; go = go.Parent )\r\n\t\t\t\tvisible.Add( go );\r\n\t\t}\r\n\r\n\t\tvar changedObjects = scene.GetAllObjects( true )\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene )\r\n\t\t\t.Where( go => go.Tags.Has( HiddenTag ) == visible.Contains( go ) )\r\n\t\t\t.ToArray();\r\n\t\tif ( changedObjects.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Isolate Selection\" )\r\n\t\t\t.WithGameObjectChanges( changedObjects, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in changedObjects )\r\n\t\t\t{\r\n\t\t\t\tif ( visible.Contains( go ) ) go.Tags.Remove( HiddenTag );\r\n\t\t\t\telse go.Tags.Add( HiddenTag );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Apply a HierarchyColor to the current selection.</summary>\r\n\tpublic static void ApplyColorToSelection( Color color, bool includeChildren = false )\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar gos = CollectSelection( includeChildren );\r\n\t\tif ( gos.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Set Hierarchy Color\" )\r\n\t\t\t.WithGameObjectChanges( gos, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in gos )\r\n\t\t\t{\r\n\t\t\t\tvar marker = go.Components.GetOrCreate<HierarchyColor>();\r\n\t\t\t\tmarker.Tint = color;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tGetHierarchyTree()?.Update();\r\n\t}\r\n\r\n\t/// <summary>Remove HierarchyColor from the current selection.</summary>\r\n\tpublic static void ClearColorFromSelection( bool includeChildren = false )\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar gos = CollectSelection( includeChildren )\r\n\t\t\t.Where( go => go.Components.Get<HierarchyColor>( true ) is not null )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( gos.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Clear Hierarchy Color\" )\r\n\t\t\t.WithGameObjectChanges( gos, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in gos )\r\n\t\t\t{\r\n\t\t\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\t\t\tmarker?.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tGetHierarchyTree()?.Update();\r\n\t}\r\n\r\n\t/// <summary>Remove HierarchyColor from every object in the active scene.</summary>\r\n\tpublic static void ClearAllColors()\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session?.Scene is not { } scene || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar gos = scene.GetAllObjects( true )\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene && go.Components.Get<HierarchyColor>( true ) is not null )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( gos.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Clear All Hierarchy Colors\" )\r\n\t\t\t.WithGameObjectChanges( gos, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in gos )\r\n\t\t\t{\r\n\t\t\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\t\t\tmarker?.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tGetHierarchyTree()?.Update();\r\n\t}\r\n\r\n\t/// <summary>Remove HierarchyColor from every object whose tint matches <paramref name=\"color\"/>.</summary>\r\n\tpublic static void ClearColorsMatching( Color color, float epsilon = 0.02f )\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session?.Scene is not { } scene || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar gos = scene.GetAllObjects( true )\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene )\r\n\t\t\t.Where( go =>\r\n\t\t\t{\r\n\t\t\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\t\t\treturn marker is not null && marker.IsValid() && ColorsMatch( marker.Tint, color, epsilon );\r\n\t\t\t} )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( gos.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( \"Clear Matching Hierarchy Colors\" )\r\n\t\t\t.WithGameObjectChanges( gos, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tforeach ( var go in gos )\r\n\t\t\t{\r\n\t\t\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\t\t\tmarker?.Destroy();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tGetHierarchyTree()?.Update();\r\n\t}\r\n\r\n\tstatic bool ColorsMatch( Color a, Color b, float epsilon )\r\n\t{\r\n\t\treturn MathF.Abs( a.r - b.r ) < epsilon\r\n\t\t\t&& MathF.Abs( a.g - b.g ) < epsilon\r\n\t\t\t&& MathF.Abs( a.b - b.b ) < epsilon;\r\n\t}\r\n\r\n\tstatic GameObject[] CollectSelection( bool includeChildren )\r\n\t{\r\n\t\tvar selected = EditorScene.Selection\r\n\t\t\t.OfType<GameObject>()\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( !includeChildren || selected.Length == 0 )\r\n\t\t\treturn selected;\r\n\r\n\t\tvar set = new HashSet<GameObject>();\r\n\t\tforeach ( var root in selected )\r\n\t\t\tCollectSelfAndDescendants( root, set );\r\n\r\n\t\treturn set.ToArray();\r\n\t}\r\n\r\n\tstatic void CollectSelfAndDescendants( GameObject go, HashSet<GameObject> set )\r\n\t{\r\n\t\tif ( !go.IsValid() || go is Scene || !set.Add( go ) )\r\n\t\t\treturn;\r\n\r\n\t\tforeach ( var child in go.Children )\r\n\t\t\tCollectSelfAndDescendants( child, set );\r\n\t}\r\n\r\n\t/// <summary>Sort every parent's children A\u2013Z by name (recursive).</summary>\r\n\tpublic static void ReorganizeAlphabetically()\r\n\t{\r\n\t\tReorganizeHierarchy(\r\n\t\t\t\"Reorganize Hierarchy Alphabetically\",\r\n\t\t\tchildren => children\r\n\t\t\t\t.OrderBy( go => go.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase )\r\n\t\t\t\t.ThenBy( go => go.Id )\r\n\t\t\t\t.ToList() );\r\n\t}\r\n\r\n\t/// <summary>Sort every parent's children by HierarchyColor hue, then name (recursive).</summary>\r\n\tpublic static void ReorganizeByColor()\r\n\t{\r\n\t\tReorganizeHierarchy(\r\n\t\t\t\"Reorganize Hierarchy By Color\",\r\n\t\t\tchildren => children\r\n\t\t\t\t.OrderBy( GetColorSortKey )\r\n\t\t\t\t.ThenBy( go => go.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase )\r\n\t\t\t\t.ThenBy( go => go.Id )\r\n\t\t\t\t.ToList() );\r\n\t}\r\n\r\n\tstatic void ReorganizeHierarchy( string undoName, Func<List<GameObject>, List<GameObject>> sorter )\r\n\t{\r\n\t\tusing var scope = SceneEditorSession.Scope();\r\n\t\tvar session = SceneEditorSession.Active;\r\n\t\tif ( session?.Scene is not { } scene || !scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar affected = scene.GetAllObjects( true )\r\n\t\t\t.Where( go => go.IsValid() && go is not Scene )\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( affected.Length == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tusing ( session.UndoScope( undoName )\r\n\t\t\t.WithGameObjectChanges( affected, GameObjectUndoFlags.All )\r\n\t\t\t.Push() )\r\n\t\t{\r\n\t\t\tSortChildrenRecursive( scene, sorter );\r\n\t\t}\r\n\r\n\t\tGetHierarchyTree()?.Update();\r\n\t}\r\n\r\n\tstatic void SortChildrenRecursive( GameObject parent, Func<List<GameObject>, List<GameObject>> sorter )\r\n\t{\r\n\t\tif ( !parent.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tSortChildrenOf( parent, sorter );\r\n\r\n\t\tforeach ( var child in parent.Children.ToArray() )\r\n\t\t\tSortChildrenRecursive( child, sorter );\r\n\t}\r\n\r\n\tstatic void SortChildrenOf( GameObject parent, Func<List<GameObject>, List<GameObject>> sorter )\r\n\t{\r\n\t\tvar children = parent.Children\r\n\t\t\t.Where( go => go.IsValid() && ShouldReorder( go ) )\r\n\t\t\t.ToList();\r\n\r\n\t\tif ( children.Count < 2 )\r\n\t\t\treturn;\r\n\r\n\t\tvar sorted = sorter( children );\r\n\t\tvar alreadyOrdered = true;\r\n\t\tfor ( var i = 0; i < sorted.Count; i++ )\r\n\t\t{\r\n\t\t\tif ( !ReferenceEquals( children[i], sorted[i] ) )\r\n\t\t\t{\r\n\t\t\t\talreadyOrdered = false;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( alreadyOrdered )\r\n\t\t\treturn;\r\n\r\n\t\tfor ( var i = 0; i < sorted.Count; i++ )\r\n\t\t{\r\n\t\t\tvar desired = sorted[i];\r\n\t\t\tvar live = parent.Children.Where( ShouldReorder ).ToList();\r\n\t\t\tif ( i >= live.Count )\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tif ( ReferenceEquals( live[i], desired ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( i == 0 )\r\n\t\t\t\tlive[0].AddSibling( desired, before: true );\r\n\t\t\telse\r\n\t\t\t\tsorted[i - 1].AddSibling( desired, before: false );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic bool ShouldReorder( GameObject go )\r\n\t{\r\n\t\tif ( !go.IsValid() || go is Scene )\r\n\t\t\treturn false;\r\n\r\n\t\t// Leave engine-generated hierarchy nodes alone.\r\n\t\tif ( go.Flags.Contains( GameObjectFlags.Bone ) )\r\n\t\t\treturn false;\r\n\t\tif ( go.Flags.Contains( GameObjectFlags.Attachment ) )\r\n\t\t\treturn false;\r\n\t\tif ( go.Flags.Contains( GameObjectFlags.PhysicsBone ) )\r\n\t\t\treturn false;\r\n\t\tif ( go.Flags.Contains( GameObjectFlags.ProceduralBone ) )\r\n\t\t\treturn false;\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// <summary>Colored objects by hue; uncolored last.</summary>\r\n\tstatic float GetColorSortKey( GameObject go )\r\n\t{\r\n\t\tvar marker = go.Components.Get<HierarchyColor>( true );\r\n\t\tif ( marker is null || !marker.IsValid() )\r\n\t\t\treturn float.MaxValue;\r\n\r\n\t\tvar hsv = marker.Tint.ToHsv();\r\n\t\treturn hsv.Hue;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "HierarchyColor.cs",
            "FileName": "HierarchyColor.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace SubHierarchy;\r\n\r\n/// <summary>\r\n/// Marks a GameObject with a Hierarchy row tint color (editor organization).\r\n/// </summary>\r\n[Title( \"Hierarchy Color\" )]\r\n[Category( \"Editor\" )]\r\n[Icon( \"palette\" )]\r\npublic sealed class HierarchyColor : Component\r\n{\r\n\t/// <summary>Background tint drawn behind the object name in the Hierarchy.</summary>\r\n\t[Property]\r\n\tpublic Color Tint { get; set; } = new Color( 0.2f, 0.55f, 0.95f, 1f );\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Code/HierarchyColor.cs",
            "FileName": "HierarchyColor.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\nnamespace SubHierarchy;\r\n\r\n/// <summary>\r\n/// Marks a GameObject with a Hierarchy row tint color (editor organization).\r\n/// </summary>\r\n[Title( \"Hierarchy Color\" )]\r\n[Category( \"Editor\" )]\r\n[Icon( \"palette\" )]\r\npublic sealed class HierarchyColor : Component\r\n{\r\n\t/// <summary>Background tint drawn behind the object name in the Hierarchy.</summary>\r\n\t[Property]\r\n\tpublic Color Tint { get; set; } = new Color( 0.2f, 0.55f, 0.95f, 1f );\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subhierarchy",
            "Path": "Editor/Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381047,
            "IsPrivate": false,
            "Code": "global using Sandbox;\r\nglobal using Editor;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\nglobal using SubHierarchy;\r\n"
        }
    ]
}