diff --git a/ApplicationView/ApplicationController.cs b/ApplicationView/ApplicationController.cs index 3d499c027..f1d9d22c1 100644 --- a/ApplicationView/ApplicationController.cs +++ b/ApplicationView/ApplicationController.cs @@ -163,10 +163,6 @@ namespace MatterHackers.MatterControl public static Func>> GetProfileHistory; - private readonly static object thumbsLock = new object(); - - private Queue> queuedThumbCallbacks = new Queue>(); - public async Task SetActivePrinter(PrinterConfig printer, bool allowChangedEvent = true) { var initialPrinter = this.ActivePrinter; @@ -279,69 +275,6 @@ namespace MatterHackers.MatterControl }*/ } - private AutoResetEvent thumbGenResetEvent = new AutoResetEvent(false); - - Task thumbnailGenerator = null; - - internal void QueueForGeneration(Func func) - { - lock (thumbsLock) - { - if (thumbnailGenerator == null) - { - // Spin up a new thread once needed - thumbnailGenerator = Task.Run((Action)ThumbGeneration); - } - - queuedThumbCallbacks.Enqueue(func); - thumbGenResetEvent.Set(); - } - } - - private async void ThumbGeneration() - { - Thread.CurrentThread.Name = $"ThumbnailGeneration"; - - while (!this.ApplicationExiting) - { - Thread.Sleep(100); - - try - { - if (queuedThumbCallbacks.Count > 0) - { - Func callback; - lock (thumbsLock) - { - callback = queuedThumbCallbacks.Dequeue(); - } - - await callback(); - } - else - { - // Process until queuedThumbCallbacks is empty then wait for new tasks via QueueForGeneration - thumbGenResetEvent.WaitOne(); - } - } - catch (AppDomainUnloadedException) - { - return; - } - catch (ThreadAbortException) - { - return; - } - catch (Exception ex) - { - Console.WriteLine("Error generating thumbnail: " + ex.Message); - } - } - - // Null task reference on exit - thumbnailGenerator = null; - } - public static Func> GetPrinterProfileAsync; public static Func, Task> SyncPrinterProfiles; public static Func> GetPublicProfileList; @@ -359,7 +292,7 @@ namespace MatterHackers.MatterControl private List registeredSceneOperations; - public Dictionary OperationsByType { get; private set; } + public ThumbnailsConfig Thumbnails { get; } = new ThumbnailsConfig(); private void RebuildSceneOperations(ThemeConfig theme) { @@ -585,17 +518,25 @@ namespace MatterHackers.MatterControl }, }; - var operationsByType = new Dictionary(); + var operationIconsByType = new Dictionary(); - foreach(var operation in registeredSceneOperations) + foreach (var operation in registeredSceneOperations) { if (operation.OperationType != null) { - operationsByType.Add(operation.OperationType, operation); + operationIconsByType.Add(operation.OperationType, operation.Icon); } } - this.OperationsByType = operationsByType; + // TODO: Use custom selection group icon if reusing group icon seems incorrect + // + // Explicitly register SelectionGroup icon + if (operationIconsByType.TryGetValue(typeof(Group3D), out ImageBuffer groupIcon)) + { + operationIconsByType.Add(typeof(SelectionGroup), groupIcon); + } + + this.Thumbnails.OperationIcons = operationIconsByType; } public ImageSequence GetProcessingSequence(Color color) @@ -1095,7 +1036,7 @@ namespace MatterHackers.MatterControl // Ensure all threads shutdown gracefully on close // Release any waiting generator threads - thumbGenResetEvent?.Set(); + this.Thumbnails.Shutdown(); // Kill all long running tasks (this will release the silcing thread if running) foreach (var task in Tasks.RunningTasks) @@ -1294,8 +1235,7 @@ namespace MatterHackers.MatterControl public async void OnApplicationClosed() { - // Release the waiting ThumbnailGeneration task so it can shutdown gracefully - thumbGenResetEvent?.Set(); + this.Thumbnails.Shutdown(); // Save changes before close if (this.ActivePrinter != null @@ -1344,17 +1284,6 @@ namespace MatterHackers.MatterControl public string ShortProductName => "MatterControl"; public string ProductName => "MatterHackers: MatterControl"; - public string ThumbnailCachePath(string cacheId) - { - // TODO: Use content SHA - return ApplicationController.CacheablePath("ItemThumbnails", $"{cacheId}.png"); - } - - public string ThumbnailCachePath(string id, int width, int height) - { - return ApplicationController.CacheablePath("ItemThumbnails", $"{id}-{width}x{height}.png"); - } - public void SwitchToPurchasedLibrary() { var purchasedContainer = Library.RootLibaryContainer.ChildContainers.Where(c => c.ID == "LibraryProviderPurchasedKey").FirstOrDefault(); diff --git a/ApplicationView/PrinterModels.cs b/ApplicationView/PrinterModels.cs index 35e7fd905..2850acca4 100644 --- a/ApplicationView/PrinterModels.cs +++ b/ApplicationView/PrinterModels.cs @@ -595,7 +595,7 @@ namespace MatterHackers.MatterControl { if (!this.FreezeGCode) { - var thumbnailPath = ApplicationController.Instance.ThumbnailCachePath(this.SourceItem.ID); + var thumbnailPath = ApplicationController.Instance.Thumbnails.CachePath(this.SourceItem.ID); if (File.Exists(thumbnailPath)) { File.Delete(thumbnailPath); diff --git a/ApplicationView/ThumbnailsConfig.cs b/ApplicationView/ThumbnailsConfig.cs new file mode 100644 index 000000000..243790a0f --- /dev/null +++ b/ApplicationView/ThumbnailsConfig.cs @@ -0,0 +1,168 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +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. +*/ + +namespace MatterHackers.MatterControl +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using MatterHackers.Agg.Image; + using MatterHackers.Agg.Platform; + using MatterHackers.MatterControl.Library; + + public class ThumbnailsConfig + { + private readonly static object thumbsLock = new object(); + + private Queue> queuedThumbCallbacks = new Queue>(); + + public Dictionary OperationIcons { get; internal set; } + + public ImageBuffer DefaultThumbnail { get; } = AggContext.StaticData.LoadIcon("cube.png"); + + public ImageBuffer LoadCachedImage(string cacheId, int width, int height) + { + ImageBuffer cachedItem = LoadImage(this.CachePath(cacheId, width, height)); + if (cachedItem != null) + { + return cachedItem; + } + + if (width < 100 + && height < 100) + { + // check for a 100x100 image + var cachedAt100x100 = LoadImage(this.CachePath(cacheId, 100, 100)); + if (cachedAt100x100 != null) + { + return cachedAt100x100.CreateScaledImage(width, height); + } + } + + return null; + } + + public string CachePath(string cacheId) + { + // TODO: Use content SHA + return ApplicationController.CacheablePath("ItemThumbnails", $"{cacheId}.png"); + } + + public string CachePath(string id, int width, int height) + { + return ApplicationController.CacheablePath("ItemThumbnails", $"{id}-{width}x{height}.png"); + } + + private AutoResetEvent thumbGenResetEvent = new AutoResetEvent(false); + + private Task thumbnailGenerator = null; + + internal void QueueForGeneration(Func func) + { + lock (thumbsLock) + { + if (thumbnailGenerator == null) + { + // Spin up a new thread once needed + thumbnailGenerator = Task.Run((Action)ThumbGeneration); + } + + queuedThumbCallbacks.Enqueue(func); + thumbGenResetEvent.Set(); + } + } + + private async void ThumbGeneration() + { + Thread.CurrentThread.Name = $"ThumbnailGeneration"; + + while (!ApplicationController.Instance.ApplicationExiting) + { + Thread.Sleep(100); + + try + { + if (queuedThumbCallbacks.Count > 0) + { + Func callback; + lock (thumbsLock) + { + callback = queuedThumbCallbacks.Dequeue(); + } + + await callback(); + } + else + { + // Process until queuedThumbCallbacks is empty then wait for new tasks via QueueForGeneration + thumbGenResetEvent.WaitOne(); + } + } + catch (AppDomainUnloadedException) + { + return; + } + catch (ThreadAbortException) + { + return; + } + catch (Exception ex) + { + Console.WriteLine("Error generating thumbnail: " + ex.Message); + } + } + + // Null task reference on exit + thumbnailGenerator = null; + } + + + private static ImageBuffer LoadImage(string filePath) + { + try + { + if (File.Exists(filePath)) + { + return AggContext.ImageIO.LoadImage(filePath).SetPreMultiply(); + } + } + catch { } // Suppress exceptions, return null on any errors + + return null; + } + + public void Shutdown() + { + // Release the waiting ThumbnailGeneration task so it can shutdown gracefully + thumbGenResetEvent?.Set(); + } + } +} \ No newline at end of file diff --git a/DesignTools/Operations/Group3D.cs b/DesignTools/Operations/Group3D.cs index 2e67bda52..f2928e1c8 100644 --- a/DesignTools/Operations/Group3D.cs +++ b/DesignTools/Operations/Group3D.cs @@ -27,11 +27,8 @@ of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ -using System.Linq; -using MatterHackers.Agg.UI; using MatterHackers.DataConverters3D; using MatterHackers.Localizations; -using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.DesignTools.Operations { @@ -41,7 +38,7 @@ namespace MatterHackers.MatterControl.DesignTools.Operations { Name = "Group".Localize(); } - + public override bool CanRemove => true; } } \ No newline at end of file diff --git a/Library/ContentProviders/MeshContentProvider.cs b/Library/ContentProviders/MeshContentProvider.cs index d2468dba4..3efcd398a 100644 --- a/Library/ContentProviders/MeshContentProvider.cs +++ b/Library/ContentProviders/MeshContentProvider.cs @@ -122,38 +122,20 @@ namespace MatterHackers.MatterControl string thumbnailId = libraryItem.ID; - return GetThumbnail(object3D, thumbnailId, width, height, false); + return GetThumbnail(object3D, thumbnailId, width, height); } - public ImageBuffer GetThumbnail(IObject3D item, string thumbnailId, int width, int height, bool onlyUseCache) + public ImageBuffer GetThumbnail(IObject3D item, string thumbnailId, int width, int height) { if (item == null) { return DefaultImage; } - var image = LoadCachedImage(thumbnailId, width, height); - - if(image == null) - { - // check the mesh cache - image = LoadCachedImage(item.MeshRenderId().ToString(), width, height); - } - - if(image != null) - { - return image; - } - - if(onlyUseCache) - { - return DefaultImage; - } - int estimatedMemorySize = item.EstimatedMemory(); if (estimatedMemorySize > MaxFileSizeForThumbnail) { - return null; + return DefaultImage; } bool forceOrthographic = false; @@ -173,14 +155,9 @@ namespace MatterHackers.MatterControl if (thumbnail != null) { + // TODO: Consider and resolve who should own populating the cache // Cache at requested size - string cachePath = ApplicationController.Instance.ThumbnailCachePath(item.MeshRenderId().ToString(), width, height); - - // TODO: Lookup best large image and downscale if required - if (false) - { - thumbnail = LibraryProviderHelpers.ResizeImage(thumbnail, width, height); - } + string cachePath = ApplicationController.Instance.Thumbnails.CachePath(item.MeshRenderId().ToString(), width, height); AggContext.ImageIO.SaveImageData(cachePath, thumbnail); } @@ -188,42 +165,6 @@ namespace MatterHackers.MatterControl return thumbnail ?? DefaultImage; } - internal static ImageBuffer LoadCachedImage(string cacheId, int width, int height) - { - ImageBuffer cachedItem = LoadImage(ApplicationController.Instance.ThumbnailCachePath(cacheId, width, height)); - if (cachedItem != null) - { - return cachedItem; - } - - if (width < 100 - && height < 100) - { - // check for a 100x100 image - var cachedAt100x100 = LoadImage(ApplicationController.Instance.ThumbnailCachePath(cacheId, 100, 100)); - if (cachedAt100x100 != null) - { - return cachedAt100x100.CreateScaledImage(width, height); - } - } - - return null; - } - - private static ImageBuffer LoadImage(string filePath) - { - try - { - if (File.Exists(filePath)) - { - return AggContext.ImageIO.LoadImage(filePath).SetPreMultiply(); - } - } - catch { } // Suppress exceptions, return null on any errors - - return null; - } - public ImageBuffer DefaultImage => AggContext.StaticData.LoadIcon("mesh.png"); } } \ No newline at end of file diff --git a/Library/Widgets/ListView/ListViewItemBase.cs b/Library/Widgets/ListView/ListViewItemBase.cs index 9a9b70092..1a34b542a 100644 --- a/Library/Widgets/ListView/ListViewItemBase.cs +++ b/Library/Widgets/ListView/ListViewItemBase.cs @@ -72,7 +72,7 @@ namespace MatterHackers.MatterControl.CustomWidgets string thumbnailId = libraryItem.ID; - var thumbnail = MeshContentProvider.LoadCachedImage(thumbnailId, thumbWidth, thumbHeight); + var thumbnail = ApplicationController.Instance.Thumbnails.LoadCachedImage(thumbnailId, thumbWidth, thumbHeight); if (thumbnail != null) { this.SetItemThumbnail(thumbnail); @@ -135,7 +135,7 @@ namespace MatterHackers.MatterControl.CustomWidgets return; } - ApplicationController.Instance.QueueForGeneration(async () => + ApplicationController.Instance.Thumbnails.QueueForGeneration(async () => { // When dequeued for generation, ensure visible before raytracing. Off-screen widgets are dequeue and will reschedule if redrawn if (!this.ActuallyVisibleOnScreen()) diff --git a/MatterControl.csproj b/MatterControl.csproj index 6a3b68958..4f8cfb9a0 100644 --- a/MatterControl.csproj +++ b/MatterControl.csproj @@ -75,6 +75,7 @@ + diff --git a/PartPreviewWindow/Object3DTreeBuilder.cs b/PartPreviewWindow/Object3DTreeBuilder.cs index 8f0df10c6..7e5fb20f4 100644 --- a/PartPreviewWindow/Object3DTreeBuilder.cs +++ b/PartPreviewWindow/Object3DTreeBuilder.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MatterHackers.Agg.Image; using MatterHackers.Agg.UI; using MatterHackers.DataConverters3D; using MatterHackers.Localizations; @@ -91,32 +92,18 @@ namespace MatterHackers.MatterControl.PartPreviewWindow }; // Check for operation resulting in the given type - if (ApplicationController.Instance.OperationsByType.TryGetValue(item.Source.GetType(), out SceneSelectionOperation operation)) + if (ApplicationController.Instance.Thumbnails.OperationIcons.TryGetValue(item.Source.GetType(), out ImageBuffer icon)) { // If exists, use the operation icon - node.Image = operation.Icon; + node.Image = icon; } else { node.Load += (s, e) => { - ApplicationController.Instance.QueueForGeneration(() => - { - // When this widget is dequeued for generation, validate before processing. Off-screen widgets should be skipped and will requeue next time they become visible - if (node.ActuallyVisibleOnScreen() - && ApplicationController.Instance.Library.ContentProviders.TryGetValue("mcx", out IContentProvider contentProvider) - && contentProvider is MeshContentProvider meshContentProvider) - { - node.Image = meshContentProvider.GetThumbnail( - item.Source, - item.Source.MeshRenderId().ToString(), - 16, - 16, - true); - } - - return Task.CompletedTask; - }); + string contentID = item.Source.MeshRenderId().ToString(); + var thumbnail = ApplicationController.Instance.Thumbnails.LoadCachedImage(contentID, 16, 16); + node.Image = thumbnail ?? ApplicationController.Instance.Thumbnails.DefaultThumbnail; }; }