s&box Package Code Search

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

Showing code results for query: * (13 total matches found)
trashpandagames.hierarchyextension / Editor/Unselectable.cs
Editor library
using System.Collections.Generic;
using System.Linq;

namespace HierarchyExtension;

/// <summary>
/// Objects that can't be selected in the scene view. A click goes through them to whatever is behind, and a drag
/// selection leaves them out. They still select from the Hierarchy, so they can be inspected and edited as usual.
/// <para>
/// An object is made unselectable with the <see cref="Tag"/> tag, the way the stock Hide marks hidden objects with
/// "hidden". It saves with the scene, shows in the object's Tags, takes undo, and counts as an override on a prefab
/// instance. Tags pass down to children, so everything under an unselectable object is unselectable too.
/// </para>
/// <para>
/// Set it with the hand on each Hierarchy row (<see cref="SelectableColumn"/>), with <b>Scene > View > Toggle
/// Selectable</b> (Alt+L) for the selected objects, or with <b>Make All Selectable</b>. <see cref="ScenePicking"/>
/// makes the scene view honour it.
/// </para>
/// </summary>
public static class Unselectable
{
	/// <summary>The tag that makes an object, and everything under it, unselectable in the scene view.</summary>
	public const string Tag = "unselectable";

	/// <summary>Whether <paramref name="go"/> can't be selected in the scene view, from its own tag or a parent's.</summary>
	public static bool Has( GameObject go ) => go.IsValid() && go is not Scene && go.Tags.Has( Tag );

	/// <summary>Whether <paramref name="go"/> has the tag itself, not only from a parent.</summary>
	public static bool IsSet( GameObject go ) => go.IsValid() && go is not Scene && go.Tags.Has( Tag, false );

	/// <summary>
	/// The object whose tag makes <paramref name="go"/> unselectable: itself, or the nearest parent with the tag.
	/// Null when it's selectable.
	/// </summary>
	public static GameObject Source( GameObject go )
	{
		for ( var o = go; o.IsValid() && o is not Scene; o = o.Parent )
		{
			if ( IsSet( o ) ) return o;
		}

		return null;
	}

	/// <summary>
	/// Make <paramref name="objects"/> unselectable in the scene view, or selectable again, as one undo step. Objects
	/// that are that way already are left alone. Making an object selectable only takes its own tag off, so one under
	/// an unselectable parent stays unselectable; pass <see cref="Source"/> to free it.
	/// </summary>
	public static void Set( IEnumerable<GameObject> objects, bool unselectable )
	{
		var changing = objects.Where( x => x.IsValid() && x is not Scene && IsSet( x ) != unselectable ).Distinct().ToArray();
		if ( changing.Length == 0 ) return;

		var session = SessionOf( changing[0] );
		if ( session is null ) return;

		var name = unselectable ? "Make Unselectable" : "Make Selectable";
		if ( changing.Length == 1 ) name = $"{name}: {changing[0].Name}";

		using var sceneScope = session.Scene.Push();

		// only the objects' own properties, which is where tags live: a tile layer's children and components can be big
		using ( session.UndoScope( name ).WithGameObjectChanges( changing, GameObjectUndoFlags.Properties ).Push() )
		{
			foreach ( var go in changing )
			{
				if ( unselectable ) go.Tags.Add( Tag );
				else go.Tags.Remove( Tag );
			}
		}
	}

	/// <summary>
	/// Alt+L: make the selected objects unselectable in the scene view or, when they all are already, selectable again.
	/// One that's unselectable through a parent frees that parent, and so everything under it.
	/// </summary>
	[Menu( "Editor", "Scene/View/Toggle Selectable", "do_not_touch" )]
	[Shortcut( "editor.toggle-selectable", "Alt+L" )]
	public static void ToggleSelected()
	{
		var session = SceneEditorSession.Active;
		if ( session is null ) return;

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

		if ( selected.Any( x => !Has( x ) ) ) Set( selected, true );
		else Set( selected.Select( Source ), false );
	}

	/// <summary>Make everything in the open scene selectable in the scene view again.</summary>
	[Menu( "Editor", "Scene/View/Make All Selectable", "touch_app" )]
	public static void MakeAllSelectable()
	{
		var scene = SceneEditorSession.Active?.Scene;
		if ( !scene.IsValid() ) return;

		Set( scene.GetAllObjects( false ).Where( IsSet ), false );
	}

	/// <summary>Whether anything in <paramref name="scene"/> is unselectable. Cheap enough to ask on every click.</summary>
	public static bool AnyIn( Scene scene )
	{
		if ( !scene.IsValid() ) return false;

		foreach ( var go in scene.GetAllObjects( true ) )
		{
			if ( IsSet( go ) ) return true;
		}

		return false;
	}

	/// <summary>The editor session whose scene holds <paramref name="go"/>, the play session's included.</summary>
	public static SceneEditorSession SessionOf( GameObject go ) => SessionOf( go?.Scene );

	/// <summary>The editor session of <paramref name="scene"/>, the play session's included.</summary>
	public static SceneEditorSession SessionOf( Scene scene )
	{
		if ( scene is null ) return null;

		return SceneEditorSession.All.FirstOrDefault( x => x.Scene == scene );
	}
}
trashpandagames.hierarchyextension / Editor/Assembly.cs
Editor library
global using Sandbox;
global using Editor;
global using System.Collections.Generic;
global using System.Linq;
trashpandagames.hierarchyextension / Editor/SelectionOutline.cs
Editor library
using System.Collections.Generic;
using System.Linq;

namespace HierarchyExtension;

/// <summary>
/// Outlines what's selected in the scene view: one box around each selected object and everything under it (its
/// sprites, models, colliders, particles and icons: <see cref="ScenePicking.VisibleBounds"/>), a thin line drawn over
/// everything in the editor's own selected colour. An object inside another selected one gets no box of its own; the
/// other's covers it. During a drag selection it outlines what the drag will select (<see cref="ScenePicking.DragResult"/>).
/// <para>
/// Why: the stock scene view only outlines an object's own sprite or model (<c>SpriteRenderer.DrawGizmos</c> asks
/// <c>Gizmo.IsSelected</c> in the sprite's own object; the viewport's <c>DrawSelection</c> takes the selected object's
/// own <c>ModelRenderer</c>s). A prefab, or a group, keeps its sprites on its children, and selecting it selects the
/// root, so a selected decoration, or Sera, showed nothing but the move handle (tools and engine source, 26.09.29).
/// </para>
/// </summary>
public static class SelectionOutline
{
	/// <summary>Draws the outlines. Called from the scene view's gizmo pass (<see cref="ScenePicking"/>).</summary>
	internal static void Draw( IEnumerable<object> selected )
	{
		var objects = selected.OfType<GameObject>().Where( x => x.IsValid() && x is not Scene ).ToHashSet();
		var boxes = new List<BBox>();

		foreach ( var go in objects )
		{
			if ( HasAncestorIn( go, objects ) ) continue;

			// a point (an icon and nothing else) is shown by the icon itself
			if ( ScenePicking.VisibleBounds( go ) is { } bounds && bounds.Size.Length > 0.01f ) boxes.Add( bounds );
		}

		if ( boxes.Count == 0 ) return;

		// One colour and one thickness, so it's all one batch. A dark edge under the line looked better, but it has to be a
		// thicker line, and a change of thickness starts another batch (Gizmo.Draw.CanReuseVertexObject). Batches drawn
		// over everything have no order between them, so after a reselect the edge sometimes landed on top of the line.
		using var scope = Gizmo.Scope( "hierarchyextension-selection" );
		Gizmo.Transform = global::Transform.Zero;
		Gizmo.Draw.IgnoreDepth = true;
		Gizmo.Draw.Color = Gizmo.Colors.Selected;
		Gizmo.Draw.LineThickness = 1.5f;

		foreach ( var box in boxes ) Gizmo.Draw.LineBBox( box );
	}

	static bool HasAncestorIn( GameObject go, HashSet<GameObject> objects )
	{
		for ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )
		{
			if ( objects.Contains( o ) ) return true;
		}

		return false;
	}
}
trashpandagames.hierarchyextension / Editor/ClickThrough.cs
Editor library
using System.Collections.Generic;
using System.Linq;

namespace HierarchyExtension;

/// <summary>
/// Works out what a click in the scene view would select if the unselectable objects weren't there. It plays the click
/// again through a gizmo instance of its own, over the scene's own gizmos, with the unselectable objects held out:
/// the engine picks, so whatever it picks normally (render meshes, sprites, handles, a component's hitboxes, selection
/// bases) is picked the same way.
/// <para>
/// Why not stop the engine picking them in the first place: the scene view has no per-object switch. The only objects
/// it skips are ones tagged "hidden" or flagged Hidden, and both stop them drawing or listing too (engine source,
/// <c>GameObject.DrawGizmos</c> and <c>SceneViewportWidget.UpdateHovered</c>, 26.09.29). So the objects are flagged
/// Hidden only while the click is replayed here, and put back straight after, before anything draws or lists them.
/// </para>
/// </summary>
public static class ClickThrough
{
	/// <summary>A change a click makes to the selection, in the order the click makes them.</summary>
	public enum Change { Clear, Add, Remove }

	/// <summary>
	/// The selection changes a click with <paramref name="input"/> would make in <paramref name="scene"/> if the
	/// unselectable objects weren't there, in order. Empty when it would hit nothing. The selection isn't touched:
	/// the click reads <paramref name="selection"/>, to know what's selected, and only records what it would do.
	/// Also used by <see cref="ScenePicking"/>; call it outside any gizmo drawing.
	/// </summary>
	public static List<(Change Change, object Target)> Run( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, SelectionSystem selection )
	{
		return Run( scene, settings, input, selection, scene.GetAllObjects( true ).Where( Unselectable.IsSet ).ToArray() );
	}

	/// <summary>The same with <paramref name="heldOut"/>, and everything under them, held out.</summary>
	public static List<(Change Change, object Target)> Run( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, SelectionSystem selection, IReadOnlyList<GameObject> heldOut )
	{
		var recorder = new Recorder( selection );
		if ( !scene.IsValid() ) return recorder.Changes;

		// a GameObject flagged Hidden draws no gizmos, and nor do its children
		var flagged = new List<GameObject>();
		foreach ( var go in heldOut )
		{
			if ( !go.IsValid() || go.Flags.Contains( GameObjectFlags.Hidden ) ) continue;

			go.Flags |= GameObjectFlags.Hidden;
			flagged.Add( go );
		}

		try
		{
			using var instance = new Gizmo.Instance { Settings = settings, Selection = recorder };

			// The gizmo state reads the mouse a frame late: a press is seen the frame after the button goes down, and a
			// click the frame after it comes up, when the object hovered then is the one pressed.
			input.IsHovered = true;
			Frame( instance, scene, input, false, heldOut, draw: false );
			Frame( instance, scene, input, true, heldOut );
			Frame( instance, scene, input, false, heldOut );
			Frame( instance, scene, input, false, heldOut );
		}
		finally
		{
			foreach ( var go in flagged )
			{
				if ( go.IsValid() ) go.Flags &= ~GameObjectFlags.Hidden;
			}
		}

		return recorder.Changes;
	}

	/// <summary>
	/// Plays the button going down at <paramref name="input"/>, nothing held out, and asks <paramref name="ask"/> on the
	/// frame the press is seen, as the scene view's own code would see it (<c>Gizmo.Pressed</c>). For the bridge's
	/// press probe; call it outside any gizmo drawing.
	/// </summary>
	public static T WhilePressed<T>( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, System.Func<T> ask )
	{
		var none = System.Array.Empty<GameObject>();

		using var instance = new Gizmo.Instance { Settings = settings, Selection = new Recorder( null ) };

		input.IsHovered = true;
		Frame( instance, scene, input, false, none, draw: false );
		Frame( instance, scene, input, true, none );

		input.LeftMouse = true;
		instance.Input = input;

		using var gizmos = instance.Push();
		using var sceneScope = scene.Push();
		return ask();
	}

	static void Frame( Gizmo.Instance instance, Scene scene, Gizmo.Inputs input, bool leftMouse, IReadOnlyList<GameObject> heldOut, bool draw = true )
	{
		input.LeftMouse = leftMouse;
		instance.Input = input;

		using var gizmos = instance.Push();
		using var sceneScope = scene.Push();

		if ( !draw ) return;

		HoverRenderMeshes( scene, heldOut );
		scene.EditorDraw();
	}

	/// <summary>
	/// What the scene view does before the gizmos draw: trace the render meshes under the cursor and hover the object
	/// hit. The stock one is private (<c>SceneViewportWidget.UpdateHovered</c>); this is it, holding out the objects.
	/// </summary>
	static void HoverRenderMeshes( Scene scene, IReadOnlyList<GameObject> heldOut )
	{
		if ( !Gizmo.Settings.Selection ) return;

		var trace = scene.Trace.Ray( Gizmo.CurrentRay, Gizmo.RayDepth )
			.UseRenderMeshes( true )
			.UsePhysicsWorld( false )
			.WithoutTags( "hidden" );

		foreach ( var go in heldOut )
		{
			if ( go.IsValid() ) trace = trace.IgnoreGameObjectHierarchy( go );
		}

		var tr = trace.Run();
		if ( !tr.Hit || !tr.Component.IsValid() ) return;

		using ( Gizmo.ObjectScope( tr.GameObject, tr.GameObject.WorldTransform ) )
		{
			Gizmo.Hitbox.DepthBias = 1;
			Gizmo.Hitbox.TrySetHovered( tr.Distance );
		}
	}

	/// <summary>
	/// The replayed click's selection: it answers what's selected from the real one, and writes down the changes
	/// instead of making them.
	/// </summary>
	sealed class Recorder : SelectionSystem
	{
		readonly SelectionSystem _selection;

		public readonly List<(Change Change, object Target)> Changes = new();

		public Recorder( SelectionSystem selection )
		{
			_selection = selection;
		}

		public override bool Contains( object obj ) => _selection?.Contains( obj ) ?? false;

		public override bool Any() => _selection?.Any() ?? false;

		public override void Clear() => Changes.Add( (Change.Clear, null) );

		public override bool Add( object obj )
		{
			if ( obj is null ) return false;

			Changes.Add( (Change.Add, obj) );
			return true;
		}

		public override bool Remove( object obj )
		{
			if ( obj is null ) return false;

			Changes.Add( (Change.Remove, obj) );
			return true;
		}

		public override bool Set( object obj )
		{
			Clear();
			return Add( obj );
		}
	}
}
trashpandagames.hierarchyextension / 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 );
		}
	}

}
trashpandagames.hierarchyextension / Editor/ScenePicking.cs
Editor library
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace HierarchyExtension;

/// <summary>
/// Makes the scene view honour <see cref="Unselectable"/>, and makes its drag selections take only what's fully inside.
/// <list type="bullet">
/// <item><description><b>Clicks</b>: each scene viewport's gizmos select through a <see cref="ClickFilter"/>. A click
/// that would select an unselectable object is held back. On the next editor frame <see cref="ClickThrough"/> plays it
/// again without the unselectable objects, and its selection is made instead, as one undo step. With nothing behind,
/// the click deselects, like a click on empty space.</description></item>
/// <item><description><b>Drags</b> that start on an unselectable object are drags on empty space: drag selections,
/// and the camera, take them as they would there (<see cref="LetGoOfDraggedPress"/>).</description></item>
/// <item><description><b>Drag selections</b> with the Object Select tool take only objects fully inside the box, never
/// unselectable ones, and a prefab only whole (<see cref="WatchDragSelection"/>).</description></item>
/// <item><description><b>Right-clicking</b> the scene view offers to make unselectable objects selectable again.</description></item>
/// </list>
/// <para>
/// The filter goes where the engine lets it: a viewport's <c>GizmoInstance.Selection</c> is a public setting, and only
/// the gizmos' clicks select through it (tools source, <c>SceneViewportWidget</c>, 26.09.29). The Hierarchy, the
/// inspector, tools and code select through the session's selection, so they can still pick anything.
/// </para>
/// </summary>
public sealed class ScenePicking : EditorEvent.ISceneView
{
	/// <summary>How far the cursor goes before a drag selection starts (<c>EditorTool.UpdateBoxSelection</c>), in pixels.</summary>
	const float DragDistance = 5;

	static ScenePicking _listener;
	static PendingClick _pending;
	static DragWatch _drag;

	/// <summary>Whether the press going on has been checked for landing on an unselectable object.</summary>
	static bool _pressChecked;

	/// <summary>
	/// While a drag selection is going on in the scene view: what it will select when the mouse comes up. Null otherwise.
	/// The Hierarchy shows this meanwhile (<see cref="HierarchySelection"/>).
	/// </summary>
	internal static HashSet<object> DragResult => _drag?.Result;

	/// <summary>A click that was held back, with what the viewport's gizmos saw when it landed.</summary>
	sealed record PendingClick( SceneViewportWidget Viewport, SelectionSystem Selection, Gizmo.Inputs Input );

	/// <summary>A press begun in a viewport (known by its camera), and the drag selection it may become.</summary>
	sealed class DragWatch
	{
		public Scene Scene;
		public SceneCamera Camera;

