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