Merge branch 'scene-actor-multi-select' of https://github.com/Tryibion/FlaxEngine into Tryibion-scene-actor-multi-select

This commit is contained in:
Wojtek Figat
2025-03-11 10:24:00 +01:00
12 changed files with 403 additions and 4 deletions

View File

@@ -42,6 +42,7 @@ namespace FlaxEditor.Gizmo
private bool _isDuplicating;
private bool _isTransforming;
private bool _isSelected;
private Vector3 _lastIntersectionPosition;
private Quaternion _rotationDelta = Quaternion.Identity;
@@ -455,7 +456,7 @@ namespace FlaxEditor.Gizmo
}
/// <inheritdoc />
public override bool IsControllingMouse => _isTransforming;
public override bool IsControllingMouse => _isTransforming || _isSelected;
/// <inheritdoc />
public override void Update(float dt)
@@ -480,6 +481,7 @@ namespace FlaxEditor.Gizmo
// Check if user is holding left mouse button and any axis is selected
if (isLeftBtnDown && _activeAxis != Axis.None)
{
_isSelected = true; // setting later is too late, need to set here for rubber band selection in GizmoViewport
switch (_activeMode)
{
case Mode.Translate:
@@ -497,6 +499,7 @@ namespace FlaxEditor.Gizmo
}
else
{
_isSelected = false;
// If nothing selected, try to select any axis
if (!isLeftBtnDown && !Owner.IsRightMouseButtonDown)
{
@@ -564,6 +567,7 @@ namespace FlaxEditor.Gizmo
// Clear cache
_accMoveDelta = Vector3.Zero;
_lastIntersectionPosition = _intersectPosition = Vector3.Zero;
_isSelected = false;
EndTransforming();
}
}

View File

@@ -0,0 +1,209 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph;
using FlaxEditor.Viewport;
using FlaxEngine.GUI;
namespace FlaxEngine.Gizmo;
/// <summary>
/// Class for adding viewport rubber band selection.
/// </summary>
public class ViewportRubberBandSelector
{
private bool _isRubberBandSpanning;
private bool _tryStartRubberBand;
private Float2 _cachedStartingMousePosition;
private Rectangle _rubberBandRect;
private Rectangle _lastRubberBandRect;
private IGizmoOwner _owner;
/// <summary>
/// Constructs a rubber band selector with a designated gizmo owner.
/// </summary>
/// <param name="owner">The gizmo owner.</param>
public ViewportRubberBandSelector(IGizmoOwner owner)
{
_owner = owner;
}
/// <summary>
/// Triggers the start of a rubber band selection.
/// </summary>
public void TryStartingRubberBandSelection()
{
if (!_isRubberBandSpanning && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
{
_tryStartRubberBand = true;
}
}
/// <summary>
/// Release the rubber band selection.
/// </summary>
/// <returns>Returns true if rubber band is currently spanning</returns>
public bool ReleaseRubberBandSelection()
{
if (_tryStartRubberBand)
{
_tryStartRubberBand = false;
}
if (_isRubberBandSpanning)
{
_isRubberBandSpanning = false;
return true;
}
return false;
}
/// <summary>
/// Tries to create a rubber band selection.
/// </summary>
/// <param name="canStart">Whether the creation can start.</param>
/// <param name="mousePosition">The current mouse position.</param>
/// <param name="viewFrustum">The view frustum.</param>
public void TryCreateRubberBand(bool canStart, Float2 mousePosition, BoundingFrustum viewFrustum)
{
if (_isRubberBandSpanning && !canStart)
{
_isRubberBandSpanning = false;
return;
}
if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart)
{
_isRubberBandSpanning = true;
_cachedStartingMousePosition = mousePosition;
_rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
_tryStartRubberBand = false;
}
else if (_isRubberBandSpanning && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
{
_rubberBandRect.Width = mousePosition.X - _cachedStartingMousePosition.X;
_rubberBandRect.Height = mousePosition.Y - _cachedStartingMousePosition.Y;
if (_lastRubberBandRect != _rubberBandRect)
{
// Select rubberbanded rect actor nodes
var adjustedRect = _rubberBandRect;
_lastRubberBandRect = _rubberBandRect;
if (adjustedRect.Width < 0 || adjustedRect.Height < 0)
{
// make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner
var size = adjustedRect.Size;
adjustedRect.X = Mathf.Min(adjustedRect.X, adjustedRect.X + adjustedRect.Width);
adjustedRect.Y = Mathf.Min(adjustedRect.Y, adjustedRect.Y + adjustedRect.Height);
size.X = Mathf.Abs(size.X);
size.Y = Mathf.Abs(size.Y);
adjustedRect.Size = size;
}
// Get hits from graph nodes.
List<SceneGraphNode> hits = new List<SceneGraphNode>();
var nodes = _owner.SceneGraphRoot.GetAllChildActorNodes();
foreach (var node in nodes)
{
// Check for custom can select code
if (!node.CanSelectActorNodeWithSelector())
continue;
var a = node.Actor;
// Skip actor if outside of view frustum
var actorBox = a.EditorBox;
if (viewFrustum.Contains(actorBox) == ContainmentType.Disjoint)
continue;
// Get valid selection points
var points = node.GetActorSelectionPoints();
bool containsAllPoints = points.Length != 0;
foreach (var point in points)
{
_owner.Viewport.ProjectPoint(point, out var loc);
if (!adjustedRect.Contains(loc))
{
containsAllPoints = false;
break;
}
}
if (containsAllPoints)
{
if (a.HasPrefabLink && _owner is not PrefabWindowViewport)
hits.Add(_owner.SceneGraphRoot.Find(a.GetPrefabRoot()));
else
hits.Add(node);
}
}
var editor = Editor.Instance;
if (_owner.IsControlDown)
{
var newSelection = new List<SceneGraphNode>();
var currentSelection = _owner.SceneGraphRoot.Selection;
newSelection.AddRange(currentSelection);
foreach (var hit in hits)
{
if (currentSelection.Contains(hit))
newSelection.Remove(hit);
else
newSelection.Add(hit);
}
_owner.Select(newSelection);
}
else if (Input.GetKey(KeyboardKeys.Shift))
{
var newSelection = new List<SceneGraphNode>();
var currentSelection = _owner.SceneGraphRoot.Selection;
newSelection.AddRange(hits);
newSelection.AddRange(currentSelection);
_owner.Select(newSelection);
}
else
{
_owner.Select(hits);
}
}
}
}
/// <summary>
/// Used to draw the rubber band. Begins render 2D.
/// </summary>
/// <param name="context">The GPU Context.</param>
/// <param name="target">The GPU texture target.</param>
/// <param name="targetDepth">The GPU texture target depth.</param>
public void Draw(GPUContext context, GPUTexture target, GPUTexture targetDepth)
{
// Draw RubberBand for rect selection
if (!_isRubberBandSpanning)
return;
Render2D.Begin(context, target, targetDepth);
Draw2D();
Render2D.End();
}
/// <summary>
/// Used to draw the rubber band. Use if already rendering 2D context.
/// </summary>
public void Draw2D()
{
if (!_isRubberBandSpanning)
return;
Render2D.FillRectangle(_rubberBandRect, Style.Current.Selection);
Render2D.DrawRectangle(_rubberBandRect, Style.Current.SelectionBorder);
}
/// <summary>
/// Immediately stops the rubber band.
/// </summary>
public void StopRubberBand()
{
_isRubberBandSpanning = false;
_tryStartRubberBand = false;
}
}

View File

@@ -182,6 +182,51 @@ namespace FlaxEditor.SceneGraph
return null;
}
/// <summary>
/// Get all nested actor nodes under this actor node.
/// </summary>
/// <returns>An array of ActorNodes</returns>
public ActorNode[] GetAllChildActorNodes()
{
// Check itself
if (ChildNodes == null || ChildNodes.Count == 0)
return [];
// Check deeper
var nodes = new List<ActorNode>();
for (int i = 0; i < ChildNodes.Count; i++)
{
if (ChildNodes[i] is ActorNode node)
{
nodes.Add(node);
var childNodes = node.GetAllChildActorNodes();
if (childNodes.Length > 0)
{
nodes.AddRange(childNodes);
}
}
}
return nodes.ToArray();
}
/// <summary>
/// Whether an actor node can be selected with a selector.
/// </summary>
/// <returns>True if the actor node can be selected</returns>
public virtual bool CanSelectActorNodeWithSelector()
{
return Actor && Actor.HideFlags is not (HideFlags.DontSelect or HideFlags.FullyHidden) && Actor is not EmptyActor && IsActive;
}
/// <summary>
/// The selection points used to check if an actor node can be selected.
/// </summary>
/// <returns>The points to use if the actor can be selected.</returns>
public virtual Vector3[] GetActorSelectionPoints()
{
return Actor.EditorBox.GetCorners();
}
/// <summary>
/// Gets a value indicating whether this actor can be used to create prefab from it (as a root).
/// </summary>

View File

@@ -58,5 +58,11 @@ namespace FlaxEditor.SceneGraph.Actors
return Camera.Internal_IntersectsItselfEditor(FlaxEngine.Object.GetUnmanagedPtr(_actor), ref ray.Ray, out distance);
}
/// <inheritdoc />
public override Vector3[] GetActorSelectionPoints()
{
return [Actor.Position];
}
}
}

