// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #if USE_LARGE_WORLDS using Real = System.Double; #else using Real = System.Single; #endif using System; using System.Collections.Generic; using FlaxEditor.SceneGraph; using FlaxEngine; namespace FlaxEditor.Gizmo { /// /// The most import gizmo tool used to move, rotate, scale and select scene objects in editor viewport. /// /// [HideInEditor] public class TransformGizmo : TransformGizmoBase { /// /// Applies scale to the selected objects pool. /// /// The selected objects pool. /// The translation delta. /// The rotation delta. /// The scale delta. public delegate void ApplyTransformationDelegate(List selection, ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta); private readonly List _selection = new List(); private readonly List _selectionParents = new List(); /// /// The event to apply objects transformation. /// public ApplyTransformationDelegate ApplyTransformation; /// /// The event to duplicate selected objects. /// public Action Duplicate; /// /// Gets the array of selected parent objects (as actors). /// public List SelectedParents => _selectionParents; /// /// Initializes a new instance of the class. /// /// The gizmos owner. public TransformGizmo(IGizmoOwner owner) : base(owner) { } /// /// Helper function, recursively finds the Prefab Root of node or null. /// /// The node from which to start. /// The prefab root or null. public ActorNode GetPrefabRootInParent(ActorNode node) { if (!node.HasPrefabLink) return null; if (node.Actor.IsPrefabRoot) return node; if (node.ParentNode is ActorNode parAct) return GetPrefabRootInParent(parAct); return null; } /// /// Recursively walks up from the node up to ceiling node(inclusive) or selection(exclusive). /// /// The node from which to start /// The ceiling(inclusive) /// The node to select. public ActorNode WalkUpAndFindActorNodeBeforeSelection(ActorNode node, ActorNode ceiling) { if (node == ceiling || _selection.Contains(node)) return node; if (node.ParentNode is ActorNode parentNode) { if (_selection.Contains(node.ParentNode)) return node; return WalkUpAndFindActorNodeBeforeSelection(parentNode, ceiling); } return node; } /// public override void SnapToGround() { if (Owner.SceneGraphRoot == null) return; var ray = new Ray(Position, Vector3.Down); while (true) { var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives | SceneGraphNode.RayCastData.FlagTypes.SkipTriggers; var hit = Owner.SceneGraphRoot.RayCast(ref ray, ref view, out var distance, out _, rayCastFlags); if (hit != null) { // Skip snapping selection to itself bool isSelected = false; for (var e = hit; e != null && !isSelected; e = e.ParentNode) isSelected |= IsSelected(e); if (isSelected) { GetSelectedObjectsBounds(out var selectionBounds, out _); ray.Position = ray.GetPoint(selectionBounds.Size.Y * 0.5f); continue; } // Include objects bounds into target snap location var editorBounds = BoundingBox.Empty; Real bottomToCenter = 100000.0f; for (int i = 0; i < _selectionParents.Count; i++) { if (_selectionParents[i] is ActorNode actorNode) { var b = actorNode.Actor.EditorBoxChildren; BoundingBox.Merge(ref editorBounds, ref b, out editorBounds); bottomToCenter = Mathf.Min(bottomToCenter, actorNode.Actor.Position.Y - editorBounds.Minimum.Y); } } var newPosition = ray.GetPoint(distance) + new Vector3(0, bottomToCenter, 0); // Snap var translationDelta = newPosition - Position; var rotationDelta = Quaternion.Identity; var scaleDelta = Vector3.Zero; if (translationDelta.IsZero) break; StartTransforming(); OnApplyTransformation(ref translationDelta, ref rotationDelta, ref scaleDelta); EndTransforming(); } break; } } /// public override void Pick() { // Ensure player is not moving objects if (ActiveAxis != Axis.None) return; // Get mouse ray and try to hit any object var ray = Owner.MouseRay; var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); var renderView = Owner.RenderTask.View; bool selectColliders = (renderView.Flags & ViewFlags.PhysicsDebug) == ViewFlags.PhysicsDebug || renderView.Mode == ViewMode.PhysicsColliders; SceneGraphNode.RayCastData.FlagTypes rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.None; if (!selectColliders) rayCastFlags |= SceneGraphNode.RayCastData.FlagTypes.SkipColliders; var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags); // Update selection var sceneEditing = Editor.Instance.SceneEditing; if (hit != null) { // For child actor nodes (mesh, link or sth) we need to select it's owning actor node first or any other child node (but not a child actor) if (hit is ActorChildNode actorChildNode && !actorChildNode.CanBeSelectedDirectly) { var parentNode = actorChildNode.ParentNode; bool canChildBeSelected = sceneEditing.Selection.Contains(parentNode); if (!canChildBeSelected) { for (int i = 0; i < parentNode.ChildNodes.Count; i++) { if (sceneEditing.Selection.Contains(parentNode.ChildNodes[i])) { canChildBeSelected = true; break; } } } if (canChildBeSelected && sceneEditing.Selection.Count > 1) { // Don't select child node if multiple nodes are selected canChildBeSelected = false; } if (!canChildBeSelected) { // Select parent hit = parentNode; } } // Select prefab root and then go down until you find the actual item in which case select the prefab root again if (hit is ActorNode actorNode) { ActorNode prefabRoot = GetPrefabRootInParent(actorNode); if (prefabRoot != null && actorNode != prefabRoot) { hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot); } } bool addRemove = Owner.IsControlDown; bool isSelected = sceneEditing.Selection.Contains(hit); if (addRemove) { if (isSelected) sceneEditing.Deselect(hit); else sceneEditing.Select(hit, true); } else { sceneEditing.Select(hit); } } else { sceneEditing.Deselect(); } } /// public override void OnSelectionChanged(List newSelection) { // End current action EndTransforming(); // Prepare collections _selection.Clear(); _selectionParents.Clear(); int count = newSelection.Count; if (_selection.Capacity < count) { _selection.Capacity = Mathf.NextPowerOfTwo(count); _selectionParents.Capacity = Mathf.NextPowerOfTwo(count); } // Cache selected objects _selection.AddRange(newSelection); // Build selected objects parents list. // Note: because selection may contain objects and their children we have to split them and get only parents. // Later during transformation we apply translation/scale/rotation only on them (children inherit transformations) SceneGraphTools.BuildNodesParents(_selection, _selectionParents); } /// protected override int SelectionCount => _selectionParents.Count; /// protected override Transform GetSelectedObject(int index) { return _selectionParents[index].Transform; } /// protected override void GetSelectedObjectsBounds(out BoundingBox bounds, out bool navigationDirty) { bounds = BoundingBox.Empty; navigationDirty = false; for (int i = 0; i < _selectionParents.Count; i++) { if (_selectionParents[i] is ActorNode actorNode) { bounds = BoundingBox.Merge(bounds, actorNode.Actor.BoxWithChildren); navigationDirty |= actorNode.AffectsNavigationWithChildren; } } } /// protected override bool IsSelected(SceneGraphNode obj) { return _selection.Contains(obj); } /// protected override void OnApplyTransformation(ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta) { base.OnApplyTransformation(ref translationDelta, ref rotationDelta, ref scaleDelta); ApplyTransformation(_selectionParents, ref translationDelta, ref rotationDelta, ref scaleDelta); } /// protected override void OnEndTransforming() { base.OnEndTransforming(); // Record undo action Owner.Undo.AddAction(new TransformObjectsAction(SelectedParents, _startTransforms, ref _startBounds, _navigationDirty)); } /// protected override void OnDuplicate() { base.OnDuplicate(); Duplicate(); } } }