diff --git a/Source/Editor/Gizmo/TransformGizmoBase.cs b/Source/Editor/Gizmo/TransformGizmoBase.cs index 456697f13..92846c907 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.cs @@ -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 } /// - public override bool IsControllingMouse => _isTransforming; + public override bool IsControllingMouse => _isTransforming || _isSelected; /// 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(); } } diff --git a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs new file mode 100644 index 000000000..ddb889dd8 --- /dev/null +++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs @@ -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; + +/// +/// Class for adding viewport rubber band selection. +/// +public class ViewportRubberBandSelector +{ + private bool _isRubberBandSpanning; + private bool _tryStartRubberBand; + private Float2 _cachedStartingMousePosition; + private Rectangle _rubberBandRect; + private Rectangle _lastRubberBandRect; + + private IGizmoOwner _owner; + + /// + /// Constructs a rubber band selector with a designated gizmo owner. + /// + /// The gizmo owner. + public ViewportRubberBandSelector(IGizmoOwner owner) + { + _owner = owner; + } + + /// + /// Triggers the start of a rubber band selection. + /// + public void TryStartingRubberBandSelection() + { + if (!_isRubberBandSpanning && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown) + { + _tryStartRubberBand = true; + } + } + + /// + /// Release the rubber band selection. + /// + /// Returns true if rubber band is currently spanning + public bool ReleaseRubberBandSelection() + { + if (_tryStartRubberBand) + { + _tryStartRubberBand = false; + } + + if (_isRubberBandSpanning) + { + _isRubberBandSpanning = false; + return true; + } + return false; + } + + /// + /// Tries to create a rubber band selection. + /// + /// Whether the creation can start. + /// The current mouse position. + /// The view frustum. + 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 hits = new List(); + 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(); + 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(); + var currentSelection = _owner.SceneGraphRoot.Selection; + newSelection.AddRange(hits); + newSelection.AddRange(currentSelection); + _owner.Select(newSelection); + } + else + { + _owner.Select(hits); + } + } + } + } + + /// + /// Used to draw the rubber band. Begins render 2D. + /// + /// The GPU Context. + /// The GPU texture target. + /// The GPU texture target depth. + 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(); + } + + /// + /// Used to draw the rubber band. Use if already rendering 2D context. + /// + public void Draw2D() + { + if (!_isRubberBandSpanning) + return; + Render2D.FillRectangle(_rubberBandRect, Style.Current.Selection); + Render2D.DrawRectangle(_rubberBandRect, Style.Current.SelectionBorder); + } + + /// + /// Immediately stops the rubber band. + /// + public void StopRubberBand() + { + _isRubberBandSpanning = false; + _tryStartRubberBand = false; + } +} diff --git a/Source/Editor/SceneGraph/ActorNode.cs b/Source/Editor/SceneGraph/ActorNode.cs index a1e1643a8..5191f83c3 100644 --- a/Source/Editor/SceneGraph/ActorNode.cs +++ b/Source/Editor/SceneGraph/ActorNode.cs @@ -182,6 +182,51 @@ namespace FlaxEditor.SceneGraph return null; } + /// + /// Get all nested actor nodes under this actor node. + /// + /// An array of ActorNodes + public ActorNode[] GetAllChildActorNodes() + { + // Check itself + if (ChildNodes == null || ChildNodes.Count == 0) + return []; + + // Check deeper + var nodes = new List(); + 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(); + } + + /// + /// Whether an actor node can be selected with a selector. + /// + /// True if the actor node can be selected + public virtual bool CanSelectActorNodeWithSelector() + { + return Actor && Actor.HideFlags is not (HideFlags.DontSelect or HideFlags.FullyHidden) && Actor is not EmptyActor && IsActive; + } + + /// + /// The selection points used to check if an actor node can be selected. + /// + /// The points to use if the actor can be selected. + public virtual Vector3[] GetActorSelectionPoints() + { + return Actor.EditorBox.GetCorners(); + } + /// /// Gets a value indicating whether this actor can be used to create prefab from it (as a root). /// diff --git a/Source/Editor/SceneGraph/Actors/CameraNode.cs b/Source/Editor/SceneGraph/Actors/CameraNode.cs index 6c485314d..cada3364c 100644 --- a/Source/Editor/SceneGraph/Actors/CameraNode.cs +++ b/Source/Editor/SceneGraph/Actors/CameraNode.cs @@ -58,5 +58,11 @@ namespace FlaxEditor.SceneGraph.Actors return Camera.Internal_IntersectsItselfEditor(FlaxEngine.Object.GetUnmanagedPtr(_actor), ref ray.Ray, out distance); } + + /// + public override Vector3[] GetActorSelectionPoints() + { + return [Actor.Position]; + } } } diff --git a/Source/Editor/SceneGraph/Actors/SceneNode.cs b/Source/Editor/SceneGraph/Actors/SceneNode.cs index bb5998ef4..9e380ff91 100644 --- a/Source/Editor/SceneGraph/Actors/SceneNode.cs +++ b/Source/Editor/SceneGraph/Actors/SceneNode.cs @@ -33,6 +33,12 @@ namespace FlaxEditor.SceneGraph.Actors } } + /// + public override bool CanSelectActorNodeWithSelector() + { + return false; + } + /// /// Gets the scene. /// diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index 885cbd5b5..02161d277 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -88,6 +88,30 @@ namespace FlaxEditor.SceneGraph.Actors contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null; } + /// + 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(); + 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 diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index 91bd6c530..ef7663e5d 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -78,5 +78,11 @@ namespace FlaxEditor.SceneGraph.Actors if (Actor is UICanvas uiCanvas && uiCanvas.Is3D) DebugDraw.DrawWireBox(uiCanvas.Bounds, Color.BlueViolet); } + + /// + public override bool CanSelectActorNodeWithSelector() + { + return Actor is UICanvas uiCanvas && uiCanvas.Is3D && base.CanSelectActorNodeWithSelector(); + } } } diff --git a/Source/Editor/SceneGraph/Actors/UIControlNode.cs b/Source/Editor/SceneGraph/Actors/UIControlNode.cs index 1d912db48..57a956665 100644 --- a/Source/Editor/SceneGraph/Actors/UIControlNode.cs +++ b/Source/Editor/SceneGraph/Actors/UIControlNode.cs @@ -40,5 +40,31 @@ namespace FlaxEditor.SceneGraph.Actors control.PerformLayout(); } } + + /// + 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(); + } } } diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index cbb35e155..b8b7271c2 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -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; + /// /// Drag and drop handlers /// @@ -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(); } + /// + public override void OnLostFocus() + { + base.OnLostFocus(); + _rubberBandSelector.StopRubberBand(); + } + + /// + public override void OnMouseLeave() + { + base.OnMouseLeave(); + _rubberBandSelector.StopRubberBand(); + } + /// /// Focuses the viewport on the current selection of the gizmo. /// @@ -576,6 +602,24 @@ namespace FlaxEditor.Viewport base.OrientViewport(ref orientation); } + /// + 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); + } + + /// + protected override void OnLeftMouseButtonDown() + { + base.OnLeftMouseButtonDown(); + + _rubberBandSelector.TryStartingRubberBandSelection(); + } + /// 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(); diff --git a/Source/Engine/Graphics/Mesh.cs b/Source/Engine/Graphics/Mesh.cs index 42604c872..e87a38ae7 100644 --- a/Source/Engine/Graphics/Mesh.cs +++ b/Source/Engine/Graphics/Mesh.cs @@ -633,5 +633,14 @@ namespace FlaxEngine throw new Exception("Failed to download mesh data."); return result; } + + /// + /// Gets the collision proxy points for the mesh. + /// + /// The triangle points in the collision proxy. + internal Float3[] GetCollisionProxyPoints() + { + return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _); + } } } diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp index 77c2089e1..ac194d905 100644 --- a/Source/Engine/Graphics/Models/Mesh.cpp +++ b/Source/Engine/Graphics/Models/Mesh.cpp @@ -847,4 +847,19 @@ MArray* Mesh::DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI return result; } +Array Mesh::GetCollisionProxyPoints() const +{ + Array 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 diff --git a/Source/Engine/Graphics/Models/Mesh.h b/Source/Engine/Graphics/Models/Mesh.h index 813836b85..2c8f8beda 100644 --- a/Source/Engine/Graphics/Models/Mesh.h +++ b/Source/Engine/Graphics/Models/Mesh.h @@ -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 GetCollisionProxyPoints() const; #endif };