		/// <summary>Editor frames in a row the left button has been up while this drag still went on.</summary>
		public int FramesUp;

		/// <summary>What was selected when the press began.</summary>
		public HashSet<object> Before;

		/// <summary>The cursor's ray when the press began, and on the last frame the button was down, as the tool takes them.</summary>
		public Ray Start, Last;

		/// <summary>While it's a drag selection: what the selection will be when the mouse comes up. Null otherwise.</summary>
		public HashSet<object> Result;

		readonly Dictionary<GameObject, BBox?> _bounds = new();

		/// <summary>
		/// <see cref="VisibleBounds"/>, worked out once per drag, as the tool does its bounds: nothing moves while the
		/// mouse is busy with a drag selection, and they're slow to get.
		/// </summary>
		public BBox? BoundsOf( GameObject go )
		{
			if ( !_bounds.TryGetValue( go, out var bounds ) ) _bounds[go] = bounds = VisibleBounds( go );
			return bounds;
		}
	}

	[EditorEvent.Frame]
	static void Frame()
	{
		if ( _listener is null )
		{
			_listener = new ScenePicking();
			EditorEvent.Register( _listener );
		}

		InstallFilters();
		ResolvePendingClick();

		// a drag whose viewport stopped drawing before its release was seen: end it once the button has been up a while
		if ( _drag is { } drag )
		{
			drag.FramesUp = Editor.Application.MouseButtons.HasFlag( MouseButtons.Left ) ? 0 : drag.FramesUp + 1;
			if ( drag.FramesUp > 3 ) Finish( drag );
		}
	}

	/// <summary>A code reload keeps the listener; register it again in case the reload dropped it.</summary>
	[EditorEvent.Hotload]
	static void OnHotload()
	{
		if ( _listener is null ) return;

		EditorEvent.Unregister( _listener );
		EditorEvent.Register( _listener );
	}

	/// <summary>
	/// Puts a filter on every viewport of the scene view in use, and on any it's lost: a viewport sets its selection
	/// again when play starts or stops. Also lets through a clear that's still waiting for its add.
	/// </summary>
	static void InstallFilters()
	{
		var view = SceneViewWidget.Current;
		if ( !view.IsValid() || view._viewports is null ) return;

		foreach ( var viewport in view._viewports.Values )
		{
			var gizmos = viewport.IsValid() ? viewport.GizmoInstance : null;
			if ( gizmos?.Selection is null ) continue;

			if ( gizmos.Selection is ClickFilter filter )
			{
				filter.Flush();
				continue;
			}

			// one from before a code reload that wasn't carried over: leave it rather than stack another on it
			if ( gizmos.Selection.GetType().FullName == typeof( ClickFilter ).FullName ) continue;

			gizmos.Selection = new ClickFilter( viewport, gizmos.Selection );
		}
	}

	static void ResolvePendingClick()
	{
		var click = _pending;
		if ( click is null ) return;
		_pending = null;

		var session = SceneEditorSession.All.FirstOrDefault( x => x.Selection == click.Selection );
		var scene = session?.Scene;
		if ( !scene.IsValid() || !click.Viewport.IsValid() ) return;

		var changes = ClickThrough.Run( scene, click.Viewport.GizmoInstance.Settings, click.Input, click.Selection );

		using var sceneScope = scene.Push();
		using ( session.UndoScope( UndoName( changes ) ).Push() )
		{
			if ( changes.Count == 0 )
			{
				click.Selection.Clear();
				return;
			}

			foreach ( var (change, target) in changes )
			{
				switch ( change )
				{
					case ClickThrough.Change.Clear: click.Selection.Clear(); break;
					case ClickThrough.Change.Add: click.Selection.Add( target ); break;
					case ClickThrough.Change.Remove: click.Selection.Remove( target ); break;
				}
			}
		}
	}

	/// <summary>Named as the stock click's undo steps are: "Select {name}", or "Deselect all" for empty space.</summary>
	static string UndoName( List<(ClickThrough.Change Change, object Target)> changes )
	{
		var last = changes.LastOrDefault( x => x.Change != ClickThrough.Change.Clear );
		if ( last.Target is GameObject go ) return last.Change == ClickThrough.Change.Add ? $"Select {go.Name}" : $"Deselect {go.Name}";

		return "Deselect all";
	}

	/// <summary>Runs in each viewport's frame after its tool, before the scene's gizmos.</summary>
	void EditorEvent.ISceneView.DrawGizmos( Scene scene )
	{
		LetGoOfDraggedPress( scene );
		WatchDragSelection( scene );

		// what's selected, or during a drag selection what it will select
		var selected = DragResult ?? (IEnumerable<object>)Unselectable.SessionOf( scene )?.Selection;
		if ( selected is not null ) SelectionOutline.Draw( selected );
	}

	/// <summary>
	/// A press on an unselectable object still counts as a press on something: the scene view hovers what's under the
	/// cursor before anyone clicks, with no way to leave objects out. While anything is pressed, a drag doesn't start a
	/// drag selection (<c>EditorTool.UpdateBoxSelection</c>) or move the camera (<c>SceneViewportWidget</c>), so a drag
	/// that began on a backdrop did nothing. Once such a press has moved as far as a drag selection waits for, let go of
	/// it (<c>Gizmo.Pressed.ClearPath</c>, as the engine does itself for a dragged object press): from the next frame the
	/// drag is a drag on empty space, and a drag selection starts from where the press began. A press that doesn't move
	/// stays a click, which the <see cref="ClickFilter"/> plays through to what's behind.
	/// </summary>
	static void LetGoOfDraggedPress( Scene scene )
	{
		if ( !Gizmo.HasMouseFocus ) return;
		if ( Gizmo.WasLeftMousePressed ) _pressChecked = false;

		if ( _pressChecked || !Gizmo.IsLeftMouseDown || !Gizmo.Pressed.Any ) return;
		if ( Gizmo.Pressed.CursorDelta.Length < DragDistance ) return;

		_pressChecked = true;
		if ( PressedUnselectable( scene ).IsValid() ) Gizmo.Pressed.ClearPath();
	}

	/// <summary>
	/// The unselectable object the press is on, or null. The gizmos name what's pressed by its path: an object's is
	/// "object-{hash}" (<c>Gizmo.ObjectScope</c>), its handle's ".../Handle", and its components' hitboxes ".../Scope0",
	/// ".../Scope1" and so on, in order (<c>GameObject.DrawGizmos</c>). <c>Gizmo.Pressed.This</c> compares the path it's
	/// asked in, so each is tried. A hitbox a component draws inside a scope of its own isn't found.
	/// </summary>
	internal static GameObject PressedUnselectable( Scene scene ) => PressedObject( scene, Unselectable.Has );

	/// <summary>The object the press is on, among those <paramref name="among"/> accepts, or null. See <see cref="PressedUnselectable"/>.</summary>
	internal static GameObject PressedObject( Scene scene, System.Func<GameObject, bool> among )
	{
		using var scope = Gizmo.Scope( "hierarchyextension" );

		foreach ( var go in scene.GetAllObjects( true ) )
		{
			if ( !among( go ) ) continue;

			var path = $"object-{go.GetHashCode()}";
			if ( IsPressed( path ) || IsPressed( $"{path}/Handle" ) ) return go;

			for ( var i = 0; i < go.Components.Count; i++ )
			{
				if ( IsPressed( $"{path}/Scope{i}" ) ) return go;
			}
		}

		return null;
	}

	static bool IsPressed( string path )
	{
		Gizmo.Path = path;
		return Gizmo.Pressed.This;
	}

	/// <summary>
	/// Makes the Object Select tool's drag selections take only what's fully inside the box. The stock one takes anything
	/// whose bounds the box touches, and an object's bounds include everything under it (<c>ObjectEditorTool.OnBoxSelect</c>,
	/// <c>GameObject.GetBounds</c>), so a small box over a few decorations also took their group and the map they're in.
	/// It has no way in, so it runs as before and the result is redone here, from the same box:
	/// <list type="bullet">
	/// <item><description>An object is taken when it's fully inside with everything under it, and then stands for all
	/// of that: what's under it isn't taken as well.</description></item>
	/// <item><description>Inside a prefab, or anything else a click selects as a whole (<see cref="IsSelectionBase"/>),
	/// only that whole counts.</description></item>
	/// <item><description>Hidden and unselectable objects are never taken.</description></item>
	/// <item><description>Ctrl takes them out of the selection, Shift adds them, and otherwise they're the selection,
	/// as with the stock one (<c>EditorTool.ApplyDragSelection</c>).</description></item>
	/// </list>
	/// While the drag goes on the scene view (<see cref="ClickFilter.Contains"/>) and the Hierarchy
	/// (<see cref="HierarchySelection"/>) show the result; it goes into the selection when the mouse comes up, the frame
	/// the stock one finishes. Putting it in every frame would have the tool undo it every frame, and the inspector rebuild
	/// and the Hierarchy scroll each time, so the inspector and the move handles show the stock one's picks until then.
	/// </summary>
	static void WatchDragSelection( Scene scene )
	{
		var camera = Gizmo.Camera;

		if ( Gizmo.WasLeftMousePressed && Gizmo.HasMouseFocus )
		{
			var before = Unselectable.SessionOf( scene )?.Selection;
			_drag = before is null ? null : new DragWatch { Scene = scene, Camera = camera, Before = before.ToHashSet(), Start = Gizmo.CurrentRay, Last = Gizmo.CurrentRay };
			return;
		}

		var drag = _drag;
		if ( drag is null || drag.Camera != camera ) return;

		// the frame the mouse comes up still counts: the tool finishes its drag selection on it, from the last box. With the
		// cursor out of the view the tool leaves its box as it was, and so does this
		if ( Gizmo.HasMouseFocus && (Gizmo.IsLeftMouseDown || Gizmo.WasLeftMouseReleased) )
		{
			if ( Gizmo.IsLeftMouseDown ) drag.Last = Gizmo.CurrentRay;
			drag.Result = IsBoxSelecting( drag, out var frustum ) ? BoxResult( scene, drag, frustum ) : null;

			// the Hierarchy shows the result as it changes, not what the stock one has put in the selection meanwhile
			HierarchySelection.Current?.Sync();
		}

		// the release, or a later frame if the release went by unseen (a code reload, say)
		if ( !Gizmo.IsLeftMouseDown ) Finish( drag );
	}

	/// <summary>Ends the drag: a drag selection's result goes into the selection, and the Hierarchy shows the selection again.</summary>
	static void Finish( DragWatch drag )
	{
		if ( _drag == drag ) _drag = null;

		var result = drag.Result;
		var selection = Unselectable.SessionOf( drag.Scene )?.Selection;
		if ( result is not null && selection is not null )
		{
			foreach ( var obj in selection.ToArray() )
			{
				if ( !result.Contains( obj ) ) selection.Remove( obj );
			}

			foreach ( var obj in result )
			{
				if ( !selection.Contains( obj ) ) selection.Add( obj );
			}
		}

		HierarchySelection.Current?.Sync();
	}

	/// <summary>
	/// Whether the Object Select tool is making a drag selection this frame, decided as it decides it
	/// (<c>EditorTool.UpdateBoxSelection</c>): the cursor 5 px or more from where the press began, and nothing pressed.
	/// Gives the box's frustum.
	/// </summary>
	static bool IsBoxSelecting( DragWatch drag, out Frustum frustum )
	{
		frustum = default;

		if ( EditorToolManager.CurrentModeName != nameof( ObjectEditorTool ) ) return false;
		if ( Gizmo.Pressed.Any ) return false;

		var c1 = Gizmo.Camera.ToScreen( drag.Start.Project( 100 ) );
		var c2 = Gizmo.Camera.ToScreen( drag.Last.Project( 100 ) );
		if ( Vector2.Distance( c1, c2 ) < DragDistance ) return false;

		frustum = Gizmo.Camera.GetFrustum( Rect.FromPoints( c1, c2 ) );
		return true;
	}

	/// <summary>What the selection will be when the drag selection in <paramref name="frustum"/> finishes.</summary>
	static HashSet<object> BoxResult( Scene scene, DragWatch drag, Frustum frustum )
	{
		var taken = Take( scene, frustum, go => drag.BoundsOf( go ) );
		var result = new HashSet<object>( drag.Before );

		if ( Gizmo.IsCtrlPressed )
		{
			result.ExceptWith( taken );
			return result;
		}

		if ( !Gizmo.IsShiftPressed ) result.RemoveWhere( x => x is GameObject );

		result.UnionWith( taken );
		return result;
	}

	/// <summary>
	/// The objects a drag selection in <paramref name="frustum"/> takes, before Ctrl or Shift: those fully inside with
	/// everything under them (<paramref name="boundsOf"/>, usually <see cref="VisibleBounds"/>), none that are part of
	/// one taken, hidden or unselectable, and a prefab only whole.
	/// </summary>
	internal static HashSet<GameObject> Take( Scene scene, Frustum frustum, System.Func<GameObject, BBox?> boundsOf )
	{
		var taken = new HashSet<GameObject>();

		foreach ( var go in scene.GetAllObjects( true ) )
		{
			if ( go is Scene || go.Tags.Has( "hidden" ) || Unselectable.Has( go ) ) continue;

			// part of a prefab: the prefab is taken whole or not at all
			if ( InsideSelectionBase( go ) ) continue;

			if ( boundsOf( go ) is { } bounds && frustum.IsInside( bounds, false ) ) taken.Add( go );
		}

		// an object taken with everything under it stands for all of it
		taken.RemoveWhere( go => HasAncestorIn( go, taken ) );
		return taken;
	}

	/// <summary>
	/// What the scene view shows of <paramref name="go"/> and everything under it, as one world box, or null when there's
	/// nothing. That's what the stock drag selection measures (<c>GameObject.GetBounds</c>: colliders, models, particles),
	/// plus sprites, which it leaves out, and the icon of each object that has one, as a point. A
	/// <c>SpriteRenderer</c> has no bounds of its own (it isn't a <c>Component.IHasBounds</c>, engine source 26.09.29),
	/// so with the stock drag selection no sprite can be drag-selected, and a 2D level's decorations never are.
	/// </summary>
	internal static BBox? VisibleBounds( GameObject go )
	{
		BBox? result = null;
		void Add( BBox bounds ) => result = result is { } r ? r.AddBBox( bounds ) : bounds;

		foreach ( var o in go.GetAllObjects( true ) )
		{
			if ( o.Tags.Has( "hidden" ) ) continue;

			foreach ( var c in o.Components.GetAll( FindMode.EnabledInSelf ) )
			{
				if ( c is Component.IHasBounds hasBounds ) Add( hasBounds.LocalBounds.Transform( c.WorldTransform ) );
				if ( c is SpriteRenderer sprite ) Add( SpriteBounds( sprite ) );
			}

			if ( o.HasGizmoHandle ) Add( BBox.FromPositionAndSize( o.WorldPosition ) );
		}

		return result;
	}

	/// <summary>A sprite's box in the world, worked out as its own hitbox is (<c>SpriteRenderer.DrawGizmos</c>).</summary>
	internal static BBox SpriteBounds( SpriteRenderer sprite )
	{
		var world = sprite.WorldTransform;
		var billboard = sprite.Billboard is SpriteRenderer.BillboardMode.Always or SpriteRenderer.BillboardMode.YOnly;

		var transform = billboard ? world.WithRotation( new Rotation() ) : world;
		transform = transform.WithScale( new Vector3( world.Scale.x, world.Scale.x, world.Scale.z ) );

		var pivot = ((sprite.CurrentAnimation?.Origin ?? new Vector2( 0.5f, 0.5f )) - 0.5f) * sprite.Size;
		var size = new Vector2( sprite.Size.x, sprite.Size.y );

		if ( billboard )
		{
			size += Vector2.Abs( pivot * 2 );

			var angle = MathX.DegreeToRadian( world.Rotation.Roll() );
			var cos = MathF.Abs( MathF.Cos( angle ) );
			var sin = MathF.Abs( MathF.Sin( angle ) );
			size = new Vector2( size.x * cos + size.y * sin, size.x * sin + size.y * cos );
		}

		var local = BBox.FromPositionAndSize(
			billboard ? Vector3.Zero : new Vector3( 0, pivot.x, pivot.y ),
			new Vector3( billboard ? size.x : 0.5f, size.x, size.y ) );

		return local.Transform( transform );
	}

	/// <summary>
	/// Whether a click selects <paramref name="go"/> as a whole when it lands on something under it, as the scene view
	/// decides it (<c>GameObject.FindSelectionBase</c>): a network root, the outermost root of a prefab instance, or an
	/// object with a <see cref="SelectionBaseAttribute"/> component.
	/// </summary>
	static bool IsSelectionBase( GameObject go )
	{
		// GameObject.IsOutermostPrefabInstanceRoot isn't public; the stock Hierarchy asks this
		if ( go.IsNetworkRoot || EditorUtility.Prefabs.IsOuterMostPrefabRoot( go ) ) return true;

		return go.Components.GetAll().Any( x => EditorTypeLibrary.GetType( x.GetType() )?.HasAttribute<SelectionBaseAttribute>() == true );
	}

