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..8b0fb1e97 --- /dev/null +++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs @@ -0,0 +1,262 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +using System.Collections.Generic; +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 List _nodesCache; + private List _hitsCache; + 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. + /// + /// True if selection started, otherwise false. + public bool TryStartingRubberBandSelection() + { + if (!_isRubberBandSpanning && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown) + { + _tryStartRubberBand = true; + return true; + } + return false; + } + + /// + /// 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) + { + UpdateRubberBand(ref viewFrustum); + } + } + } + + private struct ViewportProjection + { + private Viewport _viewport; + private Matrix _viewProjection; + + public void Init(EditorViewport editorViewport) + { + // Inline EditorViewport.ProjectPoint to save on calculation for large set of points + _viewport = new Viewport(0, 0, editorViewport.Width, editorViewport.Height); + var frustum = editorViewport.ViewFrustum; + _viewProjection = frustum.Matrix; + } + + public void ProjectPoint(ref Vector3 worldSpaceLocation, out Float2 viewportSpaceLocation) + { + _viewport.Project(ref worldSpaceLocation, ref _viewProjection, out var projected); + viewportSpaceLocation = new Float2((float)projected.X, (float)projected.Y); + } + } + + private void UpdateRubberBand(ref BoundingFrustum viewFrustum) + { + Profiler.BeginEvent("UpdateRubberBand"); + + // 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 + if (_nodesCache == null) + _nodesCache = new List(); + else + _nodesCache.Clear(); + var nodes = _nodesCache; + _owner.SceneGraphRoot.GetAllChildActorNodes(nodes); + if (_hitsCache == null) + _hitsCache = new List(); + else + _hitsCache.Clear(); + var hits = _hitsCache; + + // Process all nodes + var projection = new ViewportProjection(); + projection.Init(_owner.Viewport); + 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(); + if (LoopOverPoints(points, ref adjustedRect, ref projection)) + { + if (a.HasPrefabLink && _owner is not PrefabWindowViewport) + hits.Add(_owner.SceneGraphRoot.Find(a.GetPrefabRoot())); + else + hits.Add(node); + } + } + + // Process selection + if (_owner.IsControlDown) + { + var newSelection = new List(); + var currentSelection = new List(_owner.SceneGraphRoot.SceneContext.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 = new List(_owner.SceneGraphRoot.SceneContext.Selection); + newSelection.AddRange(hits); + newSelection.AddRange(currentSelection); + _owner.Select(newSelection); + } + else + { + _owner.Select(hits); + } + + Profiler.EndEvent(); + } + + private bool LoopOverPoints(Vector3[] points, ref Rectangle adjustedRect, ref ViewportProjection projection) + { + Profiler.BeginEvent("LoopOverPoints"); + bool containsAllPoints = points.Length != 0; + for (int i = 0; i < points.Length; i++) + { + projection.ProjectPoint(ref points[i], out var loc); + if (!adjustedRect.Contains(loc)) + { + containsAllPoints = false; + break; + } + } + Profiler.EndEvent(); + return containsAllPoints; + } + + /// + /// 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. + /// + /// True if rubber band was active before stopping. + public bool StopRubberBand() + { + var result = _tryStartRubberBand; + _isRubberBandSpanning = false; + _tryStartRubberBand = false; + return result; + } +} diff --git a/Source/Editor/SceneGraph/ActorNode.cs b/Source/Editor/SceneGraph/ActorNode.cs index a1e1643a8..0740fb84f 100644 --- a/Source/Editor/SceneGraph/ActorNode.cs +++ b/Source/Editor/SceneGraph/ActorNode.cs @@ -182,6 +182,54 @@ namespace FlaxEditor.SceneGraph return null; } + /// + /// Get all nested actor nodes under this actor node. + /// + /// An array of ActorNodes + public ActorNode[] GetAllChildActorNodes() + { + var nodes = new List(); + GetAllChildActorNodes(nodes); + return nodes.ToArray(); + } + + /// + /// Get all nested actor nodes under this actor node. + /// + /// The output list to fill with results. + public void GetAllChildActorNodes(List nodes) + { + var children = ChildNodes; + if (children == null) + return; + for (int i = 0; i < children.Count; i++) + { + if (children[i] is ActorNode node) + { + nodes.Add(node); + node.GetAllChildActorNodes(nodes); + } + } + } + + /// + /// 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..b024c0287 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -24,6 +24,9 @@ namespace FlaxEditor.SceneGraph.Actors public sealed class StaticModelNode : ActorNode { private Dictionary _vertices; + private Vector3[] _selectionPoints; + private Transform _selectionPointsTransform; + private Model _selectionPointsModel; /// public StaticModelNode(Actor actor) @@ -31,6 +34,16 @@ namespace FlaxEditor.SceneGraph.Actors { } + /// + public override void OnDispose() + { + _vertices = null; + _selectionPoints = null; + _selectionPointsModel = null; + + base.OnDispose(); + } + /// public override bool OnVertexSnap(ref Ray ray, Real hitDistance, out Vector3 result) { @@ -88,6 +101,45 @@ namespace FlaxEditor.SceneGraph.Actors contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null; } + /// + public override Vector3[] GetActorSelectionPoints() + { + if (Actor is StaticModel sm && sm.Model) + { + // Try to use cache + var model = sm.Model; + var transform = Actor.Transform; + if (_selectionPoints != null && + _selectionPointsTransform == transform && + _selectionPointsModel == model) + return _selectionPoints; + Profiler.BeginEvent("GetActorSelectionPoints"); + + // Check collision proxy points for more accurate selection + var vecPoints = new List(); + var m = model.LODs[0]; + foreach (var mesh in m.Meshes) + { + var points = mesh.GetCollisionProxyPoints(); + vecPoints.EnsureCapacity(vecPoints.Count + points.Length); + for (int i = 0; i < points.Length; i++) + { + vecPoints.Add(transform.LocalToWorld(points[i])); + } + } + + Profiler.EndEvent(); + if (vecPoints.Count != 0) + { + _selectionPoints = vecPoints.ToArray(); + _selectionPointsTransform = transform; + _selectionPointsModel = model; + return _selectionPoints; + } + } + return base.GetActorSelectionPoints(); + } + 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..a1dc61b4a 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 @@ -107,6 +111,7 @@ namespace FlaxEditor.Viewport private double _lockedFocusOffset; private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private EditorSpritesRenderer _editorSpritesRenderer; + private ViewportRubberBandSelector _rubberBandSelector; /// /// Drag and drop handlers @@ -213,6 +218,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 +375,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 +489,24 @@ namespace FlaxEditor.Viewport TransformGizmo.EndTransforming(); } + /// + public override void OnLostFocus() + { + base.OnLostFocus(); + + if (_rubberBandSelector.StopRubberBand()) + EndMouseCapture(); + } + + /// + public override void OnMouseLeave() + { + base.OnMouseLeave(); + + if (_rubberBandSelector.StopRubberBand()) + EndMouseCapture(); + } + /// /// Focuses the viewport on the current selection of the gizmo. /// @@ -576,6 +605,27 @@ namespace FlaxEditor.Viewport base.OrientViewport(ref orientation); } + /// + public override void OnMouseMove(Float2 location) + { + base.OnMouseMove(location); + + // Don't allow rubber band selection when gizmo is controlling mouse, vertex painting mode, or cloth painting is enabled + bool canStart = !((Gizmos.Active.IsControllingMouse || Gizmos.Active is VertexPaintingGizmo || Gizmos.Active is ClothPaintingGizmo) || IsControllingMouse || IsRightMouseButtonDown); + _rubberBandSelector.TryCreateRubberBand(canStart, _viewMousePos, ViewFrustum); + } + + /// + protected override void OnLeftMouseButtonDown() + { + base.OnLeftMouseButtonDown(); + + if (_rubberBandSelector.TryStartingRubberBandSelection()) + { + StartMouseCapture(); + } + } + /// protected override void OnLeftMouseButtonUp() { @@ -583,8 +633,14 @@ 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()) + { + EndMouseCapture(); + + // 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..1b8c16dbe 100644 --- a/Source/Engine/Graphics/Mesh.cs +++ b/Source/Engine/Graphics/Mesh.cs @@ -633,5 +633,16 @@ namespace FlaxEngine throw new Exception("Failed to download mesh data."); return result; } + +#if FLAX_EDITOR + /// + /// Gets the collision proxy points for the mesh. + /// + /// The triangle points in the collision proxy. + internal Vector3[] GetCollisionProxyPoints() + { + return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _); + } +#endif } } diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp index 77c2089e1..721aaffd8 100644 --- a/Source/Engine/Graphics/Models/Mesh.cpp +++ b/Source/Engine/Graphics/Models/Mesh.cpp @@ -847,4 +847,24 @@ MArray* Mesh::DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI return result; } +#if USE_EDITOR + +Array Mesh::GetCollisionProxyPoints() const +{ + PROFILE_CPU(); + Array result; +#if USE_PRECISE_MESH_INTERSECTS + for (int32 i = 0; i < _collisionProxy.Triangles.Count(); i++) + { + auto triangle = _collisionProxy.Triangles.Get()[i]; + result.Add(triangle.V0); + result.Add(triangle.V1); + result.Add(triangle.V2); + } +#endif + return result; +} + +#endif + #endif diff --git a/Source/Engine/Graphics/Models/Mesh.h b/Source/Engine/Graphics/Models/Mesh.h index 813836b85..78c60ec50 100644 --- a/Source/Engine/Graphics/Models/Mesh.h +++ b/Source/Engine/Graphics/Models/Mesh.h @@ -320,5 +320,8 @@ 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); +#if USE_EDITOR + API_FUNCTION(NoProxy) Array GetCollisionProxyPoints() const; +#endif #endif };