View File

@@ -33,6 +33,12 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
/// <inheritdoc />
public override bool CanSelectActorNodeWithSelector()
{
return false;
}
/// <summary>
/// Gets the scene.
/// </summary>

View File

@@ -88,6 +88,30 @@ namespace FlaxEditor.SceneGraph.Actors
contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null;
}
/// <inheritdoc />
public override Vector3[] GetActorSelectionPoints()
{
if (Actor is not StaticModel sm || !sm.Model)
return base.GetActorSelectionPoints();
// Check collision proxy points for more accurate selection.
var vecPoints = new List<Vector3>();
var m = sm.Model.LODs[0];
foreach (var mesh in m.Meshes)
{
var points = mesh.GetCollisionProxyPoints();
foreach (var point in points)
{
vecPoints.Add(Actor.Transform.LocalToWorld(point));
}
}
// Fall back to base actor editor box if no points from collision proxy.
if (vecPoints.Count == 0)
return base.GetActorSelectionPoints();
return vecPoints.ToArray();
}
private void OnAddMeshCollider(EditorWindow window)
{
// Allow collider to be added to evey static model selection

View File

@@ -78,5 +78,11 @@ namespace FlaxEditor.SceneGraph.Actors
if (Actor is UICanvas uiCanvas && uiCanvas.Is3D)
DebugDraw.DrawWireBox(uiCanvas.Bounds, Color.BlueViolet);
}
/// <inheritdoc />
public override bool CanSelectActorNodeWithSelector()
{
return Actor is UICanvas uiCanvas && uiCanvas.Is3D && base.CanSelectActorNodeWithSelector();
}
}
}