	static bool InsideSelectionBase( GameObject go )
	{
		for ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )
		{
			if ( IsSelectionBase( o ) ) return true;
		}

		return false;
	}

	static bool HasAncestorIn( GameObject go, HashSet<GameObject> objects )
	{
		for ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )
		{
			if ( objects.Contains( o ) ) return true;
		}

		return false;
	}

	void EditorEvent.ISceneView.ShowContextMenu( EditorEvent.ShowContextMenuEvent ev )
	{
		var scene = ev.Session?.Scene;
		if ( !Unselectable.AnyIn( scene ) ) return;

		ev.Menu.AddSeparator();

		if ( ev.Trace is { Hit: true } trace && Unselectable.Source( trace.GameObject ) is { } source )
		{
			ev.Menu.AddOption( $"Make {source.Name} Selectable", "touch_app", () => Unselectable.Set( new[] { source }, false ) );
		}

		ev.Menu.AddOption( "Make All Selectable", "touch_app", Unselectable.MakeAllSelectable );
	}

	/// <summary>
	/// A scene viewport's gizmo selection. It passes everything on to the scene's own selection, except a click on an
	/// unselectable object, which it holds back for <see cref="ResolvePendingClick"/>.
	/// </summary>
	sealed class ClickFilter : SelectionSystem, IEnumerable<object>
	{
		readonly SceneViewportWidget _viewport;
		readonly SelectionSystem _selection;

		/// <summary>
		/// A click clears the selection just before it adds what it clicked (<c>Gizmo.Select</c>). The clear waits for
		/// the add: if the click is held back, the clear isn't wanted either.
		/// </summary>
		bool _clearing;

		public ClickFilter( SceneViewportWidget viewport, SelectionSystem selection )
		{
			_viewport = viewport;
			_selection = selection;
		}

		public override void Clear() => _clearing = true;

		public override bool Add( object obj )
		{
			if ( HoldBack( obj ) ) return false;

			Flush();
			return _selection.Add( obj );
		}

		public override bool Remove( object obj )
		{
			if ( HoldBack( obj ) ) return false;

			Flush();
			return _selection.Remove( obj );
		}

		public override bool Set( object obj )
		{
			Clear();
			return Add( obj );
		}

		/// <summary>
		/// Also what the gizmos draw as selected. While a drag selection goes on, that's what it will select when the mouse
		/// comes up (<see cref="WatchDragSelection"/>), not what the stock one has put in the selection meanwhile.
		/// </summary>
		public override bool Contains( object obj ) => _drag?.Result is { } result ? result.Contains( obj ) : _selection.Contains( obj );

		public override bool Any() => _drag?.Result is { } result ? result.Count > 0 : _selection.Any();

		public override int GetHashCode() => _selection.GetHashCode();

		IEnumerator<object> IEnumerable<object>.GetEnumerator() => _selection.GetEnumerator();

		IEnumerator IEnumerable.GetEnumerator() => _selection.GetEnumerator();

		/// <summary>A clear still waiting for its add goes through.</summary>
		public void Flush()
		{
			if ( !_clearing ) return;

			_clearing = false;
			_selection.Clear();
		}

		bool HoldBack( object obj )
		{
			if ( obj is not GameObject go || !Unselectable.Has( go ) ) return false;

			_clearing = false;
			_pending = new PendingClick( _viewport, _selection, _viewport.GizmoInstance.Input );
			return true;
		}
	}
}
trashpandagames.hierarchyextension / .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", "Hierarchy Extension" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "hierarchyextension" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "trashpandagames" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "trashpandagames.hierarchyextension" )]
[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-30T19:33:36.0810281Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.122.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.122.0")]
trashpandagames.hierarchyextension / Assembly.cs
Game library
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
trashpandagames.hierarchyextension / Editor/HierarchySelection.cs
Editor library
using System.Collections.Generic;
using System.Linq;

namespace HierarchyExtension;

/// <summary>
/// The Hierarchy's selection, so its rows are right while a drag selection is going on in the scene view.
/// <para>
/// The stock drag selection puts whatever its box touches into the scene's selection on every frame of the drag, and
/// the Hierarchy highlights exactly that: parents, maps and unselectable objects that the fully-inside rule
/// (<see cref="ScenePicking"/>) only takes out when the mouse comes up. Taking them out every frame would have the stock
/// one put them back every frame. So the Hierarchy gets a selection of its own instead: its tree's
/// <c>TreeView.Selection</c> is a public setting, and the tree paints rows by asking it, repaints when its hash changes,
/// and scrolls to what's added to it (tools source, <c>BaseItemWidget</c>, 26.09.29).
/// </para>
/// <para>
/// It holds a copy of what the Hierarchy should show: the scene's selection, or during a drag selection what that will
/// select (<see cref="ScenePicking.DragResult"/>). Selecting in the Hierarchy goes straight to the scene's selection,
/// as before. The inspector and the move handles follow the scene's selection, so during a drag they still show the
/// stock one's picks.
/// </para>
/// </summary>
public sealed class HierarchySelection : SelectionSystem
{
	static HierarchySelection _current;

	/// <summary>The one on the Hierarchy in use, if any.</summary>
	internal static HierarchySelection Current => _current;

	readonly SelectionSystem _selection;

	HierarchySelection( SelectionSystem selection )
	{
		_selection = selection;
	}

	/// <summary>
	/// Puts one on the Hierarchy's tree, and again whenever the tree is given the scene's selection back: it does that
	/// each time it rebuilds, and on a new scene. Then brings its copy up to date.
	/// </summary>
	[EditorEvent.Frame]
	static void Frame()
	{
		var tree = SceneTreeWidget.Current?.TreeView;
		var selection = SceneEditorSession.Active?.Selection;
		if ( !tree.IsValid() || selection is null ) return;

		if ( tree.Selection == _current && _current?._selection == selection )
		{
			_current.Sync();
			return;
		}

		// the tree has the scene's own, or one of these from before a code reload that wasn't carried over
		var ours = tree.Selection?.GetType().FullName == typeof( HierarchySelection ).FullName;
		if ( tree.Selection != selection && !ours ) return;

		var view = new HierarchySelection( selection );

		// filled before the tree has it, so filling it doesn't scroll the tree
		view.Sync();
		tree.Selection = view;
		_current = view;
	}

	/// <summary>Makes the copy what the Hierarchy should show now, adding and removing only what differs.</summary>
	internal void Sync()
	{
		IEnumerable<object> target = ScenePicking.DragResult ?? (IEnumerable<object>)_selection;
		var wanted = target.ToHashSet();

		foreach ( var obj in this.ToArray() )
		{
			if ( !wanted.Contains( obj ) ) base.Remove( obj );
		}

		foreach ( var obj in target )
		{
			if ( !base.Contains( obj ) ) base.Add( obj );
		}
	}

	public override bool Contains( object obj ) => ScenePicking.DragResult is null ? _selection.Contains( obj ) : base.Contains( obj );

	public override bool Any() => ScenePicking.DragResult is null ? _selection.Any() : base.Any();

	public override bool Add( object obj )
	{
		var added = _selection.Add( obj );
		Sync();
		return added;
	}

	public override bool Remove( object obj )
	{
		var removed = _selection.Remove( obj );
		Sync();
		return removed;
	}

	public override void Clear()
	{
		_selection.Clear();
		Sync();
	}

	public override bool Set( object obj )
	{
		var changed = _selection.Set( obj );
		Sync();
		return changed;
	}
}
trashpandagames.hierarchyextension / Editor/SelectableColumn.cs
Editor library
using System;
using System.Collections.Generic;

namespace HierarchyExtension;

/// <summary>
/// The Hierarchy's column of hands, one on each row just left of where the stock Hide shows its eye. A crossed hand marks
/// an object that can't be selected in the scene view (<see cref="Unselectable"/>); it's faint when that comes from a
/// parent. Hovering a row shows a faint hand to click. A click makes the row's object unselectable, or selectable again;
/// on a row that's unselectable through a parent, it frees that parent.
/// <para>
/// The stock Hierarchy has no way in for this: <c>GameObjectNode.OnPaint</c> paints each row whole, and
/// <c>GameObjectNode.AddGameObjectMenuItems</c> builds the row's context menu, with no event or interface to add to
/// either (tools source, <c>Scene/SceneTree</c>, 26.09.29). So this is a narrow strip laid over the right-hand end of the
/// Hierarchy's rows. It paints the hands and takes the clicks there; everywhere else the tree works as before.
/// </para>
/// </summary>
public sealed class SelectableColumn : Widget
{
	/// <summary>An icon's box, as wide as the stock eye's.</summary>
	const float IconWidth = 18;

	/// <summary>The stock eye's box and its gap: 18 wide, 4 in from the row's end (<c>GameObjectNode.OnPaint</c>).</summary>
	const float EyeWidth = 4 + 18;

	static SelectableColumn _current;

	/// <summary>The column on the Hierarchy in use, if it's open.</summary>
	internal static SelectableColumn Current => _current.IsValid() && _current._tree.IsValid() ? _current : null;

	readonly TreeView _tree;

	/// <summary>The Hierarchy's tree this column lies over.</summary>
	internal TreeView Tree => _tree;

	/// <summary>A hash of what was painted, so the strip repaints only when rows move or change.</summary>
	int _look;

	SelectableColumn( TreeView tree ) : base( tree )
	{
		_tree = tree;
		MouseTracking = true;
		Cursor = CursorShape.Finger;

		Place();
		Visible = true;
	}

	/// <summary>Puts one on the Hierarchy's tree, and again whenever the dock builds a new tree.</summary>
	[EditorEvent.Frame]
	static void Attach()
	{
		var tree = SceneTreeWidget.Current?.TreeView;
		if ( !tree.IsValid() ) return;
		if ( _current.IsValid() && _current._tree == tree ) return;

		_current = new SelectableColumn( tree );
	}

	[EditorEvent.Frame]
	void Frame()
	{
		if ( !_tree.IsValid() ) return;

		Place();

		var look = Look();
		if ( look == _look ) return;

		_look = look;
		Update();
	}

	/// <summary>
	/// Over the rows' right-hand end, just left of the eye's box, from the top down to the last row in view, so a click
	/// on the empty space under the rows still reaches the tree.
	/// </summary>
	void Place()
	{
		var right = _tree.CanvasRect.Right;
		var bottom = 0f;
		foreach ( var (row, _) in Rows() )
		{
			right = row.Rect.Right;
			bottom = row.Rect.Bottom;
		}

		var position = new Vector2( right - EyeWidth - IconWidth, 0 );
		var size = new Vector2( IconWidth, MathF.Min( bottom, _tree.Height ) );

		if ( Position != position ) Position = position;
		if ( Size != size ) Size = size;
	}

	/// <summary>The rows in view that show an object, top to bottom, in the tree's coordinates.</summary>
	internal IEnumerable<(VirtualWidget Row, GameObject Object)> Rows()
	{
		var x = _tree.CanvasRect.Left + 1;
		var y = 0f;

		while ( y < _tree.Height )
		{
			var row = _tree.GetItemAt( new Vector2( x, y ) );
			if ( row is null )
			{
				y += 2;
				continue;
			}

			if ( row.Object is TreeNode { Value: GameObject go } && go.IsValid() && go is not Scene )
				yield return (row, go);

			y = MathF.Max( y + 1, row.Rect.Bottom + 1 );
		}
	}

	/// <summary>The object on the row at <paramref name="y"/>, in this strip's coordinates.</summary>
	GameObject ObjectAt( float y )
	{
		var row = _tree.GetItemAt( new Vector2( _tree.CanvasRect.Left + 1, y + Position.y ) );
		return row?.Object is TreeNode { Value: GameObject go } && go.IsValid() && go is not Scene ? go : null;
	}

	/// <summary>The row under the cursor, in the tree or in this strip.</summary>
	internal VirtualWidget HoveredRow()
	{
		var cursor = _tree.FromScreen( Editor.Application.CursorPosition );
		if ( !_tree.LocalRect.IsInside( cursor ) ) return null;

		return _tree.GetItemAt( new Vector2( _tree.CanvasRect.Left + 1, cursor.y ) );
	}

	int Look()
	{
		var hash = new HashCode();
		hash.Add( Size );

		var hovered = HoveredRow();
		foreach ( var (row, go) in Rows() )
		{
			hash.Add( row.Rect.Top );
			hash.Add( go.Id );
			hash.Add( go.Active );
			hash.Add( Unselectable.IsSet( go ) );
			hash.Add( Unselectable.Has( go ) );
			hash.Add( row == hovered );
		}

		return hash.ToHashCode();
	}

	protected override void OnPaint()
	{
		var hovered = HoveredRow();

		foreach ( var (row, go) in Rows() )
		{
			var hand = Unselectable.IsSet( go ) ? Hand.Own : Unselectable.Has( go ) ? Hand.FromParent : row == hovered ? Hand.Offered : Hand.None;
			PaintHand( new Rect( 0, row.Rect.Top - Position.y, IconWidth, row.Rect.Height ), hand, go.Active );
		}
	}

	/// <summary>What a row's hand shows.</summary>
	internal enum Hand
	{
		/// <summary>Selectable, and not hovered: nothing.</summary>
		None,

		/// <summary>Selectable and hovered: a faint hand to click.</summary>
		Offered,

		/// <summary>Unselectable through a parent: a faint crossed hand.</summary>
		FromParent,

		/// <summary>Unselectable by its own tag: a crossed hand.</summary>
		Own,
	}

	/// <summary>Paints <paramref name="hand"/> in <paramref name="rect"/>, dimmed for a disabled object as its row's text is.</summary>
	internal static void PaintHand( Rect rect, Hand hand, bool active )
	{
		if ( hand == Hand.None ) return;

		var icon = hand == Hand.Offered ? "touch_app" : "do_not_touch";
		var alpha = hand == Hand.Own ? 0.9f : 0.35f;
		if ( !active ) alpha *= 0.5f;

		Paint.Pen = Theme.TextControl.WithAlpha( alpha );
		Paint.DrawIcon( rect, icon, 14, TextFlag.Center );
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		// taken here, so the row under the hand isn't selected as well
		e.Accepted = true;
		if ( !e.LeftMouseButton ) return;

		var go = ObjectAt( e.LocalPosition.y );
		if ( go is null ) return;

		if ( Unselectable.IsSet( go ) ) Unselectable.Set( new[] { go }, false );
		else if ( Unselectable.Source( go ) is { } source ) Unselectable.Set( new[] { source }, false );
		else Unselectable.Set( new[] { go }, true );

		ToolTip = TooltipFor( go );
		_look = 0;
		Update();
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );

		ToolTip = TooltipFor( ObjectAt( e.LocalPosition.y ) );
	}

	/// <summary>The strip lies over the tree, so the wheel over it scrolls the tree, the way the tree scrolls itself.</summary>
	protected override void OnMouseWheel( WheelEvent e )
	{
		if ( !_tree.SmoothScrolling )
		{
			e.Ignore();
			return;
		}

		e.Accept();
		_tree.SmoothScrollTarget += e.Delta < 0 ? _tree.VerticalScrollbar.SingleStep : -_tree.VerticalScrollbar.SingleStep;
	}

	static string TooltipFor( GameObject go )
	{
		if ( go is null ) return null;

		if ( Unselectable.IsSet( go ) )
			return "Not selectable in the scene view: clicks go through it. Click to make it selectable.";

		if ( Unselectable.Source( go ) is { } source )
			return $"Not selectable in the scene view, because {source.Name} isn't. Click to make {source.Name} selectable.";

		return "Click to make it not selectable in the scene view: clicks will go through it to what's behind.";
	}
}
trashpandagames.hierarchyextension / Editor/HierarchyExtensionMcpTools.cs
Editor library
using System;
using Editor.Mcp;

namespace HierarchyExtension;

/// <summary>
/// Editor bridge tools for Hierarchy Extension, so an agent driving the editor can see and set which objects are
/// unselectable in the scene view (<see cref="Unselectable"/>), try a click without making it, and see the Hierarchy's
/// hand column, which the bridge can't otherwise see.
/// </summary>
[McpToolset( "hierarchyextension", "Hierarchy Extension - objects unselectable in the scene view: list, set, try a click through them, draw the Hierarchy's hand column" )]
public static class HierarchyExtensionMcpTools
{
	/// <summary>
	/// The open scene's unselectable objects (each with how many objects under it that makes unselectable too), whether
	/// each viewport of the scene view picks through the click filter, and where the Hierarchy's hand column sits
	/// against the rows in view.
	/// </summary>
	[McpTool.ReadOnly( "hierarchy_unselectable" )]
	public static object List()
	{
		var scene = SceneEditorSession.Active?.Scene ?? throw new Exception( "No scene is open" );

		var view = SceneViewWidget.Current;
		var column = SelectableColumn.Current;
		var rows = column?.Rows().ToList();
		var firstRow = rows?.Count > 0 ? rows[0].Row.Rect : (Rect?)null;

