// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Drag; using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph.Actors; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Previews; using FlaxEditor.Viewport.Widgets; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Viewport { /// /// Editor viewport used by the /// /// /// /// public class PrefabWindowViewport : PrefabPreview, IEditorPrimitivesOwner { private readonly PrefabWindow _window; private UpdateDelegate _update; private readonly ViewportWidgetButton _gizmoModeTranslate; private readonly ViewportWidgetButton _gizmoModeRotate; private readonly ViewportWidgetButton _gizmoModeScale; private ViewportWidgetButton _translateSnappng; private ViewportWidgetButton _rotateSnapping; private ViewportWidgetButton _scaleSnapping; private readonly DragAssets _dragAssets = new DragAssets(ValidateDragItem); private readonly DragActorType _dragActorType = new DragActorType(ValidateDragActorType); private readonly DragHandlers _dragHandlers = new DragHandlers(); /// /// The transform gizmo. /// public readonly TransformGizmo TransformGizmo; /// /// The selection outline postFx. /// public SelectionOutline SelectionOutline; /// /// The editor primitives postFx. /// public EditorPrimitives EditorPrimitives; /// /// 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(); // Prepare rendering task Task.ActorsSource = ActorsSources.CustomActors; Task.ViewFlags = ViewFlags.DefaultEditor & ~ViewFlags.EditorSprites; Task.PostRender += OnPostRender; // Create post effects SelectionOutline = FlaxEngine.Object.New(); SelectionOutline.SelectionGetter = () => TransformGizmo.SelectedParents; Task.CustomPostFx.Add(SelectionOutline); EditorPrimitives = FlaxEngine.Object.New(); EditorPrimitives.Viewport = this; Task.CustomPostFx.Add(EditorPrimitives); // Add transformation gizmo TransformGizmo = new TransformGizmo(this); TransformGizmo.ApplyTransformation += ApplyTransform; TransformGizmo.ModeChanged += OnGizmoModeChanged; TransformGizmo.Duplicate += _window.Duplicate; Gizmos.Active = TransformGizmo; // Transform space widget var transformSpaceWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); var transformSpaceToggle = new ViewportWidgetButton(string.Empty, window.Editor.Icons.World16, null, true) { Checked = TransformGizmo.ActiveTransformSpace == TransformGizmo.TransformSpace.World, TooltipText = "Gizmo transform space (world or local)", Parent = transformSpaceWidget }; transformSpaceToggle.Toggled += OnTransformSpaceToggle; transformSpaceWidget.Parent = this; // Scale snapping widget var scaleSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); var enableScaleSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.ScaleStep16, null, true) { Checked = TransformGizmo.ScaleSnapEnabled, TooltipText = "Enable scale snapping", Parent = scaleSnappingWidget }; enableScaleSnapping.Toggled += OnScaleSnappingToggle; var scaleSnappingCM = new ContextMenu(); _scaleSnapping = new ViewportWidgetButton(TransformGizmo.ScaleSnapValue.ToString(), SpriteHandle.Invalid, scaleSnappingCM); _scaleSnapping.TooltipText = "Scale snapping values"; for (int i = 0; i < EditorViewportScaleSnapValues.Length; i++) { var v = EditorViewportScaleSnapValues[i]; var button = scaleSnappingCM.AddButton(v.ToString()); button.Tag = v; } scaleSnappingCM.ButtonClicked += OnWidgetScaleSnapClick; scaleSnappingCM.VisibleChanged += OnWidgetScaleSnapShowHide; _scaleSnapping.Parent = scaleSnappingWidget; scaleSnappingWidget.Parent = this; // Rotation snapping widget var rotateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); var enableRotateSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.RotateStep16, null, true) { Checked = TransformGizmo.RotationSnapEnabled, TooltipText = "Enable rotation snapping", Parent = rotateSnappingWidget }; enableRotateSnapping.Toggled += OnRotateSnappingToggle; var rotateSnappingCM = new ContextMenu(); _rotateSnapping = new ViewportWidgetButton(TransformGizmo.RotationSnapValue.ToString(), SpriteHandle.Invalid, rotateSnappingCM); _rotateSnapping.TooltipText = "Rotation snapping values"; for (int i = 0; i < EditorViewportRotateSnapValues.Length; i++) { var v = EditorViewportRotateSnapValues[i]; var button = rotateSnappingCM.AddButton(v.ToString()); button.Tag = v; } rotateSnappingCM.ButtonClicked += OnWidgetRotateSnapClick; rotateSnappingCM.VisibleChanged += OnWidgetRotateSnapShowHide; _rotateSnapping.Parent = rotateSnappingWidget; rotateSnappingWidget.Parent = this; // Translation snapping widget var translateSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); var enableTranslateSnapping = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Grid16, null, true) { Checked = TransformGizmo.TranslationSnapEnable, TooltipText = "Enable position snapping", Parent = translateSnappingWidget }; enableTranslateSnapping.Toggled += OnTranslateSnappingToggle; var translateSnappingCM = new ContextMenu(); _translateSnappng = new ViewportWidgetButton(TransformGizmo.TranslationSnapValue.ToString(), SpriteHandle.Invalid, translateSnappingCM); _translateSnappng.TooltipText = "Position snapping values"; for (int i = 0; i < EditorViewportTranslateSnapValues.Length; i++) { var v = EditorViewportTranslateSnapValues[i]; var button = translateSnappingCM.AddButton(v.ToString()); button.Tag = v; } translateSnappingCM.ButtonClicked += OnWidgetTranslateSnapClick; translateSnappingCM.VisibleChanged += OnWidgetTranslateSnapShowHide; _translateSnappng.Parent = translateSnappingWidget; translateSnappingWidget.Parent = this; // Gizmo mode widget var gizmoMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); _gizmoModeTranslate = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Translate16, null, true) { Tag = TransformGizmo.Mode.Translate, TooltipText = "Translate gizmo mode", Checked = true, Parent = gizmoMode }; _gizmoModeTranslate.Toggled += OnGizmoModeToggle; _gizmoModeRotate = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Rotate16, null, true) { Tag = TransformGizmo.Mode.Rotate, TooltipText = "Rotate gizmo mode", Parent = gizmoMode }; _gizmoModeRotate.Toggled += OnGizmoModeToggle; _gizmoModeScale = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Scale16, null, true) { Tag = TransformGizmo.Mode.Scale, TooltipText = "Scale gizmo mode", Parent = gizmoMode }; _gizmoModeScale.Toggled += OnGizmoModeToggle; gizmoMode.Parent = this; _dragHandlers.Add(_dragActorType); _dragHandlers.Add(_dragAssets); // Setup input actions InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate); InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate); InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale); InputActions.Add(options => options.FocusSelection, ShowSelectedActors); SetUpdate(ref _update, OnUpdate); } private void OnUpdate(float deltaTime) { for (int i = 0; i < Gizmos.Count; i++) { Gizmos[i].Update(deltaTime); } } private void OnPostRender(GPUContext context, RenderContext renderContext) { if (renderContext.View.Mode != ViewMode.Default) { var task = renderContext.Task; // Render editor primitives, gizmo and debug shapes in debug view modes // Note: can use Output buffer as both input and output because EditorPrimitives is using a intermediate buffers EditorPrimitives.Render(context, ref renderContext, task.Output, task.Output); } } /// /// Moves the viewport to visualize selected actors. /// public void ShowSelectedActors() { ((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents); } /// public GizmosCollection Gizmos { get; } = new GizmosCollection(); /// 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 Vector2 MouseDelta => _mouseDeltaLeft * 1000; /// public bool UseSnapping => Root.GetKey(KeyboardKeys.Control); /// public bool UseDuplicate => Root.GetKey(KeyboardKeys.Shift); /// public Undo Undo { get; } /// 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 OnGizmoModeToggle(ViewportWidgetButton button) { TransformGizmo.ActiveMode = (TransformGizmo.Mode)(int)button.Tag; } private void OnTranslateSnappingToggle(ViewportWidgetButton button) { TransformGizmo.TranslationSnapEnable = !TransformGizmo.TranslationSnapEnable; } private void OnRotateSnappingToggle(ViewportWidgetButton button) { TransformGizmo.RotationSnapEnabled = !TransformGizmo.RotationSnapEnabled; } private void OnScaleSnappingToggle(ViewportWidgetButton button) { TransformGizmo.ScaleSnapEnabled = !TransformGizmo.ScaleSnapEnabled; } private void OnTransformSpaceToggle(ViewportWidgetButton button) { TransformGizmo.ToggleTransformSpace(); } private void OnGizmoModeChanged() { // Update all viewport widgets status var mode = TransformGizmo.ActiveMode; _gizmoModeTranslate.Checked = mode == TransformGizmo.Mode.Translate; _gizmoModeRotate.Checked = mode == TransformGizmo.Mode.Rotate; _gizmoModeScale.Checked = mode == TransformGizmo.Mode.Scale; } private static readonly float[] EditorViewportScaleSnapValues = { 0.05f, 0.1f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 6.0f, 8.0f, }; private void OnWidgetScaleSnapClick(ContextMenuButton button) { var v = (float)button.Tag; TransformGizmo.ScaleSnapValue = v; _scaleSnapping.Text = v.ToString(); } private void OnWidgetScaleSnapShowHide(Control control) { if (control.Visible == false) return; var ccm = (ContextMenu)control; foreach (var e in ccm.Items) { if (e is ContextMenuButton b) { var v = (float)b.Tag; b.Icon = Mathf.Abs(TransformGizmo.ScaleSnapValue - v) < 0.001f ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } } private static readonly float[] EditorViewportRotateSnapValues = { 1.0f, 5.0f, 10.0f, 15.0f, 30.0f, 45.0f, 60.0f, 90.0f, }; private void OnWidgetRotateSnapClick(ContextMenuButton button) { var v = (float)button.Tag; TransformGizmo.RotationSnapValue = v; _rotateSnapping.Text = v.ToString(); } private void OnWidgetRotateSnapShowHide(Control control) { if (control.Visible == false) return; var ccm = (ContextMenu)control; foreach (var e in ccm.Items) { if (e is ContextMenuButton b) { var v = (float)b.Tag; b.Icon = Mathf.Abs(TransformGizmo.RotationSnapValue - v) < 0.001f ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } } private static readonly float[] EditorViewportTranslateSnapValues = { 0.1f, 0.5f, 1.0f, 5.0f, 10.0f, 100.0f, 1000.0f, }; private void OnWidgetTranslateSnapClick(ContextMenuButton button) { var v = (float)button.Tag; TransformGizmo.TranslationSnapValue = v; _translateSnappng.Text = v.ToString(); } private void OnWidgetTranslateSnapShowHide(Control control) { if (control.Visible == false) return; var ccm = (ContextMenu)control; foreach (var e in ccm.Items) { if (e is ContextMenuButton b) { var v = (float)b.Tag; b.Icon = Mathf.Abs(TransformGizmo.TranslationSnapValue - v) < 0.001f ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } } 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 applyRotation = !rotationDelta.IsIdentity; bool useObjCenter = TransformGizmo.ActivePivot == TransformGizmo.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 const float scaleLimit = 99_999_999.0f; trans.Scale = Vector3.Clamp(trans.Scale + scaleDelta, new Vector3(-scaleLimit), new Vector3(scaleLimit)); // Apply translation trans.Translation += translationDelta; 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 != TransformGizmo.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) { 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 Vector2 location, DragData data) { var result = base.OnDragEnter(ref location, data); if (result != DragDropEffect.None) return result; return _dragHandlers.OnDragEnter(data); } private static bool ValidateDragItem(ContentItem contentItem) { if (contentItem is AssetItem assetItem) { if (assetItem.IsOfType()) return true; if (assetItem.IsOfType()) return true; if (assetItem.IsOfType()) return true; if (assetItem.IsOfType()) return true; if (assetItem.IsOfType()) return true; } return false; } private static bool ValidateDragActorType(ScriptType actorType) { return true; } /// public override DragDropEffect OnDragMove(ref Vector2 location, DragData data) { var result = base.OnDragMove(ref location, data); if (result != DragDropEffect.None) return result; return _dragHandlers.Effect; } /// public override void OnDragLeave() { _dragHandlers.OnDragLeave(); base.OnDragLeave(); } private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation) { BoundingBox box; Editor.GetActorEditorBox(actor, out box); // Place the object var location = hitLocation - (box.Size.Length * 0.5f) * ViewDirection; // Apply grid snapping if enabled if (UseSnapping || TransformGizmo.TranslationSnapEnable) { float snapValue = TransformGizmo.TranslationSnapValue; location = new Vector3( (int)(location.X / snapValue) * snapValue, (int)(location.Y / snapValue) * snapValue, (int)(location.Z / snapValue) * snapValue); } return location; } private void Spawn(AssetItem item, SceneGraphNode hit, ref Vector2 location, ref Vector3 hitLocation) { if (item is BinaryAssetItem binaryAssetItem) { if (binaryAssetItem.Type == typeof(ParticleSystem)) { var particleSystem = FlaxEngine.Content.LoadAsync(item.ID); var actor = new ParticleEffect(); actor.Name = item.ShortName; actor.ParticleSystem = particleSystem; actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); Spawn(actor); return; } if (typeof(MaterialBase).IsAssignableFrom(binaryAssetItem.Type)) { if (hit is StaticModelNode staticModelNode) { var staticModel = (StaticModel)staticModelNode.Actor; var ray = ConvertMouseToRay(ref location); if (staticModel.IntersectsEntry(ref ray, out _, out _, out var entryIndex)) { var material = FlaxEngine.Content.LoadAsync(item.ID); using (new UndoBlock(Undo, staticModel, "Change material")) staticModel.SetMaterial(entryIndex, material); } } return; } if (typeof(SkinnedModel).IsAssignableFrom(binaryAssetItem.Type)) { var model = FlaxEngine.Content.LoadAsync(item.ID); var actor = new AnimatedModel(); actor.Name = item.ShortName; actor.SkinnedModel = model; actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); Spawn(actor); return; } if (typeof(Model).IsAssignableFrom(binaryAssetItem.Type)) { var model = FlaxEngine.Content.LoadAsync(item.ID); var actor = new StaticModel(); actor.Name = item.ShortName; actor.Model = model; actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); Spawn(actor); return; } if (typeof(AudioClip).IsAssignableFrom(binaryAssetItem.Type)) { var clip = FlaxEngine.Content.LoadAsync(item.ID); var actor = new AudioSource(); actor.Name = item.ShortName; actor.Clip = clip; actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); Spawn(actor); return; } if (typeof(Prefab).IsAssignableFrom(binaryAssetItem.Type)) { var prefab = FlaxEngine.Content.LoadAsync(item.ID); var actor = PrefabManager.SpawnPrefab(prefab, null); actor.Name = item.ShortName; actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); Spawn(actor); return; } } } private void Spawn(Actor actor) { _window.Spawn(actor); } private void Spawn(ScriptType item, SceneGraphNode hit, ref Vector3 hitLocation) { var actor = item.CreateInstance() as Actor; if (actor == null) { Editor.LogWarning("Failed to spawn actor of type " + item.TypeName); return; } actor.Name = item.Name; actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); Spawn(actor); } /// public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data) { var result = base.OnDragDrop(ref location, data); if (result != DragDropEffect.None) return result; // Check if drag sth Vector3 hitLocation = ViewPosition; SceneGraphNode hit = null; if (_dragHandlers.HasValidDrag) { // Get mouse ray and try to hit any object var ray = ConvertMouseToRay(ref location); var view = new Ray(ViewPosition, ViewDirection); hit = _window.Graph.Root.RayCast(ref ray, ref view, out var closest, SceneGraphNode.RayCastData.FlagTypes.SkipColliders); if (hit != null) { // Use hit location hitLocation = ray.Position + ray.Direction * closest; } else { // Use area in front of the viewport hitLocation = ViewPosition + ViewDirection * 100; } } // Drag assets if (_dragAssets.HasValidDrag) { result = _dragAssets.Effect; // Process items for (int i = 0; i < _dragAssets.Objects.Count; i++) { var item = _dragAssets.Objects[i]; Spawn(item, hit, ref location, ref hitLocation); } } // Drag actor type else if (_dragActorType.HasValidDrag) { result = _dragActorType.Effect; // Process items for (int i = 0; i < _dragActorType.Objects.Count; i++) { var item = _dragActorType.Objects[i]; Spawn(item, hit, ref hitLocation); } } return result; } /// public override void OnDestroy() { FlaxEngine.Object.Destroy(ref SelectionOutline); FlaxEngine.Object.Destroy(ref EditorPrimitives); base.OnDestroy(); } /// public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { } } }