{"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/// \u003Csummary\u003E\n/// Objects that can\u0027t 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/// \u003Cpara\u003E\n/// An object is made unselectable with the \u003Csee cref=\u0022Tag\u0022/\u003E tag, the way the stock Hide marks hidden objects with\n/// \u0022hidden\u0022. It saves with the scene, shows in the object\u0027s 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/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// Set it with the hand on each Hierarchy row (\u003Csee cref=\u0022SelectableColumn\u0022/\u003E), with \u003Cb\u003EScene \u003E View \u003E Toggle\n/// Selectable\u003C/b\u003E (Alt\u002BL) for the selected objects, or with \u003Cb\u003EMake All Selectable\u003C/b\u003E. \u003Csee cref=\u0022ScenePicking\u0022/\u003E\n/// makes the scene view honour it.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic static class Unselectable\n{\n\t/// \u003Csummary\u003EThe tag that makes an object, and everything under it, unselectable in the scene view.\u003C/summary\u003E\n\tpublic const string Tag = \u0022unselectable\u0022;\n\n\t/// \u003Csummary\u003EWhether \u003Cparamref name=\u0022go\u0022/\u003E can\u0027t be selected in the scene view, from its own tag or a parent\u0027s.\u003C/summary\u003E\n\tpublic static bool Has( GameObject go ) =\u003E go.IsValid() \u0026\u0026 go is not Scene \u0026\u0026 go.Tags.Has( Tag );\n\n\t/// \u003Csummary\u003EWhether \u003Cparamref name=\u0022go\u0022/\u003E has the tag itself, not only from a parent.\u003C/summary\u003E\n\tpublic static bool IsSet( GameObject go ) =\u003E go.IsValid() \u0026\u0026 go is not Scene \u0026\u0026 go.Tags.Has( Tag, false );\n\n\t/// \u003Csummary\u003E\n\t/// The object whose tag makes \u003Cparamref name=\u0022go\u0022/\u003E unselectable: itself, or the nearest parent with the tag.\n\t/// Null when it\u0027s selectable.\n\t/// \u003C/summary\u003E\n\tpublic static GameObject Source( GameObject go )\n\t{\n\t\tfor ( var o = go; o.IsValid() \u0026\u0026 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/// \u003Csummary\u003E\n\t/// Make \u003Cparamref name=\u0022objects\u0022/\u003E 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 \u003Csee cref=\u0022Source\u0022/\u003E to free it.\n\t/// \u003C/summary\u003E\n\tpublic static void Set( IEnumerable\u003CGameObject\u003E objects, bool unselectable )\n\t{\n\t\tvar changing = objects.Where( x =\u003E x.IsValid() \u0026\u0026 x is not Scene \u0026\u0026 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 ? \u0022Make Unselectable\u0022 : \u0022Make Selectable\u0022;\n\t\tif ( changing.Length == 1 ) name = $\u0022{name}: {changing[0].Name}\u0022;\n\n\t\tusing var sceneScope = session.Scene.Push();\n\n\t\t// only the objects\u0027 own properties, which is where tags live: a tile layer\u0027s 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/// \u003Csummary\u003E\n\t/// Alt\u002BL: make the selected objects unselectable in the scene view or, when they all are already, selectable again.\n\t/// One that\u0027s unselectable through a parent frees that parent, and so everything under it.\n\t/// \u003C/summary\u003E\n\t[Menu( \u0022Editor\u0022, \u0022Scene/View/Toggle Selectable\u0022, \u0022do_not_touch\u0022 )]\n\t[Shortcut( \u0022editor.toggle-selectable\u0022, \u0022Alt\u002BL\u0022 )]\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\u003CGameObject\u003E().Where( x =\u003E x.IsValid() \u0026\u0026 x is not Scene ).ToArray();\n\t\tif ( selected.Length == 0 ) return;\n\n\t\tif ( selected.Any( x =\u003E !Has( x ) ) ) Set( selected, true );\n\t\telse Set( selected.Select( Source ), false );\n\t}\n\n\t/// \u003Csummary\u003EMake everything in the open scene selectable in the scene view again.\u003C/summary\u003E\n\t[Menu( \u0022Editor\u0022, \u0022Scene/View/Make All Selectable\u0022, \u0022touch_app\u0022 )]\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/// \u003Csummary\u003EWhether anything in \u003Cparamref name=\u0022scene\u0022/\u003E is unselectable. Cheap enough to ask on every click.\u003C/summary\u003E\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/// \u003Csummary\u003EThe editor session whose scene holds \u003Cparamref name=\u0022go\u0022/\u003E, the play session\u0027s included.\u003C/summary\u003E\n\tpublic static SceneEditorSession SessionOf( GameObject go ) =\u003E SessionOf( go?.Scene );\n\n\t/// \u003Csummary\u003EThe editor session of \u003Cparamref name=\u0022scene\u0022/\u003E, the play session\u0027s included.\u003C/summary\u003E\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 =\u003E 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/// \u003Csummary\u003E\n/// Outlines what\u0027s selected in the scene view: one box around each selected object and everything under it (its\n/// sprites, models, colliders, particles and icons: \u003Csee cref=\u0022ScenePicking.VisibleBounds\u0022/\u003E), a thin line drawn over\n/// everything in the editor\u0027s own selected colour. An object inside another selected one gets no box of its own; the\n/// other\u0027s covers it. During a drag selection it outlines what the drag will select (\u003Csee cref=\u0022ScenePicking.DragResult\u0022/\u003E).\n/// \u003Cpara\u003E\n/// Why: the stock scene view only outlines an object\u0027s own sprite or model (\u003Cc\u003ESpriteRenderer.DrawGizmos\u003C/c\u003E asks\n/// \u003Cc\u003EGizmo.IsSelected\u003C/c\u003E in the sprite\u0027s own object; the viewport\u0027s \u003Cc\u003EDrawSelection\u003C/c\u003E takes the selected object\u0027s\n/// own \u003Cc\u003EModelRenderer\u003C/c\u003Es). 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/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic static class SelectionOutline\n{\n\t/// \u003Csummary\u003EDraws the outlines. Called from the scene view\u0027s gizmo pass (\u003Csee cref=\u0022ScenePicking\u0022/\u003E).\u003C/summary\u003E\n\tinternal static void Draw( IEnumerable\u003Cobject\u003E selected )\n\t{\n\t\tvar objects = selected.OfType\u003CGameObject\u003E().Where( x =\u003E x.IsValid() \u0026\u0026 x is not Scene ).ToHashSet();\n\t\tvar boxes = new List\u003CBBox\u003E();\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 \u0026\u0026 bounds.Size.Length \u003E 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\u0027s 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( \u0022hierarchyextension-selection\u0022 );\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\u003CGameObject\u003E objects )\n\t{\n\t\tfor ( var o = go.Parent; o.IsValid() \u0026\u0026 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/// \u003Csummary\u003E\n/// Works out what a click in the scene view would select if the unselectable objects weren\u0027t there. It plays the click\n/// again through a gizmo instance of its own, over the scene\u0027s own gizmos, with the unselectable objects held out:\n/// the engine picks, so whatever it picks normally (render meshes, sprites, handles, a component\u0027s hitboxes, selection\n/// bases) is picked the same way.\n/// \u003Cpara\u003E\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 \u0022hidden\u0022 or flagged Hidden, and both stop them drawing or listing too (engine source,\n/// \u003Cc\u003EGameObject.DrawGizmos\u003C/c\u003E and \u003Cc\u003ESceneViewportWidget.UpdateHovered\u003C/c\u003E, 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/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic static class ClickThrough\n{\n\t/// \u003Csummary\u003EA change a click makes to the selection, in the order the click makes them.\u003C/summary\u003E\n\tpublic enum Change { Clear, Add, Remove }\n\n\t/// \u003Csummary\u003E\n\t/// The selection changes a click with \u003Cparamref name=\u0022input\u0022/\u003E would make in \u003Cparamref name=\u0022scene\u0022/\u003E if the\n\t/// unselectable objects weren\u0027t there, in order. Empty when it would hit nothing. The selection isn\u0027t touched:\n\t/// the click reads \u003Cparamref name=\u0022selection\u0022/\u003E, to know what\u0027s selected, and only records what it would do.\n\t/// Also used by \u003Csee cref=\u0022ScenePicking\u0022/\u003E; call it outside any gizmo drawing.\n\t/// \u003C/summary\u003E\n\tpublic static List\u003C(Change Change, object Target)\u003E 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/// \u003Csummary\u003EThe same with \u003Cparamref name=\u0022heldOut\u0022/\u003E, and everything under them, held out.\u003C/summary\u003E\n\tpublic static List\u003C(Change Change, object Target)\u003E Run( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, SelectionSystem selection, IReadOnlyList\u003CGameObject\u003E 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\u003CGameObject\u003E();\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 \u0026= ~GameObjectFlags.Hidden;\n\t\t\t}\n\t\t}\n\n\t\treturn recorder.Changes;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Plays the button going down at \u003Cparamref name=\u0022input\u0022/\u003E, nothing held out, and asks \u003Cparamref name=\u0022ask\u0022/\u003E on the\n\t/// frame the press is seen, as the scene view\u0027s own code would see it (\u003Cc\u003EGizmo.Pressed\u003C/c\u003E). For the bridge\u0027s\n\t/// press probe; call it outside any gizmo drawing.\n\t/// \u003C/summary\u003E\n\tpublic static T WhilePressed\u003CT\u003E( Scene scene, Gizmo.SceneSettings settings, Gizmo.Inputs input, System.Func\u003CT\u003E ask )\n\t{\n\t\tvar none = System.Array.Empty\u003CGameObject\u003E();\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\u003CGameObject\u003E 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/// \u003Csummary\u003E\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 (\u003Cc\u003ESceneViewportWidget.UpdateHovered\u003C/c\u003E); this is it, holding out the objects.\n\t/// \u003C/summary\u003E\n\tstatic void HoverRenderMeshes( Scene scene, IReadOnlyList\u003CGameObject\u003E 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( \u0022hidden\u0022 );\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/// \u003Csummary\u003E\n\t/// The replayed click\u0027s selection: it answers what\u0027s selected from the real one, and writes down the changes\n\t/// instead of making them.\n\t/// \u003C/summary\u003E\n\tsealed class Recorder : SelectionSystem\n\t{\n\t\treadonly SelectionSystem _selection;\n\n\t\tpublic readonly List\u003C(Change Change, object Target)\u003E 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 ) =\u003E _selection?.Contains( obj ) ?? false;\n\n\t\tpublic override bool Any() =\u003E _selection?.Any() ?? false;\n\n\t\tpublic override void Clear() =\u003E 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/// \u003Csummary\u003E\n/// Makes the scene view honour \u003Csee cref=\u0022Unselectable\u0022/\u003E, and makes its drag selections take only what\u0027s fully inside.\n/// \u003Clist type=\u0022bullet\u0022\u003E\n/// \u003Citem\u003E\u003Cdescription\u003E\u003Cb\u003EClicks\u003C/b\u003E: each scene viewport\u0027s gizmos select through a \u003Csee cref=\u0022ClickFilter\u0022/\u003E. A click\n/// that would select an unselectable object is held back. On the next editor frame \u003Csee cref=\u0022ClickThrough\u0022/\u003E 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.\u003C/description\u003E\u003C/item\u003E\n/// \u003Citem\u003E\u003Cdescription\u003E\u003Cb\u003EDrags\u003C/b\u003E that start on an unselectable object are drags on empty space: drag selections,\n/// and the camera, take them as they would there (\u003Csee cref=\u0022LetGoOfDraggedPress\u0022/\u003E).\u003C/description\u003E\u003C/item\u003E\n/// \u003Citem\u003E\u003Cdescription\u003E\u003Cb\u003EDrag selections\u003C/b\u003E with the Object Select tool take only objects fully inside the box, never\n/// unselectable ones, and a prefab only whole (\u003Csee cref=\u0022WatchDragSelection\u0022/\u003E).\u003C/description\u003E\u003C/item\u003E\n/// \u003Citem\u003E\u003Cdescription\u003E\u003Cb\u003ERight-clicking\u003C/b\u003E the scene view offers to make unselectable objects selectable again.\u003C/description\u003E\u003C/item\u003E\n/// \u003C/list\u003E\n/// \u003Cpara\u003E\n/// The filter goes where the engine lets it: a viewport\u0027s \u003Cc\u003EGizmoInstance.Selection\u003C/c\u003E is a public setting, and only\n/// the gizmos\u0027 clicks select through it (tools source, \u003Cc\u003ESceneViewportWidget\u003C/c\u003E, 26.09.29). The Hierarchy, the\n/// inspector, tools and code select through the session\u0027s selection, so they can still pick anything.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class ScenePicking : EditorEvent.ISceneView\n{\n\t/// \u003Csummary\u003EHow far the cursor goes before a drag selection starts (\u003Cc\u003EEditorTool.UpdateBoxSelection\u003C/c\u003E), in pixels.\u003C/summary\u003E\n\tconst float DragDistance = 5;\n\n\tstatic ScenePicking _listener;\n\tstatic PendingClick _pending;\n\tstatic DragWatch _drag;\n\n\t/// \u003Csummary\u003EWhether the press going on has been checked for landing on an unselectable object.\u003C/summary\u003E\n\tstatic bool _pressChecked;\n\n\t/// \u003Csummary\u003E\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 (\u003Csee cref=\u0022HierarchySelection\u0022/\u003E).\n\t/// \u003C/summary\u003E\n\tinternal static HashSet\u003Cobject\u003E DragResult =\u003E _drag?.Result;\n\n\t/// \u003Csummary\u003EA click that was held back, with what the viewport\u0027s gizmos saw when it landed.\u003C/summary\u003E\n\tsealed record PendingClick( SceneViewportWidget Viewport, SelectionSystem Selection, Gizmo.Inputs Input );\n\n\t/// \u003Csummary\u003EA press begun in a viewport (known by its camera), and the drag selection it may become.\u003C/summary\u003E\n\tsealed class DragWatch\n\t{\n\t\tpublic Scene Scene;\n\t\tpublic SceneCamera Camera;\n\n\t\t/// \u003Csummary\u003EEditor frames in a row the left button has been up while this drag still went on.\u003C/summary\u003E\n\t\tpublic int FramesUp;\n\n\t\t/// \u003Csummary\u003EWhat was selected when the press began.\u003C/summary\u003E\n\t\tpublic HashSet\u003Cobject\u003E Before;\n\n\t\t/// \u003Csummary\u003EThe cursor\u0027s ray when the press began, and on the last frame the button was down, as the tool takes them.\u003C/summary\u003E\n\t\tpublic Ray Start, Last;\n\n\t\t/// \u003Csummary\u003EWhile it\u0027s a drag selection: what the selection will be when the mouse comes up. Null otherwise.\u003C/summary\u003E\n\t\tpublic HashSet\u003Cobject\u003E Result;\n\n\t\treadonly Dictionary\u003CGameObject, BBox?\u003E _bounds = new();\n\n\t\t/// \u003Csummary\u003E\n\t\t/// \u003Csee cref=\u0022VisibleBounds\u0022/\u003E, 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\u0027re slow to get.\n\t\t/// \u003C/summary\u003E\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 \u002B 1;\n\t\t\tif ( drag.FramesUp \u003E 3 ) Finish( drag );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA code reload keeps the listener; register it again in case the reload dropped it.\u003C/summary\u003E\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/// \u003Csummary\u003E\n\t/// Puts a filter on every viewport of the scene view in use, and on any it\u0027s lost: a viewport sets its selection\n\t/// again when play starts or stops. Also lets through a clear that\u0027s still waiting for its add.\n\t/// \u003C/summary\u003E\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\u0027t 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 =\u003E 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/// \u003Csummary\u003ENamed as the stock click\u0027s undo steps are: \u0022Select {name}\u0022, or \u0022Deselect all\u0022 for empty space.\u003C/summary\u003E\n\tstatic string UndoName( List\u003C(ClickThrough.Change Change, object Target)\u003E changes )\n\t{\n\t\tvar last = changes.LastOrDefault( x =\u003E x.Change != ClickThrough.Change.Clear );\n\t\tif ( last.Target is GameObject go ) return last.Change == ClickThrough.Change.Add ? $\u0022Select {go.Name}\u0022 : $\u0022Deselect {go.Name}\u0022;\n\n\t\treturn \u0022Deselect all\u0022;\n\t}\n\n\t/// \u003Csummary\u003ERuns in each viewport\u0027s frame after its tool, before the scene\u0027s gizmos.\u003C/summary\u003E\n\tvoid EditorEvent.ISceneView.DrawGizmos( Scene scene )\n\t{\n\t\tLetGoOfDraggedPress( scene );\n\t\tWatchDragSelection( scene );\n\n\t\t// what\u0027s selected, or during a drag selection what it will select\n\t\tvar selected = DragResult ?? (IEnumerable\u003Cobject\u003E)Unselectable.SessionOf( scene )?.Selection;\n\t\tif ( selected is not null ) SelectionOutline.Draw( selected );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A press on an unselectable object still counts as a press on something: the scene view hovers what\u0027s under the\n\t/// cursor before anyone clicks, with no way to leave objects out. While anything is pressed, a drag doesn\u0027t start a\n\t/// drag selection (\u003Cc\u003EEditorTool.UpdateBoxSelection\u003C/c\u003E) or move the camera (\u003Cc\u003ESceneViewportWidget\u003C/c\u003E), 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 (\u003Cc\u003EGizmo.Pressed.ClearPath\u003C/c\u003E, 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\u0027t move\n\t/// stays a click, which the \u003Csee cref=\u0022ClickFilter\u0022/\u003E plays through to what\u0027s behind.\n\t/// \u003C/summary\u003E\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 \u003C DragDistance ) return;\n\n\t\t_pressChecked = true;\n\t\tif ( PressedUnselectable( scene ).IsValid() ) Gizmo.Pressed.ClearPath();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The unselectable object the press is on, or null. The gizmos name what\u0027s pressed by its path: an object\u0027s is\n\t/// \u0022object-{hash}\u0022 (\u003Cc\u003EGizmo.ObjectScope\u003C/c\u003E), its handle\u0027s \u0022.../Handle\u0022, and its components\u0027 hitboxes \u0022.../Scope0\u0022,\n\t/// \u0022.../Scope1\u0022 and so on, in order (\u003Cc\u003EGameObject.DrawGizmos\u003C/c\u003E). \u003Cc\u003EGizmo.Pressed.This\u003C/c\u003E compares the path it\u0027s\n\t/// asked in, so each is tried. A hitbox a component draws inside a scope of its own isn\u0027t found.\n\t/// \u003C/summary\u003E\n\tinternal static GameObject PressedUnselectable( Scene scene ) =\u003E PressedObject( scene, Unselectable.Has );\n\n\t/// \u003Csummary\u003EThe object the press is on, among those \u003Cparamref name=\u0022among\u0022/\u003E accepts, or null. See \u003Csee cref=\u0022PressedUnselectable\u0022/\u003E.\u003C/summary\u003E\n\tinternal static GameObject PressedObject( Scene scene, System.Func\u003CGameObject, bool\u003E among )\n\t{\n\t\tusing var scope = Gizmo.Scope( \u0022hierarchyextension\u0022 );\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 = $\u0022object-{go.GetHashCode()}\u0022;\n\t\t\tif ( IsPressed( path ) || IsPressed( $\u0022{path}/Handle\u0022 ) ) return go;\n\n\t\t\tfor ( var i = 0; i \u003C go.Components.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( IsPressed( $\u0022{path}/Scope{i}\u0022 ) ) 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/// \u003Csummary\u003E\n\t/// Makes the Object Select tool\u0027s drag selections take only what\u0027s fully inside the box. The stock one takes anything\n\t/// whose bounds the box touches, and an object\u0027s bounds include everything under it (\u003Cc\u003EObjectEditorTool.OnBoxSelect\u003C/c\u003E,\n\t/// \u003Cc\u003EGameObject.GetBounds\u003C/c\u003E), so a small box over a few decorations also took their group and the map they\u0027re 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/// \u003Clist type=\u0022bullet\u0022\u003E\n\t/// \u003Citem\u003E\u003Cdescription\u003EAn object is taken when it\u0027s fully inside with everything under it, and then stands for all\n\t/// of that: what\u0027s under it isn\u0027t taken as well.\u003C/description\u003E\u003C/item\u003E\n\t/// \u003Citem\u003E\u003Cdescription\u003EInside a prefab, or anything else a click selects as a whole (\u003Csee cref=\u0022IsSelectionBase\u0022/\u003E),\n\t/// only that whole counts.\u003C/description\u003E\u003C/item\u003E\n\t/// \u003Citem\u003E\u003Cdescription\u003EHidden and unselectable objects are never taken.\u003C/description\u003E\u003C/item\u003E\n\t/// \u003Citem\u003E\u003Cdescription\u003ECtrl takes them out of the selection, Shift adds them, and otherwise they\u0027re the selection,\n\t/// as with the stock one (\u003Cc\u003EEditorTool.ApplyDragSelection\u003C/c\u003E).\u003C/description\u003E\u003C/item\u003E\n\t/// \u003C/list\u003E\n\t/// While the drag goes on the scene view (\u003Csee cref=\u0022ClickFilter.Contains\u0022/\u003E) and the Hierarchy\n\t/// (\u003Csee cref=\u0022HierarchySelection\u0022/\u003E) 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\u0027s picks until then.\n\t/// \u003C/summary\u003E\n\tstatic void WatchDragSelection( Scene scene )\n\t{\n\t\tvar camera = Gizmo.Camera;\n\n\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 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 \u0026\u0026 (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/// \u003Csummary\u003EEnds the drag: a drag selection\u0027s result goes into the selection, and the Hierarchy shows the selection again.\u003C/summary\u003E\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 \u0026\u0026 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/// \u003Csummary\u003E\n\t/// Whether the Object Select tool is making a drag selection this frame, decided as it decides it\n\t/// (\u003Cc\u003EEditorTool.UpdateBoxSelection\u003C/c\u003E): the cursor 5 px or more from where the press began, and nothing pressed.\n\t/// Gives the box\u0027s frustum.\n\t/// \u003C/summary\u003E\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 ) \u003C DragDistance ) return false;\n\n\t\tfrustum = Gizmo.Camera.GetFrustum( Rect.FromPoints( c1, c2 ) );\n\t\treturn true;\n\t}\n\n\t/// \u003Csummary\u003EWhat the selection will be when the drag selection in \u003Cparamref name=\u0022frustum\u0022/\u003E finishes.\u003C/summary\u003E\n\tstatic HashSet\u003Cobject\u003E BoxResult( Scene scene, DragWatch drag, Frustum frustum )\n\t{\n\t\tvar taken = Take( scene, frustum, go =\u003E drag.BoundsOf( go ) );\n\t\tvar result = new HashSet\u003Cobject\u003E( 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 =\u003E x is GameObject );\n\n\t\tresult.UnionWith( taken );\n\t\treturn result;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The objects a drag selection in \u003Cparamref name=\u0022frustum\u0022/\u003E takes, before Ctrl or Shift: those fully inside with\n\t/// everything under them (\u003Cparamref name=\u0022boundsOf\u0022/\u003E, usually \u003Csee cref=\u0022VisibleBounds\u0022/\u003E), none that are part of\n\t/// one taken, hidden or unselectable, and a prefab only whole.\n\t/// \u003C/summary\u003E\n\tinternal static HashSet\u003CGameObject\u003E Take( Scene scene, Frustum frustum, System.Func\u003CGameObject, BBox?\u003E boundsOf )\n\t{\n\t\tvar taken = new HashSet\u003CGameObject\u003E();\n\n\t\tforeach ( var go in scene.GetAllObjects( true ) )\n\t\t{\n\t\t\tif ( go is Scene || go.Tags.Has( \u0022hidden\u0022 ) || 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 \u0026\u0026 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 =\u003E HasAncestorIn( go, taken ) );\n\t\treturn taken;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// What the scene view shows of \u003Cparamref name=\u0022go\u0022/\u003E and everything under it, as one world box, or null when there\u0027s\n\t/// nothing. That\u0027s what the stock drag selection measures (\u003Cc\u003EGameObject.GetBounds\u003C/c\u003E: 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/// \u003Cc\u003ESpriteRenderer\u003C/c\u003E has no bounds of its own (it isn\u0027t a \u003Cc\u003EComponent.IHasBounds\u003C/c\u003E, engine source 26.09.29),\n\t/// so with the stock drag selection no sprite can be drag-selected, and a 2D level\u0027s decorations never are.\n\t/// \u003C/summary\u003E\n\tinternal static BBox? VisibleBounds( GameObject go )\n\t{\n\t\tBBox? result = null;\n\t\tvoid Add( BBox bounds ) =\u003E 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( \u0022hidden\u0022 ) ) 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/// \u003Csummary\u003EA sprite\u0027s box in the world, worked out as its own hitbox is (\u003Cc\u003ESpriteRenderer.DrawGizmos\u003C/c\u003E).\u003C/summary\u003E\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 \u002B= 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 \u002B size.y * sin, size.x * sin \u002B 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/// \u003Csummary\u003E\n\t/// Whether a click selects \u003Cparamref name=\u0022go\u0022/\u003E as a whole when it lands on something under it, as the scene view\n\t/// decides it (\u003Cc\u003EGameObject.FindSelectionBase\u003C/c\u003E): a network root, the outermost root of a prefab instance, or an\n\t/// object with a \u003Csee cref=\u0022SelectionBaseAttribute\u0022/\u003E component.\n\t/// \u003C/summary\u003E\n\tstatic bool IsSelectionBase( GameObject go )\n\t{\n\t\t// GameObject.IsOutermostPrefabInstanceRoot isn\u0027t 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 =\u003E EditorTypeLibrary.GetType( x.GetType() )?.HasAttribute\u003CSelectionBaseAttribute\u003E() == true );\n\t}\n\n\tstatic bool InsideSelectionBase( GameObject go )\n\t{\n\t\tfor ( var o = go.Parent; o.IsValid() \u0026\u0026 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\u003CGameObject\u003E objects )\n\t{\n\t\tfor ( var o = go.Parent; o.IsValid() \u0026\u0026 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 \u0026\u0026 Unselectable.Source( trace.GameObject ) is { } source )\n\t\t{\n\t\t\tev.Menu.AddOption( $\u0022Make {source.Name} Selectable\u0022, \u0022touch_app\u0022, () =\u003E Unselectable.Set( new[] { source }, false ) );\n\t\t}\n\n\t\tev.Menu.AddOption( \u0022Make All Selectable\u0022, \u0022touch_app\u0022, Unselectable.MakeAllSelectable );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A scene viewport\u0027s gizmo selection. It passes everything on to the scene\u0027s own selection, except a click on an\n\t/// unselectable object, which it holds back for \u003Csee cref=\u0022ResolvePendingClick\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\tsealed class ClickFilter : SelectionSystem, IEnumerable\u003Cobject\u003E\n\t{\n\t\treadonly SceneViewportWidget _viewport;\n\t\treadonly SelectionSystem _selection;\n\n\t\t/// \u003Csummary\u003E\n\t\t/// A click clears the selection just before it adds what it clicked (\u003Cc\u003EGizmo.Select\u003C/c\u003E). The clear waits for\n\t\t/// the add: if the click is held back, the clear isn\u0027t wanted either.\n\t\t/// \u003C/summary\u003E\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() =\u003E _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/// \u003Csummary\u003E\n\t\t/// Also what the gizmos draw as selected. While a drag selection goes on, that\u0027s what it will select when the mouse\n\t\t/// comes up (\u003Csee cref=\u0022WatchDragSelection\u0022/\u003E), not what the stock one has put in the selection meanwhile.\n\t\t/// \u003C/summary\u003E\n\t\tpublic override bool Contains( object obj ) =\u003E _drag?.Result is { } result ? result.Contains( obj ) : _selection.Contains( obj );\n\n\t\tpublic override bool Any() =\u003E _drag?.Result is { } result ? result.Count \u003E 0 : _selection.Any();\n\n\t\tpublic override int GetHashCode() =\u003E _selection.GetHashCode();\n\n\t\tIEnumerator\u003Cobject\u003E IEnumerable\u003Cobject\u003E.GetEnumerator() =\u003E _selection.GetEnumerator();\n\n\t\tIEnumerator IEnumerable.GetEnumerator() =\u003E _selection.GetEnumerator();\n\n\t\t/// \u003Csummary\u003EA clear still waiting for its add goes through.\u003C/summary\u003E\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( \u0022AddonTitle\u0022, \u0022Hierarchy Extension\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022hierarchyextension\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022trashpandagames\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022trashpandagames.hierarchyextension\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002229\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-09-30T19:33:36.0810281Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.122.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.122.0\u0022)]"},{"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/// \u003Csummary\u003E\n/// The Hierarchy\u0027s selection, so its rows are right while a drag selection is going on in the scene view.\n/// \u003Cpara\u003E\n/// The stock drag selection puts whatever its box touches into the scene\u0027s 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/// (\u003Csee cref=\u0022ScenePicking\u0022/\u003E) 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\u0027s\n/// \u003Cc\u003ETreeView.Selection\u003C/c\u003E is a public setting, and the tree paints rows by asking it, repaints when its hash changes,\n/// and scrolls to what\u0027s added to it (tools source, \u003Cc\u003EBaseItemWidget\u003C/c\u003E, 26.09.29).\n/// \u003C/para\u003E\n/// \u003Cpara\u003E\n/// It holds a copy of what the Hierarchy should show: the scene\u0027s selection, or during a drag selection what that will\n/// select (\u003Csee cref=\u0022ScenePicking.DragResult\u0022/\u003E). Selecting in the Hierarchy goes straight to the scene\u0027s selection,\n/// as before. The inspector and the move handles follow the scene\u0027s selection, so during a drag they still show the\n/// stock one\u0027s picks.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class HierarchySelection : SelectionSystem\n{\n\tstatic HierarchySelection _current;\n\n\t/// \u003Csummary\u003EThe one on the Hierarchy in use, if any.\u003C/summary\u003E\n\tinternal static HierarchySelection Current =\u003E _current;\n\n\treadonly SelectionSystem _selection;\n\n\tHierarchySelection( SelectionSystem selection )\n\t{\n\t\t_selection = selection;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Puts one on the Hierarchy\u0027s tree, and again whenever the tree is given the scene\u0027s 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/// \u003C/summary\u003E\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 \u0026\u0026 _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\u0027s own, or one of these from before a code reload that wasn\u0027t carried over\n\t\tvar ours = tree.Selection?.GetType().FullName == typeof( HierarchySelection ).FullName;\n\t\tif ( tree.Selection != selection \u0026\u0026 !ours ) return;\n\n\t\tvar view = new HierarchySelection( selection );\n\n\t\t// filled before the tree has it, so filling it doesn\u0027t scroll the tree\n\t\tview.Sync();\n\t\ttree.Selection = view;\n\t\t_current = view;\n\t}\n\n\t/// \u003Csummary\u003EMakes the copy what the Hierarchy should show now, adding and removing only what differs.\u003C/summary\u003E\n\tinternal void Sync()\n\t{\n\t\tIEnumerable\u003Cobject\u003E target = ScenePicking.DragResult ?? (IEnumerable\u003Cobject\u003E)_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 ) =\u003E ScenePicking.DragResult is null ? _selection.Contains( obj ) : base.Contains( obj );\n\n\tpublic override bool Any() =\u003E 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/// \u003Csummary\u003E\n/// The Hierarchy\u0027s 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\u0027t be selected in the scene view (\u003Csee cref=\u0022Unselectable\u0022/\u003E); it\u0027s faint when that comes from a\n/// parent. Hovering a row shows a faint hand to click. A click makes the row\u0027s object unselectable, or selectable again;\n/// on a row that\u0027s unselectable through a parent, it frees that parent.\n/// \u003Cpara\u003E\n/// The stock Hierarchy has no way in for this: \u003Cc\u003EGameObjectNode.OnPaint\u003C/c\u003E paints each row whole, and\n/// \u003Cc\u003EGameObjectNode.AddGameObjectMenuItems\u003C/c\u003E builds the row\u0027s context menu, with no event or interface to add to\n/// either (tools source, \u003Cc\u003EScene/SceneTree\u003C/c\u003E, 26.09.29). So this is a narrow strip laid over the right-hand end of the\n/// Hierarchy\u0027s rows. It paints the hands and takes the clicks there; everywhere else the tree works as before.\n/// \u003C/para\u003E\n/// \u003C/summary\u003E\npublic sealed class SelectableColumn : Widget\n{\n\t/// \u003Csummary\u003EAn icon\u0027s box, as wide as the stock eye\u0027s.\u003C/summary\u003E\n\tconst float IconWidth = 18;\n\n\t/// \u003Csummary\u003EThe stock eye\u0027s box and its gap: 18 wide, 4 in from the row\u0027s end (\u003Cc\u003EGameObjectNode.OnPaint\u003C/c\u003E).\u003C/summary\u003E\n\tconst float EyeWidth = 4 \u002B 18;\n\n\tstatic SelectableColumn _current;\n\n\t/// \u003Csummary\u003EThe column on the Hierarchy in use, if it\u0027s open.\u003C/summary\u003E\n\tinternal static SelectableColumn Current =\u003E _current.IsValid() \u0026\u0026 _current._tree.IsValid() ? _current : null;\n\n\treadonly TreeView _tree;\n\n\t/// \u003Csummary\u003EThe Hierarchy\u0027s tree this column lies over.\u003C/summary\u003E\n\tinternal TreeView Tree =\u003E _tree;\n\n\t/// \u003Csummary\u003EA hash of what was painted, so the strip repaints only when rows move or change.\u003C/summary\u003E\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/// \u003Csummary\u003EPuts one on the Hierarchy\u0027s tree, and again whenever the dock builds a new tree.\u003C/summary\u003E\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() \u0026\u0026 _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/// \u003Csummary\u003E\n\t/// Over the rows\u0027 right-hand end, just left of the eye\u0027s 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/// \u003C/summary\u003E\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/// \u003Csummary\u003EThe rows in view that show an object, top to bottom, in the tree\u0027s coordinates.\u003C/summary\u003E\n\tinternal IEnumerable\u003C(VirtualWidget Row, GameObject Object)\u003E Rows()\n\t{\n\t\tvar x = _tree.CanvasRect.Left \u002B 1;\n\t\tvar y = 0f;\n\n\t\twhile ( y \u003C _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 \u002B= 2;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( row.Object is TreeNode { Value: GameObject go } \u0026\u0026 go.IsValid() \u0026\u0026 go is not Scene )\n\t\t\t\tyield return (row, go);\n\n\t\t\ty = MathF.Max( y \u002B 1, row.Rect.Bottom \u002B 1 );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EThe object on the row at \u003Cparamref name=\u0022y\u0022/\u003E, in this strip\u0027s coordinates.\u003C/summary\u003E\n\tGameObject ObjectAt( float y )\n\t{\n\t\tvar row = _tree.GetItemAt( new Vector2( _tree.CanvasRect.Left \u002B 1, y \u002B Position.y ) );\n\t\treturn row?.Object is TreeNode { Value: GameObject go } \u0026\u0026 go.IsValid() \u0026\u0026 go is not Scene ? go : null;\n\t}\n\n\t/// \u003Csummary\u003EThe row under the cursor, in the tree or in this strip.\u003C/summary\u003E\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 \u002B 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/// \u003Csummary\u003EWhat a row\u0027s hand shows.\u003C/summary\u003E\n\tinternal enum Hand\n\t{\n\t\t/// \u003Csummary\u003ESelectable, and not hovered: nothing.\u003C/summary\u003E\n\t\tNone,\n\n\t\t/// \u003Csummary\u003ESelectable and hovered: a faint hand to click.\u003C/summary\u003E\n\t\tOffered,\n\n\t\t/// \u003Csummary\u003EUnselectable through a parent: a faint crossed hand.\u003C/summary\u003E\n\t\tFromParent,\n\n\t\t/// \u003Csummary\u003EUnselectable by its own tag: a crossed hand.\u003C/summary\u003E\n\t\tOwn,\n\t}\n\n\t/// \u003Csummary\u003EPaints \u003Cparamref name=\u0022hand\u0022/\u003E in \u003Cparamref name=\u0022rect\u0022/\u003E, dimmed for a disabled object as its row\u0027s text is.\u003C/summary\u003E\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 ? \u0022touch_app\u0022 : \u0022do_not_touch\u0022;\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\u0027t 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/// \u003Csummary\u003EThe strip lies over the tree, so the wheel over it scrolls the tree, the way the tree scrolls itself.\u003C/summary\u003E\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 \u002B= e.Delta \u003C 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 \u0022Not selectable in the scene view: clicks go through it. Click to make it selectable.\u0022;\n\n\t\tif ( Unselectable.Source( go ) is { } source )\n\t\t\treturn $\u0022Not selectable in the scene view, because {source.Name} isn\u0027t. Click to make {source.Name} selectable.\u0022;\n\n\t\treturn \u0022Click to make it not selectable in the scene view: clicks will go through it to what\u0027s behind.\u0022;\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/// \u003Csummary\u003E\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 (\u003Csee cref=\u0022Unselectable\u0022/\u003E), try a click without making it, and see the Hierarchy\u0027s\n/// hand column, which the bridge can\u0027t otherwise see.\n/// \u003C/summary\u003E\n[McpToolset( \u0022hierarchyextension\u0022, \u0022Hierarchy Extension - objects unselectable in the scene view: list, set, try a click through them, draw the Hierarchy\u0027s hand column\u0022 )]\npublic static class HierarchyExtensionMcpTools\n{\n\t/// \u003Csummary\u003E\n\t/// The open scene\u0027s 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\u0027s hand column sits\n\t/// against the rows in view.\n\t/// \u003C/summary\u003E\n\t[McpTool.ReadOnly( \u0022hierarchy_unselectable\u0022 )]\n\tpublic static object List()\n\t{\n\t\tvar scene = SceneEditorSession.Active?.Scene ?? throw new Exception( \u0022No scene is open\u0022 );\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 \u003E 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 =\u003E 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 =\u003E 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 =\u003E x is GameObject go ? go.Name : x?.ToString() ),\n\t\t\t\tSelected = SceneEditorSession.Active?.Selection.Select( x =\u003E 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/// \u003Csummary\u003E\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\u0027s hands. Objects under an unselectable one stay unselectable until it\u0027s made selectable.\n\t/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022ids\u0022\u003EObject ids, comma separated, from scene_tree or find_game_objects.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022selectable\u0022\u003ETrue to make them selectable again, false to make them unselectable.\u003C/param\u003E\n\t[McpTool( \u0022hierarchy_set_selectable\u0022 )]\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 =\u003E new { x.Name, x.Id, Unselectable = Unselectable.Has( x ), Own = Unselectable.IsSet( x ) } );\n\t}\n\n\t/// \u003Csummary\u003E\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 (\u003Csee cref=\u0022ClickThrough\u0022/\u003E). 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/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022point\u0022\u003EThe world point to click, \u0022x,y,z\u0022.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022holdOut\u0022\u003EIds to hold out instead of the unselectable objects, comma separated. Empty holds out nothing.\u003C/param\u003E\n\t[McpTool.ReadOnly( \u0022hierarchy_click_through\u0022 )]\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\u003CGameObject\u003E 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 =\u003E x.Name ),\n\t\t\tChanges = changes.Select( x =\u003E new\n\t\t\t{\n\t\t\t\tChange = x.Change.ToString(),\n\t\t\t\tTarget = x.Target is GameObject go ? $\u0022{go.Name} ({go.Id})\u0022 : x.Target?.ToString(),\n\t\t\t} ),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003E\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\u0027s 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/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022point\u0022\u003EThe world point to press, \u0022x,y,z\u0022.\u003C/param\u003E\n\t[McpTool.ReadOnly( \u0022hierarchy_press_probe\u0022 )]\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() =\u003E (Gizmo.Pressed.Any, ScenePicking.PressedObject( session.Scene, _ =\u003E 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 : $\u0022{pressed.Name} ({pressed.Id})\u0022,\n\t\t\tUnselectable = pressed is not null \u0026\u0026 Unselectable.Has( pressed ),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// What a drag selection between two world points would take, by Hierarchy Extension\u0027s rule (fully inside, a prefab\n\t/// only whole, never what\u0027s 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/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022from\u0022\u003EOne corner of the box, a world point \u0022x,y,z\u0022.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022to\u0022\u003EThe opposite corner.\u003C/param\u003E\n\t[McpTool.ReadOnly( \u0022hierarchy_box_probe\u0022 )]\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\u003CGameObject, BBox\u003E();\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 =\u003E go is not Scene \u0026\u0026 !go.Tags.Has( \u0022hidden\u0022 ) )\n\t\t\t.Where( go =\u003E !(BoundsOf( go ).Size.AlmostEqual( 0.0f ) \u0026\u0026 !go.HasGizmoHandle) \u0026\u0026 frustum.IsInside( BoundsOf( go ), true ) )\n\t\t\t.Select( x =\u003E 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 =\u003E x.Name ),\n\t\t\tStockTakes = stock,\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EThe session, gizmo instance and cursor input for pointing at \u003Cparamref name=\u0022point\u0022/\u003E in the scene view.\u003C/summary\u003E\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( \u0022No scene view is open\u0022 );\n\t\tvar viewport = view.LastSelectedViewportWidget ?? view._viewports?.Values.FirstOrDefault() ?? throw new Exception( \u0022The scene view has no viewport\u0022 );\n\t\tvar gizmos = viewport.GizmoInstance;\n\t\tvar camera = gizmos.Input.Camera ?? throw new Exception( \u0022The viewport hasn\u0027t drawn yet\u0022 );\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/// \u003Csummary\u003E\n\t/// Draws the Hierarchy\u0027s 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\u0027t 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/// \u003C/summary\u003E\n\t/// \u003Cparam name=\u0022path\u0022\u003EWhere to write the PNG, an absolute path.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022preview\u0022\u003EIds to draw as unselectable, comma separated, as if their hands had been clicked.\u003C/param\u003E\n\t/// \u003Cparam name=\u0022hovered\u0022\u003EAn id to draw as the row under the cursor.\u003C/param\u003E\n\t[McpTool.ReadOnly( \u0022hierarchy_render_rows\u0022 )]\n\tpublic static object RenderRows( string path, string preview = null, string hovered = null )\n\t{\n\t\tvar previewed = preview is null ? new HashSet\u003CGameObject\u003E() : 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() \u0026\u0026 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( \u0022The Hierarchy isn\u0027t open\u0022 );\n\t\tvar tree = column.Tree;\n\t\tvar rows = column.Rows().ToList();\n\t\tif ( rows.Count == 0 ) throw new Exception( \u0022No rows in view\u0022 );\n\n\t\tvar top = rows.Min( x =\u003E x.Row.Rect.Top );\n\t\tvar bottom = rows.Max( x =\u003E 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 \u002B tree.ExpandWidth;\n\t\t\t\trow.Rect.Left \u002B= 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 =\u003E x.Object.Name ) };\n\t}\n\n\tstatic GameObject[] Find( string ids )\n\t{\n\t\tvar scene = SceneEditorSession.Active?.Scene ?? throw new Exception( \u0022No scene is open\u0022 );\n\n\t\tvar found = new List\u003CGameObject\u003E();\n\t\tforeach ( var id in (ids ?? \u0022\u0022).Split( \u0027,\u0027, 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( $\u0022No object with id {id}\u0022 ) );\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"}]}