// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Viewport { /// /// Editor viewport used by the /// /// /// /// public class PrefabWindowViewport : PrefabPreview, IGizmoOwner { [HideInEditor] private sealed class PrefabSpritesRenderer : MainEditorGizmoViewport.EditorSpritesRenderer { public PrefabWindowViewport Viewport; public override bool CanRender() { return (Task.View.Flags & ViewFlags.EditorSprites) == ViewFlags.EditorSprites && Enabled; } protected override void Draw(ref RenderContext renderContext) { ViewportIconsRenderer.DrawIcons(ref renderContext, Viewport.Instance); } } [HideInEditor] private sealed class PrefabUIEditorRoot : UIEditorRoot { private readonly PrefabWindowViewport _viewport; private bool UI => _viewport._hasUILinkedCached; public PrefabUIEditorRoot(PrefabWindowViewport viewport) : base(true) { _viewport = viewport; Parent = viewport; } public override bool EnableInputs => !UI; public override bool EnableSelecting => UI; public override bool EnableBackground => UI; public override TransformGizmo TransformGizmo => _viewport.TransformGizmo; } private readonly PrefabWindow _window; private UpdateDelegate _update; private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private PrefabSpritesRenderer _spritesRenderer; private IntPtr _tempDebugDrawContext; private bool _hasUILinkedCached; private PrefabUIEditorRoot _uiRoot; /// /// Drag and drop handlers /// public readonly ViewportDragHandlers DragHandlers; /// /// The transform gizmo. /// public readonly TransformGizmo TransformGizmo; /// /// The selection outline postFx. /// public SelectionOutline SelectionOutline; /// /// Initializes a new instance of the class. /// /// Editor window. public PrefabWindowViewport(PrefabWindow window) : base(true) { _window = window; _window.SelectionChanged += OnSelectionChanged; Undo = window.Undo; ViewportCamera = new FPSCamera(); DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem); ShowDebugDraw = true; ShowEditorPrimitives = true; Gizmos = new GizmosCollection(this); var inputOptions = window.Editor.Options.Options.Input; // Prepare rendering task Task.ActorsSource = ActorsSources.CustomActors; Task.ViewFlags = ViewFlags.DefaultEditor; Task.Begin += OnBegin; Task.CollectDrawCalls += OnCollectDrawCalls; Task.PostRender += OnPostRender; // Create post effects SelectionOutline = FlaxEngine.Object.New(); SelectionOutline.SelectionGetter = () => TransformGizmo.SelectedParents; Task.AddCustomPostFx(SelectionOutline); _spritesRenderer = FlaxEngine.Object.New(); _spritesRenderer.Task = Task; _spritesRenderer.Viewport = this; Task.AddCustomPostFx(_spritesRenderer); // Add transformation gizmo TransformGizmo = new TransformGizmo(this); TransformGizmo.ApplyTransformation += ApplyTransform; TransformGizmo.Duplicate += _window.Duplicate; Gizmos.Active = TransformGizmo; // Use custom root for UI controls _uiRoot = new PrefabUIEditorRoot(this); _uiRoot.IndexInParent = 0; // Move viewport down below other widgets in the viewport _uiParentLink = _uiRoot.UIRoot; EditorGizmoViewport.AddGizmoViewportWidgets(this, TransformGizmo); // Setup input actions InputActions.Add(options => options.FocusSelection, ShowSelectedActors); SetUpdate(ref _update, OnUpdate); } /// /// Updates the viewport's gizmos, especially to toggle between 3D and UI editing modes. /// internal void UpdateGizmoMode() { // Skip if gizmo mode was unmodified if (_hasUILinked == _hasUILinkedCached) return; _hasUILinkedCached = _hasUILinked; if (_hasUILinked) { // UI widget Gizmos.Active = null; ViewportCamera = new UIEditorCamera { UIEditor = _uiRoot }; // Hide 3D visuals ShowEditorPrimitives = false; ShowDefaultSceneActors = false; ShowDebugDraw = false; // Show whole UI on startup var canvas = (CanvasRootControl)_uiParentLink.Children.FirstOrDefault(x => x is CanvasRootControl); if (canvas != null) ViewportCamera.ShowActor(canvas.Canvas); else if (Instance is UIControl) ViewportCamera.ShowActor(Instance); } else { // Generic prefab Gizmos.Active = TransformGizmo; ViewportCamera = new FPSCamera(); } // Update default components usage bool defaultFeatures = !_hasUILinked; _disableInputUpdate = _hasUILinked; _spritesRenderer.Enabled = defaultFeatures; SelectionOutline.Enabled = defaultFeatures; _showDefaultSceneButton.Visible = defaultFeatures; _cameraWidget.Visible = defaultFeatures; _cameraButton.Visible = defaultFeatures; _orthographicModeButton.Visible = defaultFeatures; Task.Enabled = defaultFeatures; UseAutomaticTaskManagement = defaultFeatures; TintColor = defaultFeatures ? Color.White : Color.Transparent; } private void OnUpdate(float deltaTime) { UpdateGizmoMode(); for (int i = 0; i < Gizmos.Count; i++) { Gizmos[i].Update(deltaTime); } } private void OnBegin(RenderTask task, GPUContext context) { _debugDrawData.Clear(); // Collect selected objects debug shapes and visuals var selectedParents = TransformGizmo.SelectedParents; if (selectedParents.Count > 0) { // Use temporary Debug Draw context to pull any debug shapes drawing in Scene Graph Nodes - those are used in OnDebugDraw down below if (_tempDebugDrawContext == IntPtr.Zero) _tempDebugDrawContext = DebugDraw.AllocateContext(); DebugDraw.SetContext(_tempDebugDrawContext); DebugDraw.UpdateContext(_tempDebugDrawContext, 1.0f); for (int i = 0; i < selectedParents.Count; i++) { if (selectedParents[i].IsActiveInHierarchy) selectedParents[i].OnDebugDraw(_debugDrawData); } DebugDraw.SetContext(IntPtr.Zero); } } private void OnCollectDrawCalls(ref RenderContext renderContext) { DragHandlers.CollectDrawCalls(_debugDrawData, ref renderContext); _debugDrawData.OnDraw(ref renderContext); } private void OnPostRender(GPUContext context, ref RenderContext renderContext) { if (renderContext.View.Mode != ViewMode.Default) { var task = renderContext.Task; // Render editor sprites if (_spritesRenderer && _spritesRenderer.CanRender()) { _spritesRenderer.Render(context, ref renderContext, task.Output, task.Output); } // Render selection outline if (SelectionOutline && SelectionOutline.CanRender()) { // Use temporary intermediate buffer var desc = task.Output.Description; var temp = RenderTargetPool.Get(ref desc); SelectionOutline.Render(context, ref renderContext, task.Output, temp); // Copy the results back to the output context.CopyTexture(task.Output, 0, 0, 0, 0, temp, 0); RenderTargetPool.Release(temp); } } } /// /// Moves the viewport to visualize selected actors. /// public void ShowSelectedActors() { var orient = ViewOrientation; ViewportCamera.ShowActors(TransformGizmo.SelectedParents, ref orient); } /// public EditorViewport Viewport => this; /// public GizmosCollection Gizmos { get; } /// public SceneRenderTask RenderTask => Task; /// public float ViewFarPlane => FarPlane; /// public bool IsLeftMouseButtonDown => _input.IsMouseLeftDown; /// public bool IsRightMouseButtonDown => _input.IsMouseRightDown; /// public bool IsAltKeyDown => _input.IsAltDown; /// public bool IsControlDown => _input.IsControlDown; /// public bool SnapToGround => false; /// public bool SnapToVertex => ContainsFocus && Editor.Instance.Options.Options.Input.SnapToVertex.Process(Root); /// public Float2 MouseDelta => _mouseDelta; /// public bool UseSnapping => Root.GetKey(KeyboardKeys.Control); /// public bool UseDuplicate => Root.GetKey(KeyboardKeys.Shift); /// public Undo Undo { get; } /// public RootNode SceneGraphRoot => _window.Graph.Root; /// public void Select(List nodes) { _window.Select(nodes); } /// public void Spawn(Actor actor) { _window.Spawn(actor); } /// public void OpenContextMenu() { var mouse = PointFromWindow(Root.MousePosition); _window.ShowContextMenu(this, ref mouse); } /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; /// protected override void AddUpdateCallbacks(RootControl root) { base.AddUpdateCallbacks(root); root.UpdateCallbacksToAdd.Add(_update); } /// protected override void RemoveUpdateCallbacks(RootControl root) { base.RemoveUpdateCallbacks(root); root.UpdateCallbacksToRemove.Add(_update); } private void OnSelectionChanged() { Gizmos.ForEach(x => x.OnSelectionChanged(_window.Selection)); } /// /// Applies the transform to the collection of scene graph nodes. /// /// The selection. /// The translation delta. /// The rotation delta. /// The scale delta. public void ApplyTransform(List selection, ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta) { bool applyTranslation = !translationDelta.IsZero; bool applyRotation = !rotationDelta.IsIdentity; bool applyScale = !scaleDelta.IsZero; bool useObjCenter = TransformGizmo.ActivePivot == TransformGizmoBase.PivotType.ObjectCenter; Vector3 gizmoPosition = TransformGizmo.Position; // Transform selected objects for (int i = 0; i < selection.Count; i++) { var obj = selection[i]; var trans = obj.Transform; // Apply rotation if (applyRotation) { Vector3 pivotOffset = trans.Translation - gizmoPosition; if (useObjCenter || pivotOffset.IsZero) { //trans.Orientation *= rotationDelta; trans.Orientation *= Quaternion.Invert(trans.Orientation) * rotationDelta * trans.Orientation; } else { Matrix.RotationQuaternion(ref trans.Orientation, out var transWorld); Matrix.RotationQuaternion(ref rotationDelta, out var deltaWorld); Matrix world = transWorld * Matrix.Translation(pivotOffset) * deltaWorld * Matrix.Translation(-pivotOffset); trans.SetRotation(ref world); trans.Translation += world.TranslationVector; } } // Apply scale if (applyScale) { const float scaleLimit = 99_999_999.0f; trans.Scale = Float3.Clamp(trans.Scale + scaleDelta, new Float3(-scaleLimit), new Float3(scaleLimit)); } // Apply translation if (applyTranslation) { trans.Translation += translationDelta; } if (obj is ActorNode actorNode) { if (applyTranslation) actorNode.Actor.Position = trans.Translation; if (applyRotation) actorNode.Actor.Orientation = trans.Orientation; if (applyScale) actorNode.Actor.Scale = trans.Scale; } else obj.Transform = trans; } } /// protected override void OnLeftMouseButtonUp() { // Skip if was controlling mouse or mouse is not over the area if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos)) return; if (TransformGizmo.IsActive) { // Ensure player is not moving objects if (TransformGizmo.ActiveAxis != TransformGizmoBase.Axis.None) return; } else { // For now just pick objects in transform gizmo mode return; } // Get mouse ray and try to hit any object var ray = MouseRay; var view = new Ray(ViewPosition, ViewDirection); var hit = _window.Graph.Root.RayCast(ref ray, ref view, out _, SceneGraphNode.RayCastData.FlagTypes.SkipColliders); // Update selection 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 = _window.Selection.Contains(parentNode); if (!canChildBeSelected) { for (int i = 0; i < parentNode.ChildNodes.Count; i++) { if (_window.Selection.Contains(parentNode.ChildNodes[i])) { canChildBeSelected = true; break; } } } if (!canChildBeSelected) { // Select parent hit = parentNode; } } bool addRemove = Root.GetKey(KeyboardKeys.Control); bool isSelected = _window.Selection.Contains(hit); if (addRemove) { if (isSelected) _window.Deselect(hit); else _window.Select(hit, true); } else { _window.Select(hit); } } else { _window.Deselect(); } // Keep focus Focus(); base.OnLeftMouseButtonUp(); } /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { DragHandlers.ClearDragEffects(); var result = base.OnDragEnter(ref location, data); if (result != DragDropEffect.None) return result; return DragHandlers.DragEnter(ref location, data); } private bool ValidateDragItem(ContentItem contentItem) { if (contentItem is AssetItem assetItem) { if (assetItem.OnEditorDrag(this)) return true; if (assetItem.IsOfType()) return true; } return false; } private static bool ValidateDragActorType(ScriptType actorType) { return true; } private static bool ValidateDragScriptItem(ScriptItem script) { return Editor.Instance.CodeEditing.Actors.Get(script) != ScriptType.Null; } /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { DragHandlers.ClearDragEffects(); var result = base.OnDragMove(ref location, data); if (result != DragDropEffect.None) return result; return DragHandlers.DragEnter(ref location, data); } /// public override void OnDragLeave() { DragHandlers.ClearDragEffects(); DragHandlers.OnDragLeave(); base.OnDragLeave(); } /// /// Focuses the viewport on the current selection of the gizmo. /// public void FocusSelection() { var orientation = ViewOrientation; FocusSelection(ref orientation); } /// /// Focuses the viewport on the current selection of the gizmo. /// /// The target view orientation. public void FocusSelection(ref Quaternion orientation) { ViewportCamera.FocusSelection(Gizmos, ref orientation); } /// protected override void OrientViewport(ref Quaternion orientation) { if (TransformGizmo.SelectedParents.Count != 0) FocusSelection(ref orientation); else base.OrientViewport(ref orientation); } /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { DragHandlers.ClearDragEffects(); var result = base.OnDragDrop(ref location, data); if (result != DragDropEffect.None) return result; return DragHandlers.DragDrop(ref location, data); } /// public override void OnDestroy() { if (IsDisposing) return; if (_tempDebugDrawContext != IntPtr.Zero) { DebugDraw.FreeContext(_tempDebugDrawContext); _tempDebugDrawContext = IntPtr.Zero; } FlaxEngine.Object.Destroy(ref SelectionOutline); FlaxEngine.Object.Destroy(ref _spritesRenderer); base.OnDestroy(); } /// public override void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { // Draw gizmos for (int i = 0; i < Gizmos.Count; i++) { Gizmos[i].Draw(ref renderContext); } base.DrawEditorPrimitives(context, ref renderContext, target, targetDepth); } /// protected override void OnDebugDraw(GPUContext context, ref RenderContext renderContext) { base.OnDebugDraw(context, ref renderContext); // Collect selected objects debug shapes again when DebugDraw is active with a custom context _debugDrawData.Clear(); var selectedParents = TransformGizmo.SelectedParents; for (int i = 0; i < selectedParents.Count; i++) { if (selectedParents[i].IsActiveInHierarchy) selectedParents[i].OnDebugDraw(_debugDrawData); } unsafe { fixed (IntPtr* actors = _debugDrawData.ActorsPtrs) { DebugDraw.DrawActors(new IntPtr(actors), _debugDrawData.ActorsCount, false); } } } } }