Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/sn4k3/UVtools.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTiago Conceição <Tiago_caza@hotmail.com>2021-05-07 05:09:51 +0300
committerTiago Conceição <Tiago_caza@hotmail.com>2021-05-07 05:09:51 +0300
commit1c37636dd9b4b08a43f46032b544f2759a6ddc6c (patch)
treefb8170c4459a60c79e740cd513c75bf78702388e
parent36881041d6de156832b246c3000b893a7f8813fb (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
-rw-r--r--CHANGELOG.md17
-rw-r--r--README.md6
-rw-r--r--UVtools.Core/Extensions/EmguExtensions.cs8
-rw-r--r--UVtools.Core/Extensions/IEnumerableExtensions.cs29
-rw-r--r--UVtools.Core/FileFormats/FileFormat.cs9
-rw-r--r--UVtools.Core/FileFormats/UVJFile.cs4
-rw-r--r--UVtools.Core/FileFormats/VDTFile.cs4
-rw-r--r--UVtools.Core/FileFormats/ZCodexFile.cs12
-rw-r--r--UVtools.Core/GCode/GCodeBuilder.cs16
-rw-r--r--UVtools.Core/Layer/Layer.cs15
-rw-r--r--UVtools.Core/Layer/LayerManager.cs4
-rw-r--r--UVtools.Core/Operations/Operation.cs14
-rw-r--r--UVtools.Core/Operations/OperationCalibrateExposureFinder.cs469
-rw-r--r--UVtools.Core/Operations/OperationDynamicLifts.cs344
-rw-r--r--UVtools.Core/Operations/OperationLayerExportGif.cs344
-rw-r--r--UVtools.Core/UVtools.Core.csproj3
-rw-r--r--UVtools.InstallerMM/UVtools.InstallerMM.wxs3
-rw-r--r--UVtools.ScriptSample/ScriptTester.cs68
-rw-r--r--UVtools.WPF/Assets/Icons/angle-double-up-16x16.pngbin0 -> 126 bytes
-rw-r--r--UVtools.WPF/Assets/Icons/gif-16x16.pngbin0 -> 266 bytes
-rw-r--r--UVtools.WPF/Assets/Styles/Styles.xaml2
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml264
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml.cs2
-rw-r--r--UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml207
-rw-r--r--UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml.cs28
-rw-r--r--UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml127
-rw-r--r--UVtools.WPF/Controls/Tools/ToolLayerExportGifControl.axaml.cs43
-rw-r--r--UVtools.WPF/MainWindow.axaml.cs16
-rw-r--r--UVtools.WPF/Structures/AppVersionChecker.cs11
-rw-r--r--UVtools.WPF/Structures/OperationProfiles.cs2
-rw-r--r--UVtools.WPF/UVtools.WPF.csproj2
-rw-r--r--UVtools.WPF/Windows/ToolWindow.axaml1
32 files changed, 1934 insertions, 140 deletions
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. <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" />
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 @@
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" />
<Directory Id="MergeRedirectFolder">
+ <Component Id="owc8E67DDA19FDE9EB745B0ADE651E8DC86" Guid="3744e071-b6de-ada6-7887-761d99e8f73b">
+ <File Id="owf8E67DDA19FDE9EB745B0ADE651E8DC86" Source="$(var.SourceDir)\AnimatedGif.dll" KeyPath="yes" />
+ </Component>
<Component Id="owcF3EB3D7C133F5B48E5859309ABDC2EE0" Guid="5a6040ae-b91e-47b6-8438-d9cd47fb947a">
<File Id="owfF3EB3D7C133F5B48E5859309ABDC2EE0" Source="$(var.SourceDir)\api-ms-win-core-console-l1-1-0.dll" KeyPath="yes" />
</Component>
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. <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 UVtools.Core.Scripting;
+using System.IO;
+using UVtools.Core.Extensions;
+
+namespace UVtools.ScriptSample
+{
+ /// <summary>
+ /// Change layer properties to random values
+ /// </summary>
+ public class ScriptChangeLayesrPropertiesSample : ScriptGlobals
+ {
+ /// <summary>
+ /// Set configurations here, this function trigger just after load a script
+ /// </summary>
+ 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);
+ }
+
+ /// <summary>
+ /// Validate user inputs here, this function trigger when user click on execute
+ /// </summary>
+ /// <returns>A error message, empty or null if validation passes.</returns>
+ public string ScriptValidate()
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Execute the script, this function trigger when when user click on execute and validation passes
+ /// </summary>
+ /// <returns>True if executes successfully to the end, otherwise false.</returns>
+ 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
--- /dev/null
+++ b/UVtools.WPF/Assets/Icons/angle-double-up-16x16.png
Binary files 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
--- /dev/null
+++ b/UVtools.WPF/Assets/Icons/gif-16x16.png
Binary files 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 @@
</Style>
<Style Selector="Border.FooterActions">
- <Setter Property="Padding" Value="5,20" />
+ <Setter Property="Padding" Value="10,20" />
<Setter Property="Margin" Value="0,10,0, 0" />
</Style>
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 @@
</Expander.Header>
- <Grid RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ <Grid RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
ColumnDefinitions="Auto,10,Auto,5,Auto,20,Auto,10,Auto,5,Auto,20,Auto,10,Auto,5,Auto">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="Base height:"
@@ -231,7 +231,7 @@
<NumericUpDown Grid.Row="0" Grid.Column="14"
Increment="0.5"
- Minimum="1"
+ Minimum="0"
Maximum="100"
FormatString="F2"
Value="{Binding Operation.FeaturesMargin}"/>
@@ -240,205 +240,363 @@
Text="mm"/>
<TextBlock Grid.Row="2" Grid.Column="0"
+ ToolTip.Tip="Creates an incremental stair at top from left to right that goes up to the top layer"
+ Text="Staircase:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+
+ Increment="1"
+ Minimum="0"
+ Maximum="65535"
+ Value="{Binding Operation.StaircaseThickness}"/>
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="4" Grid.Column="0"
Grid.ColumnSpan="17"
FontWeight="Bold"
- Text="Pin (positive) / holes (negative):"
- VerticalAlignment="Center"/>
+ Content="Pin (positive) / holes (negative):"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.HolesEnabled}"/>
- <TextBlock Grid.Row="4" Grid.Column="0"
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ IsEnabled="{Binding Operation.HolesEnabled}"
Text="Shape:"
VerticalAlignment="Center"/>
- <ComboBox Grid.Row="4" Grid.Column="2"
+ <ComboBox Grid.Row="6" Grid.Column="2"
+ IsEnabled="{Binding Operation.HolesEnabled}"
HorizontalAlignment="Stretch"
Items="{Binding Operation.ShapesItems}"
SelectedItem="{Binding Operation.HoleShape}"/>
- <TextBlock Grid.Row="4" Grid.Column="6"
+ <TextBlock Grid.Row="6" Grid.Column="6"
Text="Unit of measure:"
VerticalAlignment="Center"/>
- <ComboBox Grid.Row="4" Grid.Column="8"
+ <ComboBox Grid.Row="6" Grid.Column="8"
HorizontalAlignment="Stretch"
Items="{Binding Operation.MeasuresItems}"
SelectedItem="{Binding Operation.UnitOfMeasure}"/>
- <TextBlock Grid.Row="6" Grid.Column="0"
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ IsEnabled="{Binding Operation.HolesEnabled}"
Text="Diameters:"
ToolTip.Tip="Diameters separated by a comma (,).
&#x0a;Order doesn't matter.
&#x0a;Values are pixel square, eg: 3 = 3x3 = 9 pixel hole"
VerticalAlignment="Center"/>
- <TextBox Grid.Row="6" Grid.Column="2"
+ <TextBox Grid.Row="8" Grid.Column="2"
Grid.ColumnSpan="13"
+ IsEnabled="{Binding Operation.HolesEnabled}"
IsVisible="{Binding Operation.IsUnitOfMeasureMm}"
Text="{Binding Operation.HoleDiametersMm}"/>
- <TextBox Grid.Row="6" Grid.Column="2"
+ <TextBox Grid.Row="8" Grid.Column="2"
Grid.ColumnSpan="13"
+ IsEnabled="{Binding Operation.HolesEnabled}"
IsVisible="{Binding !Operation.IsUnitOfMeasureMm}"
Text="{Binding Operation.HoleDiametersPx}"/>
- <TextBlock Grid.Row="6" Grid.Column="16"
+ <TextBlock Grid.Row="8" Grid.Column="16"
+ IsEnabled="{Binding Operation.HolesEnabled}"
IsVisible="{Binding Operation.IsUnitOfMeasureMm}"
VerticalAlignment="Center"
Text="mm"/>
- <TextBlock Grid.Row="6" Grid.Column="16"
+ <TextBlock Grid.Row="8" Grid.Column="16"
+ IsEnabled="{Binding Operation.HolesEnabled}"
IsVisible="{Binding !Operation.IsUnitOfMeasureMm}"
VerticalAlignment="Center"
Text="px"/>
- <TextBlock Grid.Row="8" Grid.Column="0"
+ <CheckBox Grid.Row="10" Grid.Column="0"
Grid.ColumnSpan="17"
FontWeight="Bold"
- Text="Zebra bars:"
- VerticalAlignment="Center"/>
+ Content="Zebra bars:"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.BarsEnabled}"/>
- <TextBlock Grid.Row="10" Grid.Column="0"
+ <TextBlock Grid.Row="12" Grid.Column="0"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Text="Bar spacing:"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="10" Grid.Column="2"
-
+ <NumericUpDown Grid.Row="12" Grid.Column="2"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Increment="0.5"
Minimum="0.01"
Maximum="100"
FormatString="F2"
Value="{Binding Operation.BarSpacing}"/>
- <TextBlock Grid.Row="10" Grid.Column="4"
+ <TextBlock Grid.Row="12" Grid.Column="4"
+ IsEnabled="{Binding Operation.BarsEnabled}"
VerticalAlignment="Center"
Text="mm"/>
- <TextBlock Grid.Row="10" Grid.Column="6"
+ <TextBlock Grid.Row="12" Grid.Column="6"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Text="Bar length:"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="10" Grid.Column="8"
-
+ <NumericUpDown Grid.Row="12" Grid.Column="8"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Increment="0.5"
Minimum="0.01"
Maximum="100"
FormatString="F2"
Value="{Binding Operation.BarLength}"/>
- <TextBlock Grid.Row="10" Grid.Column="10"
+ <TextBlock Grid.Row="12" Grid.Column="10"
+ IsEnabled="{Binding Operation.BarsEnabled}"
VerticalAlignment="Center"
Text="mm"/>
- <TextBlock Grid.Row="10" Grid.Column="12"
+ <TextBlock Grid.Row="12" Grid.Column="12"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Text="Vertical splitter:"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="10" Grid.Column="14"
-
+ <NumericUpDown Grid.Row="12" Grid.Column="14"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Increment="1"
Minimum="-128"
Maximum="127"
Value="{Binding Operation.BarVerticalSplitter}"/>
<TextBlock Grid.Row="10" Grid.Column="16"
+ IsEnabled="{Binding Operation.BarsEnabled}"
VerticalAlignment="Center"
Text="px"/>
- <TextBlock Grid.Row="12" Grid.Column="0"
+ <TextBlock Grid.Row="14" Grid.Column="0"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Text="Fence thick:"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="12" Grid.Column="2"
+ <NumericUpDown Grid.Row="14" Grid.Column="2"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Increment="2"
Minimum="0"
Maximum="255"
Value="{Binding Operation.BarFenceThickness}"/>
- <TextBlock Grid.Row="12" Grid.Column="4"
+ <TextBlock Grid.Row="14" Grid.Column="4"
+ IsEnabled="{Binding Operation.BarsEnabled}"
VerticalAlignment="Center"
Text="px"/>
- <TextBlock Grid.Row="12" Grid.Column="6"
+ <TextBlock Grid.Row="14" Grid.Column="6"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Text="Fence offset:"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="12" Grid.Column="8"
+ <NumericUpDown Grid.Row="14" Grid.Column="8"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Increment="1"
- Minimum="0"
- Maximum="255"
+ Minimum="-128"
+ Maximum="127"
Value="{Binding Operation.BarFenceOffset}"/>
- <TextBlock Grid.Row="12" Grid.Column="10"
+ <TextBlock Grid.Row="14" Grid.Column="10"
+ IsEnabled="{Binding Operation.BarsEnabled}"
VerticalAlignment="Center"
Text="px"/>
- <TextBlock Grid.Row="14" Grid.Column="0"
+ <TextBlock Grid.Row="16" Grid.Column="0"
+ IsEnabled="{Binding Operation.BarsEnabled}"
Text="Thicknesses:"
VerticalAlignment="Center"/>
- <TextBox Grid.Row="14" Grid.Column="2"
+ <TextBox Grid.Row="16" Grid.Column="2"
Grid.ColumnSpan="13"
+ IsEnabled="{Binding Operation.BarsEnabled}"
IsVisible="{Binding Operation.IsUnitOfMeasureMm}"
Text="{Binding Operation.BarThicknessesMm}"/>
- <TextBox Grid.Row="14" Grid.Column="2"
+ <TextBox Grid.Row="16" Grid.Column="2"
Grid.ColumnSpan="13"
+ IsEnabled="{Binding Operation.BarsEnabled}"
IsVisible="{Binding !Operation.IsUnitOfMeasureMm}"
Text="{Binding Operation.BarThicknessesPx}"/>
- <TextBlock Grid.Row="14" Grid.Column="16"
+ <TextBlock Grid.Row="16" Grid.Column="16"
+ IsEnabled="{Binding Operation.BarsEnabled}"
IsVisible="{Binding Operation.IsUnitOfMeasureMm}"
VerticalAlignment="Center"
Text="mm"/>
- <TextBlock Grid.Row="14" Grid.Column="16"
+ <TextBlock Grid.Row="16" Grid.Column="16"
+ IsEnabled="{Binding Operation.BarsEnabled}"
IsVisible="{Binding !Operation.IsUnitOfMeasureMm}"
VerticalAlignment="Center"
Text="px"/>
- <TextBlock Grid.Row="16" Grid.Column="0"
+ <CheckBox Grid.Row="18" Grid.Column="0"
Grid.ColumnSpan="17"
FontWeight="Bold"
- Text="Text:"
- VerticalAlignment="Center"/>
+ Content="Text:"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.TextEnabled}"/>
- <TextBlock Grid.Row="18" Grid.Column="0"
+ <TextBlock Grid.Row="20" Grid.Column="0"
+ IsEnabled="{Binding Operation.TextEnabled}"
Text="Font:"
VerticalAlignment="Center"/>
- <ComboBox Grid.Row="18" Grid.Column="2"
+ <ComboBox Grid.Row="20" Grid.Column="2"
+ IsEnabled="{Binding Operation.TextEnabled}"
Grid.ColumnSpan="3"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Items="{Binding Operation.TextFonts}"
SelectedItem="{Binding Operation.TextFont}"/>
- <TextBlock Grid.Row="18" Grid.Column="6"
+ <TextBlock Grid.Row="20" Grid.Column="6"
+ IsEnabled="{Binding Operation.TextEnabled}"
Text="Text scale:"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="18" Grid.Column="8"
+ <NumericUpDown Grid.Row="20" Grid.Column="8"
+ IsEnabled="{Binding Operation.TextEnabled}"
Increment="0.5"
Minimum="0.1"
Maximum="100"
FormatString="F2"
Value="{Binding Operation.TextScale}"/>
- <TextBlock Grid.Row="18" Grid.Column="10"
+ <TextBlock Grid.Row="20" Grid.Column="10"
+ IsEnabled="{Binding Operation.TextEnabled}"
VerticalAlignment="Center"
Text="x"/>
- <TextBlock Grid.Row="18" Grid.Column="12"
+ <TextBlock Grid.Row="20" Grid.Column="12"
+ IsEnabled="{Binding Operation.TextEnabled}"
Text="Text thickness:"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
- <NumericUpDown Grid.Row="18" Grid.Column="14"
+ <NumericUpDown Grid.Row="20" Grid.Column="14"
+ IsEnabled="{Binding Operation.TextEnabled}"
Increment="1"
Minimum="1"
Maximum="255"
Value="{Binding Operation.TextThickness}"/>
- <TextBlock Grid.Row="18" Grid.Column="16"
+ <TextBlock Grid.Row="20" Grid.Column="16"
+ IsEnabled="{Binding Operation.TextEnabled}"
VerticalAlignment="Center"
Text="px"/>
- <TextBlock Grid.Row="20" Grid.Column="0"
+ <TextBlock Grid.Row="22" Grid.Column="0"
+ IsEnabled="{Binding Operation.TextEnabled}"
Text="Text:"
VerticalAlignment="Center"/>
- <TextBox Grid.Row="20" Grid.Column="2"
+ <TextBox Grid.Row="22" Grid.Column="2"
+ IsEnabled="{Binding Operation.TextEnabled}"
Grid.ColumnSpan="13"
Text="{Binding Operation.Text}"/>
+ <CheckBox Grid.Row="24" Grid.Column="0"
+ Grid.ColumnSpan="17"
+ FontWeight="Bold"
+ Content="Bullseye:"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.BullsEyeEnabled}"/>
+
+
+ <TextBlock Grid.Row="26" Grid.Column="0"
+ Text="Configuration:"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ ToolTip.Tip="Diameter:Thickness, ..."
+ VerticalAlignment="Center"/>
+
+ <TextBox Grid.Row="26" Grid.Column="2"
+ Grid.ColumnSpan="13"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ IsVisible="{Binding Operation.IsUnitOfMeasureMm}"
+ Text="{Binding Operation.BullsEyeConfigurationMm}"/>
+
+ <TextBox Grid.Row="26" Grid.Column="2"
+ Grid.ColumnSpan="13"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ IsVisible="{Binding !Operation.IsUnitOfMeasureMm}"
+ Text="{Binding Operation.BullsEyeConfigurationPx}"/>
+ <TextBlock Grid.Row="26" Grid.Column="16"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ IsVisible="{Binding Operation.IsUnitOfMeasureMm}"
+ VerticalAlignment="Center"
+ Text="mm"/>
+ <TextBlock Grid.Row="26" Grid.Column="16"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ IsVisible="{Binding !Operation.IsUnitOfMeasureMm}"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="28" Grid.Column="0"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ Text="Fence thick:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="28" Grid.Column="2"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ Increment="2"
+ Minimum="0"
+ Maximum="255"
+ Value="{Binding Operation.BullsEyeFenceThickness}"/>
+ <TextBlock Grid.Row="28" Grid.Column="4"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="28" Grid.Column="6"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ Text="Fence offset:"
+ HorizontalAlignment="Right"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="28" Grid.Column="8"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ Increment="1"
+ Minimum="-128"
+ Maximum="127"
+ Value="{Binding Operation.BullsEyeFenceOffset}"/>
+ <TextBlock Grid.Row="28" Grid.Column="10"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="28" Grid.Column="12"
+ Grid.ColumnSpan="5"
+ Content="Invert quadrants"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.BullsEyeEnabled}"
+ IsChecked="{Binding Operation.BullsEyeInvertQuadrants}"/>
+
+
+ <CheckBox Grid.Row="30" Grid.Column="0"
+ Grid.ColumnSpan="17"
+ FontWeight="Bold"
+ Content="Counter triangles:"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.CounterTrianglesEnabled}"/>
+
+
+ <TextBlock Grid.Row="32" Grid.Column="0"
+ Text="Tip Offset:"
+ IsEnabled="{Binding Operation.CounterTrianglesEnabled}"
+ VerticalAlignment="Center"/>
+
+ <NumericUpDown Grid.Row="32" Grid.Column="2"
+ IsEnabled="{Binding Operation.CounterTrianglesEnabled}"
+ Increment="1"
+ Minimum="-128"
+ Maximum="127"
+ Value="{Binding Operation.CounterTrianglesTipOffset}"/>
+
+ <TextBlock Grid.Row="32" Grid.Column="4"
+ IsEnabled="{Binding Operation.CounterTrianglesEnabled}"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="32" Grid.Column="6"
+ Grid.ColumnSpan="5"
+ IsEnabled="{Binding Operation.CounterTrianglesEnabled}"
+ Content="Fence the triangles"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.CounterTrianglesFence}"/>
+
</Grid>
</Expander>
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 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="UVtools.WPF.Controls.Tools.ToolDynamicLiftsControl">
+ <StackPanel Spacing="10">
+ <Grid RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,150,20,150,5,Auto">
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="Property"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="Minimum"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="Maximum"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="6"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="Unit"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom lift height:"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ VerticalAlignment="Center"
+ Minimum="1"
+ Maximum="100"
+ Increment="0.5"
+ FormatString="F2"
+ Value="{Binding Operation.MinBottomLiftHeight}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="3"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="/"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Minimum="1"
+ Maximum="100"
+ Increment="0.5"
+ FormatString="F2"
+ Value="{Binding Operation.MaxBottomLiftHeight}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom lift speed:"/>
+
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ VerticalAlignment="Center"
+ Minimum="5"
+ Maximum="1000"
+ Increment="1"
+ FormatString="F2"
+ Value="{Binding Operation.MinBottomLiftSpeed}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="3"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="/"/>
+
+ <NumericUpDown Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Minimum="5"
+ Maximum="1000"
+ Increment="1"
+ FormatString="F2"
+ Value="{Binding Operation.MaxBottomLiftSpeed}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="mm/min"/>
+
+
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Lift height:"/>
+
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
+ VerticalAlignment="Center"
+ Minimum="1"
+ Maximum="100"
+ Increment="0.5"
+ FormatString="F2"
+ Value="{Binding Operation.MinLiftHeight}"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="3"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="/"/>
+
+ <NumericUpDown Grid.Row="6" Grid.Column="4"
+ VerticalAlignment="Center"
+ Minimum="1"
+ Maximum="100"
+ Increment="0.5"
+ FormatString="F2"
+ Value="{Binding Operation.MaxLiftHeight}"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Lift speed:"/>
+
+ <NumericUpDown Grid.Row="8" Grid.Column="2"
+ VerticalAlignment="Center"
+ Minimum="5"
+ Maximum="1000"
+ Increment="1"
+ FormatString="F2"
+ Value="{Binding Operation.MinLiftSpeed}"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="3"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="/"/>
+
+ <NumericUpDown Grid.Row="8" Grid.Column="4"
+ VerticalAlignment="Center"
+ Minimum="5"
+ Maximum="1000"
+ Increment="1"
+ FormatString="F2"
+ Value="{Binding Operation.MaxLiftSpeed}"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="mm/min"/>
+ </Grid>
+
+ <CheckBox
+ Content="Update light-off delay accordingly"
+ ToolTip.Tip="If enabled, it will auto calculate the correct light-off delay based on the set lift"
+ IsChecked="{Binding Operation.UpdateLightOffDelay}"/>
+
+ <Grid RowDefinitions="Auto,10,Auto"
+ ColumnDefinitions="Auto,20,Auto,5,Auto"
+ IsEnabled="{Binding Operation.UpdateLightOffDelay}">
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="Bottom light-off extra time"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="1"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="/"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ FontWeight="Bold"
+ Text="Normal light-off extra time"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Minimum="0"
+ Maximum="100"
+ Increment="1"
+ FormatString="F2"
+ Value="{Binding Operation.LightOffDelayBottomExtraTime}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="1"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="/"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ VerticalAlignment="Center"
+ Minimum="0"
+ Maximum="100"
+ Increment="1"
+ FormatString="F2"
+ Value="{Binding Operation.LightOffDelayExtraTime}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ </Grid>
+
+ </StackPanel>
+</UserControl>
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 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="UVtools.WPF.Controls.Tools.ToolLayerExportGifControl">
+
+ <StackPanel Spacing="10">
+
+ <StackPanel Orientation="Horizontal" Spacing="5">
+ <TextBox
+ Watermark="Output filepath"
+ UseFloatingWatermark="True"
+ VerticalAlignment="Center"
+ IsReadOnly="True"
+ Width="500"
+ Text="{Binding Operation.FilePath}"/>
+ <Button
+ VerticalAlignment="Stretch"
+ Command="{Binding ChooseFilePath}">
+ <Image Source="/Assets/Icons/open-16x16.png"/>
+ </Button>
+ </StackPanel>
+
+ <CheckBox
+ Content="Clip layer image by volume bounds"
+ IsChecked="{Binding Operation.ClipByVolumeBounds}"/>
+
+ <CheckBox
+ Content="Render the layer number/count"
+ IsChecked="{Binding Operation.RenderLayerCount}"/>
+
+ <Grid RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10"
+ ColumnDefinitions="Auto,10,150,5,Auto,20,Auto">
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Frames-per-second: Number of shown layers within a second"
+ Text="FPS:" />
+
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ Minimum="1"
+ Maximum="255"
+ Value="{Binding Operation.FPS}"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Time that each layer is visible in milliseconds"
+ Text="{Binding Operation.FPSToMilliseconds, StringFormat={}{0}ms}" />
+
+ <TextBlock Grid.Row="0" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.GifDurationSeconds, StringFormat=GIF duration: {0}s}" />
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Number of times to loop the animation.
+&#x0a;0 = Infinite loop."
+ Text="Repeats:" />
+
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ Minimum="0"
+ Maximum="65535"
+ Value="{Binding Operation.Repeats}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="times" />
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="After pack a layer, skip the following layers by this value"
+ Text="Skip:" />
+
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ Minimum="0"
+ Maximum="65535"
+ Value="{Binding Operation.Skip}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="layers" />
+
+ <TextBlock Grid.Row="4" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.TotalLayers, StringFormat=Layers to pack: {0}}" />
+
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Scale layer resolution by this percentage amount."
+ Text="Scale:" />
+
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
+ Minimum="1"
+ Maximum="100"
+ FormatString="F2"
+ Value="{Binding Operation.Scale}"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="%" />
+
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Rotate:" />
+
+ <ComboBox Grid.Row="8" Grid.Column="2"
+ Grid.ColumnSpan="3"
+ HorizontalAlignment="Stretch"
+ Items="{Binding Operation.RotateDirection, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}"
+ SelectedItem="{Binding Operation.RotateDirection, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/>
+
+ <TextBlock Grid.Row="10" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Flip:" />
+
+ <ComboBox Grid.Row="10" Grid.Column="2"
+ Grid.ColumnSpan="3"
+ HorizontalAlignment="Stretch"
+ Items="{Binding Operation.FlipDirection, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}"
+ SelectedItem="{Binding Operation.FlipDirection, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/>
+
+
+ </Grid>
+
+ </StackPanel>
+
+</UserControl>
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<FileDialogFilter>
+ {
+ new()
+ {
+ Extensions = new List<string>{"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
@@ -198,6 +198,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 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
- <Version>2.9.3</Version>
+ <Version>2.10.0</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
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}"