diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-05-07 05:09:51 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-05-07 05:09:51 +0300 |
commit | 1c37636dd9b4b08a43f46032b544f2759a6ddc6c (patch) | |
tree | fb8170c4459a60c79e740cd513c75bf78702388e /UVtools.Core | |
parent | 36881041d6de156832b246c3000b893a7f8813fb (diff) |
v2.10.0v2.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
Diffstat (limited to 'UVtools.Core')
-rw-r--r-- | UVtools.Core/Extensions/EmguExtensions.cs | 8 | ||||
-rw-r--r-- | UVtools.Core/Extensions/IEnumerableExtensions.cs | 29 | ||||
-rw-r--r-- | UVtools.Core/FileFormats/FileFormat.cs | 9 | ||||
-rw-r--r-- | UVtools.Core/FileFormats/UVJFile.cs | 4 | ||||
-rw-r--r-- | UVtools.Core/FileFormats/VDTFile.cs | 4 | ||||
-rw-r--r-- | UVtools.Core/FileFormats/ZCodexFile.cs | 12 | ||||
-rw-r--r-- | UVtools.Core/GCode/GCodeBuilder.cs | 16 | ||||
-rw-r--r-- | UVtools.Core/Layer/Layer.cs | 15 | ||||
-rw-r--r-- | UVtools.Core/Layer/LayerManager.cs | 4 | ||||
-rw-r--r-- | UVtools.Core/Operations/Operation.cs | 14 | ||||
-rw-r--r-- | UVtools.Core/Operations/OperationCalibrateExposureFinder.cs | 469 | ||||
-rw-r--r-- | UVtools.Core/Operations/OperationDynamicLifts.cs | 344 | ||||
-rw-r--r-- | UVtools.Core/Operations/OperationLayerExportGif.cs | 344 | ||||
-rw-r--r-- | UVtools.Core/UVtools.Core.csproj | 3 |
14 files changed, 1199 insertions, 76 deletions
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. <https://fsf.org/> + * 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<TSource> DistinctBy<TSource, TKey> + (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) + { + HashSet<TKey> 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)); + } } /// <summary> @@ -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<ExposureItem> _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<int>(); + } + List<int> 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<int>(); + } + List<int> 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); + } + + /// <summary> + /// Gets all holes in pixels and ordered + /// </summary> + public BullsEyeCircle[] BullsEyes + { + get + { + if (!_bullsEyeEnabled) + { + return Array.Empty<BullsEyeCircle>(); + } + + List<BullsEyeCircle> 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. <https://fsf.org/> + * 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<Layer, uint>(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. <https://fsf.org/> + * 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, + /// <summary>Rotate 90 degrees clockwise</summary> + [Description("Rotate 90º CW")] + Rotate90Clockwise = 0, + /// <summary>Rotate 180 degrees clockwise</summary> + [Description("Rotate 180º")] + Rotate180 = 1, + /// <summary>Rotate 270 degrees clockwise</summary> + [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 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>2.9.3</Version> + <Version>2.10.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> @@ -46,6 +46,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="AnimatedGif" Version="1.0.5" /> <PackageReference Include="BinarySerializer" Version="8.6.0" /> <PackageReference Include="Emgu.CV" Version="4.5.1.4349" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.10.0-2.final" /> |