diff --git a/MatterControlLib/ApplicationView/Config/BedConfig.cs b/MatterControlLib/ApplicationView/Config/BedConfig.cs index ebbe16ab6..1f95c89e7 100644 --- a/MatterControlLib/ApplicationView/Config/BedConfig.cs +++ b/MatterControlLib/ApplicationView/Config/BedConfig.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2018, Lars Brubaker, John Lewin +Copyright (c) 2022, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -203,12 +203,12 @@ namespace MatterHackers.MatterControl } } - public InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd) + public InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd, bool addUndoCheckPoint = true) { - return this.AddToPlate(itemsToAdd, (this.Printer != null) ? this.Printer.Bed.BedCenter : Vector2.Zero, true); + return this.AddToPlate(itemsToAdd, (this.Printer != null) ? this.Printer.Bed.BedCenter : Vector2.Zero, true, addUndoCheckPoint); } - public InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd, Vector2 initialPosition, bool moveToOpenPosition) + public InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd, Vector2 initialPosition, bool moveToOpenPosition, bool addUndoCheckPoint = true) { if (this.Printer != null && this.Printer.ViewState.ViewMode != PartViewMode.Model) @@ -234,13 +234,14 @@ namespace MatterHackers.MatterControl { PlatingHelper.MoveToOpenPositionRelativeGroup(item, itemsToAvoid); } - })); + }, + addUndoCheckPoint: addUndoCheckPoint)); }); return insertionGroup; } - public async void AddToPlate(string[] filesToLoadIncludingZips) + public async void AddToPlate(string[] filesToLoadIncludingZips, bool addUndoCheckPoint = true) { if (filesToLoadIncludingZips?.Any() == true) { @@ -296,7 +297,7 @@ namespace MatterHackers.MatterControl }); var itemCache = new Dictionary(); - this.AddToPlate(filePaths.Select(f => new FileSystemFileItem(f))); + this.AddToPlate(filePaths.Select(f => new FileSystemFileItem(f)), addUndoCheckPoint); } } diff --git a/MatterControlLib/ApplicationView/ISceneContext.cs b/MatterControlLib/ApplicationView/ISceneContext.cs index 7125de53e..21bb8cbc4 100644 --- a/MatterControlLib/ApplicationView/ISceneContext.cs +++ b/MatterControlLib/ApplicationView/ISceneContext.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2019, Lars Brubaker, John Lewin +Copyright (c) 2022, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -62,13 +62,13 @@ namespace MatterHackers.MatterControl event EventHandler SceneLoaded; - InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd); + InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd, bool addUndoCheckPoint = true); - InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd, Vector2 initialPosition, bool moveToOpenPosition); + InsertionGroupObject3D AddToPlate(IEnumerable itemsToAdd, Vector2 initialPosition, bool moveToOpenPosition, bool addUndoCheckPoint = true); List GetBaseViewOptions(); - void AddToPlate(string[] filesToLoadIncludingZips); + void AddToPlate(string[] filesToLoadIncludingZips, bool addUndoCheckPoint = true); void ClearPlate(); diff --git a/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs b/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs index 8be9dbd4e..692bd757a 100644 --- a/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs +++ b/MatterControlLib/DesignTools/Operations/Object3DExtensions.cs @@ -256,7 +256,7 @@ namespace MatterHackers.MatterControl.DesignTools.Operations // ******************************************************************************************************************************* // SHA1 value is based on UTF8 encoded file contents - using (var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(object3D.ToJson()))) + using (var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(object3D.ToJson().Result))) { return HashGenerator.ComputeSHA1(memoryStream); } diff --git a/MatterControlLib/Library/Providers/FileSystem/FileSystemContainer.cs b/MatterControlLib/Library/Providers/FileSystem/FileSystemContainer.cs index 27d9e880e..8a4c5a80d 100644 --- a/MatterControlLib/Library/Providers/FileSystem/FileSystemContainer.cs +++ b/MatterControlLib/Library/Providers/FileSystem/FileSystemContainer.cs @@ -89,7 +89,10 @@ namespace MatterHackers.MatterControl.Library if (fileItem.FilePath.Contains(ApplicationDataStorage.Instance.ApplicationLibraryDataPath)) { // save using the normal uncompressed mcx file - base.Save(item, content); + // Serialize the scene to disk using a modified Json.net pipeline with custom ContractResolvers and JsonConverters + File.WriteAllText(fileItem.FilePath, content.ToJson().Result); + + this.OnItemContentChanged(new LibraryItemChangedEventArgs(fileItem)); } else { @@ -103,6 +106,27 @@ namespace MatterHackers.MatterControl.Library Status = "Saving Asset".Localize() }; + var directory = Path.GetDirectoryName(fileItem.FilePath); + var filename = Path.GetFileNameWithoutExtension(fileItem.FilePath); + var backupName = Path.Combine(directory, Path.ChangeExtension(filename + "_bak", ".mcx")); + + try + { + if (File.Exists(backupName)) + { + File.Delete(backupName); + } + + // rename any existing file + if (File.Exists(fileItem.FilePath)) + { + File.Move(fileItem.FilePath, backupName); + } + } + catch + { + } + // make sure we have all the mesh items in the cache for saving to the archive await content.PersistAssets((percentComplete, text) => { @@ -110,16 +134,6 @@ namespace MatterHackers.MatterControl.Library reporter.Report(status); }, true); - var backupName = ""; - // rename any existing file - if (File.Exists(fileItem.FilePath)) - { - var directory = Path.GetDirectoryName(fileItem.FilePath); - var filename = Path.GetFileNameWithoutExtension(fileItem.FilePath); - backupName = Path.Combine(directory, Path.ChangeExtension(filename + "_bak", ".mcx")); - File.Move(fileItem.FilePath, backupName); - } - var persistableItems = content.GetPersistable(true); var persistCount = persistableItems.Count(); var savedCount = 0; @@ -147,9 +161,13 @@ namespace MatterHackers.MatterControl.Library reporter.Report(status); } - using (var writer = new StreamWriter(zip.CreateEntry("scene.mcx").Open())) + var sceneEntry = zip.CreateEntry("scene.mcx"); + using (var sceneStream = sceneEntry.Open()) { - writer.Write(content.ToJson()); + using (var writer = new StreamWriter(sceneStream)) + { + writer.Write(await content.ToJson()); + } } } } @@ -158,10 +176,14 @@ namespace MatterHackers.MatterControl.Library this.OnItemContentChanged(new LibraryItemChangedEventArgs(fileItem)); // remove the existing file after a successfull save - if (!string.IsNullOrEmpty(backupName)) + try { - File.Delete(backupName); + if (File.Exists(backupName)) + { + File.Delete(backupName); + } } + catch { } }); } } diff --git a/MatterControlLib/Library/Providers/MatterControl/PlatingHistoryContainer.cs b/MatterControlLib/Library/Providers/MatterControl/PlatingHistoryContainer.cs index 4f4be3753..0ee26ce89 100644 --- a/MatterControlLib/Library/Providers/MatterControl/PlatingHistoryContainer.cs +++ b/MatterControlLib/Library/Providers/MatterControl/PlatingHistoryContainer.cs @@ -87,7 +87,7 @@ namespace MatterHackers.MatterControl.Library var filename = ApplicationController.Instance.SanitizeFileName($"{name} - {now}.mcx"); string mcxPath = Path.Combine(this.FullPath, filename); - File.WriteAllText(mcxPath, new Object3D().ToJson()); + File.WriteAllText(mcxPath, new Object3D().ToJson().Result); return new FileSystemFileItem(mcxPath); } diff --git a/MatterControlLib/Library/Providers/MatterControl/PrintQueueContainer.cs b/MatterControlLib/Library/Providers/MatterControl/PrintQueueContainer.cs index 0b7066c60..86c66db9d 100644 --- a/MatterControlLib/Library/Providers/MatterControl/PrintQueueContainer.cs +++ b/MatterControlLib/Library/Providers/MatterControl/PrintQueueContainer.cs @@ -34,6 +34,7 @@ using System.Linq; using System.Threading.Tasks; using MatterHackers.Agg; using MatterHackers.Agg.Image; +using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.DataStorage; using MatterHackers.MatterControl.PrintQueue; @@ -71,6 +72,18 @@ namespace MatterHackers.MatterControl.Library this.ReloadContent(); } + public override void Save(ILibraryItem item, IObject3D content) + { + if (item is FileSystemFileItem fileItem) + { + // save using the normal uncompressed mcx file + // Serialize the scene to disk using a modified Json.net pipeline with custom ContractResolvers and JsonConverters + File.WriteAllText(fileItem.FilePath, content.ToJson().Result); + + this.OnItemContentChanged(new LibraryItemChangedEventArgs(fileItem)); + } + } + public override void SetThumbnail(ILibraryItem item, int width, int height, ImageBuffer imageBuffer) { #if DEBUG diff --git a/MatterControlLib/Library/Providers/Sqlite/SqliteLibraryContainer.cs b/MatterControlLib/Library/Providers/Sqlite/SqliteLibraryContainer.cs index a9b3a645c..b161c0d2c 100644 --- a/MatterControlLib/Library/Providers/Sqlite/SqliteLibraryContainer.cs +++ b/MatterControlLib/Library/Providers/Sqlite/SqliteLibraryContainer.cs @@ -30,6 +30,7 @@ either expressed or implied, of the FreeBSD Project. using MatterHackers.Agg; using MatterHackers.Agg.Image; using MatterHackers.Agg.UI; +using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.DataStorage; using MatterHackers.MatterControl.PrintQueue; @@ -189,6 +190,18 @@ namespace MatterHackers.MatterControl.Library this.ReloadContent(); } + public override void Save(ILibraryItem item, IObject3D content) + { + if (item is FileSystemFileItem fileItem) + { + // save using the normal uncompressed mcx file + // Serialize the scene to disk using a modified Json.net pipeline with custom ContractResolvers and JsonConverters + File.WriteAllText(fileItem.FilePath, content.ToJson().Result); + + this.OnItemContentChanged(new LibraryItemChangedEventArgs(fileItem)); + } + } + public override void SetThumbnail(ILibraryItem item, int width, int height, ImageBuffer imageBuffer) { #if DEBUG diff --git a/MatterControlLib/Library/Providers/WritableContainer.cs b/MatterControlLib/Library/Providers/WritableContainer.cs index deda6a3bb..c5a9502e2 100644 --- a/MatterControlLib/Library/Providers/WritableContainer.cs +++ b/MatterControlLib/Library/Providers/WritableContainer.cs @@ -49,16 +49,7 @@ namespace MatterHackers.MatterControl.Library public abstract void Remove(IEnumerable items); - public virtual void Save(ILibraryItem item, IObject3D content) - { - if (item is FileSystemFileItem fileItem) - { - // Serialize the scene to disk using a modified Json.net pipeline with custom ContractResolvers and JsonConverters - File.WriteAllText(fileItem.FilePath, content.ToJson()); - - this.OnItemContentChanged(new LibraryItemChangedEventArgs(fileItem)); - } - } + public abstract void Save(ILibraryItem item, IObject3D content); public virtual void Move(IEnumerable items, ILibraryWritableContainer sourceContainer) { diff --git a/MatterControlLib/Library/Widgets/InsertionGroupObject3D.cs b/MatterControlLib/Library/Widgets/InsertionGroupObject3D.cs index 886cdc64d..34dc2d27f 100644 --- a/MatterControlLib/Library/Widgets/InsertionGroupObject3D.cs +++ b/MatterControlLib/Library/Widgets/InsertionGroupObject3D.cs @@ -62,7 +62,8 @@ namespace MatterHackers.MatterControl.Library InteractiveScene scene, Vector2 newItemOffset, Action> layoutParts, - bool trackSourceFiles = false) + bool trackSourceFiles = false, + bool addUndoCheckPoint = true) { if (items == null) { @@ -143,7 +144,7 @@ namespace MatterHackers.MatterControl.Library } this.Children.Remove(placeholderItem); - this.Collapse(); + this.Collapse(addUndoCheckPoint); this.Invalidate(InvalidateType.Children); }); @@ -152,7 +153,7 @@ namespace MatterHackers.MatterControl.Library /// /// Collapse the InsertionGroup into the scene /// - public void Collapse() + private void Collapse(bool addUndoCheckPoint) { // Drag operation has finished, we need to perform the collapse var loadedItems = this.Children; @@ -193,7 +194,14 @@ namespace MatterHackers.MatterControl.Library } } - view3DWidget.Scene.UndoBuffer.AddAndDo(new InsertCommand(view3DWidget.Scene, loadedItems)); + if (addUndoCheckPoint) + { + view3DWidget.Scene.UndoBuffer.AddAndDo(new InsertCommand(view3DWidget.Scene, loadedItems)); + } + else + { + new InsertCommand(view3DWidget.Scene, loadedItems).Do(); + } } } } diff --git a/MatterControlLib/PartPreviewWindow/SaveAsPage.cs b/MatterControlLib/PartPreviewWindow/SaveAsPage.cs index b4738e009..183617ef8 100644 --- a/MatterControlLib/PartPreviewWindow/SaveAsPage.cs +++ b/MatterControlLib/PartPreviewWindow/SaveAsPage.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2017, Lars Brubaker, John Lewin +Copyright (c) 2022, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -67,13 +67,22 @@ namespace MatterHackers.MatterControl }; itemNameWidget.ActualTextEditWidget.EnterPressed += (s, e) => { - if (librarySelectorWidget.ActiveContainer is ILibraryWritableContainer) + if (this.acceptButton.Enabled) { - acceptButton.InvokeClick(); - // And disable it so there are not multiple fires. No need to re-enable, the dialog is going to close. - this.AcceptButton.Enabled = false; + if (librarySelectorWidget.ActiveContainer is ILibraryWritableContainer) + { + acceptButton.InvokeClick(); + // And disable it so there are not multiple fires. No need to re-enable, the dialog is going to close. + this.AcceptButton.Enabled = false; + } } }; + itemNameWidget.ActualTextEditWidget.TextChanged += (s, e) => + { + acceptButton.Enabled = libraryNavContext.ActiveContainer is ILibraryWritableContainer + && !string.IsNullOrWhiteSpace(itemNameWidget.ActualTextEditWidget.Text); + }; + contentRow.AddChild(itemNameWidget); var icon = StaticData.Instance.LoadIcon("fa-folder-new_16.png", 16, 16).SetToColor(ApplicationController.Instance.MenuTheme.TextColor); diff --git a/MatterControlLib/PartPreviewWindow/Tabs.cs b/MatterControlLib/PartPreviewWindow/Tabs.cs index d46266593..863d38faa 100644 --- a/MatterControlLib/PartPreviewWindow/Tabs.cs +++ b/MatterControlLib/PartPreviewWindow/Tabs.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2018, Lars Brubaker, John Lewin +Copyright (c) 2022, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -38,6 +38,7 @@ using MatterHackers.Agg.VertexSource; using MatterHackers.DataConverters3D; using MatterHackers.ImageProcessing; using MatterHackers.Localizations; +using MatterHackers.MatterControl.Library; using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.PartPreviewWindow @@ -463,8 +464,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow "Continue Printing".Localize()); } else if (this.TabContent is DesignTabPage partTab - && partTab?.Workspace?.SceneContext?.Scene is InteractiveScene sceneContext - && sceneContext.HasUnsavedChanges) + && partTab?.Workspace?.SceneContext?.Scene is InteractiveScene scene + && scene.HasUnsavedChanges) { StyledMessageBox.ShowYNCMessageBox( (response) => @@ -474,12 +475,30 @@ namespace MatterHackers.MatterControl.PartPreviewWindow case StyledMessageBox.ResponseType.YES: UiThread.RunOnIdle(async () => { - await ApplicationController.Instance.Tasks.Execute("Saving Changes".Localize(), this, partTab.Workspace.SceneContext.SaveChanges); + var sceneContext = partTab.Workspace.SceneContext; + if (sceneContext.EditContext.ContentStore == null) + { + // If we are about to close a tab that has never been saved it will need a name before it can actually save + // Open up the save as dialog rather than continue with saving and closing + DialogWindow.Show( + new SaveAsPage( + (container, newName) => + { + sceneContext.SaveAs(container, newName); + // If we succeed at saveing the file go ahead and finish closing this tab + this.CloseClicked?.Invoke(this, null); + // Must be called after CloseClicked otherwise listeners are cleared before event is invoked + this.parentTabControl.CloseTab(this); + })); + } + else + { + await ApplicationController.Instance.Tasks.Execute("Saving Changes".Localize(), this, partTab.Workspace.SceneContext.SaveChanges); - - this.CloseClicked?.Invoke(this, null); - // Must be called after CloseClicked otherwise listeners are cleared before event is invoked - this.parentTabControl.CloseTab(this); + this.CloseClicked?.Invoke(this, null); + // Must be called after CloseClicked otherwise listeners are cleared before event is invoked + this.parentTabControl.CloseTab(this); + } }); break; diff --git a/MatterControlLib/PartPreviewWindow/View3D/SceneActions.cs b/MatterControlLib/PartPreviewWindow/View3D/SceneActions.cs index 1039dbc72..3347115f1 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/SceneActions.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/SceneActions.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2017, Lars Brubaker, John Lewin +Copyright (c) 2022, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -27,11 +27,6 @@ of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using MatterHackers.Agg; using MatterHackers.Agg.Image; using MatterHackers.Agg.Platform; @@ -44,523 +39,527 @@ using MatterHackers.MatterControl.DesignTools; using MatterHackers.MatterControl.DesignTools.Operations; using MatterHackers.PolygonMesh; using MatterHackers.VectorMath; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace MatterHackers.MatterControl.PartPreviewWindow { - public static class SceneActions - { - private static int pasteObjectXOffset = 5; - - public static async void UngroupSelection(this InteractiveScene scene) - { - var selectedItem = scene.SelectedItem; - if (selectedItem != null) - { - if (selectedItem.CanApply) - { - selectedItem.Apply(scene.UndoBuffer); - scene.SelectedItem = null; - return; - } - - bool isGroupItemType = selectedItem.Children.Count > 0; - - // If not a Group ItemType, look for mesh volumes and split into distinct objects if found - if (isGroupItemType) - { - // Create and perform the delete operation - // Store the operation for undo/redo - scene.UndoBuffer.AddAndDo(new UngroupCommand(scene, selectedItem)); - } - else if (!selectedItem.HasChildren() - && selectedItem.Mesh != null) - { - await ApplicationController.Instance.Tasks.Execute( - "Ungroup".Localize(), - null, - (reporter, cancellationTokenSource) => - { - var progressStatus = new ProgressStatus(); - reporter.Report(progressStatus); - // clear the selection - scene.SelectedItem = null; - progressStatus.Status = "Copy".Localize(); - reporter.Report(progressStatus); - - // try to cut it up into multiple meshes - progressStatus.Status = "Split".Localize(); - - var cleanMesh = selectedItem.Mesh.Copy(cancellationTokenSource.Token); - cleanMesh.MergeVertices(.01); - - var discreetMeshes = CreateDiscreteMeshes.SplitVolumesIntoMeshes(cleanMesh, cancellationTokenSource.Token, (double progress0To1, string processingState) => - { - progressStatus.Progress0To1 = .5 + progress0To1 * .5; - progressStatus.Status = processingState; - reporter.Report(progressStatus); - }); - if (cancellationTokenSource.IsCancellationRequested) - { - return Task.CompletedTask; - } - - if (discreetMeshes.Count == 1) - { - // restore the selection - scene.SelectedItem = selectedItem; - // No further processing needed, nothing to ungroup - return Task.CompletedTask; - } - - // build the ungroup list - var addItems = new List(discreetMeshes.Select(mesh => new Object3D() - { - Mesh = mesh, - })); - - foreach (var item in addItems) - { - item.CopyProperties(selectedItem, Object3DPropertyFlags.All); - item.Visible = true; - } - - // add and do the undo data - scene.UndoBuffer.AddAndDo(new ReplaceCommand(new[] { selectedItem }, addItems)); - - foreach (var item in addItems) - { - item.MakeNameNonColliding(); - } - - return Task.CompletedTask; - }); - } - - // leave no selection - scene.SelectedItem = null; - } - } - - public static async Task AutoArrangeChildren(this InteractiveScene scene, Vector3 bedCenter) - { - await Task.Run(() => - { - // Clear selection to ensure all root level children are arranged on the bed - scene.SelectedItem = null; - - var children = scene.Children.ToList().Where(item => - { - var aabb = item.WorldAxisAlignedBoundingBox(); - if (aabb.Center.Length > 1000) - { - return true; - } - - return item.Persistable == true && item.Printable == true; - }).ToList(); - var transformData = new List(); - foreach (var child in children) - { - transformData.Add(new TransformData() { TransformedObject = child, UndoTransform = child.Matrix }); - } - - PlatingHelper.ArrangeOnBed(children, bedCenter); - int i = 0; - foreach (var child in children) - { - transformData[i].RedoTransform = child.Matrix; - i++; - } - - scene.UndoBuffer.Add(new TransformCommand(transformData)); - }); - } - - public static void Cut(this InteractiveScene scene, IObject3D sourceItem = null) - { - var selectedItem = scene.SelectedItem; - if (selectedItem != null) - { - Clipboard.Instance.SetText("!--IObjectSelection--!"); - ApplicationController.ClipboardItem = selectedItem.Clone(); - // put it back in right where we cut it from - pasteObjectXOffset = 0; - - scene.DeleteSelection(); - } - } - - public static void Copy(this InteractiveScene scene, IObject3D sourceItem = null) - { - var selectedItem = scene.SelectedItem; - if (selectedItem != null) - { - Clipboard.Instance.SetText("!--IObjectSelection--!"); - ApplicationController.ClipboardItem = selectedItem.Clone(); - // when we copy an object put it back in with a slight offset - pasteObjectXOffset = 5; - } - } - - public static void AddPhilToBed(this ISceneContext sceneContext) - { - var philStl = StaticData.Instance.MapPath(@"OEMSettings\SampleParts\Phil A Ment.stl"); - sceneContext.AddToPlate(new string[] { philStl }); - } - - public static void Paste(this ISceneContext sceneContext) - { - var scene = sceneContext.Scene; - - if (Clipboard.Instance.ContainsImage) - { - // Persist - string filePath = ApplicationDataStorage.Instance.GetNewLibraryFilePath(".png"); - ImageIO.SaveImageData( - filePath, - Clipboard.Instance.GetImage()); - - scene.UndoBuffer.AddAndDo( - new InsertCommand( - scene, - new ImageObject3D() - { - AssetPath = filePath - })); - } - else if (Clipboard.Instance.ContainsText) - { - if (Clipboard.Instance.GetText() == "!--IObjectSelection--!") - { - sceneContext.DuplicateItem(0, ApplicationController.ClipboardItem); - // each time we put in the object offset it a bit more - pasteObjectXOffset += 5; - } - } - } - - public static async void DuplicateItem(this ISceneContext sceneContext, double xOffset, IObject3D sourceItem = null) - { - var scene = sceneContext.Scene; - if (sourceItem == null) - { - var selectedItem = scene.SelectedItem; - if (selectedItem != null) - { - sourceItem = selectedItem; - } - } - - if (sourceItem != null) - { - // Copy selected item - IObject3D newItem = await Task.Run(() => - { - var namedItems = new HashSet(scene.DescendantsAndSelf().Select((d) => d.Name)); - if (sourceItem != null) - { - if (sourceItem is SelectionGroupObject3D) - { - // the selection is a group of objects that need to be copied - var copyList = sourceItem.Children.ToList(); - scene.SelectedItem = null; - foreach (var item in copyList) - { - var clonedItem = item.Clone(); - clonedItem.Translate(xOffset); - // make the name unique - var newName = agg_basics.GetNonCollidingName(item.Name, namedItems); - clonedItem.Name = newName; - // add it to the scene - scene.Children.Add(clonedItem); - // add it to the selection - scene.AddToSelection(clonedItem); - } - } - else // the selection can be cloned easily - { - var clonedItem = sourceItem.Clone(); - - clonedItem.Translate(xOffset); - // an empty string is used do denote special name processing for some container types - if (!string.IsNullOrWhiteSpace(sourceItem.Name)) - { - // make the name unique - var newName = agg_basics.GetNonCollidingName(sourceItem.Name, namedItems); - clonedItem.Name = newName; - } - - // More useful if it creates the part in the exact position and then the user can move it. - // Consistent with other software as well. LBB 2017-12-02 - // PlatingHelper.MoveToOpenPositionRelativeGroup(clonedItem, Scene.Children); - - return clonedItem; - } - } - - return null; - }); - - // it might come back null due to threading - if (newItem != null) - { - sceneContext.InsertNewItem(newItem); - } - } - } - - public static void InsertNewItem(this ISceneContext sceneContext, IObject3D newItem) - { - var scene = sceneContext.Scene; - - // Reposition first item to bed center - if (scene.Children.Count == 0) - { - var aabb = newItem.GetAxisAlignedBoundingBox(); - var center = aabb.Center; - - newItem.Matrix *= Matrix4X4.CreateTranslation( - sceneContext.BedCenter.X - center.X, - sceneContext.BedCenter.Y - center.Y, - -aabb.MinXYZ.Z); - } - - // Create and perform a new insert operation - var insertOperation = new InsertCommand(scene, newItem); - insertOperation.Do(); - - // Store the operation for undo/redo - scene.UndoBuffer.Add(insertOperation); - } - - public static void DeleteSelection(this InteractiveScene scene) - { - var selectedItem = scene.SelectedItem; - if (selectedItem != null) - { - // Create and perform the delete operation - var deleteOperation = new DeleteCommand(scene, selectedItem); - - // Store the operation for undo/redo - scene.UndoBuffer.AddAndDo(deleteOperation); - - scene.ClearSelection(); - - scene.Invalidate(new InvalidateArgs(null, InvalidateType.Children)); - } - } - - public static void MakeLowestFaceFlat(this InteractiveScene scene, IObject3D objectToLayFlatGroup) - { - var preLayFlatMatrix = objectToLayFlatGroup.Matrix; - - bool firstVertex = true; - - IObject3D objectToLayFlat = objectToLayFlatGroup; - - Vector3Float lowestPosition = Vector3Float.PositiveInfinity; - Vector3Float sourceVertexPosition = Vector3Float.NegativeInfinity; - IObject3D itemToLayFlat = null; - Mesh meshWithLowest = null; - - var items = objectToLayFlat.VisibleMeshes().Where(i => i.OutputType != PrintOutputTypes.Support); - if (!items.Any()) - { - items = objectToLayFlat.VisibleMeshes(); - } - - // Process each child, checking for the lowest vertex - foreach (var itemToCheck in items) - { - var meshToCheck = itemToCheck.Mesh.GetConvexHull(false); - - if (meshToCheck == null - && meshToCheck.Vertices.Count < 3) - { - continue; - } - - var maxArea = 0.0; - // find the lowest point on the model - for (int testFace = 0; testFace < meshToCheck.Faces.Count; testFace++) - { - var face = meshToCheck.Faces[testFace]; - var vertex = meshToCheck.Vertices[face.v0]; - var vertexPosition = vertex.Transform(itemToCheck.WorldMatrix()); - if (firstVertex) - { - meshWithLowest = meshToCheck; - lowestPosition = vertexPosition; - sourceVertexPosition = vertex; - itemToLayFlat = itemToCheck; - firstVertex = false; - } - else if (vertexPosition.Z < lowestPosition.Z) - { - if (Math.Abs(vertexPosition.Z - lowestPosition.Z) < .001) - { - // check if this face has a bigger area than the other face that is also this low - var faceArea = face.GetArea(meshToCheck); - if (faceArea > maxArea) - { - maxArea = faceArea; - meshWithLowest = meshToCheck; - lowestPosition = vertexPosition; - sourceVertexPosition = vertex; - itemToLayFlat = itemToCheck; - } - } - else - { - // reset the max area - maxArea = face.GetArea(meshToCheck); - meshWithLowest = meshToCheck; - lowestPosition = vertexPosition; - sourceVertexPosition = vertex; - itemToLayFlat = itemToCheck; - } - } - } - } - - if (meshWithLowest == null) - { - // didn't find any selected mesh - return; - } - - int faceToLayFlat = -1; - double largestAreaOfAnyFace = 0; - var facesSharingLowestVertex = meshWithLowest.Faces - .Select((face, i) => new { face, i }) - .Where(faceAndIndex => meshWithLowest.Vertices[faceAndIndex.face.v0] == sourceVertexPosition - || meshWithLowest.Vertices[faceAndIndex.face.v1] == sourceVertexPosition - || meshWithLowest.Vertices[faceAndIndex.face.v2] == sourceVertexPosition) - .Select(j => j.i); - - var lowestFacesByAngle = facesSharingLowestVertex.OrderBy(i => - { - var face = meshWithLowest.Faces[i]; - var worldNormal = face.normal.TransformNormal(itemToLayFlat.WorldMatrix()); - return worldNormal.CalculateAngle(-Vector3Float.UnitZ); - }); - - // Check all the faces that are connected to the lowest point to find out which one to lay flat. - foreach (var faceIndex in lowestFacesByAngle) - { - var face = meshWithLowest.Faces[faceIndex]; - - var worldNormal = face.normal.TransformNormal(itemToLayFlat.WorldMatrix()); - var worldAngleDegrees = MathHelper.RadiansToDegrees(worldNormal.CalculateAngle(-Vector3Float.UnitZ)); - - double largestAreaFound = 0; - var faceVeretexIndices = new int[] { face.v0, face.v1, face.v2 }; - - foreach (var vi in faceVeretexIndices) - { - if (meshWithLowest.Vertices[vi] != lowestPosition) - { - var planSurfaceArea = 0.0; - foreach (var coPlanarFace in meshWithLowest.GetCoplanarFaces(faceIndex)) - { - planSurfaceArea += meshWithLowest.GetSurfaceArea(coPlanarFace); - } - - if (largestAreaOfAnyFace == 0 - || (planSurfaceArea > largestAreaFound - && worldAngleDegrees < 45)) - { - largestAreaFound = planSurfaceArea; - } - } - } - - if (largestAreaFound > largestAreaOfAnyFace) - { - largestAreaOfAnyFace = largestAreaFound; - faceToLayFlat = faceIndex; - } - } - - double maxDistFromLowestZ = 0; - var lowestFace = meshWithLowest.Faces[faceToLayFlat]; - var lowestFaceIndices = new int[] { lowestFace.v0, lowestFace.v1, lowestFace.v2 }; - var faceVertices = new List(); - foreach (var vertex in lowestFaceIndices) - { - var vertexPosition = meshWithLowest.Vertices[vertex].Transform(itemToLayFlat.WorldMatrix()); - faceVertices.Add(vertexPosition); - maxDistFromLowestZ = Math.Max(maxDistFromLowestZ, vertexPosition.Z - lowestPosition.Z); - } - - if (maxDistFromLowestZ > .001) - { - var xPositive = (faceVertices[1] - faceVertices[0]).GetNormal(); - var yPositive = (faceVertices[2] - faceVertices[0]).GetNormal(); - var planeNormal = xPositive.Cross(yPositive).GetNormal(); - - // this code takes the minimum rotation required and looks much better. - var rotation = new Quaternion(planeNormal, new Vector3Float(0, 0, -1)); - var partLevelMatrix = Matrix4X4.CreateRotation(rotation); - - // rotate it - objectToLayFlat.Matrix = objectToLayFlatGroup.ApplyAtBoundsCenter(partLevelMatrix); - } - - if (objectToLayFlatGroup is Object3D object3D) - { - AxisAlignedBoundingBox bounds = object3D.GetAxisAlignedBoundingBox(Matrix4X4.Identity, (item) => - { - return item.OutputType != PrintOutputTypes.Support; - }); - Vector3 boundsCenter = (bounds.MaxXYZ + bounds.MinXYZ) / 2; - - object3D.Matrix *= Matrix4X4.CreateTranslation(new Vector3(0, 0, -boundsCenter.Z + bounds.ZSize / 2)); - } - else - { - PlatingHelper.PlaceOnBed(objectToLayFlatGroup); - } - - scene.UndoBuffer.Add(new TransformCommand(objectToLayFlatGroup, preLayFlatMatrix, objectToLayFlatGroup.Matrix)); - } - - public static void AddTransformSnapshot(this InteractiveScene scene, Matrix4X4 originalTransform) - { - var selectedItem = scene.SelectedItem; - if (selectedItem != null && selectedItem.Matrix != originalTransform) - { - scene.UndoBuffer.Add(new TransformCommand(selectedItem, originalTransform, selectedItem.Matrix)); - } - } - - internal class ArrangeUndoCommand : IUndoRedoCommand - { - private List allUndoTransforms = new List(); - - public ArrangeUndoCommand(View3DWidget view3DWidget, List preArrangeTarnsforms, List postArrangeTarnsforms) - { - for (int i = 0; i < preArrangeTarnsforms.Count; i++) - { - //a llUndoTransforms.Add(new TransformUndoCommand(view3DWidget, i, preArrangeTarnsforms[i], postArrangeTarnsforms[i])); - } - } - - public void Do() - { - for (int i = 0; i < allUndoTransforms.Count; i++) - { - allUndoTransforms[i].Do(); - } - } - - public void Undo() - { - for (int i = 0; i < allUndoTransforms.Count; i++) - { - allUndoTransforms[i].Undo(); - } - } - } - } -} + public static class SceneActions + { + private static int pasteObjectXOffset = 5; + + public static void AddPhilToBed(this ISceneContext sceneContext) + { + var philStl = StaticData.Instance.MapPath(@"OEMSettings\SampleParts\Phil A Ment.stl"); + sceneContext.AddToPlate(new string[] { philStl }, false); + } + + public static void AddTransformSnapshot(this InteractiveScene scene, Matrix4X4 originalTransform) + { + var selectedItem = scene.SelectedItem; + if (selectedItem != null && selectedItem.Matrix != originalTransform) + { + scene.UndoBuffer.Add(new TransformCommand(selectedItem, originalTransform, selectedItem.Matrix)); + } + } + + public static async Task AutoArrangeChildren(this InteractiveScene scene, Vector3 bedCenter) + { + await Task.Run(() => + { + // Clear selection to ensure all root level children are arranged on the bed + scene.SelectedItem = null; + + var children = scene.Children.ToList().Where(item => + { + var aabb = item.WorldAxisAlignedBoundingBox(); + if (aabb.Center.Length > 1000) + { + return true; + } + + return item.Persistable == true && item.Printable == true; + }).ToList(); + var transformData = new List(); + foreach (var child in children) + { + transformData.Add(new TransformData() { TransformedObject = child, UndoTransform = child.Matrix }); + } + + PlatingHelper.ArrangeOnBed(children, bedCenter); + int i = 0; + foreach (var child in children) + { + transformData[i].RedoTransform = child.Matrix; + i++; + } + + scene.UndoBuffer.Add(new TransformCommand(transformData)); + }); + } + + public static void Copy(this InteractiveScene scene, IObject3D sourceItem = null) + { + var selectedItem = scene.SelectedItem; + if (selectedItem != null) + { + Clipboard.Instance.SetText("!--IObjectSelection--!"); + ApplicationController.ClipboardItem = selectedItem.Clone(); + // when we copy an object put it back in with a slight offset + pasteObjectXOffset = 5; + } + } + + public static void Cut(this InteractiveScene scene, IObject3D sourceItem = null) + { + var selectedItem = scene.SelectedItem; + if (selectedItem != null) + { + Clipboard.Instance.SetText("!--IObjectSelection--!"); + ApplicationController.ClipboardItem = selectedItem.Clone(); + // put it back in right where we cut it from + pasteObjectXOffset = 0; + + scene.DeleteSelection(); + } + } + + public static void DeleteSelection(this InteractiveScene scene) + { + var selectedItem = scene.SelectedItem; + if (selectedItem != null) + { + // Create and perform the delete operation + var deleteOperation = new DeleteCommand(scene, selectedItem); + + // Store the operation for undo/redo + scene.UndoBuffer.AddAndDo(deleteOperation); + + scene.ClearSelection(); + + scene.Invalidate(new InvalidateArgs(null, InvalidateType.Children)); + } + } + + public static async void DuplicateItem(this ISceneContext sceneContext, double xOffset, IObject3D sourceItem = null) + { + var scene = sceneContext.Scene; + if (sourceItem == null) + { + var selectedItem = scene.SelectedItem; + if (selectedItem != null) + { + sourceItem = selectedItem; + } + } + + if (sourceItem != null) + { + // Copy selected item + IObject3D newItem = await Task.Run(() => + { + var namedItems = new HashSet(scene.DescendantsAndSelf().Select((d) => d.Name)); + if (sourceItem != null) + { + if (sourceItem is SelectionGroupObject3D) + { + // the selection is a group of objects that need to be copied + var copyList = sourceItem.Children.ToList(); + scene.SelectedItem = null; + foreach (var item in copyList) + { + var clonedItem = item.Clone(); + clonedItem.Translate(xOffset); + // make the name unique + var newName = agg_basics.GetNonCollidingName(item.Name, namedItems); + clonedItem.Name = newName; + // add it to the scene + scene.Children.Add(clonedItem); + // add it to the selection + scene.AddToSelection(clonedItem); + } + } + else // the selection can be cloned easily + { + var clonedItem = sourceItem.Clone(); + + clonedItem.Translate(xOffset); + // an empty string is used do denote special name processing for some container types + if (!string.IsNullOrWhiteSpace(sourceItem.Name)) + { + // make the name unique + var newName = agg_basics.GetNonCollidingName(sourceItem.Name, namedItems); + clonedItem.Name = newName; + } + + // More useful if it creates the part in the exact position and then the user can move it. + // Consistent with other software as well. LBB 2017-12-02 + // PlatingHelper.MoveToOpenPositionRelativeGroup(clonedItem, Scene.Children); + + return clonedItem; + } + } + + return null; + }); + + // it might come back null due to threading + if (newItem != null) + { + sceneContext.InsertNewItem(newItem); + } + } + } + + public static void InsertNewItem(this ISceneContext sceneContext, IObject3D newItem) + { + var scene = sceneContext.Scene; + + // Reposition first item to bed center + if (scene.Children.Count == 0) + { + var aabb = newItem.GetAxisAlignedBoundingBox(); + var center = aabb.Center; + + newItem.Matrix *= Matrix4X4.CreateTranslation( + sceneContext.BedCenter.X - center.X, + sceneContext.BedCenter.Y - center.Y, + -aabb.MinXYZ.Z); + } + + // Create and perform a new insert operation + var insertOperation = new InsertCommand(scene, newItem); + insertOperation.Do(); + + // Store the operation for undo/redo + scene.UndoBuffer.Add(insertOperation); + } + + public static void MakeLowestFaceFlat(this InteractiveScene scene, IObject3D objectToLayFlatGroup) + { + var preLayFlatMatrix = objectToLayFlatGroup.Matrix; + + bool firstVertex = true; + + IObject3D objectToLayFlat = objectToLayFlatGroup; + + Vector3Float lowestPosition = Vector3Float.PositiveInfinity; + Vector3Float sourceVertexPosition = Vector3Float.NegativeInfinity; + IObject3D itemToLayFlat = null; + Mesh meshWithLowest = null; + + var items = objectToLayFlat.VisibleMeshes().Where(i => i.OutputType != PrintOutputTypes.Support); + if (!items.Any()) + { + items = objectToLayFlat.VisibleMeshes(); + } + + // Process each child, checking for the lowest vertex + foreach (var itemToCheck in items) + { + var meshToCheck = itemToCheck.Mesh.GetConvexHull(false); + + if (meshToCheck == null + && meshToCheck.Vertices.Count < 3) + { + continue; + } + + var maxArea = 0.0; + // find the lowest point on the model + for (int testFace = 0; testFace < meshToCheck.Faces.Count; testFace++) + { + var face = meshToCheck.Faces[testFace]; + var vertex = meshToCheck.Vertices[face.v0]; + var vertexPosition = vertex.Transform(itemToCheck.WorldMatrix()); + if (firstVertex) + { + meshWithLowest = meshToCheck; + lowestPosition = vertexPosition; + sourceVertexPosition = vertex; + itemToLayFlat = itemToCheck; + firstVertex = false; + } + else if (vertexPosition.Z < lowestPosition.Z) + { + if (Math.Abs(vertexPosition.Z - lowestPosition.Z) < .001) + { + // check if this face has a bigger area than the other face that is also this low + var faceArea = face.GetArea(meshToCheck); + if (faceArea > maxArea) + { + maxArea = faceArea; + meshWithLowest = meshToCheck; + lowestPosition = vertexPosition; + sourceVertexPosition = vertex; + itemToLayFlat = itemToCheck; + } + } + else + { + // reset the max area + maxArea = face.GetArea(meshToCheck); + meshWithLowest = meshToCheck; + lowestPosition = vertexPosition; + sourceVertexPosition = vertex; + itemToLayFlat = itemToCheck; + } + } + } + } + + if (meshWithLowest == null) + { + // didn't find any selected mesh + return; + } + + int faceToLayFlat = -1; + double largestAreaOfAnyFace = 0; + var facesSharingLowestVertex = meshWithLowest.Faces + .Select((face, i) => new { face, i }) + .Where(faceAndIndex => meshWithLowest.Vertices[faceAndIndex.face.v0] == sourceVertexPosition + || meshWithLowest.Vertices[faceAndIndex.face.v1] == sourceVertexPosition + || meshWithLowest.Vertices[faceAndIndex.face.v2] == sourceVertexPosition) + .Select(j => j.i); + + var lowestFacesByAngle = facesSharingLowestVertex.OrderBy(i => + { + var face = meshWithLowest.Faces[i]; + var worldNormal = face.normal.TransformNormal(itemToLayFlat.WorldMatrix()); + return worldNormal.CalculateAngle(-Vector3Float.UnitZ); + }); + + // Check all the faces that are connected to the lowest point to find out which one to lay flat. + foreach (var faceIndex in lowestFacesByAngle) + { + var face = meshWithLowest.Faces[faceIndex]; + + var worldNormal = face.normal.TransformNormal(itemToLayFlat.WorldMatrix()); + var worldAngleDegrees = MathHelper.RadiansToDegrees(worldNormal.CalculateAngle(-Vector3Float.UnitZ)); + + double largestAreaFound = 0; + var faceVeretexIndices = new int[] { face.v0, face.v1, face.v2 }; + + foreach (var vi in faceVeretexIndices) + { + if (meshWithLowest.Vertices[vi] != lowestPosition) + { + var planSurfaceArea = 0.0; + foreach (var coPlanarFace in meshWithLowest.GetCoplanarFaces(faceIndex)) + { + planSurfaceArea += meshWithLowest.GetSurfaceArea(coPlanarFace); + } + + if (largestAreaOfAnyFace == 0 + || (planSurfaceArea > largestAreaFound + && worldAngleDegrees < 45)) + { + largestAreaFound = planSurfaceArea; + } + } + } + + if (largestAreaFound > largestAreaOfAnyFace) + { + largestAreaOfAnyFace = largestAreaFound; + faceToLayFlat = faceIndex; + } + } + + double maxDistFromLowestZ = 0; + var lowestFace = meshWithLowest.Faces[faceToLayFlat]; + var lowestFaceIndices = new int[] { lowestFace.v0, lowestFace.v1, lowestFace.v2 }; + var faceVertices = new List(); + foreach (var vertex in lowestFaceIndices) + { + var vertexPosition = meshWithLowest.Vertices[vertex].Transform(itemToLayFlat.WorldMatrix()); + faceVertices.Add(vertexPosition); + maxDistFromLowestZ = Math.Max(maxDistFromLowestZ, vertexPosition.Z - lowestPosition.Z); + } + + if (maxDistFromLowestZ > .001) + { + var xPositive = (faceVertices[1] - faceVertices[0]).GetNormal(); + var yPositive = (faceVertices[2] - faceVertices[0]).GetNormal(); + var planeNormal = xPositive.Cross(yPositive).GetNormal(); + + // this code takes the minimum rotation required and looks much better. + var rotation = new Quaternion(planeNormal, new Vector3Float(0, 0, -1)); + var partLevelMatrix = Matrix4X4.CreateRotation(rotation); + + // rotate it + objectToLayFlat.Matrix = objectToLayFlatGroup.ApplyAtBoundsCenter(partLevelMatrix); + } + + if (objectToLayFlatGroup is Object3D object3D) + { + AxisAlignedBoundingBox bounds = object3D.GetAxisAlignedBoundingBox(Matrix4X4.Identity, (item) => + { + return item.OutputType != PrintOutputTypes.Support; + }); + Vector3 boundsCenter = (bounds.MaxXYZ + bounds.MinXYZ) / 2; + + object3D.Matrix *= Matrix4X4.CreateTranslation(new Vector3(0, 0, -boundsCenter.Z + bounds.ZSize / 2)); + } + else + { + PlatingHelper.PlaceOnBed(objectToLayFlatGroup); + } + + scene.UndoBuffer.Add(new TransformCommand(objectToLayFlatGroup, preLayFlatMatrix, objectToLayFlatGroup.Matrix)); + } + + public static void Paste(this ISceneContext sceneContext) + { + var scene = sceneContext.Scene; + + if (Clipboard.Instance.ContainsImage) + { + // Persist + string filePath = ApplicationDataStorage.Instance.GetNewLibraryFilePath(".png"); + ImageIO.SaveImageData( + filePath, + Clipboard.Instance.GetImage()); + + scene.UndoBuffer.AddAndDo( + new InsertCommand( + scene, + new ImageObject3D() + { + AssetPath = filePath + })); + } + else if (Clipboard.Instance.ContainsText) + { + if (Clipboard.Instance.GetText() == "!--IObjectSelection--!") + { + sceneContext.DuplicateItem(0, ApplicationController.ClipboardItem); + // each time we put in the object offset it a bit more + pasteObjectXOffset += 5; + } + } + } + + public static async void UngroupSelection(this InteractiveScene scene) + { + var selectedItem = scene.SelectedItem; + if (selectedItem != null) + { + if (selectedItem.CanApply) + { + selectedItem.Apply(scene.UndoBuffer); + scene.SelectedItem = null; + return; + } + + bool isGroupItemType = selectedItem.Children.Count > 0; + + // If not a Group ItemType, look for mesh volumes and split into distinct objects if found + if (isGroupItemType) + { + // Create and perform the delete operation + // Store the operation for undo/redo + scene.UndoBuffer.AddAndDo(new UngroupCommand(scene, selectedItem)); + } + else if (!selectedItem.HasChildren() + && selectedItem.Mesh != null) + { + await ApplicationController.Instance.Tasks.Execute( + "Ungroup".Localize(), + null, + (reporter, cancellationTokenSource) => + { + var progressStatus = new ProgressStatus(); + reporter.Report(progressStatus); + // clear the selection + scene.SelectedItem = null; + progressStatus.Status = "Copy".Localize(); + reporter.Report(progressStatus); + + // try to cut it up into multiple meshes + progressStatus.Status = "Split".Localize(); + + var cleanMesh = selectedItem.Mesh.Copy(cancellationTokenSource.Token); + cleanMesh.MergeVertices(.01); + + var discreetMeshes = CreateDiscreteMeshes.SplitVolumesIntoMeshes(cleanMesh, cancellationTokenSource.Token, (double progress0To1, string processingState) => + { + progressStatus.Progress0To1 = .5 + progress0To1 * .5; + progressStatus.Status = processingState; + reporter.Report(progressStatus); + }); + if (cancellationTokenSource.IsCancellationRequested) + { + return Task.CompletedTask; + } + + if (discreetMeshes.Count == 1) + { + // restore the selection + scene.SelectedItem = selectedItem; + // No further processing needed, nothing to ungroup + return Task.CompletedTask; + } + + // build the ungroup list + var addItems = new List(discreetMeshes.Select(mesh => new Object3D() + { + Mesh = mesh, + })); + + foreach (var item in addItems) + { + item.CopyProperties(selectedItem, Object3DPropertyFlags.All); + item.Visible = true; + } + + // add and do the undo data + scene.UndoBuffer.AddAndDo(new ReplaceCommand(new[] { selectedItem }, addItems)); + + foreach (var item in addItems) + { + item.MakeNameNonColliding(); + } + + return Task.CompletedTask; + }); + } + + // leave no selection + scene.SelectedItem = null; + } + } + + internal class ArrangeUndoCommand : IUndoRedoCommand + { + private List allUndoTransforms = new List(); + + public ArrangeUndoCommand(View3DWidget view3DWidget, List preArrangeTarnsforms, List postArrangeTarnsforms) + { + for (int i = 0; i < preArrangeTarnsforms.Count; i++) + { + //a llUndoTransforms.Add(new TransformUndoCommand(view3DWidget, i, preArrangeTarnsforms[i], postArrangeTarnsforms[i])); + } + } + + public void Do() + { + for (int i = 0; i < allUndoTransforms.Count; i++) + { + allUndoTransforms[i].Do(); + } + } + + public void Undo() + { + for (int i = 0; i < allUndoTransforms.Count; i++) + { + allUndoTransforms[i].Undo(); + } + } + } + } +} \ No newline at end of file diff --git a/MatterControlLib/PartPreviewWindow/ViewToolBarControls.cs b/MatterControlLib/PartPreviewWindow/ViewToolBarControls.cs index de6a156d8..b0e666402 100644 --- a/MatterControlLib/PartPreviewWindow/ViewToolBarControls.cs +++ b/MatterControlLib/PartPreviewWindow/ViewToolBarControls.cs @@ -841,7 +841,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow return theme.CreateSplitButton(new SplitButtonParams() { ButtonText = "Save".Localize(), - ButtonName = "Save Button", + ButtonName = "Save", Icon = StaticData.Instance.LoadIcon("save_grey_16x.png", 16, 16).SetToColor(theme.TextColor), ButtonAction = (menuButton) => { diff --git a/MatterControlLib/RootSystemWindow.cs b/MatterControlLib/RootSystemWindow.cs index 9bfdd9ab7..019fd3253 100644 --- a/MatterControlLib/RootSystemWindow.cs +++ b/MatterControlLib/RootSystemWindow.cs @@ -37,6 +37,7 @@ using MatterHackers.Agg.Platform; using MatterHackers.Agg.UI; using MatterHackers.Localizations; using MatterHackers.MatterControl.DataStorage; +using MatterHackers.MatterControl.Library; using MatterHackers.MatterControl.PrinterCommunication; using MatterHackers.MatterControl.PrintQueue; using MatterHackers.MatterControl.SettingsManagement; @@ -353,16 +354,25 @@ namespace MatterHackers.MatterControl { if (workspace.Printer == null) { - // if we have a filename - // save - // else - // switch to the tab we are about to save - // ask the user to give us a filname - // if no filename - // abort the exit procedure - await ApplicationController.Instance.Tasks.Execute("Saving".Localize() + $" \"{workspace.Name}\" ...", workspace, workspace.SceneContext.SaveChanges); - // check for error or abort - hadSaveError |= workspace.SceneContext.HadSaveError; + var sceneContext = workspace.SceneContext; + if (sceneContext.EditContext.ContentStore == null) + { + hadSaveError = true; + // If we are about to close a tab that has never been saved it will need a name before it can actually save + // Open up the save as dialog rather than continue with saving and closing + DialogWindow.Show( + new SaveAsPage( + (container, newName) => + { + sceneContext.SaveAs(container, newName); + })); + } + else + { + await ApplicationController.Instance.Tasks.Execute("Saving".Localize() + $" \"{workspace.Name}\" ...", workspace, workspace.SceneContext.SaveChanges); + // check for error or abort + hadSaveError |= workspace.SceneContext.HadSaveError; + } } } diff --git a/Submodules/agg-sharp b/Submodules/agg-sharp index 255aa2aa1..90fb1dffa 160000 --- a/Submodules/agg-sharp +++ b/Submodules/agg-sharp @@ -1 +1 @@ -Subproject commit 255aa2aa1a9a157fb44e0aa9a62438173de67905 +Subproject commit 90fb1dffadc95d8f361fac9a5bb8db9e6c11f3fb diff --git a/Tests/MatterControl.AutomationTests/PartPreviewTests.cs b/Tests/MatterControl.AutomationTests/PartPreviewTests.cs index 58e8c27d9..948f8fc88 100644 --- a/Tests/MatterControl.AutomationTests/PartPreviewTests.cs +++ b/Tests/MatterControl.AutomationTests/PartPreviewTests.cs @@ -1,4 +1,33 @@ -using System.IO; +/* +Copyright (c) 2022, Lars Brubaker +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using System.IO; using System.Threading; using System.Threading.Tasks; using MatterHackers.Agg.UI; @@ -56,7 +85,7 @@ namespace MatterHackers.MatterControl.Tests.Automation testRunner.Require(() => scene.Children.Count == 1, "Should have 1 part (the phil)"); var tempFilaname = "Temp Test Save.mcx"; - var tempFullPath = Path.Combine(ApplicationDataStorage.Instance.MyDocumentsDirectory, tempFilaname); + var tempFullPath = Path.Combine(ApplicationDataStorage.Instance.DownloadsDirectory, tempFilaname); // delete the temp file if it exists in the Downloads folder void DeleteTempFile() @@ -70,10 +99,13 @@ namespace MatterHackers.MatterControl.Tests.Automation DeleteTempFile(); // Make sure the tab is named 'New Design' - Assert.IsNotNull(systemWindow.GetVisibleWigetWithText("New Design")); + testRunner.Require(() => systemWindow.GetVisibleWigetWithText("New Design") != null, "Must have New Design"); + + // add a new part to the bed + testRunner.AddItemToBed(); // Click the save button - testRunner.ClickByName("Save Button") + testRunner.ClickByName("Save") // Cancle the save as .ClickByName("Cancel Wizard Button"); @@ -99,7 +131,7 @@ namespace MatterHackers.MatterControl.Tests.Automation Assert.IsNotNull(systemWindow.GetVisibleWigetWithText("New Design")); // Click the save button - testRunner.ClickByName("Save Button") + testRunner.ClickByName("Save") // Save a temp file to the downloads folder .DoubleClickByName("Computer Row Item Collection") .DoubleClickByName("Downloads Row Item Collection") @@ -119,7 +151,7 @@ namespace MatterHackers.MatterControl.Tests.Automation // Click the 'Cancel' .ClickByName("Cancel Button") // Click the 'Save' button - .ClickByName("Save Button") + .ClickByName("Save") // Click the close button (now we have no edit it should cancel without request) .ClickByName("Close Tab Button"); diff --git a/Tests/MatterControl.AutomationTests/PrintQueueTests.cs b/Tests/MatterControl.AutomationTests/PrintQueueTests.cs index 59ac011f0..4c064cef8 100644 --- a/Tests/MatterControl.AutomationTests/PrintQueueTests.cs +++ b/Tests/MatterControl.AutomationTests/PrintQueueTests.cs @@ -210,7 +210,7 @@ namespace MatterHackers.MatterControl.Tests.Automation // Assert - one part added and queue count increases by one Assert.AreEqual(expectedCount, QueueData.Instance.ItemCount, "Queue count should increase by 1 when adding 1 item"); - Assert.IsTrue(testRunner.WaitForName("Row Item Rook"), "Named widget should exist after add(Rook)"); + Assert.IsTrue(testRunner.WaitForName("Row Item Rook.amf"), "Named widget should exist after add(Rook)"); return Task.CompletedTask; }); diff --git a/Tests/MatterControl.Tests/MatterControl/AssetManagerTests.cs b/Tests/MatterControl.Tests/MatterControl/AssetManagerTests.cs index 3203a9558..a036b4c33 100644 --- a/Tests/MatterControl.Tests/MatterControl/AssetManagerTests.cs +++ b/Tests/MatterControl.Tests/MatterControl/AssetManagerTests.cs @@ -85,7 +85,6 @@ namespace MatterControl.Tests.MatterControl var assetManager = new MockAssetManager(); AssetObject3D.AssetManager = assetManager; - // Store await AssetObject3D.AssetManager.StoreAsset(object3D, false, CancellationToken.None, null); diff --git a/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs b/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs index 3109a5068..46d8c05f8 100644 --- a/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs +++ b/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs @@ -230,6 +230,11 @@ namespace MatterHackers.MatterControl.Tests.Automation return testRunner.ClickByName(partNameToSelect); } + public static AutomationRunner ClickDiscardChanges(this AutomationRunner testRunner) + { + return testRunner.ClickByName("No Button"); + } + public static AutomationRunner WaitForFirstDraw(this AutomationRunner testRunner) { testRunner.GetWidgetByName("PartPreviewContent", out SystemWindow systemWindow, 10); @@ -626,45 +631,40 @@ namespace MatterHackers.MatterControl.Tests.Automation { testRunner.EnsureContentMenuOpen(); - switch (libraryRowItemName) + if (!testRunner.NameExists(libraryRowItemName, .2)) { - case "SD Card Row Item Collection": - if (ApplicationController.Instance.DragDropData.View3DWidget?.Printer is PrinterConfig printer) - { - testRunner.DoubleClickByName($"{printer.PrinterName} Row Item Collection"); + // go back to the home section + testRunner.ClickByName("Bread Crumb Button Home") + .Delay(); - testRunner.Delay(); + switch (libraryRowItemName) + { + case "SD Card Row Item Collection": + if (ApplicationController.Instance.DragDropData.View3DWidget?.Printer is PrinterConfig printer) + { + testRunner.DoubleClickByName($"{printer.PrinterName} Row Item Collection") + .Delay(); + } - testRunner.ClickByName(libraryRowItemName); - } + break; - break; - - case "Calibration Parts Row Item Collection": - case "Primitives Row Item Collection": - if (!testRunner.NameExists("Design Apps Row Item Collection")) - { - testRunner.ClickByName("Bread Crumb Button Home") - .Delay(); - } - - // If visible, navigate into Libraries container before opening target - if (testRunner.NameExists("Design Apps Row Item Collection")) - { + case "Calibration Parts Row Item Collection": + case "Primitives Row Item Collection": + // If visible, navigate into Libraries container before opening target testRunner.DoubleClickByName("Design Apps Row Item Collection") .Delay(); - } - break; + break; - case "Cloud Library Row Item Collection": - case "Print Queue Row Item Collection": - case "Local Library Row Item Collection": - if (!testRunner.NameExists(libraryRowItemName)) - { - testRunner.ClickByName("Bread Crumb Button Home") + case "Downloads Row Item Collection": + testRunner.DoubleClickByName("Computer Row Item Collection") .Delay(); - } - break; + break; + + case "Cloud Library Row Item Collection": + case "Print Queue Row Item Collection": + case "Local Library Row Item Collection": + break; + } } testRunner.DoubleClickByName(libraryRowItemName); @@ -675,7 +675,8 @@ namespace MatterHackers.MatterControl.Tests.Automation { if (!testRunner.WaitForName("FolderBreadCrumbWidget", secondsToWait: 0.2)) { - testRunner.ClickByName("Add Content Menu"); + testRunner.ClickByName("Add Content Menu") + .Delay(); } return testRunner; @@ -923,7 +924,7 @@ namespace MatterHackers.MatterControl.Tests.Automation testMethod, maxTimeToRun, defaultTestImages, - closeWindow: () => + closeWindow: (testRunner) => { foreach (var printer in ApplicationController.Instance.ActivePrinters) { @@ -934,6 +935,12 @@ namespace MatterHackers.MatterControl.Tests.Automation } rootSystemWindow.Close(); + + testRunner.Delay(); + if (testRunner.NameExists("No Button")) + { + testRunner.ClickDiscardChanges(); + } }); } diff --git a/Tests/MatterControl.Tests/SceneTests.cs b/Tests/MatterControl.Tests/SceneTests.cs index c910492f0..7bf3a065d 100644 --- a/Tests/MatterControl.Tests/SceneTests.cs +++ b/Tests/MatterControl.Tests/SceneTests.cs @@ -1,5 +1,5 @@ /* -Copyright (c) 2016, John Lewin +Copyright (c) 2022, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without @@ -48,6 +48,15 @@ namespace MatterHackers.PolygonMesh.UnitTests [TestFixture, Category("Agg.PolygonMesh"), RunInApplicationDomain] public class SceneTests { + [SetUp] + public void SetupUserSettings() + { + StaticData.RootPath = TestContext.CurrentContext.ResolveProjectPath(4, "MatterControl", "StaticData"); + MatterControlUtilities.OverrideAppDataLocation(TestContext.CurrentContext.ResolveProjectPath(4)); + + UserSettings.Instance.set(UserSettingsKey.PublicProfilesSha, "0"); //Clears DB so we will download the latest list + } + [Test] public void SaveSimpleScene() { @@ -242,11 +251,6 @@ namespace MatterHackers.PolygonMesh.UnitTests [Test] public async Task ResavedSceneRemainsConsistent() { -#if !__ANDROID__ - // Set the static data to point to the directory of MatterControl - StaticData.RootPath = TestContext.CurrentContext.ResolveProjectPath(4, "StaticData"); - MatterControlUtilities.OverrideAppDataLocation(TestContext.CurrentContext.ResolveProjectPath(4)); -#endif AssetObject3D.AssetManager = new AssetManager(); var sceneContext = new BedConfig(null); @@ -262,14 +266,9 @@ namespace MatterHackers.PolygonMesh.UnitTests // Set directory for asset resolution Object3D.AssetsPath = Path.Combine(tempPath, "Assets"); + Directory.Delete(Object3D.AssetsPath, true); Directory.CreateDirectory(Object3D.AssetsPath); - // Empty temp folder - foreach (string tempFile in Directory.GetFiles(tempPath).ToList()) - { - File.Delete(tempFile); - } - scene.Save(filePath); Assert.AreEqual(1, Directory.GetFiles(tempPath).Length, "Only .mcx file should exists"); Assert.AreEqual(1, Directory.GetFiles(Path.Combine(tempPath, "Assets")).Length, "Only 1 asset should exist"); @@ -290,8 +289,8 @@ namespace MatterHackers.PolygonMesh.UnitTests }); // Serialize and compare the two trees - string onDiskData = loadedItem.ToJson(); - string inMemoryData = scene.ToJson(); + string onDiskData = loadedItem.ToJson().Result; + string inMemoryData = scene.ToJson().Result; //File.WriteAllText(@"c:\temp\file-a.txt", onDiskData); //File.WriteAllText(@"c:\temp\file-b.txt", inMemoryData); @@ -301,7 +300,7 @@ namespace MatterHackers.PolygonMesh.UnitTests // Save the scene a second time, validate that things remain the same scene.Save(filePath); - onDiskData = loadedItem.ToJson(); + onDiskData = loadedItem.ToJson().Result; Assert.IsTrue(inMemoryData == onDiskData);