		return new
		{
			Scene = scene.Name,
			Unselectable = scene.GetAllObjects( false ).Where( Unselectable.IsSet ).Select( x => new
			{
				x.Name,
				x.Id,
				Under = x.GetAllObjects( false ).Count() - 1,
			} ),
			Viewports = view?._viewports?.Values.Select( x => new
			{
				x.Id,
				Selection = x.GizmoInstance?.Selection?.GetType().Name,
			} ),
			Hierarchy = SceneTreeWidget.Current?.TreeView?.Selection is { } shown ? new
			{
				Selection = shown.GetType().Name,
				Shows = shown.Select( x => x is GameObject go ? go.Name : x?.ToString() ),
				Selected = SceneEditorSession.Active?.Selection.Select( x => x is GameObject go ? go.Name : x?.ToString() ),
			} : null,
			Column = column is null ? null : new
			{
				Position = column.Position,
				Size = column.Size,
				Visible = column.Visible,
				Tree = column.Tree.Size,
				Canvas = column.Tree.CanvasRect,
				FirstRow = firstRow,
				StockEye = firstRow is { } r ? new Rect( r.Right - 22, r.Top, 18, r.Height ) : (Rect?)null,
				RowsInView = rows.Count,
			},
		};
	}

	/// <summary>
	/// Make objects unselectable in the scene view, or selectable again: one undo step, and the scene counts as changed,
	/// as with the Hierarchy's hands. Objects under an unselectable one stay unselectable until it's made selectable.
	/// </summary>
	/// <param name="ids">Object ids, comma separated, from scene_tree or find_game_objects.</param>
	/// <param name="selectable">True to make them selectable again, false to make them unselectable.</param>
	[McpTool( "hierarchy_set_selectable" )]
	public static object SetSelectable( string ids, bool selectable )
	{
		var objects = Find( ids );
		Unselectable.Set( objects, !selectable );

		return objects.Select( x => new { x.Name, x.Id, Unselectable = Unselectable.Has( x ), Own = Unselectable.IsSet( x ) } );
	}

	/// <summary>
	/// What a click at a world point in the scene view would select with some objects held out, worked out the way the
	/// scene view does it for real (<see cref="ClickThrough"/>). Nothing is clicked: the selection and the scene stay as
	/// they are. Uses the viewport last clicked in, and its camera, so the point should be in its view.
	/// </summary>
	/// <param name="point">The world point to click, "x,y,z".</param>
	/// <param name="holdOut">Ids to hold out instead of the unselectable objects, comma separated. Empty holds out nothing.</param>
	[McpTool.ReadOnly( "hierarchy_click_through" )]
	public static object ClickAt( string point, string holdOut = null )
	{
		var (session, gizmos, input, world) = CursorAt( point );

		IReadOnlyList<GameObject> heldOut = holdOut is null
			? session.Scene.GetAllObjects( true ).Where( Unselectable.IsSet ).ToArray()
			: Find( holdOut );

		var changes = ClickThrough.Run( session.Scene, gizmos.Settings, input, session.Selection, heldOut );

		return new
		{
			Point = world,
			HeldOut = heldOut.Select( x => x.Name ),
			Changes = changes.Select( x => new
			{
				Change = x.Change.ToString(),
				Target = x.Target is GameObject go ? $"{go.Name} ({go.Id})" : x.Target?.ToString(),
			} ),
		};
	}

	/// <summary>
	/// What a press at a world point in the scene view lands on, as its gizmos see it: whether anything is pressed (which
	/// stops a drag from starting a drag selection), which object it's found on, and whether that object is unselectable,
	/// in which case a drag from there lets go of the press. Nothing is pressed for real. Uses the viewport last clicked
	/// in, and its camera, so the point should be in its view.
	/// </summary>
	/// <param name="point">The world point to press, "x,y,z".</param>
	[McpTool.ReadOnly( "hierarchy_press_probe" )]
	public static object PressProbe( string point )
	{
		var (session, gizmos, input, world) = CursorAt( point );

		var (pressedAnything, pressed) = ClickThrough.WhilePressed( session.Scene, gizmos.Settings, input,
			() => (Gizmo.Pressed.Any, ScenePicking.PressedObject( session.Scene, _ => true )) );

		return new
		{
			Point = world,
			PressedAnything = pressedAnything,
			PressedObject = pressed is null ? null : $"{pressed.Name} ({pressed.Id})",
			Unselectable = pressed is not null && Unselectable.Has( pressed ),
		};
	}

	/// <summary>
	/// What a drag selection between two world points would take, by Hierarchy Extension's rule (fully inside, a prefab
	/// only whole, never what's under a taken object, never unselectable ones), next to what the stock rule takes (any
	/// object whose bounds, with everything under it, the box touches). Selects nothing. Uses the viewport last clicked
	/// in, and its camera, so both points should be in its view.
	/// </summary>
	/// <param name="from">One corner of the box, a world point "x,y,z".</param>
	/// <param name="to">The opposite corner.</param>
	[McpTool.ReadOnly( "hierarchy_box_probe" )]
	public static object BoxProbe( string from, string to )
	{
		var (session, gizmos, _, _) = CursorAt( from );
		var camera = gizmos.Input.Camera;
		var scene = session.Scene;

		var box = Rect.FromPoints( camera.ToScreen( Vector3.Parse( from ) ), camera.ToScreen( Vector3.Parse( to ) ) );
		var frustum = camera.GetFrustum( box );

		var bounds = new Dictionary<GameObject, BBox>();
		BBox BoundsOf( GameObject go )
		{
			if ( !bounds.TryGetValue( go, out var b ) ) bounds[go] = b = go.GetBounds();
			return b;
		}

		// ObjectEditorTool.OnBoxSelect
		var stock = scene.GetAllObjects( true )
			.Where( go => go is not Scene && !go.Tags.Has( "hidden" ) )
			.Where( go => !(BoundsOf( go ).Size.AlmostEqual( 0.0f ) && !go.HasGizmoHandle) && frustum.IsInside( BoundsOf( go ), true ) )
			.Select( x => x.Name );

		return new
		{
			ScreenBox = box,
			Takes = ScenePicking.Take( scene, frustum, ScenePicking.VisibleBounds ).Select( x => x.Name ),
			StockTakes = stock,
		};
	}

	/// <summary>The session, gizmo instance and cursor input for pointing at <paramref name="point"/> in the scene view.</summary>
	static (SceneEditorSession Session, Gizmo.Instance Gizmos, Gizmo.Inputs Input, Vector3 World) CursorAt( string point )
	{
		var view = SceneViewWidget.Current ?? throw new Exception( "No scene view is open" );
		var viewport = view.LastSelectedViewportWidget ?? view._viewports?.Values.FirstOrDefault() ?? throw new Exception( "The scene view has no viewport" );
		var gizmos = viewport.GizmoInstance;
		var camera = gizmos.Input.Camera ?? throw new Exception( "The viewport hasn't drawn yet" );

		var world = Vector3.Parse( point );
		var forward = camera.Rotation.Forward;

		var input = gizmos.Input;
		input.CursorRay = camera.Ortho
			? new Ray( world - forward * Vector3.Dot( world - camera.Position, forward ), forward )
			: new Ray( camera.Position, (world - camera.Position).Normal );
		input.CursorPosition = camera.ToScreen( world );
		input.Modifiers = KeyboardModifiers.None;
		input.LeftMouse = false;

		return (view.Session, gizmos, input, world);
	}

	/// <summary>
	/// Draws the Hierarchy's rows in view into a PNG as the dock paints them (not the expand arrows), with the hand
	/// column over them. The bridge can't see editor windows, so this is how to check the column lines up. The preview
	/// ids draw as if they were unselectable, and the hovered one with its faint hand, without changing anything.
	/// </summary>
	/// <param name="path">Where to write the PNG, an absolute path.</param>
	/// <param name="preview">Ids to draw as unselectable, comma separated, as if their hands had been clicked.</param>
	/// <param name="hovered">An id to draw as the row under the cursor.</param>
	[McpTool.ReadOnly( "hierarchy_render_rows" )]
	public static object RenderRows( string path, string preview = null, string hovered = null )
	{
		var previewed = preview is null ? new HashSet<GameObject>() : Find( preview ).ToHashSet();
		var hoveredObject = hovered is null ? null : Find( hovered ).FirstOrDefault();

		SelectableColumn.Hand HandOf( GameObject go )
		{
			if ( Unselectable.IsSet( go ) || previewed.Contains( go ) ) return SelectableColumn.Hand.Own;

			for ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )
			{
				if ( Unselectable.IsSet( o ) || previewed.Contains( o ) ) return SelectableColumn.Hand.FromParent;
			}

			return go == hoveredObject ? SelectableColumn.Hand.Offered : SelectableColumn.Hand.None;
		}

		var column = SelectableColumn.Current ?? throw new Exception( "The Hierarchy isn't open" );
		var tree = column.Tree;
		var rows = column.Rows().ToList();
		if ( rows.Count == 0 ) throw new Exception( "No rows in view" );

		var top = rows.Min( x => x.Row.Rect.Top );
		var bottom = rows.Max( x => x.Row.Rect.Bottom );
		var pixmap = new Pixmap( (int)tree.Width, (int)MathF.Ceiling( bottom - top ) );

		using ( Paint.ToPixmap( pixmap ) )
		{
			Paint.Translate( new Vector2( 0, -top ) );

			Paint.ClearPen();
			Paint.SetBrush( Theme.ControlBackground );
			Paint.DrawRect( new Rect( 0, top, tree.Width, bottom - top ) );

			foreach ( var (row, go) in rows )
			{
				if ( row.Object is not TreeNode node ) continue;

				// as TreeView.PaintItem shifts a row by its depth before the node paints it
				var indent = tree.IndentWidth * row.Column + tree.ExpandWidth;
				row.Rect.Left += indent;
				try
				{
					node.OnPaint( row );
				}
				finally
				{
					row.Rect.Left -= indent;
				}

				SelectableColumn.PaintHand( new Rect( column.Position.x, row.Rect.Top, column.Size.x, row.Rect.Height ), HandOf( go ), go.Active );
			}
		}

		pixmap.SavePng( path );

