Editor/ClickThrough.cs
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 );
}
}
}