diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs index ea187fb9cb..0c82fc7271 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameEntityTransformService.cs @@ -5,11 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Stride.Core.BuildEngine; -using Stride.Core; -using Stride.Core.Annotations; -using Stride.Core.Extensions; -using Stride.Core.Mathematics; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; using Stride.Assets.Presentation.AssetEditors.GameEditor; @@ -17,8 +12,14 @@ using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.AssetEditors.Gizmos; using Stride.Assets.Presentation.SceneEditor; +using Stride.Core; +using Stride.Core.Annotations; +using Stride.Core.BuildEngine; +using Stride.Core.Extensions; +using Stride.Core.Mathematics; using Stride.Editor.EditorGame.Game; using Stride.Engine; +using Stride.Input; using Stride.Rendering; using Stride.Rendering.Compositing; @@ -39,6 +40,12 @@ public class EditorGameEntityTransformService : EditorGameMouseServiceBase, IEdi private double gizmoSize = 1.0f; private bool dynamicSnappingInUse = false; + private bool isEntityDuplicationInProgress = false; + private Entity entityDuplicationPreviousEntityWithGizmo; + private IReadOnlyCollection entityDuplicationPreviousSelection; + private readonly Dictionary entityDuplicationSrcToPreviewEntityMap = []; + private readonly Dictionary entityDuplicationSrcIdToPreviewEntityMap = []; + public EditorGameEntityTransformService([NotNull] EntityHierarchyEditorViewModel editor, [NotNull] IEditorGameController controller) { if (editor == null) throw new ArgumentNullException(nameof(editor)); @@ -98,7 +105,14 @@ public Entity EntityWithGizmo } } - public override IEnumerable Dependencies { get { yield return typeof(IEditorGameEntitySelectionService); } } + public override IEnumerable Dependencies + { + get + { + yield return typeof(IEditorGameEntitySelectionService); + yield return typeof(EditorGameModelSelectionService); + } + } Transformation IEditorGameTransformViewModelService.ActiveTransformation { @@ -140,9 +154,7 @@ public override ValueTask DisposeAsync() { EnsureNotDestroyed(nameof(EditorGameEntityTransformService)); - var selectionService = Services.Get(); - if (selectionService != null) - selectionService.SelectionUpdated -= UpdateModifiedEntitiesList; + OnDeactivate(); return base.DisposeAsync(); } @@ -179,6 +191,9 @@ protected override Task Initialize(EditorServiceGame editorGame) TranslationGizmo = new TranslationGizmo(); RotationGizmo = new RotationGizmo(); ScaleGizmo = new ScaleGizmo(); + TranslationGizmo.TransformationStarted += OnGizmoTransformationStarted; + ScaleGizmo.TransformationStarted += OnGizmoTransformationStarted; + RotationGizmo.TransformationStarted += OnGizmoTransformationStarted; TranslationGizmo.TransformationEnded += OnGizmoTransformationFinished; ScaleGizmo.TransformationEnded += OnGizmoTransformationFinished; RotationGizmo.TransformationEnded += OnGizmoTransformationFinished; @@ -187,8 +202,6 @@ protected override Task Initialize(EditorServiceGame editorGame) transformationGizmos.Add(RotationGizmo); transformationGizmos.Add(ScaleGizmo); - Services.Get().SelectionUpdated += UpdateModifiedEntitiesList; - // Initialize and add the Gizmo entities to the gizmo scene MicrothreadLocalDatabases.MountCommonDatabase(); @@ -196,9 +209,7 @@ protected override Task Initialize(EditorServiceGame editorGame) foreach (var gizmo in transformationGizmos) gizmo.Initialize(game.Services, editorScene); - // Deactivate all transformation gizmo by default - foreach (var gizmo in transformationGizmos) - gizmo.IsEnabled = false; + OnActivate(); // set the default active transformation gizmo ActiveTransformationGizmo = TranslationGizmo; @@ -208,6 +219,40 @@ protected override Task Initialize(EditorServiceGame editorGame) return Task.FromResult(true); } + protected void OnActivate() + { + Services.Get().SelectionUpdated += UpdateModifiedEntitiesList; + + // Deactivate all transformation gizmo by default + foreach (var gizmo in transformationGizmos) + gizmo.IsEnabled = false; + } + + protected void OnDeactivate() + { + EntityWithGizmo = null; + foreach (var gizmo in transformationGizmos) + { + gizmo.CancelTransform(); + gizmo.ModifiedEntities = []; + gizmo.IsEnabled = false; + } + + isEntityDuplicationInProgress = false; + foreach (var (_, previewEntity) in entityDuplicationSrcToPreviewEntityMap) + { + // Detach from scene + previewEntity.SetParent(null); + previewEntity.Scene = null; + } + entityDuplicationSrcToPreviewEntityMap.Clear(); + entityDuplicationSrcIdToPreviewEntityMap.Clear(); + + var selectionService = Services.Get(); + if (selectionService != null) + selectionService.SelectionUpdated -= UpdateModifiedEntitiesList; + } + private async Task Update() { while (!IsDisposed) @@ -228,19 +273,19 @@ private async Task Update() // Activate transformation snapping if (game.Input.IsKeyPressed(SceneEditorSettings.TranslationGizmo.GetValue())) { - await editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = Transformation.Translation); + editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = Transformation.Translation); } // Activate rotation snapping if (game.Input.IsKeyPressed(SceneEditorSettings.RotationGizmo.GetValue())) { - await editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = Transformation.Rotation); + editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = Transformation.Rotation); } // Activate scale snapping if (game.Input.IsKeyPressed(SceneEditorSettings.ScaleGizmo.GetValue())) { - await editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = Transformation.Scale); + editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = Transformation.Scale); } // Toggle between different snapping methods @@ -248,19 +293,23 @@ private async Task Update() { var current = activeTransformation; var next = (int)(current + 1) % Enum.GetValues().Length; - await editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = (Transformation)next); + editor.Dispatcher.InvokeAsync(() => editor.Transform.ActiveTransformation = (Transformation)next); + } + + if (game.Input.IsKeyPressed(Keys.Escape) + && activeTransformationGizmo is not null + && activeTransformationGizmo.IsTransformationInProgress) + { + activeTransformationGizmo.CancelTransform(); } } - IEnumerable tasks; - lock (transformationGizmos) + foreach (var x in transformationGizmos) { - tasks = transformationGizmos.Select(x => x.Update()); + x.Update(); } - IsControllingMouse = activeTransformationGizmo != null && activeTransformationGizmo.IsUnderMouse() && IsMouseAvailable; - - await Task.WhenAll(tasks); + IsControllingMouse = activeTransformationGizmo != null && activeTransformationGizmo.IsEnabled && activeTransformationGizmo.IsUnderMouse() && IsMouseAvailable; } await game.Script.NextFrame(); @@ -300,12 +349,17 @@ private void DynamicSnapSelectionToGrid(bool useDynamicSnapping) private void UpdateModifiedEntitiesList(object sender, [NotNull] EntitySelectionEventArgs e) { + if (!IsActive || isEntityDuplicationInProgress) + { + return; + } EntityWithGizmo = e.NewSelection.LastOrDefault(); - if (ActiveTransformationGizmo != null && EntityWithGizmo == null) + if (ActiveTransformationGizmo != null && !ActiveTransformationGizmo.IsTransformationInProgress && EntityWithGizmo == null) { // Reset the transformation axes if the selection is cleared. ActiveTransformationGizmo.ClearTransformationAxes(); } + var modifiedEntities = new List(); modifiedEntities.AddRange(e.NewSelection); @@ -317,13 +371,86 @@ private void UpdateModifiedEntitiesList(object sender, [NotNull] EntitySelection } } - private void OnGizmoTransformationFinished(object sender, EventArgs e) + private void OnGizmoTransformationStarted(object sender, EventArgs e) { + isEntityDuplicationInProgress = game.Input.IsKeyDown(Keys.LeftCtrl) || game.Input.IsKeyDown(Keys.RightCtrl); + if (isEntityDuplicationInProgress) + { + // Duplication occurs in the editor so we should make the gizmo update proxy entities + // until the editor returns the duplicated entities + entityDuplicationPreviousEntityWithGizmo = EntityWithGizmo; + entityDuplicationPreviousSelection = ActiveTransformationGizmo?.ModifiedEntities ?? []; + entityDuplicationSrcToPreviewEntityMap.Clear(); + foreach (var id in Selection.GetSelectedRootIds()) + { + var entity = (Entity)controller.FindGameSidePart(id); + var previewEntity = BuildDuplicationPreviewEntity(entity); + + entityDuplicationSrcToPreviewEntityMap[entity] = previewEntity; + entityDuplicationSrcIdToPreviewEntityMap[id] = previewEntity; + } + ActiveTransformationGizmo.RemapModifyingEntities(entityDuplicationSrcToPreviewEntityMap); + if (EntityWithGizmo is not null + && entityDuplicationSrcToPreviewEntityMap.TryGetValue(EntityWithGizmo, out var anchorProxyEntity)) + { + EntityWithGizmo = anchorProxyEntity; + } + + var modelSelectionService = Services.Get(); + modelSelectionService?.ChangeSelection(entityDuplicationSrcIdToPreviewEntityMap.Values.ToList()); + } + } + + private void OnGizmoTransformationFinished(object sender, TransformationEndedEventArgs e) + { + if (isEntityDuplicationInProgress) + { + var newTransformations = new Dictionary(); + foreach (var (srcId, previewEntity) in entityDuplicationSrcIdToPreviewEntityMap) + { + newTransformations.Add(srcId, new TransformationTRS(previewEntity.Transform)); + // Detach from scene + previewEntity.SetParent(null); + previewEntity.Scene = null; + } + entityDuplicationSrcIdToPreviewEntityMap.Clear(); + entityDuplicationSrcToPreviewEntityMap.Clear(); + + if (e.IsCanceled) + { + // Reselect previous entities + ActiveTransformationGizmo?.ModifiedEntities = entityDuplicationPreviousSelection; + EntityWithGizmo = entityDuplicationPreviousEntityWithGizmo; + var modelSelectionService = Services.Get(); + modelSelectionService?.ChangeSelection(entityDuplicationPreviousSelection); + } + else + { + // Clear preview entities selection (which will select the duplicated entities after the editor finishes actual duplication) + ActiveTransformationGizmo?.ModifiedEntities = []; + EntityWithGizmo = null; + var modelSelectionService = Services.Get(); + modelSelectionService?.ChangeSelection([]); + // Confirm duplication + editor.Dispatcher.InvokeAsync(() => editor.DuplicateEntities(newTransformations)); + } + + isEntityDuplicationInProgress = false; + entityDuplicationPreviousEntityWithGizmo = null; + entityDuplicationPreviousSelection = null; + return; + } + + if (e.IsCanceled) + { + return; + } + var transformations = new Dictionary(); - foreach (var item in Selection.GetSelectedRootIds()) + foreach (var id in Selection.GetSelectedRootIds()) { - var entity = (Entity)controller.FindGameSidePart(item); - transformations.Add(item, new TransformationTRS(entity.Transform)); + var entity = (Entity)controller.FindGameSidePart(id); + transformations.Add(id, new TransformationTRS(entity.Transform)); } InvokeTransformationFinished(transformations); @@ -362,5 +489,21 @@ void IEditorGameTransformViewModelService.UpdateSnap(Transformation transformati } }); } + + private static Entity BuildDuplicationPreviewEntity(Entity srcEntity) + { + var dupePreviewEntity = srcEntity.Clone(); + dupePreviewEntity.Name = $"Duplication {dupePreviewEntity.Name}"; + var parentEntity = srcEntity.GetParent(); + if (parentEntity is not null) + { + dupePreviewEntity.SetParent(parentEntity); + } + else + { + dupePreviewEntity.Scene = srcEntity.Scene; + } + return dupePreviewEntity; + } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameModelSelectionService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameModelSelectionService.cs index bad3b4e7e9..717f93ce30 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameModelSelectionService.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameModelSelectionService.cs @@ -102,6 +102,22 @@ private void SelectionUpdated(object sender, [NotNull] EntitySelectionEventArgs }); } + public void ChangeSelection(IEnumerable entities) + { + var recursiveSelection = new HashSet(entities); + foreach (var childEntity in entities.SelectDeep(x => x.Transform.Children.Select(y => y.Entity))) + { + recursiveSelection.Add(childEntity); + } + + editor.Controller.InvokeAsync(() => + { + // update the selection on the gizmo entities. + selectedEntities.Clear(); + selectedEntities.AddRange(recursiveSelection); + }); + } + class WireframeFilter : RenderStageFilter { private readonly HashSet selectedEntities; diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs index 7e1801e957..f35efb7c44 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs @@ -5,20 +5,6 @@ using System.Collections.Specialized; using System.Linq; using System.Threading.Tasks; -using Stride.Core.Assets; -using Stride.Core.Assets.Editor.Services; -using Stride.Core.Assets.Editor.View.Behaviors; -using Stride.Core.Assets.Editor.ViewModel; -using Stride.Core; -using Stride.Core.Annotations; -using Stride.Core.Diagnostics; -using Stride.Core.Extensions; -using Stride.Core.Mathematics; -using Stride.Core.Presentation.Commands; -using Stride.Core.Presentation.Interop; -using Stride.Core.Presentation.Services; -using Stride.Core.Presentation.Windows; -using Stride.Core.Translation; using Stride.Assets.Entities; using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.EntityFactories; @@ -31,6 +17,21 @@ using Stride.Assets.Presentation.View; using Stride.Assets.Presentation.ViewModel; using Stride.Assets.Presentation.ViewModel.CopyPasteProcessors; +using Stride.Core; +using Stride.Core.Annotations; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.View.Behaviors; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Assets.Editor.ViewModel.Progress; +using Stride.Core.Diagnostics; +using Stride.Core.Extensions; +using Stride.Core.Mathematics; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.Interop; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.Windows; +using Stride.Core.Translation; using Stride.Engine; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels @@ -210,6 +211,81 @@ public ISet DuplicateSelectedEntities() return duplicatedAssets; } + [NotNull] + public async Task DuplicateEntities([NotNull] IReadOnlyDictionary transformations, bool selectDuplicatedEntites = true) + { + var dialogService = ServiceProvider.Get(); + + var logger = new LoggerResult(); + var workProgress = new WorkProgressViewModel(ServiceProvider, logger) + { + Title = Tr._p("Title", "Duplicate entities"), + KeepOpen = KeepOpen.OnWarningsOrErrors, + IsIndeterminate = true, + IsCancellable = false, + Minimum = 0, + Maximum = 1, + }; + dialogService.ShowProgressWindow(workProgress, minDelay: 500); + + try + { + // save elements to copy and remove them from current selection. + var selectedEntityViewModels = new List(); + foreach (var (id, _) in transformations) + { + var entityViewModel = (EntityViewModel)FindPartViewModel(id); + if (entityViewModel is null) + { + logger.Error(string.Format(Tr._p("Log", "Entity {0} no longer exists."), id.ObjectId)); // Can occur if a user uses undo command in the middle of transforming + return; + } + selectedEntityViewModels.Add(entityViewModel); + } + var entitiesToDuplicate = GetCommonRoots(selectedEntityViewModels); + if (entitiesToDuplicate.Count == 0) + return; + + workProgress.Maximum = entitiesToDuplicate.Count; + workProgress.UpdateProgressAsync(Tr._p("Message", "Duplicating entities..."), newProgressValue: 0); + if (selectDuplicatedEntites) + { + SelectedItems.Clear(); + } + // duplicate the elements + var duplicatedAssets = new Dictionary(); + using (var transaction = UndoRedoService.CreateTransaction()) + { + int count = 0; + foreach (var entityViewModel in entitiesToDuplicate) + { + count++; + workProgress.UpdateProgressAsync(string.Format(Tr._p("Message", "Duplicating entities {0}/{1}"), count, entitiesToDuplicate.Count), newProgressValue: count); + await Task.Delay(1); // HACK: required so the progress bar updates, since we're already in the UI thread + + var duplicatedEntity = entityViewModel.Duplicate(transformations); + duplicatedAssets[entityViewModel] = duplicatedEntity; + } + + UndoRedoService.SetName(transaction, "Duplicate entities"); + } + + if (selectDuplicatedEntites) + { + // set selection to new copied elements. + SelectedItems.AddRange(duplicatedAssets.Values); + } + } + catch (Exception e) + { + logger.Error(Tr._p("Log", "An exception occurred while duplicating entites."), e); + } + finally + { + await workProgress.NotifyWorkFinished(cancelled: false, logger.HasErrors); + } + } + [NotNull] public static EntityHierarchyRootViewModel GetRoot([NotNull] EntityViewModel entity) { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs index e94102d271..607b25e100 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs @@ -5,28 +5,27 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using Stride.Core.Assets; +using Stride.Assets.Entities; +using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.EntityFactories; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor; +using Stride.Assets.Presentation.Quantum; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core; +using Stride.Core.Annotations; using Stride.Core.Assets.Analysis; using Stride.Core.Assets.Editor.Components.Properties; using Stride.Core.Assets.Editor.Quantum.NodePresenters; using Stride.Core.Assets.Editor.View.Behaviors; using Stride.Core.Assets.Editor.ViewModel; using Stride.Core.Assets.Quantum; -using Stride.Core; -using Stride.Core.Annotations; using Stride.Core.Diagnostics; using Stride.Core.Extensions; using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Quantum; using Stride.Core.Presentation.Quantum.Presenters; using Stride.Core.Quantum; -using Stride.Assets.Entities; -using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; -using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.EntityFactories; -using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; -using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; -using Stride.Assets.Presentation.Quantum; -using Stride.Assets.Presentation.ViewModel; using Stride.Engine; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels @@ -371,12 +370,32 @@ private void FocusOnEntity() Editor.Controller.GetService().CenterOnEntity(target, meshIndex); } - public EntityViewModel Duplicate() + public EntityViewModel Duplicate(IReadOnlyDictionary transformations = null) { var flags = SubHierarchyCloneFlags.GenerateNewIdsForIdentifiableObjects; var clonedHierarchy = EntityHierarchyPropertyGraph.CloneSubHierarchies(Asset.Session.AssetNodeContainer, Asset.Asset, AssetSideEntity.Id.Yield(), flags, out Dictionary idRemapping); AssetPartsAnalysis.GenerateNewBaseInstanceIds(clonedHierarchy); + if (transformations?.Count > 0) + { + var absIdRemapping = new Dictionary(); + var clonedEntitiesById = clonedHierarchy.RootParts.BreadthFirst(x => x.Transform.Children.Select(y => y.Entity)).ToDictionary(x => x.Id); + foreach (var (srcId, transformData) in transformations) + { + if (transformData is not TransformationTRS transformDataValue) + { + continue; + } + if (idRemapping.TryGetValue(srcId.ObjectId, out var destEntityId) + && clonedEntitiesById.TryGetValue(destEntityId, out var clonedEntity)) + { + clonedEntity.Transform.Position = transformDataValue.Position; + clonedEntity.Transform.Rotation = transformDataValue.Rotation; + clonedEntity.Transform.Scale = transformDataValue.Scale; + } + } + } + var addedRoot = clonedHierarchy.Parts[clonedHierarchy.RootParts.Single().Id]; addedRoot.Folder = (Parent as EntityFolderViewModel)?.Path; diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/RotationGizmo.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/RotationGizmo.cs index 364feae806..84c2e24ce5 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/RotationGizmo.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/RotationGizmo.cs @@ -147,7 +147,7 @@ protected override void UpdateColors() rotationAxes[axis].Get().Model.Materials[0] = isSelected ? ElementSelectedMaterial : axisMaterial; } - overlaySphere.Get().Model.Materials[0] = TransformationStarted ? overlaySphereSelectedMaterial : overlaySphereDefaultMaterial; + overlaySphere.Get().Model.Materials[0] = IsTransformationInProgress ? overlaySphereSelectedMaterial : overlaySphereDefaultMaterial; } /// @@ -223,9 +223,9 @@ private Quaternion GetRotationFromLinearMovement() return Quaternion.RotationAxis(rotationAxis, rotationAngle); } - protected override void OnTransformationFinished() + protected override void OnTransformationFinished(bool wasCanceled) { - base.OnTransformationFinished(); + base.OnTransformationFinished(wasCanceled); UpdateNotSelectedAxisVisibility(false); } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/ScaleGizmo.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/ScaleGizmo.cs index f47e3853b0..e9b09a5d80 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/ScaleGizmo.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/ScaleGizmo.cs @@ -148,9 +148,9 @@ protected override Entity Create() return entity; } - public override async Task Update() + public override void Update() { - await base.Update(); + base.Update(); UpdateDrawOrder(); } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationEndedEventArgs.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationEndedEventArgs.cs new file mode 100644 index 0000000000..4a4cac4ac4 --- /dev/null +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationEndedEventArgs.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; + +namespace Stride.Assets.Presentation.AssetEditors.Gizmos; + +public struct TransformationEndedEventArgs +{ + public static TransformationEndedEventArgs Successful => new() { IsCanceled = false }; + public static TransformationEndedEventArgs Canceled => new() { IsCanceled = true }; + + public bool IsCanceled { get; init; } +} diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs index 119b63f8a6..089797fa06 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TransformationGizmo.cs @@ -43,12 +43,15 @@ public bool IsIdentity() public const RenderGroupMask TransformationGizmoGroupMask = RenderGroupMask.Group4; private bool transformationInitialized; - private bool duplicationDone; + /// + /// Event triggered when a gizmo transformation starts. + /// + public event EventHandler TransformationStarted; /// /// Event triggered when a gizmo transformation finishes. /// - public event EventHandler TransformationEnded; + public event EventHandler TransformationEnded; /// /// Gets the gizmo default scale in ratio of screen height ( 1 => full screen vertically ) @@ -58,7 +61,7 @@ public bool IsIdentity() /// /// Returns whether the transformation has started or not /// - protected bool TransformationStarted { get; private set; } + public bool IsTransformationInProgress { get; private set; } /// /// The default material for the origin elements @@ -89,7 +92,7 @@ public bool IsIdentity() /// The value of the gizmo world matrix at the beginning of the transformation. /// protected Matrix StartWorldMatrix = Matrix.Identity; - + /// /// The projection plane of the transformation. /// @@ -129,7 +132,7 @@ public bool IsIdentity() /// Gets or sets the entity modified by the gizmo. /// public IReadOnlyCollection ModifiedEntities { get; set; } - + protected TransformationGizmo() { RenderGroup = TransformationGizmoGroup; @@ -138,7 +141,7 @@ protected TransformationGizmo() protected override Entity Create() { base.Create(); - + DefaultOriginMaterial = CreateEmissiveColorMaterial(Color.White); ElementSelectedMaterial = CreateEmissiveColorMaterial(Color.Gold); TransparentElementSelectedMaterial = CreateEmissiveColorMaterial(Color.Gold.WithAlpha(86)); @@ -166,7 +169,7 @@ public bool IsUnderMouse() { return TransformationAxes != GizmoTransformationAxes.None; } - + /// /// Gets the world matrix of the gizmo /// @@ -259,23 +262,27 @@ private void UpdateTransformationAxisBase() if (Game.EditorServices.Get().IsMoving) return; - if (duplicationDone) + if (IsTransformationInProgress) return; UpdateTransformationAxis(); } + /// + /// Update which axes are hovered over by the mouse. + /// protected abstract void UpdateTransformationAxis(); protected abstract InitialTransformation CalculateTransformation(); - protected virtual void OnTransformationFinished() + protected virtual void OnTransformationFinished(bool wasCanceled) { - duplicationDone = false; + IsTransformationInProgress = false; if (InitialTransformations.Count > 0) { InitialTransformations.Clear(); - TransformationEnded?.Invoke(this, EventArgs.Empty); + var eventArgs = wasCanceled ? TransformationEndedEventArgs.Canceled : TransformationEndedEventArgs.Successful; + TransformationEnded?.Invoke(this, eventArgs); } } @@ -326,7 +333,7 @@ protected virtual void InitializeTransformation() protected virtual void OnTransformationStarted(Vector2 mouseDragPixel) { - TransformationStarted = true; + IsTransformationInProgress = true; // keep in memory all initial transformation states InitialTransformations.Clear(); @@ -345,25 +352,52 @@ protected virtual void OnTransformationStarted(Vector2 mouseDragPixel) } } + /// + /// Cancel any in-progress transform. + /// + public void CancelTransform() + { + if (!IsTransformationInProgress) + { + return; + } + + // Revert transforms + foreach (var (entity, initTransform) in InitialTransformations) + { + entity.Transform.Scale = initTransform.Scale; + entity.Transform.Position = initTransform.Translation; + entity.Transform.Rotation = initTransform.Rotation; + } + ClearTransformationAxes(); + OnTransformationFinished(wasCanceled: true); + transformationInitialized = false; + } + /// /// Update the transformation of the selected entity. The transformation applied depends on the current TransformationAxes. /// For all types of transformations, we calculate the change between the start click position and the current mouse position instead of working with delta changes. /// This ensures that when the user returns to start click position the transformation is as it was at the beginning of the transformation. /// The transformation direction is either horizontal (left->right) or vertical (bottom->top) and is determined at the beginning of the gesture depending on the user move direction. /// - private async Task TransformSceneEntityBase() + private void TransformSceneEntityBase() { - if (!Input.IsKeyDown(Keys.LeftCtrl) && !Input.IsKeyDown(Keys.RightCtrl)) - duplicationDone = false; - // skip the update if no transformation is currently performed - if (!IsEnabled || AnchorEntity == null || TransformationAxes == GizmoTransformationAxes.None || !Input.IsMouseButtonDown(MouseButton.Left)) + if (!IsEnabled || AnchorEntity == null) + { + return; + } + bool isMouseDown = Input.IsMouseButtonDown(MouseButton.Left); + if (!IsTransformationInProgress && (TransformationAxes == GizmoTransformationAxes.None || !isMouseDown)) + { + return; + } + if (IsTransformationInProgress && !isMouseDown) { if (transformationInitialized) - OnTransformationFinished(); + OnTransformationFinished(wasCanceled: false); transformationInitialized = false; - TransformationStarted = false; return; } @@ -378,7 +412,7 @@ private async Task TransformSceneEntityBase() var mouseDrag = mousePosition - StartMousePosition; // start the transformation only if user has dragged from a given amount of pixel. Determine direction of the transformation. - if (!TransformationStarted) + if (!IsTransformationInProgress) { // ensure that the mouse cursor has been moved enough var screenSize = new Vector2(GraphicsDevice.Presenter.BackBuffer.Width, GraphicsDevice.Presenter.BackBuffer.Height); @@ -393,16 +427,10 @@ private async Task TransformSceneEntityBase() if (currentTransformation.IsIdentity()) return; - // check if Ctrl is pressed and initiate a duplication in this case. - if (ModifiedEntities.Count > 0 && !duplicationDone && Input.IsKeyDown(Keys.LeftCtrl) || Input.IsKeyDown(Keys.RightCtrl)) - { - duplicationDone = true; - await Game.EditorServices.Get().DuplicateSelection(); - } - OnTransformationStarted(mouseDragPixel); + TransformationStarted?.Invoke(this, EventArgs.Empty); } - + // determine the transformation to apply var transformation = CalculateTransformation(); @@ -411,7 +439,7 @@ private async Task TransformSceneEntityBase() { var initialTransfo = InitialTransformations[entity]; var entityTransfo = entity.Transform; - + if (initialTransfo.InverseParentMatrix == Matrix.Zero) { // This usually occurs when at least one axis is scaled to zero (because the matrix inversion @@ -431,7 +459,7 @@ private async Task TransformSceneEntityBase() // calculate the gizmo to parent space matrix Matrix gizmoToParentMatrix; Matrix.Multiply(ref StartWorldMatrix, ref initialTransfo.InverseParentMatrix, out gizmoToParentMatrix); - + // the scale entityTransfo.Scale = initialTransfo.Scale * transformation.Scale; @@ -452,7 +480,7 @@ private async Task TransformSceneEntityBase() } } - public virtual async Task Update() + public virtual void Update() { if (!IsEnabled) return; @@ -460,7 +488,7 @@ public virtual async Task Update() UpdateShape(); UpdateTransformationAxisBase(); UpdateColors(); - await TransformSceneEntityBase(); + TransformSceneEntityBase(); UpdateTransformation(); } @@ -473,8 +501,31 @@ protected virtual void UpdateShape() public void ClearTransformationAxes() { - if (!duplicationDone) - TransformationAxes = GizmoTransformationAxes.None; + TransformationAxes = GizmoTransformationAxes.None; + } + + public void RemapModifyingEntities(Dictionary remapEntities) + { + if (!IsTransformationInProgress) + { + throw new InvalidOperationException($"Transform is not in-progress."); + } + + var newModifyEntities = new List(); + foreach (var (srcEntity, destEntity) in remapEntities) + { + if (InitialTransformations.Remove(srcEntity, out var initTransform)) + { + InitialTransformations[destEntity] = initTransform; + newModifyEntities.Add(destEntity); + } + else + { + throw new InvalidOperationException($"Entity '{srcEntity.Name ?? srcEntity.Id.ToString()}' was not being modified."); + } + } + + ModifiedEntities = newModifyEntities; } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TranslationGizmo.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TranslationGizmo.cs index 8eaa695926..90e4814cba 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TranslationGizmo.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/Gizmos/TranslationGizmo.cs @@ -150,9 +150,9 @@ protected override Entity Create() return entity; } - public override async Task Update() + public override void Update() { - await base.Update(); + base.Update(); UpdateDrawOrder(); }