		return new { Path = path, Size = new Vector2( pixmap.Width, pixmap.Height ), Rows = rows.Select( x => x.Object.Name ) };
	}

	static GameObject[] Find( string ids )
	{
		var scene = SceneEditorSession.Active?.Scene ?? throw new Exception( "No scene is open" );

		var found = new List<GameObject>();
		foreach ( var id in (ids ?? "").Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ) )
		{
			var go = Guid.TryParse( id, out var guid ) ? scene.Directory.FindByGuid( guid ) : null;
			found.Add( go ?? throw new Exception( $"No object with id {id}" ) );
		}

		return found.ToArray();
	}
}
trashpandagames.hierarchyextension / 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();
	}
}
trashpandagames.hierarchyextension / Code/Assembly.cs
Game library
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
Debug: View Raw JSON Response
{
    "TotalCount": 13,
    "Files": [
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/Unselectable.cs",
            "FileName": "Unselectable.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// Objects that can't be selected in the scene view. A click goes through them to whatever is behind, and a drag\n/// selection leaves them out. They still select from the Hierarchy, so they can be inspected and edited as usual.\n/// <para>\n/// An object is made unselectable with the <see cref=\"Tag\"/> tag, the way the stock Hide marks hidden objects with\n/// \"hidden\". It saves with the scene, shows in the object's Tags, takes undo, and counts as an override on a prefab\n/// instance. Tags pass down to children, so everything under an unselectable object is unselectable too.\n/// </para>\n/// <para>\n/// Set it with the hand on each Hierarchy row (<see cref=\"SelectableColumn\"/>), with <b>Scene > View > Toggle\n/// Selectable</b> (Alt+L) for the selected objects, or with <b>Make All Selectable</b>. <see cref=\"ScenePicking\"/>\n/// makes the scene view honour it.\n/// </para>\n/// </summary>\npublic static class Unselectable\n{\n\t/// <summary>The tag that makes an object, and everything under it, unselectable in the scene view.</summary>\n\tpublic const string Tag = \"unselectable\";\n\n\t/// <summary>Whether <paramref name=\"go\"/> can't be selected in the scene view, from its own tag or a parent's.</summary>\n\tpublic static bool Has( GameObject go ) => go.IsValid() && go is not Scene && go.Tags.Has( Tag );\n\n\t/// <summary>Whether <paramref name=\"go\"/> has the tag itself, not only from a parent.</summary>\n\tpublic static bool IsSet( GameObject go ) => go.IsValid() && go is not Scene && go.Tags.Has( Tag, false );\n\n\t/// <summary>\n\t/// The object whose tag makes <paramref name=\"go\"/> unselectable: itself, or the nearest parent with the tag.\n\t/// Null when it's selectable.\n\t/// </summary>\n\tpublic static GameObject Source( GameObject go )\n\t{\n\t\tfor ( var o = go; o.IsValid() && o is not Scene; o = o.Parent )\n\t\t{\n\t\t\tif ( IsSet( o ) ) return o;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/// <summary>\n\t/// Make <paramref name=\"objects\"/> unselectable in the scene view, or selectable again, as one undo step. Objects\n\t/// that are that way already are left alone. Making an object selectable only takes its own tag off, so one under\n\t/// an unselectable parent stays unselectable; pass <see cref=\"Source\"/> to free it.\n\t/// </summary>\n\tpublic static void Set( IEnumerable<GameObject> objects, bool unselectable )\n\t{\n\t\tvar changing = objects.Where( x => x.IsValid() && x is not Scene && IsSet( x ) != unselectable ).Distinct().ToArray();\n\t\tif ( changing.Length == 0 ) return;\n\n\t\tvar session = SessionOf( changing[0] );\n\t\tif ( session is null ) return;\n\n\t\tvar name = unselectable ? \"Make Unselectable\" : \"Make Selectable\";\n\t\tif ( changing.Length == 1 ) name = $\"{name}: {changing[0].Name}\";\n\n\t\tusing var sceneScope = session.Scene.Push();\n\n\t\t// only the objects' own properties, which is where tags live: a tile layer's children and components can be big\n\t\tusing ( session.UndoScope( name ).WithGameObjectChanges( changing, GameObjectUndoFlags.Properties ).Push() )\n\t\t{\n\t\t\tforeach ( var go in changing )\n\t\t\t{\n\t\t\t\tif ( unselectable ) go.Tags.Add( Tag );\n\t\t\t\telse go.Tags.Remove( Tag );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Alt+L: make the selected objects unselectable in the scene view or, when they all are already, selectable again.\n\t/// One that's unselectable through a parent frees that parent, and so everything under it.\n\t/// </summary>\n\t[Menu( \"Editor\", \"Scene/View/Toggle Selectable\", \"do_not_touch\" )]\n\t[Shortcut( \"editor.toggle-selectable\", \"Alt+L\" )]\n\tpublic static void ToggleSelected()\n\t{\n\t\tvar session = SceneEditorSession.Active;\n\t\tif ( session is null ) return;\n\n\t\tvar selected = session.Selection.OfType<GameObject>().Where( x => x.IsValid() && x is not Scene ).ToArray();\n\t\tif ( selected.Length == 0 ) return;\n\n\t\tif ( selected.Any( x => !Has( x ) ) ) Set( selected, true );\n\t\telse Set( selected.Select( Source ), false );\n\t}\n\n\t/// <summary>Make everything in the open scene selectable in the scene view again.</summary>\n\t[Menu( \"Editor\", \"Scene/View/Make All Selectable\", \"touch_app\" )]\n\tpublic static void MakeAllSelectable()\n\t{\n\t\tvar scene = SceneEditorSession.Active?.Scene;\n\t\tif ( !scene.IsValid() ) return;\n\n\t\tSet( scene.GetAllObjects( false ).Where( IsSet ), false );\n\t}\n\n\t/// <summary>Whether anything in <paramref name=\"scene\"/> is unselectable. Cheap enough to ask on every click.</summary>\n\tpublic static bool AnyIn( Scene scene )\n\t{\n\t\tif ( !scene.IsValid() ) return false;\n\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\n\t\t{\n\t\t\tif ( IsSet( go ) ) return true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/// <summary>The editor session whose scene holds <paramref name=\"go\"/>, the play session's included.</summary>\n\tpublic static SceneEditorSession SessionOf( GameObject go ) => SessionOf( go?.Scene );\n\n\t/// <summary>The editor session of <paramref name=\"scene\"/>, the play session's included.</summary>\n\tpublic static SceneEditorSession SessionOf( Scene scene )\n\t{\n\t\tif ( scene is null ) return null;\n\n\t\treturn SceneEditorSession.All.FirstOrDefault( x => x.Scene == scene );\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "global using Sandbox;\nglobal using Editor;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/SelectionOutline.cs",
            "FileName": "SelectionOutline.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// Outlines what's selected in the scene view: one box around each selected object and everything under it (its\n/// sprites, models, colliders, particles and icons: <see cref=\"ScenePicking.VisibleBounds\"/>), a thin line drawn over\n/// everything in the editor's own selected colour. An object inside another selected one gets no box of its own; the\n/// other's covers it. During a drag selection it outlines what the drag will select (<see cref=\"ScenePicking.DragResult\"/>).\n/// <para>\n/// Why: the stock scene view only outlines an object's own sprite or model (<c>SpriteRenderer.DrawGizmos</c> asks\n/// <c>Gizmo.IsSelected</c> in the sprite's own object; the viewport's <c>DrawSelection</c> takes the selected object's\n/// own <c>ModelRenderer</c>s). A prefab, or a group, keeps its sprites on its children, and selecting it selects the\n/// root, so a selected decoration, or Sera, showed nothing but the move handle (tools and engine source, 26.09.29).\n/// </para>\n/// </summary>\npublic static class SelectionOutline\n{\n\t/// <summary>Draws the outlines. Called from the scene view's gizmo pass (<see cref=\"ScenePicking\"/>).</summary>\n\tinternal static void Draw( IEnumerable<object> selected )\n\t{\n\t\tvar objects = selected.OfType<GameObject>().Where( x => x.IsValid() && x is not Scene ).ToHashSet();\n\t\tvar boxes = new List<BBox>();\n\n\t\tforeach ( var go in objects )\n\t\t{\n\t\t\tif ( HasAncestorIn( go, objects ) ) continue;\n\n\t\t\t// a point (an icon and nothing else) is shown by the icon itself\n\t\t\tif ( ScenePicking.VisibleBounds( go ) is { } bounds && bounds.Size.Length > 0.01f ) boxes.Add( bounds );\n\t\t}\n\n\t\tif ( boxes.Count == 0 ) return;\n\n\t\t// One colour and one thickness, so it's all one batch. A dark edge under the line looked better, but it has to be a\n\t\t// thicker line, and a change of thickness starts another batch (Gizmo.Draw.CanReuseVertexObject). Batches drawn\n\t\t// over everything have no order between them, so after a reselect the edge sometimes landed on top of the line.\n\t\tusing var scope = Gizmo.Scope( \"hierarchyextension-selection\" );\n\t\tGizmo.Transform = global::Transform.Zero;\n\t\tGizmo.Draw.IgnoreDepth = true;\n\t\tGizmo.Draw.Color = Gizmo.Colors.Selected;\n\t\tGizmo.Draw.LineThickness = 1.5f;\n\n\t\tforeach ( var box in boxes ) Gizmo.Draw.LineBBox( box );\n\t}\n\n\tstatic bool HasAncestorIn( GameObject go, HashSet<GameObject> objects )\n\t{\n\t\tfor ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )\n\t\t{\n\t\t\tif ( objects.Contains( o ) ) return true;\n\t\t}\n\n\t\treturn false;\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/ClickThrough.cs",
            "FileName": "ClickThrough.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// Works out what a click in the scene view would select if the unselectable objects weren't there. It plays the click\n/// again through a gizmo instance of its own, over the scene's own gizmos, with the unselectable objects held out:\n/// the engine picks, so whatever it picks normally (render meshes, sprites, handles, a component's hitboxes, selection\n/// bases) is picked the same way.\n/// <para>\n/// Why not stop the engine picking them in the first place: the scene view has no per-object switch. The only objects\n/// it skips are ones tagged \"hidden\" or flagged Hidden, and both stop them drawing or listing too (engine source,\n/// <c>GameObject.DrawGizmos</c> and <c>SceneViewportWidget.UpdateHovered</c>, 26.09.29). So the objects are flagged\n/// Hidden only while the click is replayed here, and put back straight after, before anything draws or lists them.\n/// </para>\n/// </summary>\npublic static class ClickThrough\n{\n\t/// <summary>A change a click makes to the selection, in the order the click makes them.</summary>\n\tpublic enum Change { Clear, Add, Remove }\n\n\t/// <summary>\n\t/// The selection changes a click with <paramref name=\"input\"/> would make in <paramref name=\"scene\"/> if the\n\t/// unselectable objects weren't there, in order. Empty when it would hit nothing. The selection isn't touched:\n\t/// the click reads <paramref name=\"selection\"/>, to know what's selected, and only records what it would do.\n\t/// Also used by <see cref=\"ScenePicking\"/>; call it outside any gizmo drawing.\n\t/// </summary>\n\tpublic static List<(Change Change, object Target)> Run( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, SelectionSystem selection )\n\t{\n\t\treturn Run( scene, settings, input, selection, scene.GetAllObjects( true ).Where( Unselectable.IsSet ).ToArray() );\n\t}\n\n\t/// <summary>The same with <paramref name=\"heldOut\"/>, and everything under them, held out.</summary>\n\tpublic static List<(Change Change, object Target)> Run( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, SelectionSystem selection, IReadOnlyList<GameObject> heldOut )\n\t{\n\t\tvar recorder = new Recorder( selection );\n\t\tif ( !scene.IsValid() ) return recorder.Changes;\n\n\t\t// a GameObject flagged Hidden draws no gizmos, and nor do its children\n\t\tvar flagged = new List<GameObject>();\n\t\tforeach ( var go in heldOut )\n\t\t{\n\t\t\tif ( !go.IsValid() || go.Flags.Contains( GameObjectFlags.Hidden ) ) continue;\n\n\t\t\tgo.Flags |= GameObjectFlags.Hidden;\n\t\t\tflagged.Add( go );\n\t\t}\n\n\t\ttry\n\t\t{\n\t\t\tusing var instance = new Gizmo.Instance { Settings = settings, Selection = recorder };\n\n\t\t\t// The gizmo state reads the mouse a frame late: a press is seen the frame after the button goes down, and a\n\t\t\t// click the frame after it comes up, when the object hovered then is the one pressed.\n\t\t\tinput.IsHovered = true;\n\t\t\tFrame( instance, scene, input, false, heldOut, draw: false );\n\t\t\tFrame( instance, scene, input, true, heldOut );\n\t\t\tFrame( instance, scene, input, false, heldOut );\n\t\t\tFrame( instance, scene, input, false, heldOut );\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tforeach ( var go in flagged )\n\t\t\t{\n\t\t\t\tif ( go.IsValid() ) go.Flags &= ~GameObjectFlags.Hidden;\n\t\t\t}\n\t\t}\n\n\t\treturn recorder.Changes;\n\t}\n\n\t/// <summary>\n\t/// Plays the button going down at <paramref name=\"input\"/>, nothing held out, and asks <paramref name=\"ask\"/> on the\n\t/// frame the press is seen, as the scene view's own code would see it (<c>Gizmo.Pressed</c>). For the bridge's\n\t/// press probe; call it outside any gizmo drawing.\n\t/// </summary>\n\tpublic static T WhilePressed<T>( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, System.Func<T> ask )\n\t{\n\t\tvar none = System.Array.Empty<GameObject>();\n\n\t\tusing var instance = new Gizmo.Instance { Settings = settings, Selection = new Recorder( null ) };\n\n\t\tinput.IsHovered = true;\n\t\tFrame( instance, scene, input, false, none, draw: false );\n\t\tFrame( instance, scene, input, true, none );\n\n\t\tinput.LeftMouse = true;\n\t\tinstance.Input = input;\n\n\t\tusing var gizmos = instance.Push();\n\t\tusing var sceneScope = scene.Push();\n\t\treturn ask();\n\t}\n\n\tstatic void Frame( Gizmo.Instance instance, Scene scene, Gizmo.Inputs input, bool leftMouse, IReadOnlyList<GameObject> heldOut, bool draw = true )\n\t{\n\t\tinput.LeftMouse = leftMouse;\n\t\tinstance.Input = input;\n\n\t\tusing var gizmos = instance.Push();\n\t\tusing var sceneScope = scene.Push();\n\n\t\tif ( !draw ) return;\n\n\t\tHoverRenderMeshes( scene, heldOut );\n\t\tscene.EditorDraw();\n\t}\n\n\t/// <summary>\n\t/// What the scene view does before the gizmos draw: trace the render meshes under the cursor and hover the object\n\t/// hit. The stock one is private (<c>SceneViewportWidget.UpdateHovered</c>); this is it, holding out the objects.\n\t/// </summary>\n\tstatic void HoverRenderMeshes( Scene scene, IReadOnlyList<GameObject> heldOut )\n\t{\n\t\tif ( !Gizmo.Settings.Selection ) return;\n\n\t\tvar trace = scene.Trace.Ray( Gizmo.CurrentRay, Gizmo.RayDepth )\n\t\t\t.UseRenderMeshes( true )\n\t\t\t.UsePhysicsWorld( false )\n\t\t\t.WithoutTags( \"hidden\" );\n\n\t\tforeach ( var go in heldOut )\n\t\t{\n\t\t\tif ( go.IsValid() ) trace = trace.IgnoreGameObjectHierarchy( go );\n\t\t}\n\n\t\tvar tr = trace.Run();\n\t\tif ( !tr.Hit || !tr.Component.IsValid() ) return;\n\n\t\tusing ( Gizmo.ObjectScope( tr.GameObject, tr.GameObject.WorldTransform ) )\n\t\t{\n\t\t\tGizmo.Hitbox.DepthBias = 1;\n\t\t\tGizmo.Hitbox.TrySetHovered( tr.Distance );\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// The replayed click's selection: it answers what's selected from the real one, and writes down the changes\n\t/// instead of making them.\n\t/// </summary>\n\tsealed class Recorder : SelectionSystem\n\t{\n\t\treadonly SelectionSystem _selection;\n\n\t\tpublic readonly List<(Change Change, object Target)> Changes = new();\n\n\t\tpublic Recorder( SelectionSystem selection )\n\t\t{\n\t\t\t_selection = selection;\n\t\t}\n\n\t\tpublic override bool Contains( object obj ) => _selection?.Contains( obj ) ?? false;\n\n\t\tpublic override bool Any() => _selection?.Any() ?? false;\n\n\t\tpublic override void Clear() => Changes.Add( (Change.Clear, null) );\n\n\t\tpublic override bool Add( object obj )\n\t\t{\n\t\t\tif ( obj is null ) return false;\n\n\t\t\tChanges.Add( (Change.Add, obj) );\n\t\t\treturn true;\n\t\t}\n\n\t\tpublic override bool Remove( object obj )\n\t\t{\n\t\t\tif ( obj is null ) return false;\n\n\t\t\tChanges.Add( (Change.Remove, obj) );\n\t\t\treturn true;\n\t\t}\n\n\t\tpublic override bool Set( object obj )\n\t\t{\n\t\t\tClear();\n\t\t\treturn Add( obj );\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "UnitTests/LibraryTest.cs",
            "FileName": "LibraryTest.cs",
            "PackageType": "library",
            "CodeKind": "UnitTest",
            "AssetVersionId": 398910,
            "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": "trashpandagames.hierarchyextension",
            "Path": "Editor/ScenePicking.cs",
            "FileName": "ScenePicking.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// Makes the scene view honour <see cref=\"Unselectable\"/>, and makes its drag selections take only what's fully inside.\n/// <list type=\"bullet\">\n/// <item><description><b>Clicks</b>: each scene viewport's gizmos select through a <see cref=\"ClickFilter\"/>. A click\n/// that would select an unselectable object is held back. On the next editor frame <see cref=\"ClickThrough\"/> plays it\n/// again without the unselectable objects, and its selection is made instead, as one undo step. With nothing behind,\n/// the click deselects, like a click on empty space.</description></item>\n/// <item><description><b>Drags</b> that start on an unselectable object are drags on empty space: drag selections,\n/// and the camera, take them as they would there (<see cref=\"LetGoOfDraggedPress\"/>).</description></item>\n/// <item><description><b>Drag selections</b> with the Object Select tool take only objects fully inside the box, never\n/// unselectable ones, and a prefab only whole (<see cref=\"WatchDragSelection\"/>).</description></item>\n/// <item><description><b>Right-clicking</b> the scene view offers to make unselectable objects selectable again.</description></item>\n/// </list>\n/// <para>\n/// The filter goes where the engine lets it: a viewport's <c>GizmoInstance.Selection</c> is a public setting, and only\n/// the gizmos' clicks select through it (tools source, <c>SceneViewportWidget</c>, 26.09.29). The Hierarchy, the\n/// inspector, tools and code select through the session's selection, so they can still pick anything.\n/// </para>\n/// </summary>\npublic sealed class ScenePicking : EditorEvent.ISceneView\n{\n\t/// <summary>How far the cursor goes before a drag selection starts (<c>EditorTool.UpdateBoxSelection</c>), in pixels.</summary>\n\tconst float DragDistance = 5;\n\n\tstatic ScenePicking _listener;\n\tstatic PendingClick _pending;\n\tstatic DragWatch _drag;\n\n\t/// <summary>Whether the press going on has been checked for landing on an unselectable object.</summary>\n\tstatic bool _pressChecked;\n\n\t/// <summary>\n\t/// While a drag selection is going on in the scene view: what it will select when the mouse comes up. Null otherwise.\n\t/// The Hierarchy shows this meanwhile (<see cref=\"HierarchySelection\"/>).\n\t/// </summary>\n\tinternal static HashSet<object> DragResult => _drag?.Result;\n\n\t/// <summary>A click that was held back, with what the viewport's gizmos saw when it landed.</summary>\n\tsealed record PendingClick( SceneViewportWidget Viewport, SelectionSystem Selection, Gizmo.Inputs Input );\n\n\t/// <summary>A press begun in a viewport (known by its camera), and the drag selection it may become.</summary>\n\tsealed class DragWatch\n\t{\n\t\tpublic Scene Scene;\n\t\tpublic SceneCamera Camera;\n\n\t\t/// <summary>Editor frames in a row the left button has been up while this drag still went on.</summary>\n\t\tpublic int FramesUp;\n\n\t\t/// <summary>What was selected when the press began.</summary>\n\t\tpublic HashSet<object> Before;\n\n\t\t/// <summary>The cursor's ray when the press began, and on the last frame the button was down, as the tool takes them.</summary>\n\t\tpublic Ray Start, Last;\n\n\t\t/// <summary>While it's a drag selection: what the selection will be when the mouse comes up. Null otherwise.</summary>\n\t\tpublic HashSet<object> Result;\n\n\t\treadonly Dictionary<GameObject, BBox?> _bounds = new();\n\n\t\t/// <summary>\n\t\t/// <see cref=\"VisibleBounds\"/>, worked out once per drag, as the tool does its bounds: nothing moves while the\n\t\t/// mouse is busy with a drag selection, and they're slow to get.\n\t\t/// </summary>\n\t\tpublic BBox? BoundsOf( GameObject go )\n\t\t{\n\t\t\tif ( !_bounds.TryGetValue( go, out var bounds ) ) _bounds[go] = bounds = VisibleBounds( go );\n\t\t\treturn bounds;\n\t\t}\n\t}\n\n\t[EditorEvent.Frame]\n\tstatic void Frame()\n\t{\n\t\tif ( _listener is null )\n\t\t{\n\t\t\t_listener = new ScenePicking();\n\t\t\tEditorEvent.Register( _listener );\n\t\t}\n\n\t\tInstallFilters();\n\t\tResolvePendingClick();\n\n\t\t// a drag whose viewport stopped drawing before its release was seen: end it once the button has been up a while\n\t\tif ( _drag is { } drag )\n\t\t{\n\t\t\tdrag.FramesUp = Editor.Application.MouseButtons.HasFlag( MouseButtons.Left ) ? 0 : drag.FramesUp + 1;\n\t\t\tif ( drag.FramesUp > 3 ) Finish( drag );\n\t\t}\n\t}\n\n\t/// <summary>A code reload keeps the listener; register it again in case the reload dropped it.</summary>\n\t[EditorEvent.Hotload]\n\tstatic void OnHotload()\n\t{\n\t\tif ( _listener is null ) return;\n\n\t\tEditorEvent.Unregister( _listener );\n\t\tEditorEvent.Register( _listener );\n\t}\n\n\t/// <summary>\n\t/// Puts a filter on every viewport of the scene view in use, and on any it's lost: a viewport sets its selection\n\t/// again when play starts or stops. Also lets through a clear that's still waiting for its add.\n\t/// </summary>\n\tstatic void InstallFilters()\n\t{\n\t\tvar view = SceneViewWidget.Current;\n\t\tif ( !view.IsValid() || view._viewports is null ) return;\n\n\t\tforeach ( var viewport in view._viewports.Values )\n\t\t{\n\t\t\tvar gizmos = viewport.IsValid() ? viewport.GizmoInstance : null;\n\t\t\tif ( gizmos?.Selection is null ) continue;\n\n\t\t\tif ( gizmos.Selection is ClickFilter filter )\n\t\t\t{\n\t\t\t\tfilter.Flush();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// one from before a code reload that wasn't carried over: leave it rather than stack another on it\n\t\t\tif ( gizmos.Selection.GetType().FullName == typeof( ClickFilter ).FullName ) continue;\n\n\t\t\tgizmos.Selection = new ClickFilter( viewport, gizmos.Selection );\n\t\t}\n\t}\n\n\tstatic void ResolvePendingClick()\n\t{\n\t\tvar click = _pending;\n\t\tif ( click is null ) return;\n\t\t_pending = null;\n\n\t\tvar session = SceneEditorSession.All.FirstOrDefault( x => x.Selection == click.Selection );\n\t\tvar scene = session?.Scene;\n\t\tif ( !scene.IsValid() || !click.Viewport.IsValid() ) return;\n\n\t\tvar changes = ClickThrough.Run( scene, click.Viewport.GizmoInstance.Settings, click.Input, click.Selection );\n\n\t\tusing var sceneScope = scene.Push();\n\t\tusing ( session.UndoScope( UndoName( changes ) ).Push() )\n\t\t{\n\t\t\tif ( changes.Count == 0 )\n\t\t\t{\n\t\t\t\tclick.Selection.Clear();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tforeach ( var (change, target) in changes )\n\t\t\t{\n\t\t\t\tswitch ( change )\n\t\t\t\t{\n\t\t\t\t\tcase ClickThrough.Change.Clear: click.Selection.Clear(); break;\n\t\t\t\t\tcase ClickThrough.Change.Add: click.Selection.Add( target ); break;\n\t\t\t\t\tcase ClickThrough.Change.Remove: click.Selection.Remove( target ); break;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/// <summary>Named as the stock click's undo steps are: \"Select {name}\", or \"Deselect all\" for empty space.</summary>\n\tstatic string UndoName( List<(ClickThrough.Change Change, object Target)> changes )\n\t{\n\t\tvar last = changes.LastOrDefault( x => x.Change != ClickThrough.Change.Clear );\n\t\tif ( last.Target is GameObject go ) return last.Change == ClickThrough.Change.Add ? $\"Select {go.Name}\" : $\"Deselect {go.Name}\";\n\n\t\treturn \"Deselect all\";\n\t}\n\n\t/// <summary>Runs in each viewport's frame after its tool, before the scene's gizmos.</summary>\n\tvoid EditorEvent.ISceneView.DrawGizmos( Scene scene )\n\t{\n\t\tLetGoOfDraggedPress( scene );\n\t\tWatchDragSelection( scene );\n\n\t\t// what's selected, or during a drag selection what it will select\n\t\tvar selected = DragResult ?? (IEnumerable<object>)Unselectable.SessionOf( scene )?.Selection;\n\t\tif ( selected is not null ) SelectionOutline.Draw( selected );\n\t}\n\n\t/// <summary>\n\t/// A press on an unselectable object still counts as a press on something: the scene view hovers what's under the\n\t/// cursor before anyone clicks, with no way to leave objects out. While anything is pressed, a drag doesn't start a\n\t/// drag selection (<c>EditorTool.UpdateBoxSelection</c>) or move the camera (<c>SceneViewportWidget</c>), so a drag\n\t/// that began on a backdrop did nothing. Once such a press has moved as far as a drag selection waits for, let go of\n\t/// it (<c>Gizmo.Pressed.ClearPath</c>, as the engine does itself for a dragged object press): from the next frame the\n\t/// drag is a drag on empty space, and a drag selection starts from where the press began. A press that doesn't move\n\t/// stays a click, which the <see cref=\"ClickFilter\"/> plays through to what's behind.\n\t/// </summary>\n\tstatic void LetGoOfDraggedPress( Scene scene )\n\t{\n\t\tif ( !Gizmo.HasMouseFocus ) return;\n\t\tif ( Gizmo.WasLeftMousePressed ) _pressChecked = false;\n\n\t\tif ( _pressChecked || !Gizmo.IsLeftMouseDown || !Gizmo.Pressed.Any ) return;\n\t\tif ( Gizmo.Pressed.CursorDelta.Length < DragDistance ) return;\n\n\t\t_pressChecked = true;\n\t\tif ( PressedUnselectable( scene ).IsValid() ) Gizmo.Pressed.ClearPath();\n\t}\n\n\t/// <summary>\n\t/// The unselectable object the press is on, or null. The gizmos name what's pressed by its path: an object's is\n\t/// \"object-{hash}\" (<c>Gizmo.ObjectScope</c>), its handle's \".../Handle\", and its components' hitboxes \".../Scope0\",\n\t/// \".../Scope1\" and so on, in order (<c>GameObject.DrawGizmos</c>). <c>Gizmo.Pressed.This</c> compares the path it's\n\t/// asked in, so each is tried. A hitbox a component draws inside a scope of its own isn't found.\n\t/// </summary>\n\tinternal static GameObject PressedUnselectable( Scene scene ) => PressedObject( scene, Unselectable.Has );\n\n\t/// <summary>The object the press is on, among those <paramref name=\"among\"/> accepts, or null. See <see cref=\"PressedUnselectable\"/>.</summary>\n\tinternal static GameObject PressedObject( Scene scene, System.Func<GameObject, bool> among )\n\t{\n\t\tusing var scope = Gizmo.Scope( \"hierarchyextension\" );\n\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\n\t\t{\n\t\t\tif ( !among( go ) ) continue;\n\n\t\t\tvar path = $\"object-{go.GetHashCode()}\";\n\t\t\tif ( IsPressed( path ) || IsPressed( $\"{path}/Handle\" ) ) return go;\n\n\t\t\tfor ( var i = 0; i < go.Components.Count; i++ )\n\t\t\t{\n\t\t\t\tif ( IsPressed( $\"{path}/Scope{i}\" ) ) return go;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tstatic bool IsPressed( string path )\n\t{\n\t\tGizmo.Path = path;\n\t\treturn Gizmo.Pressed.This;\n\t}\n\n\t/// <summary>\n\t/// Makes the Object Select tool's drag selections take only what's fully inside the box. The stock one takes anything\n\t/// whose bounds the box touches, and an object's bounds include everything under it (<c>ObjectEditorTool.OnBoxSelect</c>,\n\t/// <c>GameObject.GetBounds</c>), so a small box over a few decorations also took their group and the map they're in.\n\t/// It has no way in, so it runs as before and the result is redone here, from the same box:\n\t/// <list type=\"bullet\">\n\t/// <item><description>An object is taken when it's fully inside with everything under it, and then stands for all\n\t/// of that: what's under it isn't taken as well.</description></item>\n\t/// <item><description>Inside a prefab, or anything else a click selects as a whole (<see cref=\"IsSelectionBase\"/>),\n\t/// only that whole counts.</description></item>\n\t/// <item><description>Hidden and unselectable objects are never taken.</description></item>\n\t/// <item><description>Ctrl takes them out of the selection, Shift adds them, and otherwise they're the selection,\n\t/// as with the stock one (<c>EditorTool.ApplyDragSelection</c>).</description></item>\n\t/// </list>\n\t/// While the drag goes on the scene view (<see cref=\"ClickFilter.Contains\"/>) and the Hierarchy\n\t/// (<see cref=\"HierarchySelection\"/>) show the result; it goes into the selection when the mouse comes up, the frame\n\t/// the stock one finishes. Putting it in every frame would have the tool undo it every frame, and the inspector rebuild\n\t/// and the Hierarchy scroll each time, so the inspector and the move handles show the stock one's picks until then.\n\t/// </summary>\n\tstatic void WatchDragSelection( Scene scene )\n\t{\n\t\tvar camera = Gizmo.Camera;\n\n\t\tif ( Gizmo.WasLeftMousePressed && Gizmo.HasMouseFocus )\n\t\t{\n\t\t\tvar before = Unselectable.SessionOf( scene )?.Selection;\n\t\t\t_drag = before is null ? null : new DragWatch { Scene = scene, Camera = camera, Before = before.ToHashSet(), Start = Gizmo.CurrentRay, Last = Gizmo.CurrentRay };\n\t\t\treturn;\n\t\t}\n\n\t\tvar drag = _drag;\n\t\tif ( drag is null || drag.Camera != camera ) return;\n\n\t\t// the frame the mouse comes up still counts: the tool finishes its drag selection on it, from the last box. With the\n\t\t// cursor out of the view the tool leaves its box as it was, and so does this\n\t\tif ( Gizmo.HasMouseFocus && (Gizmo.IsLeftMouseDown || Gizmo.WasLeftMouseReleased) )\n\t\t{\n\t\t\tif ( Gizmo.IsLeftMouseDown ) drag.Last = Gizmo.CurrentRay;\n\t\t\tdrag.Result = IsBoxSelecting( drag, out var frustum ) ? BoxResult( scene, drag, frustum ) : null;\n\n\t\t\t// the Hierarchy shows the result as it changes, not what the stock one has put in the selection meanwhile\n\t\t\tHierarchySelection.Current?.Sync();\n\t\t}\n\n\t\t// the release, or a later frame if the release went by unseen (a code reload, say)\n\t\tif ( !Gizmo.IsLeftMouseDown ) Finish( drag );\n\t}\n\n\t/// <summary>Ends the drag: a drag selection's result goes into the selection, and the Hierarchy shows the selection again.</summary>\n\tstatic void Finish( DragWatch drag )\n\t{\n\t\tif ( _drag == drag ) _drag = null;\n\n\t\tvar result = drag.Result;\n\t\tvar selection = Unselectable.SessionOf( drag.Scene )?.Selection;\n\t\tif ( result is not null && selection is not null )\n\t\t{\n\t\t\tforeach ( var obj in selection.ToArray() )\n\t\t\t{\n\t\t\t\tif ( !result.Contains( obj ) ) selection.Remove( obj );\n\t\t\t}\n\n\t\t\tforeach ( var obj in result )\n\t\t\t{\n\t\t\t\tif ( !selection.Contains( obj ) ) selection.Add( obj );\n\t\t\t}\n\t\t}\n\n\t\tHierarchySelection.Current?.Sync();\n\t}\n\n\t/// <summary>\n\t/// Whether the Object Select tool is making a drag selection this frame, decided as it decides it\n\t/// (<c>EditorTool.UpdateBoxSelection</c>): the cursor 5 px or more from where the press began, and nothing pressed.\n\t/// Gives the box's frustum.\n\t/// </summary>\n\tstatic bool IsBoxSelecting( DragWatch drag, out Frustum frustum )\n\t{\n\t\tfrustum = default;\n\n\t\tif ( EditorToolManager.CurrentModeName != nameof( ObjectEditorTool ) ) return false;\n\t\tif ( Gizmo.Pressed.Any ) return false;\n\n\t\tvar c1 = Gizmo.Camera.ToScreen( drag.Start.Project( 100 ) );\n\t\tvar c2 = Gizmo.Camera.ToScreen( drag.Last.Project( 100 ) );\n\t\tif ( Vector2.Distance( c1, c2 ) < DragDistance ) return false;\n\n\t\tfrustum = Gizmo.Camera.GetFrustum( Rect.FromPoints( c1, c2 ) );\n\t\treturn true;\n\t}\n\n\t/// <summary>What the selection will be when the drag selection in <paramref name=\"frustum\"/> finishes.</summary>\n\tstatic HashSet<object> BoxResult( Scene scene, DragWatch drag, Frustum frustum )\n\t{\n\t\tvar taken = Take( scene, frustum, go => drag.BoundsOf( go ) );\n\t\tvar result = new HashSet<object>( drag.Before );\n\n\t\tif ( Gizmo.IsCtrlPressed )\n\t\t{\n\t\t\tresult.ExceptWith( taken );\n\t\t\treturn result;\n\t\t}\n\n\t\tif ( !Gizmo.IsShiftPressed ) result.RemoveWhere( x => x is GameObject );\n\n\t\tresult.UnionWith( taken );\n\t\treturn result;\n\t}\n\n\t/// <summary>\n\t/// The objects a drag selection in <paramref name=\"frustum\"/> takes, before Ctrl or Shift: those fully inside with\n\t/// everything under them (<paramref name=\"boundsOf\"/>, usually <see cref=\"VisibleBounds\"/>), none that are part of\n\t/// one taken, hidden or unselectable, and a prefab only whole.\n\t/// </summary>\n\tinternal static HashSet<GameObject> Take( Scene scene, Frustum frustum, System.Func<GameObject, BBox?> boundsOf )\n\t{\n\t\tvar taken = new HashSet<GameObject>();\n\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\n\t\t{\n\t\t\tif ( go is Scene || go.Tags.Has( \"hidden\" ) || Unselectable.Has( go ) ) continue;\n\n\t\t\t// part of a prefab: the prefab is taken whole or not at all\n\t\t\tif ( InsideSelectionBase( go ) ) continue;\n\n\t\t\tif ( boundsOf( go ) is { } bounds && frustum.IsInside( bounds, false ) ) taken.Add( go );\n\t\t}\n\n\t\t// an object taken with everything under it stands for all of it\n\t\ttaken.RemoveWhere( go => HasAncestorIn( go, taken ) );\n\t\treturn taken;\n\t}\n\n\t/// <summary>\n\t/// What the scene view shows of <paramref name=\"go\"/> and everything under it, as one world box, or null when there's\n\t/// nothing. That's what the stock drag selection measures (<c>GameObject.GetBounds</c>: colliders, models, particles),\n\t/// plus sprites, which it leaves out, and the icon of each object that has one, as a point. A\n\t/// <c>SpriteRenderer</c> has no bounds of its own (it isn't a <c>Component.IHasBounds</c>, engine source 26.09.29),\n\t/// so with the stock drag selection no sprite can be drag-selected, and a 2D level's decorations never are.\n\t/// </summary>\n\tinternal static BBox? VisibleBounds( GameObject go )\n\t{\n\t\tBBox? result = null;\n\t\tvoid Add( BBox bounds ) => result = result is { } r ? r.AddBBox( bounds ) : bounds;\n\n\t\tforeach ( var o in go.GetAllObjects( true ) )\n\t\t{\n\t\t\tif ( o.Tags.Has( \"hidden\" ) ) continue;\n\n\t\t\tforeach ( var c in o.Components.GetAll( FindMode.EnabledInSelf ) )\n\t\t\t{\n\t\t\t\tif ( c is Component.IHasBounds hasBounds ) Add( hasBounds.LocalBounds.Transform( c.WorldTransform ) );\n\t\t\t\tif ( c is SpriteRenderer sprite ) Add( SpriteBounds( sprite ) );\n\t\t\t}\n\n\t\t\tif ( o.HasGizmoHandle ) Add( BBox.FromPositionAndSize( o.WorldPosition ) );\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/// <summary>A sprite's box in the world, worked out as its own hitbox is (<c>SpriteRenderer.DrawGizmos</c>).</summary>\n\tinternal static BBox SpriteBounds( SpriteRenderer sprite )\n\t{\n\t\tvar world = sprite.WorldTransform;\n\t\tvar billboard = sprite.Billboard is SpriteRenderer.BillboardMode.Always or SpriteRenderer.BillboardMode.YOnly;\n\n\t\tvar transform = billboard ? world.WithRotation( new Rotation() ) : world;\n\t\ttransform = transform.WithScale( new Vector3( world.Scale.x, world.Scale.x, world.Scale.z ) );\n\n\t\tvar pivot = ((sprite.CurrentAnimation?.Origin ?? new Vector2( 0.5f, 0.5f )) - 0.5f) * sprite.Size;\n\t\tvar size = new Vector2( sprite.Size.x, sprite.Size.y );\n\n\t\tif ( billboard )\n\t\t{\n\t\t\tsize += Vector2.Abs( pivot * 2 );\n\n\t\t\tvar angle = MathX.DegreeToRadian( world.Rotation.Roll() );\n\t\t\tvar cos = MathF.Abs( MathF.Cos( angle ) );\n\t\t\tvar sin = MathF.Abs( MathF.Sin( angle ) );\n\t\t\tsize = new Vector2( size.x * cos + size.y * sin, size.x * sin + size.y * cos );\n\t\t}\n\n\t\tvar local = BBox.FromPositionAndSize(\n\t\t\tbillboard ? Vector3.Zero : new Vector3( 0, pivot.x, pivot.y ),\n\t\t\tnew Vector3( billboard ? size.x : 0.5f, size.x, size.y ) );\n\n\t\treturn local.Transform( transform );\n\t}\n\n\t/// <summary>\n\t/// Whether a click selects <paramref name=\"go\"/> as a whole when it lands on something under it, as the scene view\n\t/// decides it (<c>GameObject.FindSelectionBase</c>): a network root, the outermost root of a prefab instance, or an\n\t/// object with a <see cref=\"SelectionBaseAttribute\"/> component.\n\t/// </summary>\n\tstatic bool IsSelectionBase( GameObject go )\n\t{\n\t\t// GameObject.IsOutermostPrefabInstanceRoot isn't public; the stock Hierarchy asks this\n\t\tif ( go.IsNetworkRoot || EditorUtility.Prefabs.IsOuterMostPrefabRoot( go ) ) return true;\n\n\t\treturn go.Components.GetAll().Any( x => EditorTypeLibrary.GetType( x.GetType() )?.HasAttribute<SelectionBaseAttribute>() == true );\n\t}\n\n\tstatic bool InsideSelectionBase( GameObject go )\n\t{\n\t\tfor ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )\n\t\t{\n\t\t\tif ( IsSelectionBase( o ) ) return true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tstatic bool HasAncestorIn( GameObject go, HashSet<GameObject> objects )\n\t{\n\t\tfor ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )\n\t\t{\n\t\t\tif ( objects.Contains( o ) ) return true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tvoid EditorEvent.ISceneView.ShowContextMenu( EditorEvent.ShowContextMenuEvent ev )\n\t{\n\t\tvar scene = ev.Session?.Scene;\n\t\tif ( !Unselectable.AnyIn( scene ) ) return;\n\n\t\tev.Menu.AddSeparator();\n\n\t\tif ( ev.Trace is { Hit: true } trace && Unselectable.Source( trace.GameObject ) is { } source )\n\t\t{\n\t\t\tev.Menu.AddOption( $\"Make {source.Name} Selectable\", \"touch_app\", () => Unselectable.Set( new[] { source }, false ) );\n\t\t}\n\n\t\tev.Menu.AddOption( \"Make All Selectable\", \"touch_app\", Unselectable.MakeAllSelectable );\n\t}\n\n\t/// <summary>\n\t/// A scene viewport's gizmo selection. It passes everything on to the scene's own selection, except a click on an\n\t/// unselectable object, which it holds back for <see cref=\"ResolvePendingClick\"/>.\n\t/// </summary>\n\tsealed class ClickFilter : SelectionSystem, IEnumerable<object>\n\t{\n\t\treadonly SceneViewportWidget _viewport;\n\t\treadonly SelectionSystem _selection;\n\n\t\t/// <summary>\n\t\t/// A click clears the selection just before it adds what it clicked (<c>Gizmo.Select</c>). The clear waits for\n\t\t/// the add: if the click is held back, the clear isn't wanted either.\n\t\t/// </summary>\n\t\tbool _clearing;\n\n\t\tpublic ClickFilter( SceneViewportWidget viewport, SelectionSystem selection )\n\t\t{\n\t\t\t_viewport = viewport;\n\t\t\t_selection = selection;\n\t\t}\n\n\t\tpublic override void Clear() => _clearing = true;\n\n\t\tpublic override bool Add( object obj )\n\t\t{\n\t\t\tif ( HoldBack( obj ) ) return false;\n\n\t\t\tFlush();\n\t\t\treturn _selection.Add( obj );\n\t\t}\n\n\t\tpublic override bool Remove( object obj )\n\t\t{\n\t\t\tif ( HoldBack( obj ) ) return false;\n\n\t\t\tFlush();\n\t\t\treturn _selection.Remove( obj );\n\t\t}\n\n\t\tpublic override bool Set( object obj )\n\t\t{\n\t\t\tClear();\n\t\t\treturn Add( obj );\n\t\t}\n\n\t\t/// <summary>\n\t\t/// Also what the gizmos draw as selected. While a drag selection goes on, that's what it will select when the mouse\n\t\t/// comes up (<see cref=\"WatchDragSelection\"/>), not what the stock one has put in the selection meanwhile.\n\t\t/// </summary>\n\t\tpublic override bool Contains( object obj ) => _drag?.Result is { } result ? result.Contains( obj ) : _selection.Contains( obj );\n\n\t\tpublic override bool Any() => _drag?.Result is { } result ? result.Count > 0 : _selection.Any();\n\n\t\tpublic override int GetHashCode() => _selection.GetHashCode();\n\n\t\tIEnumerator<object> IEnumerable<object>.GetEnumerator() => _selection.GetEnumerator();\n\n\t\tIEnumerator IEnumerable.GetEnumerator() => _selection.GetEnumerator();\n\n\t\t/// <summary>A clear still waiting for its add goes through.</summary>\n\t\tpublic void Flush()\n\t\t{\n\t\t\tif ( !_clearing ) return;\n\n\t\t\t_clearing = false;\n\t\t\t_selection.Clear();\n\t\t}\n\n\t\tbool HoldBack( object obj )\n\t\t{\n\t\t\tif ( obj is not GameObject go || !Unselectable.Has( go ) ) return false;\n\n\t\t\t_clearing = false;\n\t\t\t_pending = new PendingClick( _viewport, _selection, _viewport.GizmoInstance.Input );\n\t\t\treturn true;\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 398910,
            "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\", \"Hierarchy Extension\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"hierarchyextension\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"trashpandagames\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"trashpandagames.hierarchyextension\" )]\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-30T19:33:36.0810281Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.122.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.122.0\")]"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "global using Sandbox;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/HierarchySelection.cs",
            "FileName": "HierarchySelection.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// The Hierarchy's selection, so its rows are right while a drag selection is going on in the scene view.\n/// <para>\n/// The stock drag selection puts whatever its box touches into the scene's selection on every frame of the drag, and\n/// the Hierarchy highlights exactly that: parents, maps and unselectable objects that the fully-inside rule\n/// (<see cref=\"ScenePicking\"/>) only takes out when the mouse comes up. Taking them out every frame would have the stock\n/// one put them back every frame. So the Hierarchy gets a selection of its own instead: its tree's\n/// <c>TreeView.Selection</c> is a public setting, and the tree paints rows by asking it, repaints when its hash changes,\n/// and scrolls to what's added to it (tools source, <c>BaseItemWidget</c>, 26.09.29).\n/// </para>\n/// <para>\n/// It holds a copy of what the Hierarchy should show: the scene's selection, or during a drag selection what that will\n/// select (<see cref=\"ScenePicking.DragResult\"/>). Selecting in the Hierarchy goes straight to the scene's selection,\n/// as before. The inspector and the move handles follow the scene's selection, so during a drag they still show the\n/// stock one's picks.\n/// </para>\n/// </summary>\npublic sealed class HierarchySelection : SelectionSystem\n{\n\tstatic HierarchySelection _current;\n\n\t/// <summary>The one on the Hierarchy in use, if any.</summary>\n\tinternal static HierarchySelection Current => _current;\n\n\treadonly SelectionSystem _selection;\n\n\tHierarchySelection( SelectionSystem selection )\n\t{\n\t\t_selection = selection;\n\t}\n\n\t/// <summary>\n\t/// Puts one on the Hierarchy's tree, and again whenever the tree is given the scene's selection back: it does that\n\t/// each time it rebuilds, and on a new scene. Then brings its copy up to date.\n\t/// </summary>\n\t[EditorEvent.Frame]\n\tstatic void Frame()\n\t{\n\t\tvar tree = SceneTreeWidget.Current?.TreeView;\n\t\tvar selection = SceneEditorSession.Active?.Selection;\n\t\tif ( !tree.IsValid() || selection is null ) return;\n\n\t\tif ( tree.Selection == _current && _current?._selection == selection )\n\t\t{\n\t\t\t_current.Sync();\n\t\t\treturn;\n\t\t}\n\n\t\t// the tree has the scene's own, or one of these from before a code reload that wasn't carried over\n\t\tvar ours = tree.Selection?.GetType().FullName == typeof( HierarchySelection ).FullName;\n\t\tif ( tree.Selection != selection && !ours ) return;\n\n\t\tvar view = new HierarchySelection( selection );\n\n\t\t// filled before the tree has it, so filling it doesn't scroll the tree\n\t\tview.Sync();\n\t\ttree.Selection = view;\n\t\t_current = view;\n\t}\n\n\t/// <summary>Makes the copy what the Hierarchy should show now, adding and removing only what differs.</summary>\n\tinternal void Sync()\n\t{\n\t\tIEnumerable<object> target = ScenePicking.DragResult ?? (IEnumerable<object>)_selection;\n\t\tvar wanted = target.ToHashSet();\n\n\t\tforeach ( var obj in this.ToArray() )\n\t\t{\n\t\t\tif ( !wanted.Contains( obj ) ) base.Remove( obj );\n\t\t}\n\n\t\tforeach ( var obj in target )\n\t\t{\n\t\t\tif ( !base.Contains( obj ) ) base.Add( obj );\n\t\t}\n\t}\n\n\tpublic override bool Contains( object obj ) => ScenePicking.DragResult is null ? _selection.Contains( obj ) : base.Contains( obj );\n\n\tpublic override bool Any() => ScenePicking.DragResult is null ? _selection.Any() : base.Any();\n\n\tpublic override bool Add( object obj )\n\t{\n\t\tvar added = _selection.Add( obj );\n\t\tSync();\n\t\treturn added;\n\t}\n\n\tpublic override bool Remove( object obj )\n\t{\n\t\tvar removed = _selection.Remove( obj );\n\t\tSync();\n\t\treturn removed;\n\t}\n\n\tpublic override void Clear()\n\t{\n\t\t_selection.Clear();\n\t\tSync();\n\t}\n\n\tpublic override bool Set( object obj )\n\t{\n\t\tvar changed = _selection.Set( obj );\n\t\tSync();\n\t\treturn changed;\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/SelectableColumn.cs",
            "FileName": "SelectableColumn.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// The Hierarchy's column of hands, one on each row just left of where the stock Hide shows its eye. A crossed hand marks\n/// an object that can't be selected in the scene view (<see cref=\"Unselectable\"/>); it's faint when that comes from a\n/// parent. Hovering a row shows a faint hand to click. A click makes the row's object unselectable, or selectable again;\n/// on a row that's unselectable through a parent, it frees that parent.\n/// <para>\n/// The stock Hierarchy has no way in for this: <c>GameObjectNode.OnPaint</c> paints each row whole, and\n/// <c>GameObjectNode.AddGameObjectMenuItems</c> builds the row's context menu, with no event or interface to add to\n/// either (tools source, <c>Scene/SceneTree</c>, 26.09.29). So this is a narrow strip laid over the right-hand end of the\n/// Hierarchy's rows. It paints the hands and takes the clicks there; everywhere else the tree works as before.\n/// </para>\n/// </summary>\npublic sealed class SelectableColumn : Widget\n{\n\t/// <summary>An icon's box, as wide as the stock eye's.</summary>\n\tconst float IconWidth = 18;\n\n\t/// <summary>The stock eye's box and its gap: 18 wide, 4 in from the row's end (<c>GameObjectNode.OnPaint</c>).</summary>\n\tconst float EyeWidth = 4 + 18;\n\n\tstatic SelectableColumn _current;\n\n\t/// <summary>The column on the Hierarchy in use, if it's open.</summary>\n\tinternal static SelectableColumn Current => _current.IsValid() && _current._tree.IsValid() ? _current : null;\n\n\treadonly TreeView _tree;\n\n\t/// <summary>The Hierarchy's tree this column lies over.</summary>\n\tinternal TreeView Tree => _tree;\n\n\t/// <summary>A hash of what was painted, so the strip repaints only when rows move or change.</summary>\n\tint _look;\n\n\tSelectableColumn( TreeView tree ) : base( tree )\n\t{\n\t\t_tree = tree;\n\t\tMouseTracking = true;\n\t\tCursor = CursorShape.Finger;\n\n\t\tPlace();\n\t\tVisible = true;\n\t}\n\n\t/// <summary>Puts one on the Hierarchy's tree, and again whenever the dock builds a new tree.</summary>\n\t[EditorEvent.Frame]\n\tstatic void Attach()\n\t{\n\t\tvar tree = SceneTreeWidget.Current?.TreeView;\n\t\tif ( !tree.IsValid() ) return;\n\t\tif ( _current.IsValid() && _current._tree == tree ) return;\n\n\t\t_current = new SelectableColumn( tree );\n\t}\n\n\t[EditorEvent.Frame]\n\tvoid Frame()\n\t{\n\t\tif ( !_tree.IsValid() ) return;\n\n\t\tPlace();\n\n\t\tvar look = Look();\n\t\tif ( look == _look ) return;\n\n\t\t_look = look;\n\t\tUpdate();\n\t}\n\n\t/// <summary>\n\t/// Over the rows' right-hand end, just left of the eye's box, from the top down to the last row in view, so a click\n\t/// on the empty space under the rows still reaches the tree.\n\t/// </summary>\n\tvoid Place()\n\t{\n\t\tvar right = _tree.CanvasRect.Right;\n\t\tvar bottom = 0f;\n\t\tforeach ( var (row, _) in Rows() )\n\t\t{\n\t\t\tright = row.Rect.Right;\n\t\t\tbottom = row.Rect.Bottom;\n\t\t}\n\n\t\tvar position = new Vector2( right - EyeWidth - IconWidth, 0 );\n\t\tvar size = new Vector2( IconWidth, MathF.Min( bottom, _tree.Height ) );\n\n\t\tif ( Position != position ) Position = position;\n\t\tif ( Size != size ) Size = size;\n\t}\n\n\t/// <summary>The rows in view that show an object, top to bottom, in the tree's coordinates.</summary>\n\tinternal IEnumerable<(VirtualWidget Row, GameObject Object)> Rows()\n\t{\n\t\tvar x = _tree.CanvasRect.Left + 1;\n\t\tvar y = 0f;\n\n\t\twhile ( y < _tree.Height )\n\t\t{\n\t\t\tvar row = _tree.GetItemAt( new Vector2( x, y ) );\n\t\t\tif ( row is null )\n\t\t\t{\n\t\t\t\ty += 2;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( row.Object is TreeNode { Value: GameObject go } && go.IsValid() && go is not Scene )\n\t\t\t\tyield return (row, go);\n\n\t\t\ty = MathF.Max( y + 1, row.Rect.Bottom + 1 );\n\t\t}\n\t}\n\n\t/// <summary>The object on the row at <paramref name=\"y\"/>, in this strip's coordinates.</summary>\n\tGameObject ObjectAt( float y )\n\t{\n\t\tvar row = _tree.GetItemAt( new Vector2( _tree.CanvasRect.Left + 1, y + Position.y ) );\n\t\treturn row?.Object is TreeNode { Value: GameObject go } && go.IsValid() && go is not Scene ? go : null;\n\t}\n\n\t/// <summary>The row under the cursor, in the tree or in this strip.</summary>\n\tinternal VirtualWidget HoveredRow()\n\t{\n\t\tvar cursor = _tree.FromScreen( Editor.Application.CursorPosition );\n\t\tif ( !_tree.LocalRect.IsInside( cursor ) ) return null;\n\n\t\treturn _tree.GetItemAt( new Vector2( _tree.CanvasRect.Left + 1, cursor.y ) );\n\t}\n\n\tint Look()\n\t{\n\t\tvar hash = new HashCode();\n\t\thash.Add( Size );\n\n\t\tvar hovered = HoveredRow();\n\t\tforeach ( var (row, go) in Rows() )\n\t\t{\n\t\t\thash.Add( row.Rect.Top );\n\t\t\thash.Add( go.Id );\n\t\t\thash.Add( go.Active );\n\t\t\thash.Add( Unselectable.IsSet( go ) );\n\t\t\thash.Add( Unselectable.Has( go ) );\n\t\t\thash.Add( row == hovered );\n\t\t}\n\n\t\treturn hash.ToHashCode();\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tvar hovered = HoveredRow();\n\n\t\tforeach ( var (row, go) in Rows() )\n\t\t{\n\t\t\tvar hand = Unselectable.IsSet( go ) ? Hand.Own : Unselectable.Has( go ) ? Hand.FromParent : row == hovered ? Hand.Offered : Hand.None;\n\t\t\tPaintHand( new Rect( 0, row.Rect.Top - Position.y, IconWidth, row.Rect.Height ), hand, go.Active );\n\t\t}\n\t}\n\n\t/// <summary>What a row's hand shows.</summary>\n\tinternal enum Hand\n\t{\n\t\t/// <summary>Selectable, and not hovered: nothing.</summary>\n\t\tNone,\n\n\t\t/// <summary>Selectable and hovered: a faint hand to click.</summary>\n\t\tOffered,\n\n\t\t/// <summary>Unselectable through a parent: a faint crossed hand.</summary>\n\t\tFromParent,\n\n\t\t/// <summary>Unselectable by its own tag: a crossed hand.</summary>\n\t\tOwn,\n\t}\n\n\t/// <summary>Paints <paramref name=\"hand\"/> in <paramref name=\"rect\"/>, dimmed for a disabled object as its row's text is.</summary>\n\tinternal static void PaintHand( Rect rect, Hand hand, bool active )\n\t{\n\t\tif ( hand == Hand.None ) return;\n\n\t\tvar icon = hand == Hand.Offered ? \"touch_app\" : \"do_not_touch\";\n\t\tvar alpha = hand == Hand.Own ? 0.9f : 0.35f;\n\t\tif ( !active ) alpha *= 0.5f;\n\n\t\tPaint.Pen = Theme.TextControl.WithAlpha( alpha );\n\t\tPaint.DrawIcon( rect, icon, 14, TextFlag.Center );\n\t}\n\n\tprotected override void OnMousePress( MouseEvent e )\n\t{\n\t\tbase.OnMousePress( e );\n\n\t\t// taken here, so the row under the hand isn't selected as well\n\t\te.Accepted = true;\n\t\tif ( !e.LeftMouseButton ) return;\n\n\t\tvar go = ObjectAt( e.LocalPosition.y );\n\t\tif ( go is null ) return;\n\n\t\tif ( Unselectable.IsSet( go ) ) Unselectable.Set( new[] { go }, false );\n\t\telse if ( Unselectable.Source( go ) is { } source ) Unselectable.Set( new[] { source }, false );\n\t\telse Unselectable.Set( new[] { go }, true );\n\n\t\tToolTip = TooltipFor( go );\n\t\t_look = 0;\n\t\tUpdate();\n\t}\n\n\tprotected override void OnMouseMove( MouseEvent e )\n\t{\n\t\tbase.OnMouseMove( e );\n\n\t\tToolTip = TooltipFor( ObjectAt( e.LocalPosition.y ) );\n\t}\n\n\t/// <summary>The strip lies over the tree, so the wheel over it scrolls the tree, the way the tree scrolls itself.</summary>\n\tprotected override void OnMouseWheel( WheelEvent e )\n\t{\n\t\tif ( !_tree.SmoothScrolling )\n\t\t{\n\t\t\te.Ignore();\n\t\t\treturn;\n\t\t}\n\n\t\te.Accept();\n\t\t_tree.SmoothScrollTarget += e.Delta < 0 ? _tree.VerticalScrollbar.SingleStep : -_tree.VerticalScrollbar.SingleStep;\n\t}\n\n\tstatic string TooltipFor( GameObject go )\n\t{\n\t\tif ( go is null ) return null;\n\n\t\tif ( Unselectable.IsSet( go ) )\n\t\t\treturn \"Not selectable in the scene view: clicks go through it. Click to make it selectable.\";\n\n\t\tif ( Unselectable.Source( go ) is { } source )\n\t\t\treturn $\"Not selectable in the scene view, because {source.Name} isn't. Click to make {source.Name} selectable.\";\n\n\t\treturn \"Click to make it not selectable in the scene view: clicks will go through it to what's behind.\";\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "Editor/HierarchyExtensionMcpTools.cs",
            "FileName": "HierarchyExtensionMcpTools.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "using System;\nusing Editor.Mcp;\n\nnamespace HierarchyExtension;\n\n/// <summary>\n/// Editor bridge tools for Hierarchy Extension, so an agent driving the editor can see and set which objects are\n/// unselectable in the scene view (<see cref=\"Unselectable\"/>), try a click without making it, and see the Hierarchy's\n/// hand column, which the bridge can't otherwise see.\n/// </summary>\n[McpToolset( \"hierarchyextension\", \"Hierarchy Extension - objects unselectable in the scene view: list, set, try a click through them, draw the Hierarchy's hand column\" )]\npublic static class HierarchyExtensionMcpTools\n{\n\t/// <summary>\n\t/// The open scene's unselectable objects (each with how many objects under it that makes unselectable too), whether\n\t/// each viewport of the scene view picks through the click filter, and where the Hierarchy's hand column sits\n\t/// against the rows in view.\n\t/// </summary>\n\t[McpTool.ReadOnly( \"hierarchy_unselectable\" )]\n\tpublic static object List()\n\t{\n\t\tvar scene = SceneEditorSession.Active?.Scene ?? throw new Exception( \"No scene is open\" );\n\n\t\tvar view = SceneViewWidget.Current;\n\t\tvar column = SelectableColumn.Current;\n\t\tvar rows = column?.Rows().ToList();\n\t\tvar firstRow = rows?.Count > 0 ? rows[0].Row.Rect : (Rect?)null;\n\n\t\treturn new\n\t\t{\n\t\t\tScene = scene.Name,\n\t\t\tUnselectable = scene.GetAllObjects( false ).Where( Unselectable.IsSet ).Select( x => new\n\t\t\t{\n\t\t\t\tx.Name,\n\t\t\t\tx.Id,\n\t\t\t\tUnder = x.GetAllObjects( false ).Count() - 1,\n\t\t\t} ),\n\t\t\tViewports = view?._viewports?.Values.Select( x => new\n\t\t\t{\n\t\t\t\tx.Id,\n\t\t\t\tSelection = x.GizmoInstance?.Selection?.GetType().Name,\n\t\t\t} ),\n\t\t\tHierarchy = SceneTreeWidget.Current?.TreeView?.Selection is { } shown ? new\n\t\t\t{\n\t\t\t\tSelection = shown.GetType().Name,\n\t\t\t\tShows = shown.Select( x => x is GameObject go ? go.Name : x?.ToString() ),\n\t\t\t\tSelected = SceneEditorSession.Active?.Selection.Select( x => x is GameObject go ? go.Name : x?.ToString() ),\n\t\t\t} : null,\n\t\t\tColumn = column is null ? null : new\n\t\t\t{\n\t\t\t\tPosition = column.Position,\n\t\t\t\tSize = column.Size,\n\t\t\t\tVisible = column.Visible,\n\t\t\t\tTree = column.Tree.Size,\n\t\t\t\tCanvas = column.Tree.CanvasRect,\n\t\t\t\tFirstRow = firstRow,\n\t\t\t\tStockEye = firstRow is { } r ? new Rect( r.Right - 22, r.Top, 18, r.Height ) : (Rect?)null,\n\t\t\t\tRowsInView = rows.Count,\n\t\t\t},\n\t\t};\n\t}\n\n\t/// <summary>\n\t/// Make objects unselectable in the scene view, or selectable again: one undo step, and the scene counts as changed,\n\t/// as with the Hierarchy's hands. Objects under an unselectable one stay unselectable until it's made selectable.\n\t/// </summary>\n\t/// <param name=\"ids\">Object ids, comma separated, from scene_tree or find_game_objects.</param>\n\t/// <param name=\"selectable\">True to make them selectable again, false to make them unselectable.</param>\n\t[McpTool( \"hierarchy_set_selectable\" )]\n\tpublic static object SetSelectable( string ids, bool selectable )\n\t{\n\t\tvar objects = Find( ids );\n\t\tUnselectable.Set( objects, !selectable );\n\n\t\treturn objects.Select( x => new { x.Name, x.Id, Unselectable = Unselectable.Has( x ), Own = Unselectable.IsSet( x ) } );\n\t}\n\n\t/// <summary>\n\t/// What a click at a world point in the scene view would select with some objects held out, worked out the way the\n\t/// scene view does it for real (<see cref=\"ClickThrough\"/>). Nothing is clicked: the selection and the scene stay as\n\t/// they are. Uses the viewport last clicked in, and its camera, so the point should be in its view.\n\t/// </summary>\n\t/// <param name=\"point\">The world point to click, \"x,y,z\".</param>\n\t/// <param name=\"holdOut\">Ids to hold out instead of the unselectable objects, comma separated. Empty holds out nothing.</param>\n\t[McpTool.ReadOnly( \"hierarchy_click_through\" )]\n\tpublic static object ClickAt( string point, string holdOut = null )\n\t{\n\t\tvar (session, gizmos, input, world) = CursorAt( point );\n\n\t\tIReadOnlyList<GameObject> heldOut = holdOut is null\n\t\t\t? session.Scene.GetAllObjects( true ).Where( Unselectable.IsSet ).ToArray()\n\t\t\t: Find( holdOut );\n\n\t\tvar changes = ClickThrough.Run( session.Scene, gizmos.Settings, input, session.Selection, heldOut );\n\n\t\treturn new\n\t\t{\n\t\t\tPoint = world,\n\t\t\tHeldOut = heldOut.Select( x => x.Name ),\n\t\t\tChanges = changes.Select( x => new\n\t\t\t{\n\t\t\t\tChange = x.Change.ToString(),\n\t\t\t\tTarget = x.Target is GameObject go ? $\"{go.Name} ({go.Id})\" : x.Target?.ToString(),\n\t\t\t} ),\n\t\t};\n\t}\n\n\t/// <summary>\n\t/// What a press at a world point in the scene view lands on, as its gizmos see it: whether anything is pressed (which\n\t/// stops a drag from starting a drag selection), which object it's found on, and whether that object is unselectable,\n\t/// in which case a drag from there lets go of the press. Nothing is pressed for real. Uses the viewport last clicked\n\t/// in, and its camera, so the point should be in its view.\n\t/// </summary>\n\t/// <param name=\"point\">The world point to press, \"x,y,z\".</param>\n\t[McpTool.ReadOnly( \"hierarchy_press_probe\" )]\n\tpublic static object PressProbe( string point )\n\t{\n\t\tvar (session, gizmos, input, world) = CursorAt( point );\n\n\t\tvar (pressedAnything, pressed) = ClickThrough.WhilePressed( session.Scene, gizmos.Settings, input,\n\t\t\t() => (Gizmo.Pressed.Any, ScenePicking.PressedObject( session.Scene, _ => true )) );\n\n\t\treturn new\n\t\t{\n\t\t\tPoint = world,\n\t\t\tPressedAnything = pressedAnything,\n\t\t\tPressedObject = pressed is null ? null : $\"{pressed.Name} ({pressed.Id})\",\n\t\t\tUnselectable = pressed is not null && Unselectable.Has( pressed ),\n\t\t};\n\t}\n\n\t/// <summary>\n\t/// What a drag selection between two world points would take, by Hierarchy Extension's rule (fully inside, a prefab\n\t/// only whole, never what's under a taken object, never unselectable ones), next to what the stock rule takes (any\n\t/// object whose bounds, with everything under it, the box touches). Selects nothing. Uses the viewport last clicked\n\t/// in, and its camera, so both points should be in its view.\n\t/// </summary>\n\t/// <param name=\"from\">One corner of the box, a world point \"x,y,z\".</param>\n\t/// <param name=\"to\">The opposite corner.</param>\n\t[McpTool.ReadOnly( \"hierarchy_box_probe\" )]\n\tpublic static object BoxProbe( string from, string to )\n\t{\n\t\tvar (session, gizmos, _, _) = CursorAt( from );\n\t\tvar camera = gizmos.Input.Camera;\n\t\tvar scene = session.Scene;\n\n\t\tvar box = Rect.FromPoints( camera.ToScreen( Vector3.Parse( from ) ), camera.ToScreen( Vector3.Parse( to ) ) );\n\t\tvar frustum = camera.GetFrustum( box );\n\n\t\tvar bounds = new Dictionary<GameObject, BBox>();\n\t\tBBox BoundsOf( GameObject go )\n\t\t{\n\t\t\tif ( !bounds.TryGetValue( go, out var b ) ) bounds[go] = b = go.GetBounds();\n\t\t\treturn b;\n\t\t}\n\n\t\t// ObjectEditorTool.OnBoxSelect\n\t\tvar stock = scene.GetAllObjects( true )\n\t\t\t.Where( go => go is not Scene && !go.Tags.Has( \"hidden\" ) )\n\t\t\t.Where( go => !(BoundsOf( go ).Size.AlmostEqual( 0.0f ) && !go.HasGizmoHandle) && frustum.IsInside( BoundsOf( go ), true ) )\n\t\t\t.Select( x => x.Name );\n\n\t\treturn new\n\t\t{\n\t\t\tScreenBox = box,\n\t\t\tTakes = ScenePicking.Take( scene, frustum, ScenePicking.VisibleBounds ).Select( x => x.Name ),\n\t\t\tStockTakes = stock,\n\t\t};\n\t}\n\n\t/// <summary>The session, gizmo instance and cursor input for pointing at <paramref name=\"point\"/> in the scene view.</summary>\n\tstatic (SceneEditorSession Session, Gizmo.Instance Gizmos, Gizmo.Inputs Input, Vector3 World) CursorAt( string point )\n\t{\n\t\tvar view = SceneViewWidget.Current ?? throw new Exception( \"No scene view is open\" );\n\t\tvar viewport = view.LastSelectedViewportWidget ?? view._viewports?.Values.FirstOrDefault() ?? throw new Exception( \"The scene view has no viewport\" );\n\t\tvar gizmos = viewport.GizmoInstance;\n\t\tvar camera = gizmos.Input.Camera ?? throw new Exception( \"The viewport hasn't drawn yet\" );\n\n\t\tvar world = Vector3.Parse( point );\n\t\tvar forward = camera.Rotation.Forward;\n\n\t\tvar input = gizmos.Input;\n\t\tinput.CursorRay = camera.Ortho\n\t\t\t? new Ray( world - forward * Vector3.Dot( world - camera.Position, forward ), forward )\n\t\t\t: new Ray( camera.Position, (world - camera.Position).Normal );\n\t\tinput.CursorPosition = camera.ToScreen( world );\n\t\tinput.Modifiers = KeyboardModifiers.None;\n\t\tinput.LeftMouse = false;\n\n\t\treturn (view.Session, gizmos, input, world);\n\t}\n\n\t/// <summary>\n\t/// Draws the Hierarchy's rows in view into a PNG as the dock paints them (not the expand arrows), with the hand\n\t/// column over them. The bridge can't see editor windows, so this is how to check the column lines up. The preview\n\t/// ids draw as if they were unselectable, and the hovered one with its faint hand, without changing anything.\n\t/// </summary>\n\t/// <param name=\"path\">Where to write the PNG, an absolute path.</param>\n\t/// <param name=\"preview\">Ids to draw as unselectable, comma separated, as if their hands had been clicked.</param>\n\t/// <param name=\"hovered\">An id to draw as the row under the cursor.</param>\n\t[McpTool.ReadOnly( \"hierarchy_render_rows\" )]\n\tpublic static object RenderRows( string path, string preview = null, string hovered = null )\n\t{\n\t\tvar previewed = preview is null ? new HashSet<GameObject>() : Find( preview ).ToHashSet();\n\t\tvar hoveredObject = hovered is null ? null : Find( hovered ).FirstOrDefault();\n\n\t\tSelectableColumn.Hand HandOf( GameObject go )\n\t\t{\n\t\t\tif ( Unselectable.IsSet( go ) || previewed.Contains( go ) ) return SelectableColumn.Hand.Own;\n\n\t\t\tfor ( var o = go.Parent; o.IsValid() && o is not Scene; o = o.Parent )\n\t\t\t{\n\t\t\t\tif ( Unselectable.IsSet( o ) || previewed.Contains( o ) ) return SelectableColumn.Hand.FromParent;\n\t\t\t}\n\n\t\t\treturn go == hoveredObject ? SelectableColumn.Hand.Offered : SelectableColumn.Hand.None;\n\t\t}\n\n\t\tvar column = SelectableColumn.Current ?? throw new Exception( \"The Hierarchy isn't open\" );\n\t\tvar tree = column.Tree;\n\t\tvar rows = column.Rows().ToList();\n\t\tif ( rows.Count == 0 ) throw new Exception( \"No rows in view\" );\n\n\t\tvar top = rows.Min( x => x.Row.Rect.Top );\n\t\tvar bottom = rows.Max( x => x.Row.Rect.Bottom );\n\t\tvar pixmap = new Pixmap( (int)tree.Width, (int)MathF.Ceiling( bottom - top ) );\n\n\t\tusing ( Paint.ToPixmap( pixmap ) )\n\t\t{\n\t\t\tPaint.Translate( new Vector2( 0, -top ) );\n\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.ControlBackground );\n\t\t\tPaint.DrawRect( new Rect( 0, top, tree.Width, bottom - top ) );\n\n\t\t\tforeach ( var (row, go) in rows )\n\t\t\t{\n\t\t\t\tif ( row.Object is not TreeNode node ) continue;\n\n\t\t\t\t// as TreeView.PaintItem shifts a row by its depth before the node paints it\n\t\t\t\tvar indent = tree.IndentWidth * row.Column + tree.ExpandWidth;\n\t\t\t\trow.Rect.Left += indent;\n\t\t\t\ttry\n\t\t\t\t{\n\t\t\t\t\tnode.OnPaint( row );\n\t\t\t\t}\n\t\t\t\tfinally\n\t\t\t\t{\n\t\t\t\t\trow.Rect.Left -= indent;\n\t\t\t\t}\n\n\t\t\t\tSelectableColumn.PaintHand( new Rect( column.Position.x, row.Rect.Top, column.Size.x, row.Rect.Height ), HandOf( go ), go.Active );\n\t\t\t}\n\t\t}\n\n\t\tpixmap.SavePng( path );\n\n\t\treturn new { Path = path, Size = new Vector2( pixmap.Width, pixmap.Height ), Rows = rows.Select( x => x.Object.Name ) };\n\t}\n\n\tstatic GameObject[] Find( string ids )\n\t{\n\t\tvar scene = SceneEditorSession.Active?.Scene ?? throw new Exception( \"No scene is open\" );\n\n\t\tvar found = new List<GameObject>();\n\t\tforeach ( var id in (ids ?? \"\").Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ) )\n\t\t{\n\t\t\tvar go = Guid.TryParse( id, out var guid ) ? scene.Directory.FindByGuid( guid ) : null;\n\t\t\tfound.Add( go ?? throw new Exception( $\"No object with id {id}\" ) );\n\t\t}\n\n\t\treturn found.ToArray();\n\t}\n}\n"
        },
        {
            "Ident": "trashpandagames.hierarchyextension",
            "Path": "UnitTests/UnitTest.cs",
            "FileName": "UnitTest.cs",
            "PackageType": "library",
            "CodeKind": "UnitTest",
            "AssetVersionId": 398910,
            "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": "trashpandagames.hierarchyextension",
            "Path": "Code/Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 398910,
            "IsPrivate": false,
            "Code": "global using Sandbox;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"
        }
    ]
}