View File

@@ -40,5 +40,31 @@ namespace FlaxEditor.SceneGraph.Actors
control.PerformLayout();
}
}
/// <inheritdoc />
public override bool CanSelectActorNodeWithSelector()
{
// Check if control and skip if canvas is 2D
if (Actor is not UIControl uiControl)
return false;
UICanvas canvas = null;
var controlParent = uiControl.Parent;
while (controlParent != null && controlParent is not Scene)
{
if (controlParent is UICanvas uiCanvas)
{
canvas = uiCanvas;
break;
}
controlParent = controlParent.Parent;
}
if (canvas != null)
{
if (canvas.Is2D)
return false;
}
return base.CanSelectActorNodeWithSelector();
}
}
}

View File

@@ -7,10 +7,14 @@ using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
using FlaxEditor.Tools;
using FlaxEditor.Viewport.Modes;
using FlaxEditor.Windows;
using FlaxEngine;
using FlaxEngine.Gizmo;
using FlaxEngine.GUI;
using FlaxEngine.Tools;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport
@@ -108,6 +112,8 @@ namespace FlaxEditor.Viewport
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private EditorSpritesRenderer _editorSpritesRenderer;
private ViewportRubberBandSelector _rubberBandSelector;
/// <summary>
/// Drag and drop handlers
/// </summary>
@@ -213,6 +219,9 @@ namespace FlaxEditor.Viewport
TransformGizmo.ApplyTransformation += ApplyTransform;
TransformGizmo.Duplicate += _editor.SceneEditing.Duplicate;
Gizmos.Active = TransformGizmo;
// Add rubber band selector
_rubberBandSelector = new ViewportRubberBandSelector(this);
// Add grid
Grid = new GridGizmo(this);
@@ -367,7 +376,10 @@ namespace FlaxEditor.Viewport
{
Gizmos[i].Draw(ref renderContext);
}
// Draw RubberBand for rect selection
_rubberBandSelector.Draw(context, target, targetDepth);
// Draw selected objects debug shapes and visuals
if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw)
{
@@ -478,6 +490,20 @@ namespace FlaxEditor.Viewport
TransformGizmo.EndTransforming();
}
/// <inheritdoc />
public override void OnLostFocus()
{
base.OnLostFocus();
_rubberBandSelector.StopRubberBand();
}
/// <inheritdoc />
public override void OnMouseLeave()
{
base.OnMouseLeave();
_rubberBandSelector.StopRubberBand();
}
/// <summary>
/// Focuses the viewport on the current selection of the gizmo.
/// </summary>
@@ -576,6 +602,24 @@ namespace FlaxEditor.Viewport
base.OrientViewport(ref orientation);
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
base.OnMouseMove(location);
// Dont allow rubber band selection when gizmo is controlling mouse, vertex painting mode, or cloth painting is enabled
_rubberBandSelector.TryCreateRubberBand(!((Gizmos.Active.IsControllingMouse || Gizmos.Active is VertexPaintingGizmo || Gizmos.Active is ClothPaintingGizmo) || IsControllingMouse || IsRightMouseButtonDown),
_viewMousePos, ViewFrustum);
}
/// <inheritdoc />
protected override void OnLeftMouseButtonDown()
{
base.OnLeftMouseButtonDown();
_rubberBandSelector.TryStartingRubberBandSelection();
}
/// <inheritdoc />
protected override void OnLeftMouseButtonUp()
{
@@ -583,8 +627,12 @@ namespace FlaxEditor.Viewport
if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos))
return;
// Try to pick something with the current gizmo
Gizmos.Active?.Pick();
// Select rubberbanded rect actor nodes or pick with gizmo
if (!_rubberBandSelector.ReleaseRubberBandSelection())
{
// Try to pick something with the current gizmo
Gizmos.Active?.Pick();
}
// Keep focus
Focus();

View File

@@ -633,5 +633,14 @@ namespace FlaxEngine
throw new Exception("Failed to download mesh data.");
return result;
}
/// <summary>
/// Gets the collision proxy points for the mesh.
/// </summary>
/// <returns>The triangle points in the collision proxy.</returns>
internal Float3[] GetCollisionProxyPoints()
{
return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _);
}
}
}

View File

@@ -847,4 +847,19 @@ MArray* Mesh::DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI
return result;
}
Array<Float3> Mesh::GetCollisionProxyPoints() const
{
Array<Vector3> result;
#if USE_PRECISE_MESH_INTERSECTS
for (int i = 0; i < _collisionProxy.Triangles.Count(); ++i)
{
auto triangle = _collisionProxy.Triangles[i];
result.Add(triangle.V0);
result.Add(triangle.V1);
result.Add(triangle.V2);
}
#endif
return result;
}
#endif

View File

@@ -320,5 +320,6 @@ private:
API_FUNCTION(NoProxy) bool UpdateTrianglesUInt(int32 triangleCount, const MArray* trianglesObj);
API_FUNCTION(NoProxy) bool UpdateTrianglesUShort(int32 triangleCount, const MArray* trianglesObj);
API_FUNCTION(NoProxy) MArray* DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI);
API_FUNCTION(NoProxy) Array<Float3> GetCollisionProxyPoints() const;
#endif
};