From 1c37636dd9b4b08a43f46032b544f2759a6ddc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Fri, 7 May 2021 03:09:51 +0100 Subject: v2.10.0 - **Exposure time finder:** - Add a enable option for each feature - Add a staircase: Creates an incremental stair at top from left to right that goes up to the top layer - Add a section dedicated to the bullseye and revamp the design - Add a section for counter triangles (this will take the space of the old bullseye) - Allow negative fence offset for zebra bars - Allow to preview the exposure time information - Changed some defaults - (Add) Layer actions - Export layers to animated GIF - **Note:** Non Windows users must install 'libgdiplus' dependency in order to use this tool - (Add) Tools - Dynamic lifts: Generate dynamic lift height and speeds for each layer given it mass - (Improvement) File formats using json files are now saved with human readable indentation - (Fix) GCode builder: Raise to top on completion command was not being sent when feedrate units are in mm/min - (Fix) Tools - Layer Range Selector: Fix the 'to layer' minimum to not allow negative values and limit to 0 --- CHANGELOG.md | 17 + README.md | 6 +- UVtools.Core/Extensions/EmguExtensions.cs | 8 + UVtools.Core/Extensions/IEnumerableExtensions.cs | 29 ++ UVtools.Core/FileFormats/FileFormat.cs | 9 +- UVtools.Core/FileFormats/UVJFile.cs | 4 +- UVtools.Core/FileFormats/VDTFile.cs | 4 +- UVtools.Core/FileFormats/ZCodexFile.cs | 12 +- UVtools.Core/GCode/GCodeBuilder.cs | 16 +- UVtools.Core/Layer/Layer.cs | 15 +- UVtools.Core/Layer/LayerManager.cs | 4 +- UVtools.Core/Operations/Operation.cs | 14 +- .../Operations/OperationCalibrateExposureFinder.cs | 469 +++++++++++++++++++-- UVtools.Core/Operations/OperationDynamicLifts.cs | 344 +++++++++++++++ UVtools.Core/Operations/OperationLayerExportGif.cs | 344 +++++++++++++++ UVtools.Core/UVtools.Core.csproj | 3 +- UVtools.InstallerMM/UVtools.InstallerMM.wxs | 3 + UVtools.ScriptSample/ScriptTester.cs | 68 +++ UVtools.WPF/Assets/Icons/angle-double-up-16x16.png | Bin 0 -> 126 bytes UVtools.WPF/Assets/Icons/gif-16x16.png | Bin 0 -> 266 bytes UVtools.WPF/Assets/Styles/Styles.xaml | 2 +- .../CalibrateExposureFinderControl.axaml | 264 +++++++++--- .../CalibrateExposureFinderControl.axaml.cs | 2 +- .../Controls/Tools/ToolDynamicLiftsControl.axaml | 207 +++++++++ .../Tools/ToolDynamicLiftsControl.axaml.cs | 28 ++ .../Controls/Tools/ToolLayerExportGifControl.axaml | 127 ++++++ .../Tools/ToolLayerExportGifControl.axaml.cs | 43 ++ UVtools.WPF/MainWindow.axaml.cs | 16 + UVtools.WPF/Structures/AppVersionChecker.cs | 11 +- UVtools.WPF/Structures/OperationProfiles.cs | 2 + UVtools.WPF/UVtools.WPF.csproj | 2 +- UVtools.WPF/Windows/ToolWindow.axaml | 1 + 32 files changed, 1934 insertions(+), 140 deletions(-) create mode 100644 UVtools.Core/Extensions/IEnumerableExtensions.cs create mode 100644 UVtools.Core/Operations/OperationDynamicLifts.cs create mode 100644 UVtools.Core/Operations/OperationLayerExportGif.cs create mode 100644 UVtools.ScriptSample/ScriptTester.cs create mode 100644 UVtools.WPF/Assets/Icons/angle-double-up-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/gif-16x16.png create mode 100644 UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml create mode 100644 UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml.cs create mode 100644 UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml create mode 100644 UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ac813..67f50f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## /05/2021 - v2.10.0 + +- **Exposure time finder:** + - Add a enable option for each feature + - Add a staircase: Creates an incremental stair at top from left to right that goes up to the top layer + - Add a section dedicated to the bullseye and revamp the design + - Add a section for counter triangles (this will take the space of the old bullseye) + - Allow negative fence offset for zebra bars + - Allow to preview the exposure time information + - Changed some defaults +- (Add) Layer actions - Export layers to animated GIF + - **Note:** Non Windows users must install 'libgdiplus' dependency in order to use this tool +- (Add) Tools - Dynamic lifts: Generate dynamic lift height and speeds for each layer given it mass +- (Improvement) File formats using json files are now saved with human readable indentation +- (Fix) GCode builder: Raise to top on completion command was not being sent when feedrate units are in mm/min +- (Fix) Tools - Layer Range Selector: Fix the 'to layer' minimum to not allow negative values and limit to 0 + ## 04/05/2021 - v2.9.3 - (Upgrade) AvaloniaUI from 0.10.2 to 0.10.3 diff --git a/README.md b/README.md index 21f8070..85544ec 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ dotnet-runtime-5.0 ```bash sudo apt-get update -sudo apt-get install -y libjpeg-dev libpng-dev libgeotiff-dev libdc1394-22 libavcodec-dev libavformat-dev libswscale-dev libopenexr24 libtbb-dev +sudo apt-get install -y libjpeg-dev libpng-dev libgeotiff-dev libdc1394-22 libavcodec-dev libavformat-dev libswscale-dev libopenexr24 libtbb-dev libgdiplus ``` @@ -264,7 +264,7 @@ anyone with same system version can make use of it without the need of the compi ### Arch/Manjaro/Similars ```bash -sudo pacman -S openjpeg2 libjpeg-turbo libpng libgeotiff libdc1394 libdc1394 ffmpeg openexr tbb +sudo pacman -S openjpeg2 libjpeg-turbo libpng libgeotiff libdc1394 libdc1394 ffmpeg openexr tbb libgdiplus ``` To run UVtools open it folder on a terminal and call one of: @@ -388,7 +388,7 @@ To run UVtools open it folder on a terminal and call one of: ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" brew analytics off -brew install git cmake libjpeg libpng libgeotiff libdc1394 ffmpeg openexr tbb +brew install git cmake libjpeg libpng libgeotiff libdc1394 ffmpeg openexr tbb mono-libgdiplus brew install --cask dotnet-sdk git clone https://github.com/emgucv/emgucv emgucv cd emgucv diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 2806e43..2631597 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -13,6 +13,7 @@ using System.Runtime.InteropServices; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; +using Emgu.CV.Util; namespace UVtools.Core.Extensions { @@ -333,5 +334,12 @@ namespace UVtools.Core.Extensions return layers; } + public static byte[] GetPngByes(this Mat mat) + { + using var vector = new VectorOfByte(); + CvInvoke.Imencode(".png", mat, vector); + return vector.ToArray(); + } + } } diff --git a/UVtools.Core/Extensions/IEnumerableExtensions.cs b/UVtools.Core/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..8fe6096 --- /dev/null +++ b/UVtools.Core/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,29 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; + +namespace UVtools.Core.Extensions +{ + public static class IEnumerableExtensions + { + public static IEnumerable DistinctBy + (this IEnumerable source, Func keySelector) + { + HashSet seenKeys = new(); + foreach (var element in source) + { + if (seenKeys.Add(keySelector(element))) + { + yield return element; + } + } + } + } +} diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index ff2f5c3..ac9a755 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -914,8 +914,8 @@ namespace UVtools.Core.FileFormats break; } - var lightOffDelay = OperationCalculator.LightOffDelayC.CalculateSeconds(layer.LiftHeight, layer.LiftSpeed, layer.RetractSpeed); - time += layer.ExposureTime + lightOffDelay; + var lightOffDelay = layer.CalculateLightOffDelay(); + time += layer.ExposureTime + lightOffDelay > layer.LightOffDelay ? lightOffDelay : layer.LightOffDelay; /*if (lightOffDelay >= layer.LightOffDelay) time += lightOffDelay; else @@ -1953,6 +1953,11 @@ namespace UVtools.Core.FileFormats return result; } + public void UpdatePrintTime() + { + PrintTime = PrintTimeComputed; + } + #endregion } } diff --git a/UVtools.Core/FileFormats/UVJFile.cs b/UVtools.Core/FileFormats/UVJFile.cs index 2838811..b38ad2f 100644 --- a/UVtools.Core/FileFormats/UVJFile.cs +++ b/UVtools.Core/FileFormats/UVJFile.cs @@ -343,7 +343,7 @@ namespace UVtools.Core.FileFormats } using ZipArchive outputFile = ZipFile.Open(fileFullPath, ZipArchiveMode.Create); - outputFile.PutFileContent(FileConfigName, JsonConvert.SerializeObject(JsonSettings), ZipArchiveMode.Create); + outputFile.PutFileContent(FileConfigName, JsonConvert.SerializeObject(JsonSettings, Formatting.Indented), ZipArchiveMode.Create); if (CreatedThumbnailsCount > 0) { @@ -458,7 +458,7 @@ namespace UVtools.Core.FileFormats using (var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Update)) { - outputFile.PutFileContent(FileConfigName, JsonConvert.SerializeObject(JsonSettings), ZipArchiveMode.Update); + outputFile.PutFileContent(FileConfigName, JsonConvert.SerializeObject(JsonSettings, Formatting.Indented), ZipArchiveMode.Update); } //Decode(FileFullPath, progress); diff --git a/UVtools.Core/FileFormats/VDTFile.cs b/UVtools.Core/FileFormats/VDTFile.cs index 5dc0e6b..42a9bfc 100644 --- a/UVtools.Core/FileFormats/VDTFile.cs +++ b/UVtools.Core/FileFormats/VDTFile.cs @@ -410,7 +410,7 @@ namespace UVtools.Core.FileFormats RebuildVDTLayers(); using var outputFile = ZipFile.Open(fileFullPath, ZipArchiveMode.Create); - outputFile.PutFileContent(FileManifestName, JsonConvert.SerializeObject(ManifestFile), ZipArchiveMode.Create); + outputFile.PutFileContent(FileManifestName, JsonConvert.SerializeObject(ManifestFile, Formatting.Indented), ZipArchiveMode.Create); if (CreatedThumbnailsCount > 0) { @@ -511,7 +511,7 @@ namespace UVtools.Core.FileFormats RebuildVDTLayers(); using var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Update); - outputFile.PutFileContent(FileManifestName, JsonConvert.SerializeObject(ManifestFile), ZipArchiveMode.Update); + outputFile.PutFileContent(FileManifestName, JsonConvert.SerializeObject(ManifestFile, Formatting.Indented), ZipArchiveMode.Update); //Decode(FileFullPath, progress); } diff --git a/UVtools.Core/FileFormats/ZCodexFile.cs b/UVtools.Core/FileFormats/ZCodexFile.cs index 297ad99..f0aa3e1 100644 --- a/UVtools.Core/FileFormats/ZCodexFile.cs +++ b/UVtools.Core/FileFormats/ZCodexFile.cs @@ -373,9 +373,9 @@ namespace UVtools.Core.FileFormats using (ZipArchive outputFile = ZipFile.Open(fileFullPath, ZipArchiveMode.Create)) { - outputFile.PutFileContent("ResinMetadata", JsonConvert.SerializeObject(ResinMetadataSettings), ZipArchiveMode.Create); - outputFile.PutFileContent("UserSettingsData", JsonConvert.SerializeObject(UserSettings), ZipArchiveMode.Create); - outputFile.PutFileContent("ZCodeMetadata", JsonConvert.SerializeObject(ZCodeMetadataSettings), ZipArchiveMode.Create); + outputFile.PutFileContent("ResinMetadata", JsonConvert.SerializeObject(ResinMetadataSettings, Formatting.Indented), ZipArchiveMode.Create); + outputFile.PutFileContent("UserSettingsData", JsonConvert.SerializeObject(UserSettings, Formatting.Indented), ZipArchiveMode.Create); + outputFile.PutFileContent("ZCodeMetadata", JsonConvert.SerializeObject(ZCodeMetadataSettings, Formatting.Indented), ZipArchiveMode.Create); if (CreatedThumbnailsCount > 0) { @@ -626,9 +626,9 @@ M106 S0 using (var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Update)) { - outputFile.PutFileContent("ResinMetadata", JsonConvert.SerializeObject(ResinMetadataSettings), ZipArchiveMode.Update); - outputFile.PutFileContent("UserSettingsData", JsonConvert.SerializeObject(UserSettings), ZipArchiveMode.Update); - outputFile.PutFileContent("ZCodeMetadata", JsonConvert.SerializeObject(ZCodeMetadataSettings), ZipArchiveMode.Update); + outputFile.PutFileContent("ResinMetadata", JsonConvert.SerializeObject(ResinMetadataSettings, Formatting.Indented), ZipArchiveMode.Update); + outputFile.PutFileContent("UserSettingsData", JsonConvert.SerializeObject(UserSettings, Formatting.Indented), ZipArchiveMode.Update); + outputFile.PutFileContent("ZCodeMetadata", JsonConvert.SerializeObject(ZCodeMetadataSettings, Formatting.Indented), ZipArchiveMode.Update); outputFile.PutFileContent("ResinGCodeData", GCode.ToString(), ZipArchiveMode.Update); } diff --git a/UVtools.Core/GCode/GCodeBuilder.cs b/UVtools.Core/GCode/GCodeBuilder.cs index 2ac2609..18d5c51 100644 --- a/UVtools.Core/GCode/GCodeBuilder.cs +++ b/UVtools.Core/GCode/GCodeBuilder.cs @@ -8,6 +8,7 @@ using System; using System.ComponentModel; +using System.Data; using System.Globalization; using System.IO; using System.Linq; @@ -554,16 +555,13 @@ namespace UVtools.Core.GCode break; } - float endFeedRate = 0; - switch (GCodeSpeedUnit) + float endFeedRate = GCodeSpeedUnit switch { - case GCodeSpeedUnits.MillimetersPerSecond: - endFeedRate = (float)Math.Round(slicerFile.RetractSpeed / 60, 2); - break; - case GCodeSpeedUnits.CentimetersPerMinute: - endFeedRate = (float)Math.Round(slicerFile.RetractSpeed / 10, 2); - break; - } + GCodeSpeedUnits.MillimetersPerMinute => slicerFile.RetractSpeed, + GCodeSpeedUnits.MillimetersPerSecond => (float) Math.Round(slicerFile.RetractSpeed / 60, 2), + GCodeSpeedUnits.CentimetersPerMinute => (float) Math.Round(slicerFile.RetractSpeed / 10, 2), + _ => throw new InvalidExpressionException($"Unhandled feedrate unit for {GCodeSpeedUnit}") + }; AppendEndGCode(finalRaiseZPosition, endFeedRate); } diff --git a/UVtools.Core/Layer/Layer.cs b/UVtools.Core/Layer/Layer.cs index 84df8fb..20fa46c 100644 --- a/UVtools.Core/Layer/Layer.cs +++ b/UVtools.Core/Layer/Layer.cs @@ -15,6 +15,7 @@ using Emgu.CV.Util; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Objects; +using UVtools.Core.Operations; using Stream = System.IO.Stream; namespace UVtools.Core @@ -304,9 +305,7 @@ namespace UVtools.Core } set { - using var vector = new VectorOfByte(); - CvInvoke.Imencode(".png", value, vector); - CompressedBytes = vector.ToArray(); + CompressedBytes = value.GetPngByes(); GetBoundingRectangle(value, true); RaisePropertyChanged(); } @@ -502,6 +501,16 @@ namespace UVtools.Core #region Methods + public float CalculateLightOffDelay(float extraTime = 0) + { + return OperationCalculator.LightOffDelayC.CalculateSeconds(_liftHeight, _liftSpeed, _retractSpeed, extraTime); + } + + public void UpdateLightOffDelay(float extraTime = 0) + { + LightOffDelay = CalculateLightOffDelay(extraTime); + } + public string FormatFileName(string name) { return $"{name}{Index.ToString().PadLeft(ParentLayerManager.LayerDigits, '0')}.png"; diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs index 2abd13e..ef9f907 100644 --- a/UVtools.Core/Layer/LayerManager.cs +++ b/UVtools.Core/Layer/LayerManager.cs @@ -57,7 +57,7 @@ namespace UVtools.Core SlicerFile.RequireFullEncode = true; SlicerFile.PrintHeight = SlicerFile.PrintHeight; - SlicerFile.PrintTime = SlicerFile.PrintTimeComputed; + SlicerFile.UpdatePrintTime(); if (value is not null && LayerCount > 0) { @@ -522,7 +522,7 @@ namespace UVtools.Core if(zeroLightOffDelay) layer.LightOffDelay = 0; } SlicerFile?.RebuildGCode(); - SlicerFile.PrintTime = SlicerFile.PrintTimeComputed; + SlicerFile?.UpdatePrintTime(); } public Rectangle GetBoundingRectangle(OperationProgress progress = null) diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index fc7de71..54abe03 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -142,7 +142,11 @@ namespace UVtools.Core.Operations public virtual uint LayerIndexStart { get => _layerIndexStart; - set => RaiseAndSetIfChanged(ref _layerIndexStart, value); + set + { + if(!RaiseAndSetIfChanged(ref _layerIndexStart, value)) return; + RaisePropertyChanged(nameof(LayerRangeCount)); + } } /// @@ -151,7 +155,11 @@ namespace UVtools.Core.Operations public virtual uint LayerIndexEnd { get => _layerIndexEnd; - set => RaiseAndSetIfChanged(ref _layerIndexEnd, value); + set + { + if(!RaiseAndSetIfChanged(ref _layerIndexEnd, value)) return; + RaisePropertyChanged(nameof(LayerRangeCount)); + } } public uint LayerRangeCount => LayerIndexEnd - LayerIndexStart + 1; @@ -220,7 +228,7 @@ namespace UVtools.Core.Operations #region Constructor protected Operation() { } - protected Operation(FileFormat slicerFile) + protected Operation(FileFormat slicerFile) : this() { _slicerFile = slicerFile; SelectAllLayers(); diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index 8c886a6..799b74d 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; +using Emgu.CV.Util; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Objects; @@ -26,6 +27,23 @@ namespace UVtools.Core.Operations [Serializable] public sealed class OperationCalibrateExposureFinder : Operation { + #region Subclasses + + public sealed class BullsEyeCircle + { + public ushort Diameter { get; set; } + public ushort Radius => (ushort) (Diameter / 2); + public ushort Thickness { get; set; } = 10; + + public BullsEyeCircle() {} + + public BullsEyeCircle(ushort diameter, ushort thickness) + { + Diameter = diameter; + Thickness = thickness; + } + } + #endregion #region Constants const byte TextMarkingSpacing = 60; @@ -55,20 +73,29 @@ namespace UVtools.Core.Operations private decimal _baseHeight = 1; private decimal _featuresHeight = 1; private decimal _featuresMargin = 2m; + + private ushort _staircaseThickness = 40; + + private bool _holesEnabled = false; + private CalibrateExposureFinderShapes _holeShape = CalibrateExposureFinderShapes.Square; private Measures _unitOfMeasure = Measures.Pixels; private string _holeDiametersPx = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11"; private string _holeDiametersMm = "0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2"; + + private bool _barsEnabled = true; private decimal _barSpacing = 1.5m; private decimal _barLength = 4; private sbyte _barVerticalSplitter = 0; - private byte _barFenceThickness = 12; - private byte _barFenceOffset; - private string _barThicknessesPx = "4, 6, 8, 10, 12, 14, 16, 18, 20"; - private string _barThicknessesMm = "0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2"; + private byte _barFenceThickness = 10; + private sbyte _barFenceOffset = 2; + private string _barThicknessesPx = "4, 6, 8, 60"; //"4, 6, 8, 10, 12, 14, 16, 18, 20"; + private string _barThicknessesMm = "0.2, 0.3, 0.4, 3"; //"0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.2"; + + private bool _textEnabled = true; private FontFace _textFont = TextMarkingFontFace; private double _textScale = 1; private byte _textThickness = 2; - private string _text = "ABGHJKLMQRSTUVWXZ%&#"; + private string _text = "ABHJQRWZ%&#"; //"ABGHJKLMQRSTUVWXZ%&#"; private bool _multipleBrightness; private CalibrateExposureFinderMultipleBrightnessExcludeFrom _multipleBrightnessExcludeFrom = CalibrateExposureFinderMultipleBrightnessExcludeFrom.BottomAndBase; @@ -92,8 +119,19 @@ namespace UVtools.Core.Operations private decimal _exposureGenManualBottom; private decimal _exposureGenManualNormal; private RangeObservableCollection _exposureTable = new(); - private CalibrateExposureFinderShapes _holeShape = CalibrateExposureFinderShapes.Square; + + private bool _bullsEyeEnabled = true; + private string _bullsEyeConfigurationPx = "26:5, 60:10, 116:15, 190:20"; + private string _bullsEyeConfigurationMm = "1.3:0.25, 3:0.5, 5.8:0.75, 9.5:1"; + private bool _bullsEyeInvertQuadrants = true; + + private bool _counterTrianglesEnabled = true; + private sbyte _counterTrianglesTipOffset = 1; + private bool _counterTrianglesFence = false; + private bool _patternModel; + private byte _bullsEyeFenceThickness = 10; + private sbyte _bullsEyeFenceOffset; private bool _patternModelGlueBottomLayers = true; #endregion @@ -102,8 +140,6 @@ namespace UVtools.Core.Operations public override bool CanROI => false; - //public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; public override string Title => "Exposure time finder"; @@ -134,11 +170,6 @@ namespace UVtools.Core.Operations sb.AppendLine("Display height must be a positive value."); } - if (!_patternModel && Bars.Length <= 0 && Holes.Length <= 0 && string.IsNullOrWhiteSpace(Text)) - { - sb.AppendLine("No objects to output, enable at least 1 feature."); - } - if (_chamferLayers * _layerHeight > _baseHeight) { sb.AppendLine("The chamfer can't be higher than the base height, lower the chamfer layer count."); @@ -185,6 +216,13 @@ namespace UVtools.Core.Operations sb.AppendLine($"Pattern the loaded model requires either multiple brightness or multiple exposures to use with."); } } + else + { + if (Bars.Length <= 0 && Holes.Length <= 0 && BullsEyes.Length <= 0 && TextSize.IsEmpty) + { + sb.AppendLine("No objects to output, enable at least 1 feature."); + } + } return sb.ToString(); } @@ -197,7 +235,7 @@ namespace UVtools.Core.Operations $"[TB:{_topBottomMargin} LR:{_leftRightMargin} PM:{_partMargin} FM:{_featuresMargin}] " + $"[Chamfer: {_chamferLayers}] [Erode: {_erodeBottomIterations}] " + $"[Obj height: {_featuresHeight}] " + - $"[Holes: {Holes.Length}] [Bars: {Bars.Length}] [Text: {!string.IsNullOrWhiteSpace(_text)}]" + + $"[Holes: {Holes.Length}] [Bars: {Bars.Length}] [BE: {BullsEyes.Length}] [Text: {!string.IsNullOrWhiteSpace(_text)}]" + $"[AA: {_enableAntiAliasing}] [Mirror: {_mirrorOutput}]"; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; @@ -338,6 +376,36 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _featuresMargin, Math.Round(value, 2)); } + public ushort StaircaseThickness + { + get => _staircaseThickness; + set => RaiseAndSetIfChanged(ref _staircaseThickness, value); + } + + public bool CounterTrianglesEnabled + { + get => _counterTrianglesEnabled; + set => RaiseAndSetIfChanged(ref _counterTrianglesEnabled, value); + } + + public sbyte CounterTrianglesTipOffset + { + get => _counterTrianglesTipOffset; + set => RaiseAndSetIfChanged(ref _counterTrianglesTipOffset, value); + } + + public bool CounterTrianglesFence + { + get => _counterTrianglesFence; + set => RaiseAndSetIfChanged(ref _counterTrianglesFence, value); + } + + public bool HolesEnabled + { + get => _holesEnabled; + set => RaiseAndSetIfChanged(ref _holesEnabled, value); + } + public CalibrateExposureFinderShapes HoleShape { get => _holeShape; @@ -375,6 +443,11 @@ namespace UVtools.Core.Operations { get { + if (!_holesEnabled) + { + return Array.Empty(); + } + List holes = new(); if (_unitOfMeasure == Measures.Millimeters) @@ -387,7 +460,7 @@ namespace UVtools.Core.Operations var mmPx = (int)Math.Floor(mm * Ppmm); if (mmPx is <= 0 or > 500) continue; if(holes.Contains(mmPx)) continue; - holes.Add((int)Math.Floor(mm * Ppmm)); + holes.Add(mmPx); } } else @@ -407,12 +480,18 @@ namespace UVtools.Core.Operations } } - public int GetHolesLength(int[] holes) + public int GetHolesHeight(int[] holes) { if (holes.Length == 0) return 0; return (int) (holes.Sum() + (holes.Length-1) * _featuresMargin * Yppmm); } + public bool BarsEnabled + { + get => _barsEnabled; + set => RaiseAndSetIfChanged(ref _barsEnabled, value); + } + public decimal BarSpacing { get => _barSpacing; @@ -437,7 +516,7 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _barFenceThickness, value); } - public byte BarFenceOffset + public sbyte BarFenceOffset { get => _barFenceOffset; set => RaiseAndSetIfChanged(ref _barFenceOffset, value); @@ -462,6 +541,11 @@ namespace UVtools.Core.Operations { get { + if (!_barsEnabled) + { + return Array.Empty(); + } + List bars = new(); if (_unitOfMeasure == Measures.Millimeters) @@ -472,9 +556,9 @@ namespace UVtools.Core.Operations if (string.IsNullOrWhiteSpace(mmStr)) continue; if (!decimal.TryParse(mmStr, out var mm)) continue; var mmPx = (int)Math.Floor(mm * Xppmm); - if (mmPx <= 0 || mmPx > 500) continue; + if (mmPx is <= 0 or > 500) continue; if (bars.Contains(mmPx)) continue; - bars.Add((int)Math.Floor(mm * Xppmm)); + bars.Add(mmPx); } } else @@ -500,11 +584,17 @@ namespace UVtools.Core.Operations int len = (int) (bars.Sum() + (bars.Length + 1) * _barSpacing * Yppmm); if (_barFenceThickness > 0) { - len += _barFenceThickness * 2 + _barFenceOffset * 2; + len = Math.Max(len, len + _barFenceThickness * 2 + _barFenceOffset * 2); } return len; } + public bool TextEnabled + { + get => _textEnabled; + set => RaiseAndSetIfChanged(ref _textEnabled, value); + } + public static Array TextFonts => Enum.GetValues(typeof(FontFace)); public FontFace TextFont @@ -535,7 +625,7 @@ namespace UVtools.Core.Operations { get { - if (string.IsNullOrWhiteSpace(_text)) return Size.Empty; + if (!_textEnabled || string.IsNullOrWhiteSpace(_text)) return Size.Empty; int baseline = 0; return CvInvoke.GetTextSize(_text, _textFont, _textScale, _textThickness, ref baseline); } @@ -728,6 +818,111 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _exposureTable, value); } + public bool BullsEyeEnabled + { + get => _bullsEyeEnabled; + set => RaiseAndSetIfChanged(ref _bullsEyeEnabled, value); + } + + public string BullsEyeConfigurationPx + { + get => _bullsEyeConfigurationPx; + set => RaiseAndSetIfChanged(ref _bullsEyeConfigurationPx, value); + } + + public string BullsEyeConfigurationMm + { + get => _bullsEyeConfigurationMm; + set => RaiseAndSetIfChanged(ref _bullsEyeConfigurationMm, value); + } + + public byte BullsEyeFenceThickness + { + get => _bullsEyeFenceThickness; + set => RaiseAndSetIfChanged(ref _bullsEyeFenceThickness, value); + } + + public sbyte BullsEyeFenceOffset + { + get => _bullsEyeFenceOffset; + set => RaiseAndSetIfChanged(ref _bullsEyeFenceOffset, value); + } + + public bool BullsEyeInvertQuadrants + { + get => _bullsEyeInvertQuadrants; + set => RaiseAndSetIfChanged(ref _bullsEyeInvertQuadrants, value); + } + + /// + /// Gets all holes in pixels and ordered + /// + public BullsEyeCircle[] BullsEyes + { + get + { + if (!_bullsEyeEnabled) + { + return Array.Empty(); + } + + List bulleyes = new(); + + if (_unitOfMeasure == Measures.Millimeters) + { + var splitGroup = _bullsEyeConfigurationMm.Split(',', StringSplitOptions.TrimEntries); + foreach (var group in splitGroup) + { + var splitDiameterThickness = group.Split(':', StringSplitOptions.TrimEntries); + if (splitDiameterThickness.Length < 2) continue; + + if (string.IsNullOrWhiteSpace(splitDiameterThickness[0]) || + string.IsNullOrWhiteSpace(splitDiameterThickness[1])) continue; + if (!decimal.TryParse(splitDiameterThickness[0], out var diameterMm)) continue; + if (!decimal.TryParse(splitDiameterThickness[1], out var thicknessMm)) continue; + var diameter = (int)Math.Floor(diameterMm * Ppmm); + if (diameterMm is <= 0 or > 500) continue; + var thickness = (int)Math.Floor(thicknessMm * Ppmm); + if (thickness is <= 0 or > 500) continue; + if (bulleyes.Exists(circle => circle.Diameter == diameter)) continue; + bulleyes.Add(new BullsEyeCircle((ushort)diameter, (ushort)thickness)); + } + } + else + { + var splitGroup = _bullsEyeConfigurationPx.Split(',', StringSplitOptions.TrimEntries); + foreach (var group in splitGroup) + { + var splitDiameterThickness = group.Split(':', StringSplitOptions.TrimEntries); + if (splitDiameterThickness.Length < 2) continue; + + if (string.IsNullOrWhiteSpace(splitDiameterThickness[0]) || + string.IsNullOrWhiteSpace(splitDiameterThickness[1])) continue; + if (!int.TryParse(splitDiameterThickness[0], out var diameter)) continue; + if (!int.TryParse(splitDiameterThickness[1], out var thickness)) continue; + if (diameter is <= 0 or > 500) continue; + if (thickness is <= 0 or > 500) continue; + if (bulleyes.Exists(circle => circle.Diameter == diameter)) continue; + bulleyes.Add(new BullsEyeCircle((ushort) diameter, (ushort) thickness)); + } + } + + return bulleyes.OrderBy(circle => circle.Diameter).DistinctBy(circle => circle.Diameter).ToArray(); + } + } + public int GetBullsEyeMaxPanelDiameter(BullsEyeCircle[] bullseyes) + { + if (!_bullsEyeEnabled || bullseyes.Length == 0) return 0; + var diameter = GetBullsEyeMaxDiameter(bullseyes); + return Math.Max(diameter, diameter + _bullsEyeFenceThickness + _bullsEyeFenceOffset * 2); + } + + public int GetBullsEyeMaxDiameter(BullsEyeCircle[] bullseyes) + { + if (!_bullsEyeEnabled || bullseyes.Length == 0) return 0; + return bullseyes[^1].Diameter + bullseyes[^1].Thickness / 2; + } + public bool PatternModel { get => _patternModel; @@ -831,7 +1026,7 @@ namespace UVtools.Core.Operations private bool Equals(OperationCalibrateExposureFinder other) { - return _displayWidth == other._displayWidth && _displayHeight == other._displayHeight && _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _chamferLayers == other._chamferLayers && _erodeBottomIterations == other._erodeBottomIterations && _partMargin == other._partMargin && _enableAntiAliasing == other._enableAntiAliasing && _mirrorOutput == other._mirrorOutput && _baseHeight == other._baseHeight && _featuresHeight == other._featuresHeight && _featuresMargin == other._featuresMargin && _unitOfMeasure == other._unitOfMeasure && _holeDiametersPx == other._holeDiametersPx && _holeDiametersMm == other._holeDiametersMm && _barSpacing == other._barSpacing && _barLength == other._barLength && _barVerticalSplitter == other._barVerticalSplitter && _barFenceThickness == other._barFenceThickness && _barFenceOffset == other._barFenceOffset && _barThicknessesPx == other._barThicknessesPx && _barThicknessesMm == other._barThicknessesMm && _textFont == other._textFont && _textScale.Equals(other._textScale) && _textThickness == other._textThickness && _text == other._text && _multipleBrightness == other._multipleBrightness && _multipleBrightnessExcludeFrom == other._multipleBrightnessExcludeFrom && _multipleBrightnessValues == other._multipleBrightnessValues && _multipleBrightnessGenExposureTime == other._multipleBrightnessGenExposureTime && _multipleLayerHeight == other._multipleLayerHeight && _multipleLayerHeightMaximum == other._multipleLayerHeightMaximum && _multipleLayerHeightStep == other._multipleLayerHeightStep && _dontLiftSamePositionedLayers == other._dontLiftSamePositionedLayers && _zeroLightOffSamePositionedLayers == other._zeroLightOffSamePositionedLayers && _multipleExposures == other._multipleExposures && _exposureGenType == other._exposureGenType && _exposureGenIgnoreBaseExposure == other._exposureGenIgnoreBaseExposure && _exposureGenBottomStep == other._exposureGenBottomStep && _exposureGenNormalStep == other._exposureGenNormalStep && _exposureGenTests == other._exposureGenTests && _exposureGenManualLayerHeight == other._exposureGenManualLayerHeight && _exposureGenManualBottom == other._exposureGenManualBottom && _exposureGenManualNormal == other._exposureGenManualNormal && Equals(_exposureTable, other._exposureTable) && _holeShape == other._holeShape && _patternModel == other._patternModel && _patternModelGlueBottomLayers == other._patternModelGlueBottomLayers; + return _displayWidth == other._displayWidth && _displayHeight == other._displayHeight && _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _chamferLayers == other._chamferLayers && _erodeBottomIterations == other._erodeBottomIterations && _partMargin == other._partMargin && _enableAntiAliasing == other._enableAntiAliasing && _mirrorOutput == other._mirrorOutput && _baseHeight == other._baseHeight && _featuresHeight == other._featuresHeight && _featuresMargin == other._featuresMargin && _staircaseThickness == other._staircaseThickness && _holesEnabled == other._holesEnabled && _holeShape == other._holeShape && _unitOfMeasure == other._unitOfMeasure && _holeDiametersPx == other._holeDiametersPx && _holeDiametersMm == other._holeDiametersMm && _barsEnabled == other._barsEnabled && _barSpacing == other._barSpacing && _barLength == other._barLength && _barVerticalSplitter == other._barVerticalSplitter && _barFenceThickness == other._barFenceThickness && _barFenceOffset == other._barFenceOffset && _barThicknessesPx == other._barThicknessesPx && _barThicknessesMm == other._barThicknessesMm && _textEnabled == other._textEnabled && _textFont == other._textFont && _textScale.Equals(other._textScale) && _textThickness == other._textThickness && _text == other._text && _multipleBrightness == other._multipleBrightness && _multipleBrightnessExcludeFrom == other._multipleBrightnessExcludeFrom && _multipleBrightnessValues == other._multipleBrightnessValues && _multipleBrightnessGenExposureTime == other._multipleBrightnessGenExposureTime && _multipleLayerHeight == other._multipleLayerHeight && _multipleLayerHeightMaximum == other._multipleLayerHeightMaximum && _multipleLayerHeightStep == other._multipleLayerHeightStep && _dontLiftSamePositionedLayers == other._dontLiftSamePositionedLayers && _zeroLightOffSamePositionedLayers == other._zeroLightOffSamePositionedLayers && _multipleExposures == other._multipleExposures && _exposureGenType == other._exposureGenType && _exposureGenIgnoreBaseExposure == other._exposureGenIgnoreBaseExposure && _exposureGenBottomStep == other._exposureGenBottomStep && _exposureGenNormalStep == other._exposureGenNormalStep && _exposureGenTests == other._exposureGenTests && _exposureGenManualLayerHeight == other._exposureGenManualLayerHeight && _exposureGenManualBottom == other._exposureGenManualBottom && _exposureGenManualNormal == other._exposureGenManualNormal && Equals(_exposureTable, other._exposureTable) && _bullsEyeEnabled == other._bullsEyeEnabled && _bullsEyeConfigurationPx == other._bullsEyeConfigurationPx && _bullsEyeConfigurationMm == other._bullsEyeConfigurationMm && _bullsEyeInvertQuadrants == other._bullsEyeInvertQuadrants && _counterTrianglesEnabled == other._counterTrianglesEnabled && _counterTrianglesTipOffset == other._counterTrianglesTipOffset && _counterTrianglesFence == other._counterTrianglesFence && _patternModel == other._patternModel && _bullsEyeFenceThickness == other._bullsEyeFenceThickness && _bullsEyeFenceOffset == other._bullsEyeFenceOffset && _patternModelGlueBottomLayers == other._patternModelGlueBottomLayers; } public override bool Equals(object obj) @@ -858,9 +1053,13 @@ namespace UVtools.Core.Operations hashCode.Add(_baseHeight); hashCode.Add(_featuresHeight); hashCode.Add(_featuresMargin); + hashCode.Add(_staircaseThickness); + hashCode.Add(_holesEnabled); + hashCode.Add((int) _holeShape); hashCode.Add((int) _unitOfMeasure); hashCode.Add(_holeDiametersPx); hashCode.Add(_holeDiametersMm); + hashCode.Add(_barsEnabled); hashCode.Add(_barSpacing); hashCode.Add(_barLength); hashCode.Add(_barVerticalSplitter); @@ -868,6 +1067,7 @@ namespace UVtools.Core.Operations hashCode.Add(_barFenceOffset); hashCode.Add(_barThicknessesPx); hashCode.Add(_barThicknessesMm); + hashCode.Add(_textEnabled); hashCode.Add((int) _textFont); hashCode.Add(_textScale); hashCode.Add(_textThickness); @@ -891,8 +1091,16 @@ namespace UVtools.Core.Operations hashCode.Add(_exposureGenManualBottom); hashCode.Add(_exposureGenManualNormal); hashCode.Add(_exposureTable); - hashCode.Add((int) _holeShape); + hashCode.Add(_bullsEyeEnabled); + hashCode.Add(_bullsEyeConfigurationPx); + hashCode.Add(_bullsEyeConfigurationMm); + hashCode.Add(_bullsEyeInvertQuadrants); + hashCode.Add(_counterTrianglesEnabled); + hashCode.Add(_counterTrianglesTipOffset); + hashCode.Add(_counterTrianglesFence); hashCode.Add(_patternModel); + hashCode.Add(_bullsEyeFenceThickness); + hashCode.Add(_bullsEyeFenceOffset); hashCode.Add(_patternModelGlueBottomLayers); return hashCode.ToHashCode(); } @@ -954,32 +1162,43 @@ namespace UVtools.Core.Operations ExposureTable = new(list); } - public Mat[] GetLayers() + public Mat[] GetLayers(bool isPreview = false) { var holes = Holes; - //if (holes.Length == 0) return null; var bars = Bars; + var bulleyes = BullsEyes; var textSize = TextSize; int featuresMarginX = (int)(Xppmm * _featuresMargin); int featuresMarginY = (int)(Yppmm * _featuresMargin); - int holePanelWidth = holes.Length > 0 ? featuresMarginX * 2 + holes[^1] : featuresMarginX / 2; + int holePanelWidth = holes.Length > 0 ? featuresMarginX * 2 + holes[^1] : 0; + int holePanelHeight = GetHolesHeight(holes); int barsPanelHeight = GetBarsLength(bars); - int yMaxSize = Math.Max(Math.Max(GetHolesLength(holes), barsPanelHeight), textSize.Width); + int bulleyesDiameter = GetBullsEyeMaxDiameter(bulleyes); + int bulleyesPanelDiameter = GetBullsEyeMaxPanelDiameter(bulleyes); + int bulleyesRadius = bulleyesDiameter / 2; + int yLeftMaxSize = _staircaseThickness + featuresMarginY + Math.Max(barsPanelHeight, textSize.Width) + bulleyesPanelDiameter; + int yRightMaxSize = _staircaseThickness + holePanelHeight + featuresMarginY * 2; + + int xSize = featuresMarginX; + int ySize = TextMarkingSpacing + featuresMarginY; - int xSize = holePanelWidth * 2; - int ySize = featuresMarginX * 3 + yMaxSize + TextMarkingSpacing; + if (barsPanelHeight > 0 || textSize.Width > 0) + { + yLeftMaxSize += featuresMarginY; + } int barLengthPx = (int) (_barLength * Xppmm); int barSpacingPx = (int) (_barSpacing * Yppmm); int barsPanelWidth = 0; + if (bars.Length > 0) { barsPanelWidth = barLengthPx * 2 + _barVerticalSplitter; if (_barFenceThickness > 0) { - barsPanelWidth += _barFenceThickness * 2 + _barFenceOffset * 2; + barsPanelWidth = Math.Max(barsPanelWidth, barsPanelWidth + _barFenceThickness * 2 + _barFenceOffset * 2); } xSize += barsPanelWidth + featuresMarginX; } @@ -989,11 +1208,32 @@ namespace UVtools.Core.Operations xSize += textSize.Height + featuresMarginX; } + int bullseyeYPos = yLeftMaxSize - bulleyesPanelDiameter / 2; + + if (bulleyes.Length > 0) + { + xSize = Math.Max(xSize, bulleyesPanelDiameter + featuresMarginX * 2); + yLeftMaxSize += featuresMarginY + 24; + } + + int bullseyeXPos = xSize / 2; + + if (holePanelWidth > 0) + { + xSize -= featuresMarginX; + } + + xSize += holePanelWidth; + int negativeSideWidth = xSize; + xSize += holePanelWidth; + int positiveSideWidth = xSize - holePanelWidth; - Rectangle rect = new(new Point(1, 1), new Size(xSize, ySize)); + ySize += Math.Max(yLeftMaxSize, yRightMaxSize+10); + + Rectangle rect = new(new Point(0, 0), new Size(xSize, ySize)); var layers = new Mat[2]; - layers[0] = EmguExtensions.InitMat(rect.Size.Inflate(2)); + layers[0] = EmguExtensions.InitMat(rect.Size); CvInvoke.Rectangle(layers[0], rect, EmguExtensions.WhiteByte, -1, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); layers[1] = layers[0].CloneBlank(); @@ -1004,13 +1244,23 @@ namespace UVtools.Core.Operations EmguExtensions.WhiteByte, -1, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); } - // Print holes + int xPos = 0; int yPos = 0; + + // Print staircase + if (isPreview && _staircaseThickness > 0) + { + CvInvoke.Rectangle(layers[1], + new Rectangle(0, 0, layers[1].Size.Width-holePanelWidth, _staircaseThickness), + EmguExtensions.WhiteByte, -1, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + + // Print holes for (var layerIndex = 0; layerIndex < layers.Length; layerIndex++) { var layer = layers[layerIndex]; - yPos = featuresMarginY; + yPos = featuresMarginY + _staircaseThickness; for (int i = 0; i < holes.Length; i++) { var diameter = holes[i]; @@ -1115,7 +1365,7 @@ namespace UVtools.Core.Operations // Print Zebra bars if (bars.Length > 0) { - int yStartPos = featuresMarginY; + int yStartPos = _staircaseThickness + featuresMarginY; int xStartPos = xPos; yPos = yStartPos + _barFenceThickness / 2 + _barFenceOffset; xPos += _barFenceThickness / 2 + _barFenceOffset; @@ -1155,12 +1405,128 @@ namespace UVtools.Core.Operations if (!textSize.IsEmpty) { CvInvoke.Rotate(layers[1], layers[1], RotateFlags.Rotate90CounterClockwise); - CvInvoke.PutText(layers[1], _text, new Point(featuresMarginX, layers[1].Height - barsPanelWidth - featuresMarginX * (barsPanelWidth > 0 ? 2 : 1)), _textFont, _textScale, EmguExtensions.WhiteByte, _textThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + CvInvoke.PutText(layers[1], _text, new Point(_staircaseThickness + featuresMarginX, layers[1].Height - barsPanelWidth - featuresMarginX * (barsPanelWidth > 0 ? 2 : 1)), _textFont, _textScale, EmguExtensions.WhiteByte, _textThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); CvInvoke.Rotate(layers[1], layers[1], RotateFlags.Rotate90Clockwise); } + // Print bullseye + if (bulleyes.Length > 0) + { + yPos = bullseyeYPos; + foreach (var circle in bulleyes) + { + CvInvoke.Circle(layers[1], new Point(bullseyeXPos, yPos), circle.Radius, EmguExtensions.WhiteByte, circle.Thickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + + if (_bullsEyeInvertQuadrants) + { + var matRoi1 = new Mat(layers[1], new Rectangle(bullseyeXPos, yPos - bulleyesRadius - 5, bulleyesRadius + 6, bulleyesRadius + 5)); + var matRoi2 = new Mat(layers[1], new Rectangle(bullseyeXPos - bulleyesRadius - 5, yPos, bulleyesRadius + 5, bulleyesRadius + 6)); + //using var mask = matRoi1.CloneBlank(); + + //CvInvoke.Circle(mask, new Point(mask.Width / 2, mask.Height / 2), bulleyesRadius, EmguExtensions.WhiteByte, -1, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + //CvInvoke.Circle(mask, new Point(mask.Width / 2, mask.Height / 2), BullsEyes[^1].Radius, EmguExtensions.WhiteByte, BullsEyes[^1].Thickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + + CvInvoke.BitwiseNot(matRoi1, matRoi1); + CvInvoke.BitwiseNot(matRoi2, matRoi2); + } + + if (_bullsEyeFenceThickness > 0) + { + CvInvoke.Rectangle(layers[1], + new Rectangle( + new Point( + bullseyeXPos - bulleyesRadius - 5 - _bullsEyeFenceOffset - _bullsEyeFenceThickness / 2, + yPos - bulleyesRadius - 5 - _bullsEyeFenceOffset - _bullsEyeFenceThickness / 2), + new Size( + bulleyesDiameter + 10 + _bullsEyeFenceOffset*2 + _bullsEyeFenceThickness, + bulleyesDiameter + 10 + _bullsEyeFenceOffset*2 + _bullsEyeFenceThickness)), + EmguExtensions.WhiteByte, + _bullsEyeFenceThickness, + _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + + + yPos += bulleyesRadius; + } + + if (isPreview) + { + var textHeightStart = layers[1].Height - featuresMarginY - TextMarkingSpacing; + CvInvoke.PutText(layers[1], $"{Microns}u", new Point(TextMarkingStartX, textHeightStart), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteByte, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + CvInvoke.PutText(layers[1], $"{_bottomExposure}s", new Point(TextMarkingStartX, textHeightStart + TextMarkingLineBreak), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteByte, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + CvInvoke.PutText(layers[1], $"{_normalExposure}s", new Point(TextMarkingStartX, textHeightStart + TextMarkingLineBreak * 2), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteByte, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + if (holes.Length > 0) + { + CvInvoke.PutText(layers[1], $"{Microns}u", new Point(layers[1].Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackByte, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + CvInvoke.PutText(layers[1], $"{_bottomExposure}s", new Point(layers[1].Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart + TextMarkingLineBreak), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackByte, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + CvInvoke.PutText(layers[1], $"{_normalExposure}s", new Point(layers[1].Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart + TextMarkingLineBreak * 2), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackByte, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + } + + if (negativeSideWidth >= 200 && _counterTrianglesEnabled) + { + xPos = 120; + int triangleHeight = TextMarkingSpacing + 19; + int triangleWidth = (negativeSideWidth - xPos - featuresMarginX) / 2; + int triangleWidthQuarter = triangleWidth / 4; + + if (triangleWidth > 5) + { + yPos = layers[1].Height - featuresMarginY - triangleHeight + 1; + int yHalfPos = yPos + triangleHeight / 2; + int yPosEnd = layers[1].Height - featuresMarginY + 1; + + var triangles = new Point[4][]; + + triangles[0] = new Point[] // Left + { + new(xPos, yPos), // Top Left + new(xPos + triangleWidth, yHalfPos), // Middle + new(xPos, yPosEnd), // Bottom Left + }; + triangles[1] = new Point[] // Right + { + new(xPos + triangleWidth * 2, yPos), // Top Right + new(xPos + triangleWidth, yHalfPos), // Middle + new(xPos + triangleWidth * 2, yPosEnd), // Bottom Right + }; + triangles[2] = new Point[] // Top + { + new(xPos + triangleWidth - triangleWidthQuarter, yPos), // Top Left + new(xPos + triangleWidth + triangleWidthQuarter, yPos), // Top Right + new(xPos + triangleWidth, yHalfPos - _counterTrianglesTipOffset), // Middle + }; + triangles[3] = new Point[] // Bottom + { + new(xPos + triangleWidth - triangleWidthQuarter, yPosEnd), // Bottom Left + new(xPos + triangleWidth + triangleWidthQuarter, yPosEnd), // Bottom Right + new(xPos + triangleWidth, yHalfPos + _counterTrianglesTipOffset), // Middle + }; + + foreach (var triangle in triangles) + { + using var vec = new VectorOfPoint(triangle); + CvInvoke.FillPoly(layers[1], vec, EmguExtensions.WhiteByte, + _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + + + if (_counterTrianglesFence) + { + byte outlineThickness = 8; + //byte outlineThicknessHalf = (byte)(outlineThickness / 2); + + CvInvoke.Rectangle(layers[1], new Rectangle( + new Point(triangles[0][0].X - 0, triangles[0][0].Y - 0), + new Size(triangleWidth * 2 + 0, triangleHeight + 0) + ), EmguExtensions.WhiteByte, outlineThickness, + _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + } + } // Print a hardcoded spiral if have space - if (positiveSideWidth >= 250) + /*if (positiveSideWidth >= 250000) { var mat = layers[0].CloneBlank(); var matMask = layers[0].CloneBlank(); @@ -1177,12 +1543,6 @@ namespace UVtools.Core.Operations count++; CvInvoke.Circle(mat, new Point(xPos, yPos), radius, EmguExtensions.WhiteByte, circleThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); maxRadius = radius; - /*for (int i = 0; i < 360; i+=90) - { - CvInvoke.Ellipse(layers[1], new Point(xPos, yPos), new Size(radius, radius), 0, i, i+90, white ? EmguExtensions.WhiteByte : EmguExtensions.BlackByte, 5, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - white = !white; - } - white = !white;*/ } CvInvoke.Circle(mat, new Point(xPos, yPos), 5, EmguExtensions.WhiteByte, -1, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); @@ -1201,7 +1561,7 @@ namespace UVtools.Core.Operations mat.Dispose(); matMask.Dispose(); - } + }*/ return layers; } @@ -1393,7 +1753,7 @@ namespace UVtools.Core.Operations SlicerFile.LayerManager.Layers = layers.ToArray(); }); } - else + else // No patterned { var layers = GetLayers(); if (layers is null) return false; @@ -1416,6 +1776,8 @@ namespace UVtools.Core.Operations int featuresMarginY = (int)(Yppmm * _featuresMargin); var holes = Holes; + int holePanelWidth = holes.Length > 0 ? featuresMarginX * 2 + holes[^1] : 0; + int staircaseWidth = layers[0].Width - holePanelWidth; var brightnesses = MultipleBrightnessValuesArray; if (brightnesses.Length == 0 || !_multipleBrightness) brightnesses = new[] { byte.MaxValue }; @@ -1430,7 +1792,8 @@ namespace UVtools.Core.Operations if (!layerDifference.IsInteger()) return; // Not at right height to process with layer height //Debug.WriteLine($"{currentHeight} / {layerHeight} = {layerDifference}, Floor={Math.Floor(layerDifference)}"); - + int firstFeatureLayer = (int)Math.Floor(_baseHeight / layerHeight); + int lastLayer = (int)Math.Floor((_baseHeight + _featuresHeight) / layerHeight); int layerCountOnHeight = (int)Math.Floor(currentHeight / layerHeight); bool isBottomLayer = layerCountOnHeight <= _bottomLayers; bool isBaseLayer = currentHeight <= _baseHeight; @@ -1483,6 +1846,20 @@ namespace UVtools.Core.Operations layers[isBaseLayer ? 0 : 1].CopyTo(matRoi); + if (!isBaseLayer && _staircaseThickness > 0) + { + int staircaseWidthIncrement = (int) Math.Ceiling(staircaseWidth / (_featuresHeight / layerHeight-1)); + int staircaseLayer = layerCountOnHeight - firstFeatureLayer - 1; + int staircaseWidthForLayer = staircaseWidth - staircaseWidthIncrement * staircaseLayer; + if (staircaseWidthForLayer >= 0 && layerCountOnHeight != lastLayer) + { + CvInvoke.Rectangle(matRoi, + new Rectangle(staircaseWidth - staircaseWidthForLayer, 0, staircaseWidthForLayer, _staircaseThickness), + EmguExtensions.WhiteByte, -1, + _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + } + } + if (isBottomLayer && _erodeBottomIterations > 0) { CvInvoke.Erode(matRoi, matRoi, kernel, anchor, _erodeBottomIterations, BorderType.Reflect101, default); diff --git a/UVtools.Core/Operations/OperationDynamicLifts.cs b/UVtools.Core/Operations/OperationDynamicLifts.cs new file mode 100644 index 0000000..03c7412 --- /dev/null +++ b/UVtools.Core/Operations/OperationDynamicLifts.cs @@ -0,0 +1,344 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Linq; +using System.Text; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; + +namespace UVtools.Core.Operations +{ + [Serializable] + public sealed class OperationDynamicLifts : Operation + { + private float _minBottomLiftHeight; + private float _maxBottomLiftHeight; + private float _minLiftHeight; + private float _maxLiftHeight; + private float _minBottomLiftSpeed; + private float _maxBottomLiftSpeed; + private float _minLiftSpeed; + private float _maxLiftSpeed; + private bool _updateLightOffDelay = true; + private float _lightOffDelayBottomExtraTime = 3; + private float _lightOffDelayExtraTime = 2.5f; + + #region Members + + #endregion + + #region Overrides + + public override string Title => "Dynamic lifts"; + + public override string Description => + "Generate dynamic lift height and speeds for each layer given it mass\n" + + "Larger masses requires more lift height and less speed while smaller masses can go with shorter lift height and more speed.\n" + + "If you have a raft, start after it layer number to not influence the calculations.\n" + + "Note: Only few printers support this. Running this on an unsupported printer will cause no harm."; + + public override string ConfirmationText => + $"generate dynamic lifts from layers {LayerIndexStart} through {LayerIndexEnd}?"; + + public override string ProgressTitle => + $"Generating dynamic lifts from layers {LayerIndexStart} through {LayerIndexEnd}"; + + public override string ProgressAction => "Generated lifts"; + + public override string ValidateInternally() + { + var sb = new StringBuilder(); + + if (_minBottomLiftHeight > _maxBottomLiftHeight) + { + sb.AppendLine("Minimum bottom lift height can't be higher than the maximum."); + } + if (_minBottomLiftSpeed > _maxBottomLiftSpeed) + { + sb.AppendLine("Minimum bottom lift speed can't be higher than the maximum."); + } + + if (_minLiftHeight > _maxLiftHeight) + { + sb.AppendLine("Minimum lift height can't be higher than the maximum."); + } + if (_minLiftSpeed > _maxLiftSpeed) + { + sb.AppendLine("Minimum lift speed can't be higher than the maximum."); + } + + if (_minBottomLiftHeight == _maxBottomLiftHeight && + _minBottomLiftSpeed == _maxBottomLiftSpeed && + _minLiftHeight == _maxLiftHeight && + _minLiftSpeed == _maxLiftSpeed) + { + sb.AppendLine("The selected min/max settings are all equal and will not produce a change."); + } + + return sb.ToString(); + } + + public override string ToString() + { + var result = + $"[Bottom height: {_minBottomLiftHeight}/{_maxBottomLiftHeight}mm]" + + $" [Bottom speed: {_minBottomLiftSpeed}/{_maxBottomLiftSpeed}mm/min]" + + $" [Height: {_minLiftHeight}/{_maxLiftHeight}mm]" + + $" [Speed: {_minLiftSpeed}/{_maxLiftSpeed}mm/min]" + + $" [Light-off: {_updateLightOffDelay} {_lightOffDelayBottomExtraTime}/{_lightOffDelayExtraTime}s]" + + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + + #endregion + + #region Properties + + public float MinBottomLiftHeight + { + get => _minBottomLiftHeight; + set => RaiseAndSetIfChanged(ref _minBottomLiftHeight, (float)Math.Round(value, 2)); + } + + public float MaxBottomLiftHeight + { + get => _maxBottomLiftHeight; + set => RaiseAndSetIfChanged(ref _maxBottomLiftHeight, (float)Math.Round(value, 2)); + } + + public float MinLiftHeight + { + get => _minLiftHeight; + set => RaiseAndSetIfChanged(ref _minLiftHeight, (float)Math.Round(value, 2)); + } + + public float MaxLiftHeight + { + get => _maxLiftHeight; + set => RaiseAndSetIfChanged(ref _maxLiftHeight, (float)Math.Round(value, 2)); + } + + public float MinBottomLiftSpeed + { + get => _minBottomLiftSpeed; + set => RaiseAndSetIfChanged(ref _minBottomLiftSpeed, (float)Math.Round(value, 2)); + } + + public float MaxBottomLiftSpeed + { + get => _maxBottomLiftSpeed; + set => RaiseAndSetIfChanged(ref _maxBottomLiftSpeed, (float)Math.Round(value, 2)); + } + + public float MinLiftSpeed + { + get => _minLiftSpeed; + set => RaiseAndSetIfChanged(ref _minLiftSpeed, (float)Math.Round(value, 2)); + } + + public float MaxLiftSpeed + { + get => _maxLiftSpeed; + set => RaiseAndSetIfChanged(ref _maxLiftSpeed, (float)Math.Round(value, 2)); + } + + public bool UpdateLightOffDelay + { + get => _updateLightOffDelay; + set => RaiseAndSetIfChanged(ref _updateLightOffDelay, value); + } + + public float LightOffDelayBottomExtraTime + { + get => _lightOffDelayBottomExtraTime; + set => RaiseAndSetIfChanged(ref _lightOffDelayBottomExtraTime, (float)Math.Round(value, 2)); + } + + public float LightOffDelayExtraTime + { + get => _lightOffDelayExtraTime; + set => RaiseAndSetIfChanged(ref _lightOffDelayExtraTime, (float)Math.Round(value, 2)); + } + + //public uint MinBottomLayerPixels => SlicerFile.Where(layer => layer.IsBottomLayer && !layer.IsEmpty && layer.Index >= LayerIndexStart && layer.Index <= LayerIndexEnd).Max(layer => layer.NonZeroPixelCount); + public uint MinBottomLayerPixels => (from layer in SlicerFile + where layer.IsBottomLayer + where !layer.IsEmpty + where layer.Index >= LayerIndexStart + where layer.Index <= LayerIndexEnd + select layer.NonZeroPixelCount).Min(); + + //public uint MinNormalLayerPixels => SlicerFile.Where(layer => layer.IsNormalLayer && !layer.IsEmpty && layer.Index >= LayerIndexStart && layer.Index <= LayerIndexEnd).Max(layer => layer.NonZeroPixelCount); + public uint MinNormalLayerPixels => (from layer in SlicerFile + where layer.IsNormalLayer + where !layer.IsEmpty + where layer.Index >= LayerIndexStart + where layer.Index <= LayerIndexEnd + select layer.NonZeroPixelCount).Min(); + + //public uint MaxBottomLayerPixels => SlicerFile.Where(layer => layer.IsBottomLayer && layer.Index >= LayerIndexStart && layer.Index <= LayerIndexEnd).Max(layer => layer.NonZeroPixelCount); + public uint MaxBottomLayerPixels => (from layer in SlicerFile + where layer.IsBottomLayer + where !layer.IsEmpty + where layer.Index >= LayerIndexStart + where layer.Index <= LayerIndexEnd + select layer.NonZeroPixelCount).Max(); + //public uint MaxNormalLayerPixels => SlicerFile.Where(layer => layer.IsNormalLayer && layer.Index >= LayerIndexStart && layer.Index <= LayerIndexEnd).Max(layer => layer.NonZeroPixelCount); + public uint MaxNormalLayerPixels => (from layer in SlicerFile + where layer.IsNormalLayer + where !layer.IsEmpty + where layer.Index >= LayerIndexStart + where layer.Index <= LayerIndexEnd + select layer.NonZeroPixelCount).Max(); + + public Layer MinBottomLayer => SlicerFile.Where(layer => layer.IsBottomLayer && !layer.IsEmpty).OrderBy(layer => layer.NonZeroPixelCount).First(); + public Layer MaxBottomLayer => SlicerFile.Where(layer => layer.IsBottomLayer).OrderByDescending(layer => layer.NonZeroPixelCount).First(); + public Layer MinLayer => SlicerFile.Where(layer => layer.IsNormalLayer && !layer.IsEmpty).OrderBy(layer => layer.NonZeroPixelCount).First(); + public Layer MaxLayer => SlicerFile.Where(layer => layer.IsNormalLayer).OrderByDescending(layer => layer.NonZeroPixelCount).First(); + + #endregion + + #region Constructor + + public OperationDynamicLifts() + { } + + public OperationDynamicLifts(FileFormat slicerFile) : base(slicerFile) + { + _minBottomLiftHeight = _maxBottomLiftHeight = SlicerFile.BottomLiftHeight; + _minLiftHeight = _maxLiftHeight = SlicerFile.LiftHeight; + + _minBottomLiftSpeed = _maxBottomLiftSpeed = SlicerFile.BottomLiftSpeed; + _minLiftSpeed = _maxLiftSpeed = SlicerFile.LiftSpeed; + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + uint minBottomPixels = 0; + uint minNormalPixels = 0; + uint maxBottomPixels = 0; + uint maxNormalPixels = 0; + + try + { + minBottomPixels = MinBottomLayerPixels; + } + catch + { + } + + try + { + minNormalPixels = MinNormalLayerPixels; + } + catch + { + } + + try + { + maxBottomPixels = MaxBottomLayerPixels; + } + catch + { + } + + try + { + maxNormalPixels = MaxNormalLayerPixels; + } + catch + { + } + + float liftHeight; + float liftSpeed; + + uint max = (from layer in SlicerFile where !layer.IsBottomLayer where !layer.IsEmpty where layer.Index >= LayerIndexStart where layer.Index <= LayerIndexEnd select layer).Aggregate(0, (current, layer) => Math.Max(layer.NonZeroPixelCount, current)); + + for (uint layerIndex = LayerIndexStart; layerIndex <= LayerIndexEnd; layerIndex++) + { + progress.Token.ThrowIfCancellationRequested(); + var layer = SlicerFile[layerIndex]; + + // Height + // min - largestpixelcount + // x - pixelcount + + // Speed + // max - minpixelCount + // x - pixelcount + + if (layer.IsBottomLayer) + { + liftHeight = (_maxBottomLiftHeight * layer.NonZeroPixelCount / maxBottomPixels).Clamp(_minBottomLiftHeight, _maxBottomLiftHeight); + liftSpeed = (_maxBottomLiftSpeed - (_maxBottomLiftSpeed * layer.NonZeroPixelCount / maxNormalPixels)).Clamp(_minBottomLiftSpeed, _maxBottomLiftSpeed); + } + else + { + liftHeight = (_maxLiftHeight * layer.NonZeroPixelCount / maxNormalPixels).Clamp(_minLiftHeight, _maxLiftHeight); + liftSpeed = (_maxLiftSpeed - (_maxLiftSpeed * layer.NonZeroPixelCount / maxNormalPixels)).Clamp(_minLiftSpeed, _maxLiftSpeed); + } + + layer.LiftHeight = (float) Math.Round(liftHeight, 2); + layer.LiftSpeed = (float) Math.Round(liftSpeed, 2); + + if (_updateLightOffDelay) + { + layer.UpdateLightOffDelay(layer.IsBottomLayer ? _lightOffDelayBottomExtraTime : _lightOffDelayExtraTime); + } + + progress++; + } + + SlicerFile.UpdatePrintTime(); + + return !progress.Token.IsCancellationRequested; + } + + + #endregion + + #region Equality + + private bool Equals(OperationDynamicLifts other) + { + return _minBottomLiftHeight.Equals(other._minBottomLiftHeight) && _maxBottomLiftHeight.Equals(other._maxBottomLiftHeight) && _minLiftHeight.Equals(other._minLiftHeight) && _maxLiftHeight.Equals(other._maxLiftHeight) && _minBottomLiftSpeed.Equals(other._minBottomLiftSpeed) && _maxBottomLiftSpeed.Equals(other._maxBottomLiftSpeed) && _minLiftSpeed.Equals(other._minLiftSpeed) && _maxLiftSpeed.Equals(other._maxLiftSpeed) && _updateLightOffDelay == other._updateLightOffDelay && _lightOffDelayBottomExtraTime.Equals(other._lightOffDelayBottomExtraTime) && _lightOffDelayExtraTime.Equals(other._lightOffDelayExtraTime); + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is OperationDynamicLifts other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(_minBottomLiftHeight); + hashCode.Add(_maxBottomLiftHeight); + hashCode.Add(_minLiftHeight); + hashCode.Add(_maxLiftHeight); + hashCode.Add(_minBottomLiftSpeed); + hashCode.Add(_maxBottomLiftSpeed); + hashCode.Add(_minLiftSpeed); + hashCode.Add(_maxLiftSpeed); + hashCode.Add(_updateLightOffDelay); + hashCode.Add(_lightOffDelayBottomExtraTime); + hashCode.Add(_lightOffDelayExtraTime); + return hashCode.ToHashCode(); + } + + #endregion + } +} diff --git a/UVtools.Core/Operations/OperationLayerExportGif.cs b/UVtools.Core/Operations/OperationLayerExportGif.cs new file mode 100644 index 0000000..e863f89 --- /dev/null +++ b/UVtools.Core/Operations/OperationLayerExportGif.cs @@ -0,0 +1,344 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AnimatedGif; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; + +namespace UVtools.Core.Operations +{ + [Serializable] + public sealed class OperationLayerExportGif : Operation + { + public enum ExportGifRotateDirection : byte + { + None = 9, + /// Rotate 90 degrees clockwise + [Description("Rotate 90º CW")] + Rotate90Clockwise = 0, + /// Rotate 180 degrees clockwise + [Description("Rotate 180º")] + Rotate180 = 1, + /// Rotate 270 degrees clockwise + [Description("Rotate 90º CCW")] + Rotate90CounterClockwise = 2, + } + + public enum ExportGifFlipDirection : byte + { + None, + Horizontally, + Vertically, + Both, + } + + + #region Members + private string _filePath; + private bool _clipByVolumeBounds; + private bool _renderLayerCount = true; + private byte _fps = 30; + private ushort _repeats; + private ushort _skip; + private decimal _scale = 50; + private ExportGifRotateDirection _rotateDirection = ExportGifRotateDirection.None; + private ExportGifFlipDirection _flipDirection = ExportGifFlipDirection.None; + + #endregion + + #region Overrides + + public override string Title => "Export layers to GIF"; + + public override string Description => + "Export a layer range to an animated GIF file\n" + + "Note: This process is slow, optimize the parameters to output few layers as possible and/or scale them down."; + + public override string ConfirmationText => + $"export layers {LayerIndexStart} through {LayerIndexEnd} and pack {TotalLayers} layers?"; + + public override string ProgressTitle => + $"Exporting layers {LayerIndexStart} through {LayerIndexEnd}"; + + public override string ProgressAction => "Exported layers"; + + public override string ValidateInternally() + { + var sb = new StringBuilder(); + + if (TotalLayers == 0) + { + sb.AppendLine("There are no layers to pack, please adjust the configurations."); + } + /*else if (TotalLayers > 500) + { + sb.AppendLine("Packing more than 500 layers will cause most of the systems and browsers to not play the GIF animation.\n" + + "For that reason the pack is limited to 500 maximum layers.\n" + + "Use the 'Skip layers' option or adjust the layer range to limit the number of layers in the pack."); + }*/ + + return sb.ToString(); + } + + public override string ToString() + { + var result = $"[Clip bounds: {_clipByVolumeBounds}]" + + $" [Render count: {_renderLayerCount}]" + + $" [FPS: {_fps}]" + + $" [Repeats: {_repeats}]" + + $" [Skip: {_skip}]" + + $" [Scale: {_scale}%]" + + $" [Rotate: {_rotateDirection}]" + + $" [Flip: {_flipDirection}]" + + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(LayerRangeCount)) + { + RaisePropertyChanged(nameof(TotalLayers)); + RaisePropertyChanged(nameof(GifDurationMilliseconds)); + RaisePropertyChanged(nameof(GifDurationSeconds)); + } + base.OnPropertyChanged(e); + } + + #endregion + + #region Properties + + public string FilePath + { + get => _filePath; + set => RaiseAndSetIfChanged(ref _filePath, value); + } + + public bool ClipByVolumeBounds + { + get => _clipByVolumeBounds; + set => RaiseAndSetIfChanged(ref _clipByVolumeBounds, value); + } + + public bool RenderLayerCount + { + get => _renderLayerCount; + set => RaiseAndSetIfChanged(ref _renderLayerCount, value); + } + + public byte FPS + { + get => _fps; + set + { + if(!RaiseAndSetIfChanged(ref _fps, value)) return; + RaisePropertyChanged(nameof(FPSToMilliseconds)); + RaisePropertyChanged(nameof(GifDurationMilliseconds)); + RaisePropertyChanged(nameof(GifDurationSeconds)); + } + } + + public int FPSToMilliseconds => (int) Math.Floor(1000.0 / _fps); + + public ushort Repeats + { + get => _repeats; + set => RaiseAndSetIfChanged(ref _repeats, value); + } + + public ushort Skip + { + get => _skip; + set + { + if(!RaiseAndSetIfChanged(ref _skip, value)) return; + RaisePropertyChanged(nameof(TotalLayers)); + RaisePropertyChanged(nameof(GifDurationMilliseconds)); + RaisePropertyChanged(nameof(GifDurationSeconds)); + } + } + + public uint TotalLayers => (uint) Math.Floor(LayerRangeCount / (float) (_skip + 1)); + + public uint GifDurationMilliseconds => (uint)(TotalLayers * FPSToMilliseconds); + public float GifDurationSeconds => (float)Math.Round(GifDurationMilliseconds / 1000.0, 2); + + public decimal Scale + { + get => _scale; + set => RaiseAndSetIfChanged(ref _scale, Math.Round(value, 2)); + } + + public float ScaleFactor => (float)_scale / 100f; + + public ExportGifRotateDirection RotateDirection + { + get => _rotateDirection; + set => RaiseAndSetIfChanged(ref _rotateDirection, value); + } + + public ExportGifFlipDirection FlipDirection + { + get => _flipDirection; + set => RaiseAndSetIfChanged(ref _flipDirection, value); + } + + #endregion + + #region Constructor + + public OperationLayerExportGif() + { } + + public OperationLayerExportGif(FileFormat slicerFile) : base(slicerFile) + { + _filePath = SlicerFile.FileFullPath + ".gif"; + + _skip = TotalLayers switch + { + > 5000 => 2, + > 1000 => 1, + _ => _skip + }; + /*while (TotalLayers > 500) + { + _skip++; + }*/ + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + using var gif = AnimatedGif.AnimatedGif.Create(_filePath, FPSToMilliseconds, _repeats); + var layerBuffer = new byte[TotalLayers][]; + progress.Reset("Optimized layers", TotalLayers); + + var fontFace = FontFace.HersheyDuplex; + float fontScale = 1.5f; + int fontThickness = 2; + MCvScalar textColor = new(200); + + if (_clipByVolumeBounds) + { + ROI = SlicerFile.BoundingRectangle; + } + + Parallel.For(0, TotalLayers, i => + { + if (progress.Token.IsCancellationRequested) return; + uint layerIndex = (uint) (LayerIndexStart + i * (_skip + 1)); + var layer = SlicerFile[layerIndex]; + using var mat = layer.LayerMat; + //using var matOriginal = mat.Clone(); + var matRoi = GetRoiOrDefault(mat); + + if (_scale != 100) + { + CvInvoke.Resize(matRoi, matRoi, new Size((int) (matRoi.Width * ScaleFactor), (int)(matRoi.Height * ScaleFactor))); + } + + if (_flipDirection != ExportGifFlipDirection.None) + { + var flipType = _flipDirection switch + { + ExportGifFlipDirection.Horizontally => FlipType.Horizontal, + ExportGifFlipDirection.Vertically => FlipType.Vertical, + ExportGifFlipDirection.Both => FlipType.Horizontal | FlipType.Vertical, + _ => FlipType.None + }; + + if (flipType != FlipType.None) + CvInvoke.Flip(matRoi, matRoi, flipType); + } + + if (_rotateDirection != ExportGifRotateDirection.None) + { + CvInvoke.Rotate(matRoi, matRoi, (RotateFlags) _rotateDirection); + } + + if (_renderLayerCount) + { + int baseLine = 0; + var text = $"{layerIndex.ToString().PadLeft(SlicerFile.LayerCount.ToString().Length, '0')}/{SlicerFile.LayerCount-1}"; + var fontSize = CvInvoke.GetTextSize(text, fontFace, fontScale, fontThickness, ref baseLine); + + Point point = new( + matRoi.Width / 2 - fontSize.Width / 2, + 70); + CvInvoke.PutText(matRoi, text, point, fontFace, fontScale, textColor, fontThickness, LineType.AntiAlias); + } + + //ApplyMask(matOriginal, matRoi); + + layerBuffer[i] = matRoi.GetPngByes(); + + progress.LockAndIncrement(); + }); + + progress.ResetNameAndProcessed("Packed layers"); + foreach (var buffer in layerBuffer) + { + if (progress.Token.IsCancellationRequested) break; + using Stream stream = new MemoryStream(buffer); + using var img = Image.FromStream(stream); + gif.AddFrame(img, -1, GifQuality.Bit8); + progress++; + } + + if (progress.Token.IsCancellationRequested) + { + try + { + File.Delete(_filePath); + } + catch + { + // ignored + } + } + + return !progress.Token.IsCancellationRequested; + } + + + #endregion + + #region Equality + + private bool Equals(OperationLayerExportGif other) + { + return _clipByVolumeBounds == other._clipByVolumeBounds && _renderLayerCount == other._renderLayerCount && _fps == other._fps && _skip == other._skip && _scale == other._scale && _rotateDirection == other._rotateDirection && _flipDirection == other._flipDirection && _repeats == other._repeats; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is OperationLayerExportGif other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(_clipByVolumeBounds, _renderLayerCount, _fps, _skip, _scale, (int) _rotateDirection, (int) _flipDirection, _repeats); + } + + #endregion + } +} diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 0ab2bb8..72fcd10 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ https://github.com/sn4k3/UVtools https://github.com/sn4k3/UVtools MSLA/DLP, file analysis, calibration, repair, conversion and manipulation - 2.9.3 + 2.10.0 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 @@ -46,6 +46,7 @@ + diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index a033dd9..b80defb 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -8,6 +8,9 @@ + + + diff --git a/UVtools.ScriptSample/ScriptTester.cs b/UVtools.ScriptSample/ScriptTester.cs new file mode 100644 index 0000000..2027740 --- /dev/null +++ b/UVtools.ScriptSample/ScriptTester.cs @@ -0,0 +1,68 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using UVtools.Core.Scripting; +using System.IO; +using UVtools.Core.Extensions; + +namespace UVtools.ScriptSample +{ + /// + /// Change layer properties to random values + /// + public class ScriptChangeLayesrPropertiesSample : ScriptGlobals + { + /// + /// Set configurations here, this function trigger just after load a script + /// + public void ScriptInit() + { + Script.Name = "Change layer properties"; + Script.Description = "Change layer properties to random values :D"; + Script.Author = "Tiago Conceição"; + Script.Version = new Version(0, 1); + } + + /// + /// Validate user inputs here, this function trigger when user click on execute + /// + /// A error message, empty or null if validation passes. + public string ScriptValidate() + { + return null; + } + + /// + /// Execute the script, this function trigger when when user click on execute and validation passes + /// + /// True if executes successfully to the end, otherwise false. + public bool ScriptExecute() + { + string path = @"c:\temp\UVToolAreaExp_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".txt"; + + Progress.Reset("Changing layers", Operation.LayerRangeCount); // Sets the progress name and number of items to process + + using var sw = File.CreateText(path); + for (uint layerIndex = Operation.LayerIndexStart; layerIndex <= Operation.LayerIndexEnd; layerIndex++) + { + Progress.Token.ThrowIfCancellationRequested(); // Abort operation, user requested cancellation + var layer = SlicerFile[layerIndex]; // Unpack and expose layer variable for easier use + + sw.WriteLine($@"{layerIndex}\{layer.NonZeroPixelCount}\{layer.BoundingRectangleMillimeters.Area()}"); + //sw.WriteLine(SlicerFile.GetName); + + Progress++; // Increment progress bar by 1 + } + sw.Close(); + + // return true if not cancelled by user + return !Progress.Token.IsCancellationRequested; + } + } +} \ No newline at end of file diff --git a/UVtools.WPF/Assets/Icons/angle-double-up-16x16.png b/UVtools.WPF/Assets/Icons/angle-double-up-16x16.png new file mode 100644 index 0000000..a444e32 Binary files /dev/null and b/UVtools.WPF/Assets/Icons/angle-double-up-16x16.png differ diff --git a/UVtools.WPF/Assets/Icons/gif-16x16.png b/UVtools.WPF/Assets/Icons/gif-16x16.png new file mode 100644 index 0000000..ac06f8c Binary files /dev/null and b/UVtools.WPF/Assets/Icons/gif-16x16.png differ diff --git a/UVtools.WPF/Assets/Styles/Styles.xaml b/UVtools.WPF/Assets/Styles/Styles.xaml index e11b340..a650e7c 100644 --- a/UVtools.WPF/Assets/Styles/Styles.xaml +++ b/UVtools.WPF/Assets/Styles/Styles.xaml @@ -15,7 +15,7 @@ diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml b/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml index 3a1e20a..612f590 100644 --- a/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml +++ b/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml @@ -195,7 +195,7 @@ - @@ -240,205 +240,363 @@ Text="mm"/> + + + + + Content="Pin (positive) / holes (negative):" + VerticalAlignment="Center" + IsChecked="{Binding Operation.HolesEnabled}"/> - - - - - - - - - - + Content="Zebra bars:" + VerticalAlignment="Center" + IsChecked="{Binding Operation.BarsEnabled}"/> - - - - - - - - - - - - - - - - - - - - + Content="Text:" + VerticalAlignment="Center" + IsChecked="{Binding Operation.TextEnabled}"/> - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml.cs b/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml.cs index 1da281f..1d0e252 100644 --- a/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml.cs +++ b/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml.cs @@ -71,7 +71,7 @@ namespace UVtools.WPF.Controls.Calibrators public void UpdatePreview() { - var layers = Operation.GetLayers(); + var layers = Operation.GetLayers(true); _previewImage?.Dispose(); if (layers is not null) { diff --git a/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml b/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml new file mode 100644 index 0000000..1a638c1 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml.cs new file mode 100644 index 0000000..8bd4573 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Markup.Xaml; +using UVtools.Core.FileFormats; +using UVtools.Core.Operations; +using UVtools.WPF.Extensions; + +namespace UVtools.WPF.Controls.Tools +{ + public class ToolDynamicLiftsControl : ToolControl + { + public OperationDynamicLifts Operation => BaseOperation as OperationDynamicLifts; + public ToolDynamicLiftsControl() + { + InitializeComponent(); + BaseOperation = new OperationDynamicLifts(SlicerFile); + if (!SlicerFile.HavePrintParameterPerLayerModifier(FileFormat.PrintParameterModifier.LiftHeight) || + !SlicerFile.HavePrintParameterPerLayerModifier(FileFormat.PrintParameterModifier.LiftSpeed)) + { + App.MainWindow.MessageBoxInfo("Your printer/format does not support this tool.", "Dynamic lifts - Printer not supported"); + CanRun = false; + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml b/UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml new file mode 100644 index 0000000..3fbed79 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml.cs new file mode 100644 index 0000000..8afc78a --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using UVtools.Core.Operations; + +namespace UVtools.WPF.Controls.Tools +{ + public class ToolLayerExportGifControl : ToolControl + { + public OperationLayerExportGif Operation => BaseOperation as OperationLayerExportGif; + public ToolLayerExportGifControl() + { + InitializeComponent(); + BaseOperation = new OperationLayerExportGif(SlicerFile); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public async void ChooseFilePath() + { + var dialog = new SaveFileDialog + { + Filters = new List + { + new() + { + Extensions = new List{"gif"}, + Name = "GIF files" + } + }, + InitialFileName = Path.GetFileName(SlicerFile.FileFullPath)+".gif", + Directory = Path.GetDirectoryName(SlicerFile.FileFullPath) + }; + var file = await dialog.ShowAsync(ParentWindow); + if (string.IsNullOrWhiteSpace(file)) return; + Operation.FilePath = file; + } + } +} diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 07fc6f5..84d074a 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -197,6 +197,14 @@ namespace UVtools.WPF } }, new() + { + Tag = new OperationDynamicLifts(), + Icon = new Avalonia.Controls.Image + { + Source = new Bitmap(App.GetAsset("/Assets/Icons/angle-double-up-16x16.png")) + } + }, + new() { Tag = new OperationLayerReHeight(), Icon = new Avalonia.Controls.Image @@ -316,6 +324,14 @@ namespace UVtools.WPF Source = new Bitmap(App.GetAsset("/Assets/Icons/trash-16x16.png")) } }, + new() + { + Tag = new OperationLayerExportGif(), + Icon = new Avalonia.Controls.Image + { + Source = new Bitmap(App.GetAsset("/Assets/Icons/gif-16x16.png")) + } + }, }; #region DataSets diff --git a/UVtools.WPF/Structures/AppVersionChecker.cs b/UVtools.WPF/Structures/AppVersionChecker.cs index ccbe2b7..c043435 100644 --- a/UVtools.WPF/Structures/AppVersionChecker.cs +++ b/UVtools.WPF/Structures/AppVersionChecker.cs @@ -100,7 +100,7 @@ namespace UVtools.WPF.Structures { try { - using WebClient client = new WebClient + using WebClient client = new() { Headers = new WebHeaderCollection { @@ -114,16 +114,17 @@ namespace UVtools.WPF.Structures if (string.IsNullOrEmpty(tag_name)) return false; tag_name = tag_name.Trim(' ', 'v', 'V'); Debug.WriteLine($"Version checker: v{App.VersionStr} <=> v{tag_name}"); - + Version checkVersion = new(tag_name); Changelog = json.body; - if (string.Compare(tag_name, App.VersionStr, StringComparison.OrdinalIgnoreCase) > 0) + //if (string.Compare(tag_name, App.VersionStr, StringComparison.OrdinalIgnoreCase) > 0) + if (App.Version.CompareTo(checkVersion) < 0) { + Debug.WriteLine($"New version detected: {DownloadLink}\n" + + $"{_changelog}"); Dispatcher.UIThread.InvokeAsync(() => { Version = tag_name; - Debug.WriteLine($"New version detected: {DownloadLink}\n" + - $"{_changelog}"); }); return true; } diff --git a/UVtools.WPF/Structures/OperationProfiles.cs b/UVtools.WPF/Structures/OperationProfiles.cs index b92fd97..ff1e78e 100644 --- a/UVtools.WPF/Structures/OperationProfiles.cs +++ b/UVtools.WPF/Structures/OperationProfiles.cs @@ -33,6 +33,7 @@ namespace UVtools.WPF.Structures //[XmlElement(typeof(OperationLayerClone))] //[XmlElement(typeof(OperationLayerImport))] [XmlElement(typeof(OperationDynamicLayerHeight))] + [XmlElement(typeof(OperationDynamicLifts))] //[XmlElement(typeof(OperationLayerReHeight))] //[XmlElement(typeof(OperationLayerRemove))] //[XmlElement(typeof(OperationMask))] @@ -47,6 +48,7 @@ namespace UVtools.WPF.Structures [XmlElement(typeof(OperationResize))] [XmlElement(typeof(OperationRotate))] [XmlElement(typeof(OperationThreshold))] + [XmlElement(typeof(OperationLayerExportGif))] [XmlElement(typeof(OperationCalibrateExposureFinder))] [XmlElement(typeof(OperationCalibrateElephantFoot))] [XmlElement(typeof(OperationCalibrateXYZAccuracy))] diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 546c6cf..e0bae71 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ LICENSE https://github.com/sn4k3/UVtools Git - 2.9.3 + 2.10.0 diff --git a/UVtools.WPF/Windows/ToolWindow.axaml b/UVtools.WPF/Windows/ToolWindow.axaml index 1c8b006..7e8f766 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml +++ b/UVtools.WPF/Windows/ToolWindow.axaml @@ -95,6 +95,7 @@ Grid.Row="0" Grid.Column="3" VerticalAlignment="Center" + Minimum="0" Maximum="{Binding MaximumLayerIndex}" IsEnabled="{Binding !LayerRangeSync}" Value="{Binding LayerIndexEnd}" -- cgit v1.2.3