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>2020-12-25 07:38:28 +0300
committerTiago Conceição <Tiago_caza@hotmail.com>2020-12-25 07:38:28 +0300
commitb2617d149f1528c5ad292dacfae73de245ab07a9 (patch)
tree45d4895361dee3cc11f8a23fe126ff408100450c
parentc0c23760c3cac9331630e830469aa3a01dc7f953 (diff)
v2.0.0v2.0.0
This release bump the major version due the introduction of .NET 5.0, the discontinuation old UVtools GUI project and the new calibration wizards. * (Upgrade) From .NET Core 3.1 to .NET 5.0 * (Upgrade) From C# 8.0 to C# 9.0 * (Upgrade) From Avalonia preview6 to rc1 * Bug: The per layer data gets hidden and not auto height on this rc1 * (Add) Setting - General - Windows / dialogs: * **Take into account the screen scale factor to limit the dialogs windows maximum size**: Due wrong information UVtools can clamp the windows maximum size when you have plenty more avaliable or when use in a secondary monitor. If is the case disable this option * **Horizontal limiting margin:** Limits windows and dialogs maximum width to the screen resolution less this margin * **Vertical limiting margin:** Limits windows and dialogs maximum height to the screen resolution less this margin * (Add) Setting - General: Take into account the screen scale factor to limit the dialogs windows maximum size. Due wrong information UVtools can cap the windows maximum size when you have plenty more avaliable or when use in a secondary monitor. If is the case disable this option * (Add) Ctrl + Shift + Z to undo and edit the last operation (If contain a valid operation) * (Add) Allow to deselect the current selected profile * (Add) Allow to set a default profile to load in when open a tool * (Add) ENTER and ESC hotkeys to message box * (Add) Pixel dimming: Brightness percent equivalent value * (Add) Raft relief: Allow to define supports margin independent from wall margin for the "Relief" type * (Add) Pixel editor: Allow to adjust the remove and add pixel brightness values * (Add) Calibration Menu: * **Elephant foot:** Generates test models with various strategies and increments to verify the best method/values to remove the elephant foot. * **XYZ Accuracy:** Generates test models with various strategies and increments to verify the XYZ accuracy. * **Tolerance:** Generates test models with various strategies and increments to verify the part tolerances. * **Grayscale:** Generates test models with various strategies and increments to verify the LED power against the grayscale levels. * (Change) PW0, PWS, PWMX, PWMO, PWMS, PWX file formats to ignore preview validation and allow variations on the file format (#111) * (Change) Tool - Edit print parameters: Increments from 0.01 to 0.5 * (Change) Tool - Resize: Increments from 0.01 to 0.1 * (Change) Tool - Rotate: Increments from 0.01 to 1 * (Change) Tool - Calculator: Increments from 0.01 to 0.5 and 1 * (Fix) PW0, PWS, PWMX, PWMO, PWMS, PWX file formats to replicate missing bottom properties cloned from normal properties * (Fix) Drain holes to build plate were considered as traps, changed to be drains as when removing object resin will flow outwards * (Fix) When unable to save the file it will change extension and not delete the temporary file * (Fix) Pixel dimming wasn't saving all the fields on profiles * (Fix) Prevent a rare startup crash when using demo file * (Fix) Tool - Solifiy: Increase AA clean up threshold range, previous value wasn't solidifing when model has darker tones * (Fix) Sanitize per layer settings, due some slicers are setting 0 at some properties that can cause problems with UVtools calculations, those values are now sanitized and set to the general value if 0 * (Fix) Update partial islands: * Was leaving visible issues when the result returns an empty list of new issues * Was jumping some modified sequential layers * Was not updating the issue tracker map * (Fix) Edit print parameters was not updating the layer data table information
-rw-r--r--CHANGELOG.md42
-rw-r--r--CREDITS.md3
-rw-r--r--CreateRelease.WPF.ps140
-rw-r--r--README.md11
-rw-r--r--UVtools.Cmd/UVtools.Cmd.csproj2
-rw-r--r--UVtools.Core/Extensions/EmguExtensions.cs16
-rw-r--r--UVtools.Core/Extensions/PointExtensions.cs5
-rw-r--r--UVtools.Core/Extensions/TypeExtensions.cs2
-rw-r--r--UVtools.Core/FileFormats/ChituboxFile.cs10
-rw-r--r--UVtools.Core/FileFormats/FileFormat.cs50
-rw-r--r--UVtools.Core/FileFormats/PhotonWorkshopFile.cs20
-rw-r--r--UVtools.Core/Layer/Layer.cs65
-rw-r--r--UVtools.Core/Layer/LayerManager.cs297
-rw-r--r--UVtools.Core/Managers/ClipboardManager.cs38
-rw-r--r--UVtools.Core/Operations/Operation.cs11
-rw-r--r--UVtools.Core/Operations/OperationCalibrateElephantFoot.cs615
-rw-r--r--UVtools.Core/Operations/OperationCalibrateGrayscale.cs442
-rw-r--r--UVtools.Core/Operations/OperationCalibrateTolerance.cs700
-rw-r--r--UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs708
-rw-r--r--UVtools.Core/Operations/OperationMorph.cs2
-rw-r--r--UVtools.Core/Operations/OperationMove.cs8
-rw-r--r--UVtools.Core/Operations/OperationPixelDimming.cs408
-rw-r--r--UVtools.Core/Operations/OperationRaftRelief.cs2
-rw-r--r--UVtools.Core/PixelEditor/PixelDrainHole.cs2
-rw-r--r--UVtools.Core/PixelEditor/PixelDrawing.cs27
-rw-r--r--UVtools.Core/PixelEditor/PixelEraser.cs4
-rw-r--r--UVtools.Core/PixelEditor/PixelOperation.cs44
-rw-r--r--UVtools.Core/PixelEditor/PixelSupport.cs3
-rw-r--r--UVtools.Core/PixelEditor/PixelText.cs29
-rw-r--r--UVtools.Core/UVtools.Core.csproj4
-rw-r--r--UVtools.Core/UVtools.Core.csproj.DotSettings2
-rw-r--r--UVtools.InstallerMM/UVtools.InstallerMM.wxs57
-rw-r--r--UVtools.WPF/App.axaml.cs5
-rw-r--r--UVtools.WPF/Assets/Icons/chart-pie-16x16.pngbin0 -> 226 bytes
-rw-r--r--UVtools.WPF/Assets/Icons/cubes-16x16.pngbin0 -> 268 bytes
-rw-r--r--UVtools.WPF/Assets/Icons/elephant-foot-16x16.pngbin0 -> 335 bytes
-rw-r--r--UVtools.WPF/Controls/AdvancedImageBox.cs.old1891
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml335
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs99
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml311
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml.cs86
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml553
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml.cs131
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml478
-rw-r--r--UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml.cs132
-rw-r--r--UVtools.WPF/Controls/Tools/ToolBlurControl.axaml.cs11
-rw-r--r--UVtools.WPF/Controls/Tools/ToolCalculatorControl.axaml50
-rw-r--r--UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs4
-rw-r--r--UVtools.WPF/Controls/Tools/ToolMorphControl.axaml.cs10
-rw-r--r--UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml47
-rw-r--r--UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml.cs373
-rw-r--r--UVtools.WPF/Controls/Tools/ToolRaftReliefControl.axaml38
-rw-r--r--UVtools.WPF/Controls/Tools/ToolResizeControl.axaml8
-rw-r--r--UVtools.WPF/Controls/Tools/ToolRotateControl.axaml4
-rw-r--r--UVtools.WPF/Extensions/WindowExtensions.cs7
-rw-r--r--UVtools.WPF/MainWindow.Clipboard.cs27
-rw-r--r--UVtools.WPF/MainWindow.GCode.cs1
-rw-r--r--UVtools.WPF/MainWindow.Information.cs2
-rw-r--r--UVtools.WPF/MainWindow.Issues.cs22
-rw-r--r--UVtools.WPF/MainWindow.LayerPreview.cs11
-rw-r--r--UVtools.WPF/MainWindow.PixelEditor.cs16
-rw-r--r--UVtools.WPF/MainWindow.axaml203
-rw-r--r--UVtools.WPF/MainWindow.axaml.cs139
-rw-r--r--UVtools.WPF/Structures/OperationProfiles.cs4
-rw-r--r--UVtools.WPF/UVtools.WPF.csproj19
-rw-r--r--UVtools.WPF/UVtools.WPF.csproj.DotSettings2
-rw-r--r--UVtools.WPF/UserSettings.cs22
-rw-r--r--UVtools.WPF/Windows/AboutWindow.axaml.cs2
-rw-r--r--UVtools.WPF/Windows/SettingsWindow.axaml55
-rw-r--r--UVtools.WPF/Windows/SettingsWindow.axaml.cs3
-rw-r--r--UVtools.WPF/Windows/ToolWindow.axaml40
-rw-r--r--UVtools.WPF/Windows/ToolWindow.axaml.cs109
72 files changed, 6252 insertions, 2667 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb4f4b3..d4e6515 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,47 @@
# Changelog
+## 25/11/2020 - v2.0.0
+
+This release bump the major version due the introduction of .NET 5.0, the discontinuation old UVtools GUI project and the new calibration wizards.
+* (Upgrade) From .NET Core 3.1 to .NET 5.0
+* (Upgrade) From C# 8.0 to C# 9.0
+* (Upgrade) From Avalonia preview6 to rc1
+ * Bug: The per layer data gets hidden and not auto height on this rc1
+* (Add) Setting - General - Windows / dialogs:
+ * **Take into account the screen scale factor to limit the dialogs windows maximum size**: Due wrong information UVtools can clamp the windows maximum size when you have plenty more avaliable or when use in a secondary monitor. If is the case disable this option
+ * **Horizontal limiting margin:** Limits windows and dialogs maximum width to the screen resolution less this margin
+ * **Vertical limiting margin:** Limits windows and dialogs maximum height to the screen resolution less this margin
+* (Add) Setting - General: Take into account the screen scale factor to limit the dialogs windows maximum size. Due wrong information UVtools can cap the windows maximum size when you have plenty more avaliable or when use in a secondary monitor. If is the case disable this option
+* (Add) Ctrl + Shift + Z to undo and edit the last operation (If contain a valid operation)
+* (Add) Allow to deselect the current selected profile
+* (Add) Allow to set a default profile to load in when open a tool
+* (Add) ENTER and ESC hotkeys to message box
+* (Add) Pixel dimming: Brightness percent equivalent value
+* (Add) Raft relief: Allow to define supports margin independent from wall margin for the "Relief" type
+* (Add) Pixel editor: Allow to adjust the remove and add pixel brightness values
+* (Add) Calibration Menu:
+ * **Elephant foot:** Generates test models with various strategies and increments to verify the best method/values to remove the elephant foot.
+ * **XYZ Accuracy:** Generates test models with various strategies and increments to verify the XYZ accuracy.
+ * **Tolerance:** Generates test models with various strategies and increments to verify the part tolerances.
+ * **Grayscale:** Generates test models with various strategies and increments to verify the LED power against the grayscale levels.
+* (Change) PW0, PWS, PWMX, PWMO, PWMS, PWX file formats to ignore preview validation and allow variations on the file format (#111)
+* (Change) Tool - Edit print parameters: Increments from 0.01 to 0.5
+* (Change) Tool - Resize: Increments from 0.01 to 0.1
+* (Change) Tool - Rotate: Increments from 0.01 to 1
+* (Change) Tool - Calculator: Increments from 0.01 to 0.5 and 1
+* (Fix) PW0, PWS, PWMX, PWMO, PWMS, PWX file formats to replicate missing bottom properties cloned from normal properties
+* (Fix) Drain holes to build plate were considered as traps, changed to be drains as when removing object resin will flow outwards
+* (Fix) When unable to save the file it will change extension and not delete the temporary file
+* (Fix) Pixel dimming wasn't saving all the fields on profiles
+* (Fix) Prevent a rare startup crash when using demo file
+* (Fix) Tool - Solifiy: Increase AA clean up threshold range, previous value wasn't solidifing when model has darker tones
+* (Fix) Sanitize per layer settings, due some slicers are setting 0 at some properties that can cause problems with UVtools calculations, those values are now sanitized and set to the general value if 0
+* (Fix) Update partial islands:
+ * Was leaving visible issues when the result returns an empty list of new issues
+ * Was jumping some modified sequential layers
+ * Was not updating the issue tracker map
+* (Fix) Edit print parameters was not updating the layer data table information
+
## 08/11/2020 - v1.4.0
* (Add) Tool - Raft relief: Relief raft by adding holes in between to reduce FEP suction, save resin and easier to remove the prints.
diff --git a/CREDITS.md b/CREDITS.md
index a72a968..22e44b0 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -39,4 +39,5 @@
* Khalil Nurullah
* Nahin Mulla
* Jorge diego Robles Ayerbe
-* Timothy Gray \ No newline at end of file
+* Timothy Gray
+* David Gordon \ No newline at end of file
diff --git a/CreateRelease.WPF.ps1 b/CreateRelease.WPF.ps1
index 788a3a3..56da287 100644
--- a/CreateRelease.WPF.ps1
+++ b/CreateRelease.WPF.ps1
@@ -29,6 +29,7 @@ class FixedEncoder : System.Text.UTF8Encoding {
####################################
### Configuration ###
####################################
+$enableMSI = $false
# Profilling
$stopWatch = New-Object -TypeName System.Diagnostics.Stopwatch
$deployStopWatch = New-Object -TypeName System.Diagnostics.Stopwatch
@@ -41,7 +42,8 @@ Set-Location $PSScriptRoot
$software = "UVtools"
$project = "UVtools.WPF"
$buildWith = "Release"
-$releaseFolder = "$PSScriptRoot\$project\bin\$buildWith\netcoreapp3.1"
+$netFolder = "net5.0"
+$releaseFolder = "$PSScriptRoot\$project\bin\$buildWith\$netFolder"
$publishFolder = "$PSScriptRoot\publish"
#$version = (Get-Command "$releaseFolder\UVtools.dll").FileVersionInfo.ProductVersion
@@ -144,27 +146,29 @@ $stopWatch.Stop()
# MSI Installer for Windows
-$deployStopWatch.Restart()
-$runtime = 'win-x64'
-$msiTargetFile = "$publishFolder\${software}_${runtime}_v$version.msi"
-Write-Output "################################
-Building: $runtime MSI Installer"
-
-foreach($installer in $installers)
+if($enableMSI)
{
- # Clean and build MSI
- Remove-Item "$PSScriptRoot\$installer\obj" -Recurse -ErrorAction Ignore
- Remove-Item "$PSScriptRoot\$installer\bin" -Recurse -ErrorAction Ignore
- iex "& $msbuild $installer\$installer.wixproj"
-}
+ $deployStopWatch.Restart()
+ $runtime = 'win-x64'
+ $msiTargetFile = "$publishFolder\${software}_${runtime}_v$version.msi"
+ Write-Output "################################
+ Building: $runtime MSI Installer"
-Write-Output "Coping $runtime MSI to: $msiTargetFile"
-Copy-Item $msiSourceFile $msiTargetFile
+ foreach($installer in $installers)
+ {
+ # Clean and build MSI
+ Remove-Item "$PSScriptRoot\$installer\obj" -Recurse -ErrorAction Ignore
+ Remove-Item "$PSScriptRoot\$installer\bin" -Recurse -ErrorAction Ignore
+ iex "& $msbuild $installer\$installer.wixproj"
+ }
-Write-Output "Took: $($deployStopWatch.Elapsed)
-################################
-"
+ Write-Output "Coping $runtime MSI to: $msiTargetFile"
+ Copy-Item $msiSourceFile $msiTargetFile
+ Write-Output "Took: $($deployStopWatch.Elapsed)
+ ################################
+ "
+}
Write-Output "
diff --git a/README.md b/README.md
index ca5d680..cec8208 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/sn4k3/UVtools?include_prereleases)](https://github.com/sn4k3/UVtools/releases)
[![Downloads](https://img.shields.io/github/downloads/sn4k3/UVtools/total)](https://github.com/sn4k3/UVtools/releases)
-**MSLA/DLP, file analysis, repair, conversion and manipulation**
+**MSLA/DLP, file analysis, calibration, repair, conversion and manipulation**
This simple tool can give you insight of supports and find some failures. Did you forget what resin or other settings you used on a project? This can also save you, check every setting that were used with or simply change them!
@@ -47,9 +47,10 @@ But also, i need victims for test subject. Proceed at your own risk!
* Check islands and repair/remove them as other issues
* Export file to a folder
* Convert format to another format
+* Calibration tests
* Portable (No installation needed)
-## Known Formats
+## Known File Formats
* SL1 (PrusaSlicer)
* Zip (Chitubox)
@@ -161,14 +162,16 @@ After some tests without failure you can increase your confidence and ignore thi
1. Windows 7 or greater
1. If on Windows 10 N or NK: [Media Feature Pack](https://www.microsoft.com/download/details.aspx?id=48231) must be installed
-1. .NET Framework 4.8 installed (Comes pre-installed on Windows 10 with last updates)
+1. [.NET 5.0](https://dotnet.microsoft.com/download/dotnet/5.0) installed (Comes pre-installed on Windows 10 with last updates)
1. 4GB RAM or higher
+1. 1980 x 1080 @ 100% scale as minimum resolution
### Linux
1. 4GB RAM or higher
2. 64 bit System
+1. 1980 x 1080 @ 100% scale as minimum resolution
**Ubuntu/Mint/Debian/Similars**
@@ -237,7 +240,7 @@ There are multiple ways to open your file:
## Library -> Developers
-Are you a developer? This project include a .NET Standard library (UVtools.Core) that can be referenced in your application to make use of my work. Easy to use calls that allow you work with the formats. For more information navigate main code.
+Are you a developer? This project include a .NET 5.0 library (UVtools.Core) that can be referenced in your application to make use of my work. Easy to use calls that allow you work with the formats. For more information navigate main code.
## TODO
diff --git a/UVtools.Cmd/UVtools.Cmd.csproj b/UVtools.Cmd/UVtools.Cmd.csproj
index 9e25a50..a6cc0cb 100644
--- a/UVtools.Cmd/UVtools.Cmd.csproj
+++ b/UVtools.Cmd/UVtools.Cmd.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<AssemblyName>UVtoolsCmd</AssemblyName>
<ApplicationIcon>UVtools.ico</ApplicationIcon>
<Version>0.1</Version>
diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs
index a1a2436..d9c1bc0 100644
--- a/UVtools.Core/Extensions/EmguExtensions.cs
+++ b/UVtools.Core/Extensions/EmguExtensions.cs
@@ -18,6 +18,7 @@ namespace UVtools.Core.Extensions
public static class EmguExtensions
{
public static readonly MCvScalar WhiteByte = new MCvScalar(255);
+ public static readonly MCvScalar White3Byte = new MCvScalar(255, 255, 255);
public static readonly MCvScalar BlackByte = new MCvScalar(0);
public static readonly MCvScalar Black3Byte = new MCvScalar(0, 0, 0);
public static readonly MCvScalar Transparent4Byte = new MCvScalar(0, 0, 0, 0);
@@ -76,6 +77,17 @@ namespace UVtools.Core.Extensions
}
}
+ public static void Rotate(this Mat src, double angle)
+ {
+ var halfWidth = src.Width / 2.0f;
+ var halfHeight = src.Height / 2.0f;
+ using (var translateTransform = new Matrix<double>(2, 3))
+ {
+ CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, 1.0, translateTransform);
+ CvInvoke.WarpAffine(src, src, translateTransform, src.Size);
+ }
+ }
+
/// <summary>
/// Scale image from it center, preserving src bounds
/// https://stackoverflow.com/a/62543674/933976
@@ -188,7 +200,7 @@ namespace UVtools.Core.Extensions
/// <returns>Blanked <see cref="Mat"/></returns>
public static Mat CloneBlank(this Mat mat)
{
- return InitMat(mat.Size, mat.Depth, mat.NumberOfChannels);
+ return InitMat(mat.Size, mat.NumberOfChannels, mat.Depth);
}
public static byte GetByte(this Mat mat, int pos)
@@ -217,7 +229,7 @@ namespace UVtools.Core.Extensions
public static void SetBytes(this Mat mat, byte[] value) =>
Marshal.Copy(value, 0, mat.DataPointer, value.Length);
- public static Mat InitMat(Size size, DepthType depthType = DepthType.Cv8U, int channels = 1)
+ public static Mat InitMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U)
{
var mat = new Mat(size, depthType, channels);
switch (channels)
diff --git a/UVtools.Core/Extensions/PointExtensions.cs b/UVtools.Core/Extensions/PointExtensions.cs
index 4703ca5..077b869 100644
--- a/UVtools.Core/Extensions/PointExtensions.cs
+++ b/UVtools.Core/Extensions/PointExtensions.cs
@@ -1,12 +1,12 @@
using System;
-using System.Collections.Generic;
using System.Drawing;
-using System.Text;
namespace UVtools.Core.Extensions
{
public static class PointExtensions
{
+ public static double FindLength(Point start, Point end) => Math.Sqrt(Math.Pow(end.Y - start.Y, 2) + Math.Pow(end.X - start.X, 2));
+
public static Point Rotate(this Point point, double angleDegree, Point pivot = default)
{
double angle = angleDegree * Math.PI / 180;
@@ -20,5 +20,6 @@ namespace UVtools.Core.Extensions
Point rotated = new Point((int)Math.Round(x), (int)Math.Round(y));
return rotated;
}
+
}
}
diff --git a/UVtools.Core/Extensions/TypeExtensions.cs b/UVtools.Core/Extensions/TypeExtensions.cs
index e316033..5b982dc 100644
--- a/UVtools.Core/Extensions/TypeExtensions.cs
+++ b/UVtools.Core/Extensions/TypeExtensions.cs
@@ -31,5 +31,7 @@ namespace UVtools.Core.Extensions
{
return (T)Activator.CreateInstance(type);
}
+
+ public static byte ToByte(this bool value) => value ? 1 : 0;
}
}
diff --git a/UVtools.Core/FileFormats/ChituboxFile.cs b/UVtools.Core/FileFormats/ChituboxFile.cs
index 19e89c0..c853d19 100644
--- a/UVtools.Core/FileFormats/ChituboxFile.cs
+++ b/UVtools.Core/FileFormats/ChituboxFile.cs
@@ -1561,6 +1561,10 @@ namespace UVtools.Core.FileFormats
}
else
{
+ if (layerIndex == 0)
+ {
+ Debug.WriteLine(layerData.DataAddress - 84);
+ }
inputFile.Seek(layerData.DataAddress - 84, SeekOrigin.Begin);
LayerDefinitionsEx[layerIndex] = Helpers.Deserialize<LayerDataEx>(inputFile);
Debug.Write($"LAYER {layerIndex} -> ");
@@ -1596,8 +1600,12 @@ namespace UVtools.Core.FileFormats
LayerOffTime = LayerDefinitions[0, layerIndex].LayerOffSeconds,
};
- if (!(LayerDefinitionsEx is null))
+ if (LayerDefinitionsEx is not null)
{
+ if (layerIndex == 0)
+ {
+
+ }
layer.LiftHeight = LayerDefinitionsEx[layerIndex].LiftHeight;
layer.LiftSpeed = LayerDefinitionsEx[layerIndex].LiftSpeed;
layer.RetractSpeed = LayerDefinitionsEx[layerIndex].RetractSpeed;
diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs
index 9d45cfa..3bd20c5 100644
--- a/UVtools.Core/FileFormats/FileFormat.cs
+++ b/UVtools.Core/FileFormats/FileFormat.cs
@@ -498,16 +498,35 @@ namespace UVtools.Core.FileFormats
{
get
{
+ if (LayerCount == 0) return 0;
float time = ExtraPrintTime;
+ bool computeGeneral = LayerManager is null;
+ if (!computeGeneral)
+ {
+ foreach (var layer in this)
+ {
+ if (layer is null)
+ {
+ computeGeneral = true;
+ break;
+ }
- foreach (var layer in this)
+ var layerOff = OperationCalculator.LightOffDelayC.CalculateSeconds(layer.LiftHeight, layer.LiftSpeed, layer.RetractSpeed);
+ time += layer.ExposureTime;
+ if (layerOff >= layer.LayerOffTime)
+ time += layerOff;
+ else
+ time += layer.LayerOffTime;
+ }
+ }
+
+ if (computeGeneral)
{
- var layerOff = OperationCalculator.LightOffDelayC.CalculateSeconds(layer.LiftHeight, layer.LiftSpeed, layer.RetractSpeed);
- time += layer.ExposureTime;
- if (layerOff >= layer.LayerOffTime)
- time += layerOff;
- else
- time += layer.LayerOffTime;
+ time = ExtraPrintTime +
+ BottomLayerOffTime * BottomLayerCount +
+ LayerOffTime * NormalLayerCount +
+ OperationCalculator.LightOffDelayC.CalculateSeconds(BottomLiftHeight, BottomLiftSpeed, RetractSpeed) * BottomLayerCount +
+ OperationCalculator.LightOffDelayC.CalculateSeconds(LiftHeight, LiftSpeed, RetractSpeed) * NormalLayerCount;
}
return (float) Math.Round(time, 2);
@@ -723,6 +742,10 @@ namespace UVtools.Core.FileFormats
for (var i = 0; i < ThumbnailsCount; i++)
{
Thumbnails[i] = images[Math.Min(i, images.Length - 1)].Clone();
+ if (Thumbnails[i].Size != ThumbnailsOriginalSize[i])
+ {
+ CvInvoke.Resize(Thumbnails[i], Thumbnails[i], ThumbnailsOriginalSize[i]);
+ }
}
}
@@ -731,6 +754,10 @@ namespace UVtools.Core.FileFormats
for (var i = 0; i < ThumbnailsCount; i++)
{
Thumbnails[i] = image.Clone();
+ if (Thumbnails[i].Size != ThumbnailsOriginalSize[i])
+ {
+ CvInvoke.Resize(Thumbnails[i], Thumbnails[i], ThumbnailsOriginalSize[i]);
+ }
}
}
@@ -754,12 +781,12 @@ namespace UVtools.Core.FileFormats
for (var i = 0; i < Thumbnails.Length; i++)
{
- if (ReferenceEquals(Thumbnails[i], null)) continue;
+ if (Thumbnails[i] is null) continue;
if(Thumbnails[i].Size == ThumbnailsOriginalSize[i]) continue;
CvInvoke.Resize(Thumbnails[i], Thumbnails[i], new Size(ThumbnailsOriginalSize[i].Width, ThumbnailsOriginalSize[i].Height));
}
- if(ReferenceEquals(progress, null)) progress = new OperationProgress();
+ progress ??= new OperationProgress();
progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount);
}
@@ -1129,6 +1156,11 @@ namespace UVtools.Core.FileFormats
{
this[layerIndex].SetValuesFromPrintParametersModifiers(operation.Modifiers);
}
+
+ foreach (var modifier in operation.Modifiers)
+ {
+ modifier.OldValue = modifier.NewValue;
+ }
RebuildGCode();
}
else
diff --git a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs
index 80b3f81..445d6b4 100644
--- a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs
+++ b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs
@@ -898,6 +898,12 @@ namespace UVtools.Core.FileFormats
}
}
+ public override float BottomLayerOffTime
+ {
+ get => LayerOffTime;
+ set => LayerOffTime = value;
+ }
+
public override float LayerOffTime
{
get => HeaderSettings.LayerOffTime;
@@ -908,6 +914,12 @@ namespace UVtools.Core.FileFormats
}
}
+ public override float BottomLiftHeight
+ {
+ get => LiftHeight;
+ set => LiftHeight = value;
+ }
+
public override float LiftHeight
{
get => HeaderSettings.LiftHeight;
@@ -918,6 +930,12 @@ namespace UVtools.Core.FileFormats
}
}
+ public override float BottomLiftSpeed
+ {
+ get => LiftSpeed;
+ set => LiftSpeed = value;
+ }
+
public override float LiftSpeed
{
get => (float) Math.Round(HeaderSettings.LiftSpeed * 60, 2);
@@ -1138,7 +1156,7 @@ namespace UVtools.Core.FileFormats
Debug.Write("Preview -> ");
Debug.WriteLine(PreviewSettings);
- PreviewSettings.Validate((int) PreviewSettings.DataSize);
+ //PreviewSettings.Validate((int) PreviewSettings.DataSize);
PreviewSettings.Data = new byte[PreviewSettings.DataSize];
inputFile.ReadBytes(PreviewSettings.Data);
diff --git a/UVtools.Core/Layer/Layer.cs b/UVtools.Core/Layer/Layer.cs
index 4ae83a8..3aa1fc1 100644
--- a/UVtools.Core/Layer/Layer.cs
+++ b/UVtools.Core/Layer/Layer.cs
@@ -74,7 +74,11 @@ namespace UVtools.Core
public float ExposureTime
{
get => _exposureTime;
- set => RaiseAndSetIfChanged(ref _exposureTime, value);
+ set
+ {
+ if (value <= 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomExposureTime, SlicerFile.ExposureTime);
+ RaiseAndSetIfChanged(ref _exposureTime, value);
+ }
}
/// <summary>
@@ -83,7 +87,11 @@ namespace UVtools.Core
public float LayerOffTime
{
get => _layerOffTime;
- set => RaiseAndSetIfChanged(ref _layerOffTime, value);
+ set
+ {
+ if (value <= 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLayerOffTime, SlicerFile.LayerOffTime);
+ RaiseAndSetIfChanged(ref _layerOffTime, value);
+ }
}
/// <summary>
@@ -92,7 +100,11 @@ namespace UVtools.Core
public float LiftHeight
{
get => _liftHeight;
- set => RaiseAndSetIfChanged(ref _liftHeight, value);
+ set
+ {
+ if (value <= 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLiftHeight, SlicerFile.LiftHeight);
+ RaiseAndSetIfChanged(ref _liftHeight, value);
+ }
}
/// <summary>
@@ -101,7 +113,11 @@ namespace UVtools.Core
public float LiftSpeed
{
get => _liftSpeed;
- set => RaiseAndSetIfChanged(ref _liftSpeed, value);
+ set
+ {
+ if (value <= 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLiftSpeed, SlicerFile.LiftSpeed);
+ RaiseAndSetIfChanged(ref _liftSpeed, value);
+ }
}
/// <summary>
@@ -110,7 +126,11 @@ namespace UVtools.Core
public float RetractSpeed
{
get => _retractSpeed;
- set => RaiseAndSetIfChanged(ref _retractSpeed, value);
+ set
+ {
+ if (value <= 0) value = SlicerFile.RetractSpeed;
+ RaiseAndSetIfChanged(ref _retractSpeed, value);
+ }
}
/// <summary>
@@ -119,7 +139,11 @@ namespace UVtools.Core
public byte LightPWM
{
get => _lightPwm;
- set => RaiseAndSetIfChanged(ref _lightPwm, value);
+ set
+ {
+ if (value <= 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLightPWM, SlicerFile.LightPWM);
+ RaiseAndSetIfChanged(ref _lightPwm, value);
+ }
}
/// <summary>
@@ -427,7 +451,6 @@ namespace UVtools.Core
foreach (var modifier in modifiers)
{
if (!modifier.HasChanged) continue;
- modifier.OldValue = modifier.NewValue;
SetValueFromPrintParameterModifier(modifier, modifier.NewValue);
changed++;
}
@@ -688,29 +711,7 @@ namespace UVtools.Core
using (var mat = LayerMat)
{
Mat target = operation.GetRoiOrDefault(mat);
-
- var halfWidth = target.Width / 2.0f;
- var halfHeight = target.Height / 2.0f;
- using (var translateTransform = new Matrix<double>(2, 3))
- {
- CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), (double) -operation.AngleDegrees, 1.0, translateTransform);
- /*var rect = new RotatedRect(PointF.Empty, mat.Size, (float) angle).MinAreaRect();
- translateTransform[0, 2] += rect.Width / 2.0 - mat.Cols / 2.0;
- translateTransform[0, 2] += rect.Height / 2.0 - mat.Rows / 2.0;*/
-
- /* var abs_cos = Math.Abs(translateTransform[0, 0]);
- var abs_sin = Math.Abs(translateTransform[0, 1]);
-
- var bound_w = mat.Height * abs_sin + mat.Width * abs_cos;
- var bound_h = mat.Height * abs_cos + mat.Width * abs_sin;
-
- translateTransform[0, 2] += bound_w / 2 - halfWidth;
- translateTransform[1, 2] += bound_h / 2 - halfHeight;*/
-
-
- CvInvoke.WarpAffine(target, target, translateTransform, target.Size);
- }
-
+ target.Rotate((double) operation.AngleDegrees);
LayerMat = mat;
}
}
@@ -724,7 +725,7 @@ namespace UVtools.Core
Mat target = operation.GetRoiOrDefault(mat);
- CvInvoke.Threshold(target, filteredMat, 254, 255, ThresholdType.Binary); // Clean AA
+ CvInvoke.Threshold(target, filteredMat, 127, 255, ThresholdType.Binary); // Clean AA
using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint())
{
@@ -735,7 +736,7 @@ namespace UVtools.Core
for (int i = 0; i < contours.Size; i++)
{
if ((int)arr.GetValue(0, i, 2) != -1 || (int)arr.GetValue(0, i, 3) == -1) continue;
- if (operation.MinimumArea > 1)
+ if (operation.MinimumArea >= 1)
{
var rectangle = CvInvoke.BoundingRectangle(contours[i]);
if (operation.AreaCheckType == OperationSolidify.AreaCheckTypes.More)
diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs
index 38dd145..4ef3a93 100644
--- a/UVtools.Core/Layer/LayerManager.cs
+++ b/UVtools.Core/Layer/LayerManager.cs
@@ -10,7 +10,6 @@ using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
@@ -31,12 +30,24 @@ namespace UVtools.Core
#region Properties
public FileFormat SlicerFile { get; private set; }
+ private Layer[] _layers;
+
/// <summary>
/// Layers List
/// </summary>
- public Layer[] Layers { get; private set; }
+ public Layer[] Layers
+ {
+ get => _layers;
+ private set
+ {
+ _layers = value;
+ if (value is null) return;
+ SlicerFile.PrintTime = SlicerFile.PrintTimeComputed;
+ }
+ }
private Rectangle _boundingRectangle = Rectangle.Empty;
+
public Rectangle BoundingRectangle
{
get => GetBoundingRectangle();
@@ -714,6 +725,9 @@ namespace UVtools.Core
//progress.Reset(operation.ProgressAction);
Mat supportsMat = null;
+ var anchor = new Point(-1, -1);
+ var kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), anchor);
+
uint firstSupportLayerIndex = 0;
for (; firstSupportLayerIndex < Count; firstSupportLayerIndex++)
@@ -731,6 +745,13 @@ namespace UVtools.Core
if (supportsMat is null) return;
Mat patternMat = null;
+ if (operation.DilateIterations > 0)
+ {
+ CvInvoke.Dilate(supportsMat, supportsMat,
+ CvInvoke.GetStructuringElement(ElementShape.Ellipse, new Size(3, 3), new Point(-1, -1)),
+ new Point(-1, -1), operation.DilateIterations, BorderType.Reflect101, new MCvScalar());
+ }
+
switch (operation.ReliefType)
{
case OperationRaftRelief.RaftReliefTypes.Relief:
@@ -740,7 +761,7 @@ namespace UVtools.Core
{
int center = operation.HoleDiameter / 2;
- int centerTwo = operation.HoleDiameter + operation.HoleSpacing + operation.HoleDiameter / 2;
+ //int centerTwo = operation.HoleDiameter + operation.HoleSpacing + operation.HoleDiameter / 2;
int radius = center;
CvInvoke.Circle(shape, new Point(shapeSize / 2, shapeSize / 2), radius, EmguExtensions.WhiteByte, -1);
CvInvoke.Circle(shape, new Point(0, 0), radius / 2, EmguExtensions.WhiteByte, -1);
@@ -748,21 +769,12 @@ namespace UVtools.Core
CvInvoke.Circle(shape, new Point(shapeSize, 0), radius / 2, EmguExtensions.WhiteByte, -1);
CvInvoke.Circle(shape, new Point(shapeSize, shapeSize), radius / 2, EmguExtensions.WhiteByte, -1);
- //shape.Save("D:\\shape.png");
-
CvInvoke.Repeat(shape, supportsMat.Height / shape.Height + 1, supportsMat.Width / shape.Width + 1, patternMat);
-
-
+
patternMat = new Mat(patternMat, new Rectangle(0, 0, supportsMat.Width, supportsMat.Height));
}
break;
- case OperationRaftRelief.RaftReliefTypes.Decimate:
- if (operation.DilateIterations <= 0) break;
- CvInvoke.Dilate(supportsMat, supportsMat,
- CvInvoke.GetStructuringElement(ElementShape.Ellipse, new Size(3, 3), new Point(-1, -1)),
- new Point(-1, -1), operation.DilateIterations, BorderType.Reflect101, new MCvScalar());
- break;
}
progress.Reset(operation.ProgressAction, firstSupportLayerIndex);
@@ -777,12 +789,12 @@ namespace UVtools.Core
case OperationRaftRelief.RaftReliefTypes.Relief:
using (Mat mask = new Mat())
{
- CvInvoke.Subtract(target, supportsMat, mask);
- //target.CopyTo(mask);
- CvInvoke.Erode(mask, mask,
- CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3),
- new Point(-1, -1)),
- new Point(-1, -1), operation.WallMargin, BorderType.Reflect101, new MCvScalar());
+ /*CvInvoke.Subtract(target, supportsMat, mask);
+ CvInvoke.Erode(mask, mask, kernel, anchor, operation.WallMargin, BorderType.Reflect101, new MCvScalar());
+ CvInvoke.Subtract(target, patternMat, target, mask);*/
+
+ CvInvoke.Erode(target, mask, kernel, anchor, operation.WallMargin, BorderType.Reflect101, default);
+ CvInvoke.Subtract(mask, supportsMat, mask);
CvInvoke.Subtract(target, patternMat, target, mask);
}
@@ -906,11 +918,11 @@ namespace UVtools.Core
OperationProgress progress = null)
{
- if (islandConfig is null) islandConfig = new IslandDetectionConfiguration();
- if(overhangConfig is null) overhangConfig = new OverhangDetectionConfiguration();
- if(resinTrapConfig is null) resinTrapConfig = new ResinTrapDetectionConfiguration();
- if(touchBoundConfig is null) touchBoundConfig = new TouchingBoundDetectionConfiguration();
- if(progress is null) progress = new OperationProgress();
+ islandConfig ??= new IslandDetectionConfiguration();
+ overhangConfig ??= new OverhangDetectionConfiguration();
+ resinTrapConfig ??= new ResinTrapDetectionConfiguration();
+ touchBoundConfig ??= new TouchingBoundDetectionConfiguration();
+ progress ??= new OperationProgress();
var result = new ConcurrentBag<LayerIssue>();
var layerHollowAreas = new ConcurrentDictionary<uint, List<LayerHollowArea>>();
@@ -1257,19 +1269,13 @@ namespace UVtools.Core
CvInvoke.Subtract(image, previousImage, subtractedImage);
CvInvoke.Threshold(subtractedImage, subtractedImage, 127, 255, ThresholdType.Binary);
- //subtractedImage.Save($"D:\\subtracted_image\\subtracted{layer.Index}.png");
-
-
CvInvoke.Erode(subtractedImage, subtractedImage,
CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3,3), anchor),
anchor, overhangConfig.ErodeIterations, BorderType.Default, new MCvScalar());
-
-
CvInvoke.FindNonZero(subtractedImage, vecPoints);
if (vecPoints.Size >= overhangConfig.RequiredPixelsToConsider)
{
- //subtractedImage.Save("D:\\subtracted_image\\subtracted_erroded.png");
AddIssue(new LayerIssue(
layer, LayerIssue.IssueType.Overhang, vecPoints.ToArray(), layer.BoundingRectangle
));
@@ -1331,7 +1337,7 @@ namespace UVtools.Core
listHollowArea.Add(new LayerHollowArea(contours[i].ToArray(),
rect,
- layer.Index == Count - 1
+ layer.Index == 0 || layer.Index == Count - 1 // First and Last layers, always drains
? LayerHollowArea.AreaType.Drain
: LayerHollowArea.AreaType.Unknown));
@@ -1344,7 +1350,7 @@ namespace UVtools.Core
});
- for (uint layerIndex = 0; layerIndex < Count - 1; layerIndex++) // Last layers, always drains
+ for (uint layerIndex = 1; layerIndex < Count - 1; layerIndex++) // First and Last layers, always drains
{
if (progress.Token.IsCancellationRequested) break;
if (!layerHollowAreas.TryGetValue(layerIndex, out var areas))
@@ -2058,18 +2064,18 @@ namespace UVtools.Core
if (operationDrawing.BrushSize == 1)
{
- mat.SetByte(operation.Location.X, operation.Location.Y, operationDrawing.Color);
+ mat.SetByte(operation.Location.X, operation.Location.Y, operationDrawing.Brightness);
continue;
}
switch (operationDrawing.BrushShape)
{
case PixelDrawing.BrushShapeType.Rectangle:
- CvInvoke.Rectangle(mat, operationDrawing.Rectangle, new MCvScalar(operationDrawing.Color), operationDrawing.Thickness, operationDrawing.LineType);
+ CvInvoke.Rectangle(mat, operationDrawing.Rectangle, new MCvScalar(operationDrawing.Brightness), operationDrawing.Thickness, operationDrawing.LineType);
break;
case PixelDrawing.BrushShapeType.Circle:
CvInvoke.Circle(mat, operation.Location, operationDrawing.BrushSize / 2,
- new MCvScalar(operationDrawing.Color), operationDrawing.Thickness, operationDrawing.LineType);
+ new MCvScalar(operationDrawing.Brightness), operationDrawing.Thickness, operationDrawing.LineType);
break;
default:
throw new ArgumentOutOfRangeException();
@@ -2080,7 +2086,7 @@ namespace UVtools.Core
var operationText = (PixelText)operation;
var mat = modifiedLayers.GetOrAdd(operation.LayerIndex, u => this[operation.LayerIndex].LayerMat);
- CvInvoke.PutText(mat, operationText.Text, operationText.Location, operationText.Font, operationText.FontScale, new MCvScalar(operationText.Color), operationText.Thickness, operationText.LineType, operationText.Mirror);
+ CvInvoke.PutText(mat, operationText.Text, operationText.Location, operationText.Font, operationText.FontScale, new MCvScalar(operationText.Brightness), operationText.Thickness, operationText.LineType, operationText.Mirror);
}
else if (operation.OperationType == PixelOperation.PixelOperationType.Eraser)
{
@@ -2101,7 +2107,7 @@ namespace UVtools.Core
{
if (!(CvInvoke.PointPolygonTest(layerContours[contourIdx], operation.Location, false) >= 0))
continue;
- CvInvoke.DrawContours(mat, layerContours, contourIdx, new MCvScalar(0, 0, 0), -1);
+ CvInvoke.DrawContours(mat, layerContours, contourIdx, new MCvScalar(operation.PixelBrightness), -1);
break;
}
}
@@ -2124,7 +2130,7 @@ namespace UVtools.Core
using (Mat matCircleMask = matCircleRoi.CloneBlank())
{
CvInvoke.Circle(matCircleMask, new Point(operationSupport.TipDiameter / 2, operationSupport.TipDiameter / 2),
- operationSupport.TipDiameter / 2, new MCvScalar(255), -1);
+ operationSupport.TipDiameter / 2, new MCvScalar(operation.PixelBrightness), -1);
CvInvoke.BitwiseAnd(matCircleRoi, matCircleMask, matCircleMask);
whitePixels = (uint) CvInvoke.CountNonZero(matCircleMask);
}
@@ -2137,7 +2143,7 @@ namespace UVtools.Core
break; // White area end supporting
}
- CvInvoke.Circle(mat, operation.Location, radius, new MCvScalar(255), -1, operationSupport.LineType);
+ CvInvoke.Circle(mat, operation.Location, radius, new MCvScalar(operation.PixelBrightness), -1, operationSupport.LineType);
drawnLayers++;
}
}
@@ -2257,6 +2263,217 @@ namespace UVtools.Core
#endregion
-
+ public void CalibrateElephantFoot(OperationCalibrateElephantFoot operation, OperationProgress progress)
+ {
+ if (ReferenceEquals(progress, null)) progress = new OperationProgress();
+ progress.Reset(operation.ProgressAction, 3);
+ SlicerFile.SuppressRebuildProperties = true;
+
+ Layers = new Layer[operation.BottomLayers + operation.NormalLayers];
+
+ SlicerFile.LayerHeight = (float) operation.LayerHeight;
+ SlicerFile.BottomExposureTime = (float) operation.BottomExposure;
+ SlicerFile.ExposureTime = (float) operation.NormalExposure;
+ SlicerFile.BottomLayerCount = operation.BottomLayers;
+
+ var layers = operation.GetLayers();
+ progress++;
+
+
+ var bottomLayer = new Layer(0, layers[0], this)
+ {
+ IsModified = true
+ };
+ var layer = new Layer(0, layers[1], this)
+ {
+ IsModified = true
+ };
+ bottomLayer.Move(new OperationMove(bottomLayer.BoundingRectangle, layers[0].Size));
+ layer.Move(new OperationMove(layer.BoundingRectangle, layers[0].Size));
+ progress++;
+
+ for (uint layerIndex = 0;
+ layerIndex < operation.BottomLayers+operation.NormalLayers;
+ layerIndex++)
+ {
+ Layers[layerIndex] = SlicerFile.GetInitialLayerValueOrNormal(layerIndex, bottomLayer.Clone(), layer.Clone());
+ }
+
+ foreach (var mat in layers)
+ {
+ mat.Dispose();
+ }
+
+ SlicerFile.LayerCount = Count;
+ RebuildLayersProperties();
+
+ BoundingRectangle = Rectangle.Empty;
+
+ if(SlicerFile.ThumbnailsCount > 0)
+ SlicerFile.SetThumbnails(operation.GetThumbnail());
+
+ progress++;
+
+ SlicerFile.SuppressRebuildProperties = false;
+ }
+
+ public void CalibrateXYZAccuracy(OperationCalibrateXYZAccuracy operation, OperationProgress progress)
+ {
+ if (ReferenceEquals(progress, null)) progress = new OperationProgress();
+ progress.Reset(operation.ProgressAction, operation.LayerCount);
+
+ SlicerFile.SuppressRebuildProperties = true;
+
+ Layers = new Layer[operation.LayerCount];
+
+ SlicerFile.LayerHeight = (float)operation.LayerHeight;
+ SlicerFile.BottomExposureTime = (float)operation.BottomExposure;
+ SlicerFile.ExposureTime = (float)operation.NormalExposure;
+ SlicerFile.BottomLayerCount = operation.BottomLayers;
+
+ var layers = operation.GetLayers();
+
+ var bottomLayer = new Layer(0, layers[0], this)
+ {
+ IsModified = true
+ };
+ var layer = new Layer(0, layers[1], this)
+ {
+ IsModified = true
+ };
+
+ for (uint layerIndex = 0; layerIndex < operation.LayerCount; layerIndex++)
+ {
+ Layers[layerIndex] = SlicerFile.GetInitialLayerValueOrNormal(layerIndex, bottomLayer.Clone(), layer.Clone());
+ progress++;
+ }
+
+ foreach (var mat in layers)
+ {
+ mat.Dispose();
+ }
+
+ SlicerFile.LayerCount = Count;
+ RebuildLayersProperties();
+
+ BoundingRectangle = Rectangle.Empty;
+
+ if (SlicerFile.ThumbnailsCount > 0)
+ SlicerFile.SetThumbnails(operation.GetThumbnail());
+
+ SlicerFile.SuppressRebuildProperties = false;
+ }
+
+ public void CalibrateGrayscale(OperationCalibrateGrayscale operation, OperationProgress progress)
+ {
+ if (ReferenceEquals(progress, null)) progress = new OperationProgress();
+ progress.Reset(operation.ProgressAction, operation.LayerCount);
+ SlicerFile.SuppressRebuildProperties = true;
+
+ Layers = new Layer[operation.LayerCount];
+
+ SlicerFile.LayerHeight = (float)operation.LayerHeight;
+ SlicerFile.BottomExposureTime = (float)operation.BottomExposure;
+ SlicerFile.ExposureTime = (float)operation.NormalExposure;
+ SlicerFile.BottomLayerCount = operation.BottomLayers;
+
+ var layers = operation.GetLayers();
+ progress++;
+
+
+ var bottomLayer = new Layer(0, layers[0], this)
+ {
+ IsModified = true
+ };
+ var interfaceLayer = operation.InterfaceLayers > 0 && layers[1] is not null ? new Layer(0, layers[1], this)
+ {
+ IsModified = true
+ } : null;
+ var layer = new Layer(0, layers[2], this)
+ {
+ IsModified = true
+ };
+
+ uint layerIndex = 0;
+ for (uint i = 0; i < operation.BottomLayers; i++)
+ {
+ Layers[layerIndex] = bottomLayer.Clone();
+ progress++;
+ layerIndex++;
+ }
+
+ for (uint i = 0; i < operation.InterfaceLayers; i++)
+ {
+ Layers[layerIndex] = interfaceLayer.Clone();
+ progress++;
+ layerIndex++;
+ }
+
+
+ for (uint i = 0; i < operation.NormalLayers; i++)
+ {
+ Layers[layerIndex] = layer.Clone();
+ progress++;
+ layerIndex++;
+ }
+
+ foreach (var mat in layers)
+ {
+ mat?.Dispose();
+ }
+
+ SlicerFile.LayerCount = Count;
+ RebuildLayersProperties();
+
+ BoundingRectangle = Rectangle.Empty;
+
+ if (SlicerFile.ThumbnailsCount > 0)
+ SlicerFile.SetThumbnails(operation.GetThumbnail());
+
+ progress++;
+
+ SlicerFile.SuppressRebuildProperties = false;
+ }
+
+ public void CalibrateTolerance(OperationCalibrateTolerance operation, OperationProgress progress)
+ {
+ if (ReferenceEquals(progress, null)) progress = new OperationProgress();
+ progress.Reset(operation.ProgressAction, operation.LayerCount);
+ SlicerFile.SuppressRebuildProperties = true;
+
+ Layers = new Layer[operation.LayerCount];
+
+ SlicerFile.LayerHeight = (float)operation.LayerHeight;
+ SlicerFile.BottomExposureTime = (float)operation.BottomExposure;
+ SlicerFile.ExposureTime = (float)operation.NormalExposure;
+ SlicerFile.BottomLayerCount = operation.BottomLayers;
+
+ var layers = operation.GetLayers();
+
+ Parallel.For(0, operation.LayerCount, layerIndex =>
+ {
+ Layers[layerIndex] = new Layer((uint) layerIndex, layers[layerIndex], this);
+ layers[layerIndex].Dispose();
+ lock (progress)
+ {
+ progress++;
+ }
+ });
+
+ SlicerFile.LayerCount = Count;
+ RebuildLayersProperties();
+
+ BoundingRectangle = Rectangle.Empty;
+ Move(new OperationMove(BoundingRectangle, SlicerFile.Resolution)
+ {
+ IsCutMove = true,
+ LayerIndexEnd = Count-1
+ }, progress);
+
+ if (SlicerFile.ThumbnailsCount > 0)
+ SlicerFile.SetThumbnails(operation.GetThumbnail());
+
+ SlicerFile.SuppressRebuildProperties = false;
+ }
}
}
diff --git a/UVtools.Core/Managers/ClipboardManager.cs b/UVtools.Core/Managers/ClipboardManager.cs
index 6939bb6..abb1a68 100644
--- a/UVtools.Core/Managers/ClipboardManager.cs
+++ b/UVtools.Core/Managers/ClipboardManager.cs
@@ -11,7 +11,7 @@ namespace UVtools.Core.Managers
public sealed class ClipboardItem : IList<Layer>
{
#region Properties
- private readonly List<Layer> _layers = new List<Layer>();
+ private readonly List<Layer> _layers = new();
/// <summary>
/// Gets the LayerCount for this clip
@@ -25,9 +25,20 @@ namespace UVtools.Core.Managers
/// </summary>
public string Description { get; set; }
+
+ public Operations.Operation Operation { get; set; }
+
#endregion
#region Constructor
+ public ClipboardItem(FileFormat slicerFile, Operations.Operation operation) : this(slicerFile)
+ {
+ Operation = operation;
+ string description = operation.ToString();
+ if (!description.StartsWith(operation.Title)) description = $"{operation.Title}: {description}";
+ Description = description;
+ }
+
public ClipboardItem(FileFormat slicerFile, string description = null)
{
LayerCount = slicerFile.LayerCount;
@@ -154,6 +165,8 @@ namespace UVtools.Core.Managers
get => _snapshotLayerManager;
private set => RaiseAndSetIfChanged(ref _snapshotLayerManager, value);
}
+
+ public ClipboardItem CurrentClip => _currentIndex < 0 || _currentIndex >= Count ? null : this[_currentIndex];
public bool CanUndo => CurrentIndex < Count - 1;
public bool CanRedo => CurrentIndex > 0;
@@ -255,7 +268,7 @@ namespace UVtools.Core.Managers
/// <summary>
/// Collect differences and create a clip
/// </summary>
- public void Clip(string description = null, LayerManager layerManagerSnapshot = null)
+ public ClipboardItem Clip(string description = null, LayerManager layerManagerSnapshot = null)
{
if(!(layerManagerSnapshot is null)) Snapshot(layerManagerSnapshot);
if (SnapshotLayerManager is null) throw new InvalidOperationException("A snapshot is required before perform a clip");
@@ -276,9 +289,7 @@ namespace UVtools.Core.Managers
// Collect leftovers from snapshot
// This happens when current layer manager has less layers then the snapshot/previous
// So we need to preserve them
- for (;
- layerIndex < SlicerFile.LayerCount;
- layerIndex++)
+ for (; layerIndex < SlicerFile.LayerCount; layerIndex++)
{
clip.Add(SlicerFile[layerIndex].Clone());
}
@@ -288,7 +299,7 @@ namespace UVtools.Core.Managers
if (clip.Count == 0)
{
if(Count > 0 && this[0].LayerCount == clip.LayerCount)
- return;
+ return null;
}
SuppressRestore = true;
@@ -306,6 +317,21 @@ namespace UVtools.Core.Managers
CurrentIndex = 0;
SuppressRestore = false;
+
+ return clip;
+ }
+
+ /// <summary>
+ /// Collect differences and create a clip
+ /// </summary>
+ public ClipboardItem Clip(Operations.Operation operation, LayerManager layerManagerSnapshot = null)
+ {
+ string description = operation.ToString();
+ if (!description.StartsWith(operation.Title)) description = $"{operation.Title}: {description}";
+ var clip = Clip(description, layerManagerSnapshot);
+ if (clip is null) return null;
+ clip.Operation = operation;
+ return clip;
}
public void Undo()
diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs
index 385b6a9..2786c0d 100644
--- a/UVtools.Core/Operations/Operation.cs
+++ b/UVtools.Core/Operations/Operation.cs
@@ -15,12 +15,13 @@ using UVtools.Core.Objects;
namespace UVtools.Core.Operations
{
[Serializable]
- public abstract class Operation : BindableBase
+ public abstract class Operation : BindableBase, IDisposable
{
private Rectangle _roi = Rectangle.Empty;
private uint _layerIndexEnd;
private uint _layerIndexStart;
private string _profileName;
+ private bool _profileIsDefault;
private Enumerations.LayerRangeSelection _layerRangeSelection = Enumerations.LayerRangeSelection.All;
public const byte ClassNameLength = 9;
@@ -133,6 +134,12 @@ namespace UVtools.Core.Operations
set => RaiseAndSetIfChanged(ref _profileName, value);
}
+ public bool ProfileIsDefault
+ {
+ get => _profileIsDefault;
+ set => RaiseAndSetIfChanged(ref _profileIsDefault, value);
+ }
+
/// <summary>
/// Gets or sets an ROI to process this operation
/// </summary>
@@ -175,5 +182,7 @@ namespace UVtools.Core.Operations
return $" [Layers: {LayerRangeSelection}]";
}
}
+
+ public virtual void Dispose() { }
}
}
diff --git a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs
new file mode 100644
index 0000000..3c477a6
--- /dev/null
+++ b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs
@@ -0,0 +1,615 @@
+/*
+ * 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;
+using System.Drawing;
+using System.Text;
+using System.Xml.Serialization;
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+using Emgu.CV.Structure;
+using Emgu.CV.Util;
+using UVtools.Core.Extensions;
+using UVtools.Core.Objects;
+
+namespace UVtools.Core.Operations
+{
+ [Serializable]
+ public sealed class OperationCalibrateElephantFoot : Operation
+ {
+ #region Members
+ private Size _resolution = Size.Empty;
+ private decimal _layerHeight = 0.05M;
+ private bool _syncLayers;
+ private ushort _bottomLayers = 10;
+ private ushort _normalLayers = 70;
+ private decimal _bottomExposure = 60;
+ private decimal _normalExposure = 12;
+ private byte _margin = 30;
+ private bool _isErodeEnabled = true;
+ private byte _erodeStartIteration = 2;
+ private byte _erodeEndIteration = 6;
+ private byte _erodeIterationSteps = 1;
+ private bool _isDimmingEnabled = true;
+ private byte _dimmingWallThickness = 20;
+ private byte _dimmingStartBrightness = 140;
+ private byte _dimmingEndBrightness = 200;
+ private byte _dimmingBrightnessSteps = 20;
+ private bool _outputOriginalPart = true;
+ private bool _enableAntiAliasing = true;
+
+ #endregion
+
+ #region Overrides
+
+ public override bool CanROI => false;
+
+ public override bool CanCancel => false;
+
+ public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None;
+
+ public override string Title => "Elephant foot";
+ public override string Description =>
+ "Generates test models with various strategies and increments to verify the best method/values to remove the elephant foot.\n" +
+ "Elephant foot is removed when bottom layers are flush and aligned with normal layers.\n" +
+ "You must repeat this test when change any of the following: printer, LEDs, resin and exposure times.\n" +
+ "Note: The current opened file will be overwritten with this test, use a dummy or a not needed file.";
+
+ public override string ConfirmationText =>
+ $"generate the elephant foot test?";
+
+ public override string ProgressTitle =>
+ $"Generating the elephant foot test";
+
+ public override string ProgressAction => "Generated";
+
+ public override StringTag Validate(params object[] parameters)
+ {
+ var sb = new StringBuilder();
+
+ if (_isErodeEnabled && _erodeStartIteration > _erodeEndIteration)
+ {
+ sb.AppendLine("Erode start iterations can't be higher than end iterations.");
+ }
+
+ if (_isDimmingEnabled && _dimmingStartBrightness > _dimmingEndBrightness)
+ {
+ sb.AppendLine("Wall dimming start brightness can't be higher than end brightness.");
+ }
+
+ if (ObjectCount <= 0)
+ {
+ sb.AppendLine("No objects to output, please adjust the settings.");
+ }
+
+ return new StringTag(sb.ToString());
+ }
+
+ public override string ToString()
+ {
+ var result = $"[Layer Height: {_layerHeight}] " +
+ $"[Layers: {_bottomLayers}/{_normalLayers}] " +
+ $"[Exposure: {_bottomExposure}/{_normalExposure}] " +
+ $"[Margin: {_margin}] [ORI: {_outputOriginalPart}]" +
+ $"[E: {_erodeStartIteration}-{_erodeEndIteration} S{_erodeIterationSteps}] " +
+ $"[D: W{_dimmingWallThickness} B{_dimmingStartBrightness}-{_dimmingEndBrightness} S{_dimmingBrightnessSteps}] " +
+ $"[AA: {_enableAntiAliasing}]";
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }
+
+ #endregion
+
+ #region Properties
+
+ [XmlIgnore]
+ public Size Resolution
+ {
+ get => _resolution;
+ set => RaiseAndSetIfChanged(ref _resolution, value);
+ }
+
+
+ public decimal LayerHeight
+ {
+ get => _layerHeight;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _layerHeight, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(BottomHeight));
+ RaisePropertyChanged(nameof(NormalHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ }
+ }
+
+ public ushort Microns => (ushort) (LayerHeight * 1000);
+
+ public bool SyncLayers
+ {
+ get => _syncLayers;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _syncLayers, value)) return;
+ if (_syncLayers)
+ {
+ NormalLayers = _bottomLayers;
+ }
+ }
+ }
+
+ public ushort BottomLayers
+ {
+ get => _bottomLayers;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _bottomLayers, value)) return;
+ if (_syncLayers)
+ {
+ NormalLayers = _bottomLayers;
+ }
+ RaisePropertyChanged(nameof(BottomHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ RaisePropertyChanged(nameof(LayerCount));
+ }
+ }
+
+ public ushort NormalLayers
+ {
+ get => _normalLayers;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _normalLayers, value)) return;
+ if (_syncLayers)
+ {
+ BottomLayers = _normalLayers;
+ }
+ RaisePropertyChanged(nameof(NormalHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ RaisePropertyChanged(nameof(LayerCount));
+ }
+ }
+
+ public uint LayerCount => (uint)(_bottomLayers + _normalLayers);
+
+ public decimal BottomHeight => Math.Round(LayerHeight * BottomLayers, 2);
+ public decimal NormalHeight => Math.Round(LayerHeight * NormalLayers, 2);
+
+ public decimal TotalHeight => BottomHeight + NormalHeight;
+
+ public decimal BottomExposure
+ {
+ get => _bottomExposure;
+ set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2));
+ }
+
+ public decimal NormalExposure
+ {
+ get => _normalExposure;
+ set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2));
+ }
+
+ public byte Margin
+ {
+ get => _margin;
+ set => RaiseAndSetIfChanged(ref _margin, value);
+ }
+
+ public bool OutputOriginalPart
+ {
+ get => _outputOriginalPart;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _outputOriginalPart, value)) return;
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public bool EnableAntiAliasing
+ {
+ get => _enableAntiAliasing;
+ set => RaiseAndSetIfChanged(ref _enableAntiAliasing, value);
+ }
+
+ public bool IsErodeEnabled
+ {
+ get => _isErodeEnabled;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _isErodeEnabled, value)) return;
+ RaisePropertyChanged(nameof(ErodeObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public byte ErodeStartIteration
+ {
+ get => _erodeStartIteration;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _erodeStartIteration, value)) return;
+ RaisePropertyChanged(nameof(ErodeObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public byte ErodeEndIteration
+ {
+ get => _erodeEndIteration;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _erodeEndIteration, value)) return;
+ RaisePropertyChanged(nameof(ErodeObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public byte ErodeIterationSteps
+ {
+ get => _erodeIterationSteps;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _erodeIterationSteps, value)) return;
+ RaisePropertyChanged(nameof(ErodeObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ [XmlIgnore]
+ public Kernel ErodeKernel { get; set; } = new();
+
+ public bool IsDimmingEnabled
+ {
+ get => _isDimmingEnabled;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _isDimmingEnabled, value)) return;
+ RaisePropertyChanged(nameof(DimmingObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public byte DimmingWallThickness
+ {
+ get => _dimmingWallThickness;
+ set => RaiseAndSetIfChanged(ref _dimmingWallThickness, value);
+ }
+
+ public byte DimmingStartBrightness
+ {
+ get => _dimmingStartBrightness;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _dimmingStartBrightness, value)) return;
+ RaisePropertyChanged(nameof(DimmingStartBrightnessPercent));
+ RaisePropertyChanged(nameof(DimmingObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public float DimmingStartBrightnessPercent => (float) Math.Round(_dimmingStartBrightness * 100 / 255M, 2);
+
+ public byte DimmingEndBrightness
+ {
+ get => _dimmingEndBrightness;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _dimmingEndBrightness, value)) return;
+ RaisePropertyChanged(nameof(DimmingEndBrightnessPercent));
+ RaisePropertyChanged(nameof(DimmingObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public float DimmingEndBrightnessPercent => (float)Math.Round(_dimmingEndBrightness * 100 / 255M, 2);
+
+ public byte DimmingBrightnessSteps
+ {
+ get => _dimmingBrightnessSteps;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _dimmingBrightnessSteps, value)) return;
+ RaisePropertyChanged(nameof(DimmingObjects));
+ RaisePropertyChanged(nameof(ObjectCount));
+ }
+ }
+
+ public int ErodeObjects => _isErodeEnabled ?
+ (int)Math.Floor((_erodeEndIteration - _erodeStartIteration) / (decimal)_erodeIterationSteps) + 1
+ : 0;
+
+ public int DimmingObjects => _isDimmingEnabled ?
+ (int) Math.Floor((_dimmingEndBrightness - _dimmingStartBrightness) / (decimal) _dimmingBrightnessSteps) + 1
+ : 0;
+
+ public int ObjectCount => (_outputOriginalPart ? 1 : 0) + ErodeObjects + DimmingObjects;
+
+ #endregion
+
+ #region Equality
+
+ private bool Equals(OperationCalibrateElephantFoot other)
+ {
+ return _layerHeight == other._layerHeight && _syncLayers == other._syncLayers && _bottomLayers == other._bottomLayers && _normalLayers == other._normalLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _margin == other._margin && _isErodeEnabled == other._isErodeEnabled && _erodeStartIteration == other._erodeStartIteration && _erodeEndIteration == other._erodeEndIteration && _erodeIterationSteps == other._erodeIterationSteps && _isDimmingEnabled == other._isDimmingEnabled && _dimmingWallThickness == other._dimmingWallThickness && _dimmingStartBrightness == other._dimmingStartBrightness && _dimmingEndBrightness == other._dimmingEndBrightness && _dimmingBrightnessSteps == other._dimmingBrightnessSteps && _outputOriginalPart == other._outputOriginalPart && _enableAntiAliasing == other._enableAntiAliasing;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj) || obj is OperationCalibrateElephantFoot other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+ hashCode.Add(_layerHeight);
+ hashCode.Add(_syncLayers);
+ hashCode.Add(_bottomLayers);
+ hashCode.Add(_normalLayers);
+ hashCode.Add(_bottomExposure);
+ hashCode.Add(_normalExposure);
+ hashCode.Add(_margin);
+ hashCode.Add(_isErodeEnabled);
+ hashCode.Add(_erodeStartIteration);
+ hashCode.Add(_erodeEndIteration);
+ hashCode.Add(_erodeIterationSteps);
+ hashCode.Add(_isDimmingEnabled);
+ hashCode.Add(_dimmingWallThickness);
+ hashCode.Add(_dimmingStartBrightness);
+ hashCode.Add(_dimmingEndBrightness);
+ hashCode.Add(_dimmingBrightnessSteps);
+ hashCode.Add(_outputOriginalPart);
+ hashCode.Add(_enableAntiAliasing);
+ return hashCode.ToHashCode();
+ }
+
+ #endregion
+
+ #region Methods
+
+ /// <summary>
+ /// Gets the bottom and normal layers, 0 = bottom | 1 = normal
+ /// </summary>
+ /// <returns></returns>
+ public Mat[] GetLayers()
+ {
+ Mat[] layers = new Mat[2];
+ var anchor = new Point(-1, -1);
+ var kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), anchor);
+
+ layers[0] = EmguExtensions.InitMat(Resolution);
+ LineType lineType = _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected;
+ const int length = 250;
+ const int triangleLength = 50;
+ const byte startX = 2;
+ const byte startY = 2;
+ int x = startX;
+ int y = startY;
+
+ int maxX = x;
+ int maxY = y;
+
+ var pointList = new List<Point> { new(x, y) };
+
+ void addPoint()
+ {
+ maxX = Math.Max(maxX, x);
+ maxY = Math.Max(maxY, y);
+ pointList.Add(new Point(x, y));
+ }
+
+ x += length;
+ addPoint();
+
+ x -= triangleLength;
+ y += triangleLength;
+ addPoint();
+
+ y += 50;
+ addPoint();
+
+ x += triangleLength;
+ y += triangleLength;
+ addPoint();
+ x -= triangleLength;
+ y += triangleLength;
+ addPoint();
+
+ x += triangleLength;
+ y += triangleLength;
+ addPoint();
+ x -= triangleLength;
+ y += triangleLength;
+ addPoint();
+
+ x += triangleLength;
+ addPoint();
+ y += triangleLength;
+ addPoint();
+ x -= triangleLength;
+ y += triangleLength;
+ addPoint();
+
+ x = startX;
+ addPoint();
+
+
+ const byte ellipseHeight = 50;
+
+ maxY += ellipseHeight;
+ using Mat shape = EmguExtensions.InitMat(new Size(maxX + startX, maxY + startY));
+ CvInvoke.FillPoly(shape, new VectorOfPoint(pointList.ToArray()), EmguExtensions.WhiteByte, lineType);
+ CvInvoke.Circle(shape, new Point(0, 0), length / 4, EmguExtensions.BlackByte,
+ -1, lineType);
+ CvInvoke.Ellipse(shape, new Point(maxX / 2, maxY - ellipseHeight), new Size(maxX / 3, ellipseHeight), 0, 0, 360,
+ EmguExtensions.WhiteByte, -1, lineType);
+ CvInvoke.Circle(shape, new Point(length / 2, maxY - 100), length / 5, EmguExtensions.BlackByte, -1, lineType);
+
+ int currentX = 0;
+ int currentY = 0;
+
+ maxX = 0;
+
+ const FontFace font = FontFace.HersheyDuplex;
+ const byte fontStartX = 30;
+ const byte fontStartY = length / 4 + 42;
+ const double fontScale = 1.3;
+ const byte fontThickness = 3;
+ const byte fontMargin = 42;
+
+ void addText(Mat mat, ushort number, params string[] text)
+ {
+ CvInvoke.PutText(mat, number.ToString(), new Point(100, 55), font, 1.5, EmguExtensions.BlackByte, 4, lineType);
+ CvInvoke.PutText(mat, "UVtools EP", new Point(fontStartX, fontStartY), font, 0.8, EmguExtensions.BlackByte, 2, lineType);
+ CvInvoke.PutText(mat, $"{Microns}um", new Point(fontStartX, fontStartY + fontMargin), font, fontScale, EmguExtensions.BlackByte, fontThickness, lineType);
+ CvInvoke.PutText(mat, $"{BottomExposure}|{NormalExposure}s", new Point(fontStartX, fontStartY + fontMargin * 2), font, fontScale, EmguExtensions.BlackByte, fontThickness, lineType);
+ if (text is null) return;
+ for (var i = 0; i < text.Length; i++)
+ {
+ CvInvoke.PutText(mat, text[i], new Point(fontStartX, fontStartY + fontMargin * (i + 3)), font,
+ fontScale, EmguExtensions.BlackByte, fontThickness, lineType);
+ }
+
+ }
+
+ ushort count = 0;
+
+ if (OutputOriginalPart)
+ {
+ using var roi = new Mat(layers[0], new Rectangle(new Point(currentX, currentY), shape.Size));
+ shape.CopyTo(roi);
+ addText(roi, ++count, "ORI");
+ }
+ else
+ {
+ currentX -= shape.Width + Margin;
+ }
+
+
+ layers[1] = layers[0].Clone();
+
+ if (IsErodeEnabled)
+ {
+ for (int iteration = ErodeStartIteration;
+ iteration <= ErodeEndIteration;
+ iteration += ErodeIterationSteps)
+ {
+ currentX += shape.Width + Margin;
+ maxX = Math.Max(maxX, currentX);
+
+ if (currentX + shape.Width >= layers[0].Width)
+ {
+ currentX = startX;
+ currentY += shape.Height + Margin;
+ }
+
+ if (currentY + shape.Height >= layers[0].Height)
+ {
+ break; // Insufficient size
+ }
+
+ count++;
+ using (var roi = new Mat(layers[1], new Rectangle(new Point(currentX, currentY), shape.Size)))
+ {
+ shape.CopyTo(roi);
+ addText(roi, count, $"E: {iteration}i");
+ }
+
+ using (var roi = new Mat(layers[0],
+ new Rectangle(new Point(currentX, currentY), shape.Size)))
+ using (var erode = new Mat())
+ {
+ CvInvoke.Erode(shape, erode, ErodeKernel.Matrix, ErodeKernel.Anchor, iteration, BorderType.Reflect101,
+ default);
+ erode.CopyTo(roi);
+ addText(roi, count, $"E: {iteration}i");
+ }
+ }
+ }
+
+ if (IsDimmingEnabled)
+ {
+ for (int brightness = DimmingStartBrightness;
+ brightness <= DimmingEndBrightness;
+ brightness += DimmingBrightnessSteps)
+ {
+ currentX += shape.Width + Margin;
+
+ if (currentX + shape.Width >= layers[0].Width)
+ {
+ currentX = 0;
+ currentY += shape.Height + Margin;
+ }
+
+ if (currentY + shape.Height >= layers[0].Height)
+ {
+ break; // Insufficient size
+ }
+
+ count++;
+ using (var roi = new Mat(layers[1], new Rectangle(new Point(currentX, currentY), shape.Size)))
+ {
+ shape.CopyTo(roi);
+ addText(roi, count, $"W: {DimmingWallThickness}", $"B: {brightness}");
+ }
+
+ using (var roi = new Mat(layers[0],
+ new Rectangle(new Point(currentX, currentY), shape.Size)))
+ using (var erode = new Mat())
+ using (var diff = new Mat())
+ using (var target = new Mat())
+ using (var mask = shape.CloneBlank())
+ {
+ mask.SetTo(new MCvScalar(brightness));
+ CvInvoke.Erode(shape, erode, kernel, anchor, DimmingWallThickness, BorderType.Reflect101,
+ default);
+ CvInvoke.Subtract(shape, erode, diff);
+ CvInvoke.BitwiseAnd(diff, mask, target);
+ CvInvoke.Add(erode, target, target);
+ target.CopyTo(roi);
+ addText(roi, count, $"W: {DimmingWallThickness}", $"B: {brightness}");
+ }
+ }
+ }
+
+ // Preview
+ //layers[2] = new Mat(layers[0], new Rectangle(0, 0, Math.Min(layers[0].Width, maxX), Math.Min(layers[0].Height, currentY)));
+
+ return layers;
+ }
+
+ public Mat GetThumbnail()
+ {
+ Mat thumbnail = EmguExtensions.InitMat(new Size(400, 200), 3);
+ var fontFace = FontFace.HersheyDuplex;
+ var fontScale = 1;
+ var fontThickness = 2;
+ const byte xSpacing = 45;
+ const byte ySpacing = 45;
+ CvInvoke.PutText(thumbnail, "UVtools", new Point(140, 35), fontFace, fontScale, new MCvScalar(255, 27, 245), fontThickness + 1);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, 0), new Point(xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, ySpacing + 5), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(thumbnail.Width - xSpacing, 0), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.PutText(thumbnail, "Elephant Foot Cal.", new Point(xSpacing, ySpacing * 2), fontFace, fontScale, new MCvScalar(0, 255, 255), fontThickness);
+ CvInvoke.PutText(thumbnail, $"{Microns}um @ {BottomExposure}s/{NormalExposure}s", new Point(xSpacing, ySpacing * 3), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+ CvInvoke.PutText(thumbnail, $"{ObjectCount} Objects", new Point(xSpacing, ySpacing * 4), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+
+ /*thumbnail.SetTo(EmguExtensions.Black3Byte);
+
+ CvInvoke.Circle(thumbnail, new Point(400/2, 200/2), 200/2, EmguExtensions.White3Byte, -1);
+ for (int angle = 0; angle < 360; angle+=20)
+ {
+ CvInvoke.Line(thumbnail, new Point(400 / 2, 200 / 2), new Point((int)(400 / 2 + 100 * Math.Cos(angle * Math.PI / 180)), (int)(200 / 2 + 100 * Math.Sin(angle * Math.PI / 180))), new MCvScalar(255, 27, 245), 3);
+ }
+
+ thumbnail.Save("D:\\Thumbnail.png");*/
+ return thumbnail;
+ }
+
+ #endregion
+ }
+}
diff --git a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs
new file mode 100644
index 0000000..4e9a015
--- /dev/null
+++ b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs
@@ -0,0 +1,442 @@
+/*
+ * 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.Drawing;
+using System.Text;
+using System.Xml.Serialization;
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+using Emgu.CV.Structure;
+using Emgu.CV.Util;
+using UVtools.Core.Extensions;
+using UVtools.Core.Objects;
+
+namespace UVtools.Core.Operations
+{
+ [Serializable]
+ public sealed class OperationCalibrateGrayscale : Operation
+ {
+ #region Members
+ private Size _resolution = Size.Empty;
+ private decimal _layerHeight = 0.05M;
+ private ushort _bottomLayers = 10;
+ private ushort _interfaceLayers = 5;
+ private ushort _normalLayers = 30;
+ private decimal _bottomExposure = 60;
+ private decimal _normalExposure = 12;
+ private ushort _outerMargin = 200;
+ private ushort _innerMargin = 50;
+ private byte _startBrightness = 175;
+ private byte _endBrightness = 255;
+ private byte _brightnessSteps = 10;
+ private bool _enableCenterHoleRelief = true;
+ private ushort _centerHoleDiameter = 200;
+ private bool _enableLineDivisions = true;
+ private byte _lineDivisionThickness = 30;
+ private byte _lineDivisionBrightness = 255;
+ private short _textXOffset;
+ private bool _enableAntiAliasing = true;
+
+ #endregion
+
+ #region Overrides
+
+ public override bool CanROI => false;
+
+ public override bool CanCancel => false;
+
+ public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None;
+
+ public override string Title => "Grayscale";
+ public override string Description =>
+ "Generates test models with various strategies and increments to verify the LED power against the grayscale levels.\n" +
+ "You must repeat this test when change any of the following: printer, LEDs, resin and exposure times.\n" +
+ "Note: The current opened file will be overwritten with this test, use a dummy or a not needed file.";
+
+ public override string ConfirmationText =>
+ $"generate the grayscale test?";
+
+ public override string ProgressTitle =>
+ $"Generating the grayscale test";
+
+ public override string ProgressAction => "Generated";
+
+ public override StringTag Validate(params object[] parameters)
+ {
+ var sb = new StringBuilder();
+
+ if (_startBrightness > _endBrightness)
+ {
+ sb.AppendLine("Start brightness must be lower or equal to end brightness.");
+ }
+ if (Divisions <= 0)
+ {
+ sb.AppendLine("No divisions to output.");
+ }
+
+ return new StringTag(sb.ToString());
+ }
+
+ public override string ToString()
+ {
+ var result = $"[Layer Height: {_layerHeight}] " +
+ $"[Layers: {_bottomLayers}/{_interfaceLayers}/{_normalLayers}] " +
+ $"[Exposure: {_bottomExposure}/{_normalExposure}] " +
+ $"[Margin: {_outerMargin}] " +
+ $"[B: {_startBrightness}-{_endBrightness} S{_brightnessSteps}] " +
+ $"[AA: {_enableAntiAliasing}]";
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }
+
+ #endregion
+
+ #region Properties
+
+ [XmlIgnore]
+ public Size Resolution
+ {
+ get => _resolution;
+ set => RaiseAndSetIfChanged(ref _resolution, value);
+ }
+
+
+ public decimal LayerHeight
+ {
+ get => _layerHeight;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _layerHeight, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(BottomHeight));
+ RaisePropertyChanged(nameof(InterfaceHeight));
+ RaisePropertyChanged(nameof(NormalHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ }
+ }
+
+ public ushort Microns => (ushort) (LayerHeight * 1000);
+
+ public ushort BottomLayers
+ {
+ get => _bottomLayers;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _bottomLayers, value)) return;
+ RaisePropertyChanged(nameof(BottomHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ RaisePropertyChanged(nameof(LayerCount));
+ }
+ }
+
+ public ushort InterfaceLayers
+ {
+ get => _interfaceLayers;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _interfaceLayers, value)) return;
+ RaisePropertyChanged(nameof(InterfaceHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ RaisePropertyChanged(nameof(LayerCount));
+ }
+ }
+
+ public ushort NormalLayers
+ {
+ get => _normalLayers;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _normalLayers, value)) return;
+ RaisePropertyChanged(nameof(NormalHeight));
+ RaisePropertyChanged(nameof(TotalHeight));
+ RaisePropertyChanged(nameof(LayerCount));
+ }
+ }
+
+ public uint LayerCount => (uint) (_bottomLayers + _interfaceLayers + _normalLayers);
+
+ public decimal BottomHeight => Math.Round(LayerHeight * _bottomLayers, 2);
+ public decimal InterfaceHeight => Math.Round(LayerHeight * _interfaceLayers, 2);
+ public decimal NormalHeight => Math.Round(LayerHeight * _normalLayers, 2);
+
+ public decimal TotalHeight => BottomHeight + InterfaceHeight + NormalHeight;
+
+ public decimal BottomExposure
+ {
+ get => _bottomExposure;
+ set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2));
+ }
+
+ public decimal NormalExposure
+ {
+ get => _normalExposure;
+ set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2));
+ }
+
+ public ushort OuterMargin
+ {
+ get => _outerMargin;
+ set => RaiseAndSetIfChanged(ref _outerMargin, value);
+ }
+
+ public ushort InnerMargin
+ {
+ get => _innerMargin;
+ set => RaiseAndSetIfChanged(ref _innerMargin, value);
+ }
+
+ public byte StartBrightness
+ {
+ get => _startBrightness;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _startBrightness, value)) return;
+ RaisePropertyChanged(nameof(StartBrightnessPercent));
+ RaisePropertyChanged(nameof(Divisions));
+ RaisePropertyChanged(nameof(AngleStep));
+ }
+ }
+
+ public float StartBrightnessPercent => (float)Math.Round(_startBrightness * 100 / 255M, 2);
+
+ public byte EndBrightness
+ {
+ get => _endBrightness;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _endBrightness, value)) return;
+ RaisePropertyChanged(nameof(EndBrightnessPercent));
+ RaisePropertyChanged(nameof(Divisions));
+ RaisePropertyChanged(nameof(AngleStep));
+ }
+ }
+
+ public float EndBrightnessPercent => (float)Math.Round(_endBrightness * 100 / 255M, 2);
+
+ public byte BrightnessSteps
+ {
+ get => _brightnessSteps;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _brightnessSteps, value)) return;
+ RaisePropertyChanged(nameof(Divisions));
+ RaisePropertyChanged(nameof(AngleStep));
+ }
+ }
+
+ public int Divisions => (int)Math.Floor((_endBrightness - _startBrightness) / (decimal)_brightnessSteps) + 1;
+ public float AngleStep => 360f / Divisions;
+
+ public bool EnableCenterHoleRelief
+ {
+ get => _enableCenterHoleRelief;
+ set => RaiseAndSetIfChanged(ref _enableCenterHoleRelief, value);
+ }
+
+ public ushort CenterHoleDiameter
+ {
+ get => _centerHoleDiameter;
+ set => RaiseAndSetIfChanged(ref _centerHoleDiameter, value);
+ }
+
+ public bool EnableLineDivisions
+ {
+ get => _enableLineDivisions;
+ set => RaiseAndSetIfChanged(ref _enableLineDivisions, value);
+ }
+
+ public byte LineDivisionThickness
+ {
+ get => _lineDivisionThickness;
+ set => RaiseAndSetIfChanged(ref _lineDivisionThickness, value);
+ }
+
+ public byte LineDivisionBrightness
+ {
+ get => _lineDivisionBrightness;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _lineDivisionBrightness, value)) return;
+ RaisePropertyChanged(nameof(LineDivisionBrightnessPercent));
+ }
+ }
+
+ public float LineDivisionBrightnessPercent => (float)Math.Round(_lineDivisionBrightness * 100 / 255M, 2);
+
+ public short TextXOffset
+ {
+ get => _textXOffset;
+ set => RaiseAndSetIfChanged(ref _textXOffset, value);
+ }
+
+ public bool EnableAntiAliasing
+ {
+ get => _enableAntiAliasing;
+ set => RaiseAndSetIfChanged(ref _enableAntiAliasing, value);
+ }
+
+ #endregion
+
+ #region Equality
+
+ private bool Equals(OperationCalibrateGrayscale other)
+ {
+ return _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _normalLayers == other._normalLayers && _interfaceLayers == other._interfaceLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _outerMargin == other._outerMargin && _innerMargin == other._innerMargin && _startBrightness == other._startBrightness && _endBrightness == other._endBrightness && _brightnessSteps == other._brightnessSteps && _enableCenterHoleRelief == other._enableCenterHoleRelief && _centerHoleDiameter == other._centerHoleDiameter && _enableLineDivisions == other._enableLineDivisions && _lineDivisionThickness == other._lineDivisionThickness && _lineDivisionBrightness == other._lineDivisionBrightness && _textXOffset == other._textXOffset && _enableAntiAliasing == other._enableAntiAliasing;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj) || obj is OperationCalibrateGrayscale other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+ hashCode.Add(_layerHeight);
+ hashCode.Add(_bottomLayers);
+ hashCode.Add(_normalLayers);
+ hashCode.Add(_interfaceLayers);
+ hashCode.Add(_bottomExposure);
+ hashCode.Add(_normalExposure);
+ hashCode.Add(_outerMargin);
+ hashCode.Add(_innerMargin);
+ hashCode.Add(_startBrightness);
+ hashCode.Add(_endBrightness);
+ hashCode.Add(_brightnessSteps);
+ hashCode.Add(_enableCenterHoleRelief);
+ hashCode.Add(_centerHoleDiameter);
+ hashCode.Add(_enableLineDivisions);
+ hashCode.Add(_lineDivisionThickness);
+ hashCode.Add(_lineDivisionBrightness);
+ hashCode.Add(_textXOffset);
+ hashCode.Add(_enableAntiAliasing);
+ return hashCode.ToHashCode();
+ }
+
+ #endregion
+
+ #region Methods
+
+ /// <summary>
+ /// Gets the bottom and normal layers, 0 = bottom | 1 = normal
+ /// </summary>
+ /// <returns></returns>
+ public Mat[] GetLayers()
+ {
+ Mat[] layers = new Mat[3];
+
+ layers[0] = EmguExtensions.InitMat(Resolution);
+
+ int radius = Math.Max(100, Math.Min(Resolution.Width, Resolution.Height) - _outerMargin * 2) / 2 ;
+ Point center = new Point(Resolution.Width / 2, Resolution.Height / 2);
+ int innerRadius = Math.Max(100, radius - _innerMargin);
+ double topLineLength = 0;
+
+ CvInvoke.Circle(layers[0], center, radius, EmguExtensions.WhiteByte, -1, LineType.AntiAlias);
+ layers[1] = layers[0].Clone();
+ layers[2] = layers[0].Clone();
+
+ LineType lineType = _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected;
+
+ int i = 0;
+ for (ushort brightness = _startBrightness; brightness <= _endBrightness; brightness += _brightnessSteps)
+ {
+ var radians = new float[2];
+ var degrees = new SizeF[2];
+ for (int n = 0; n < 2; n++)
+ {
+ radians[n] = -AngleStep * (i + n);
+ degrees[n] = new SizeF((float) Math.Cos(radians[n] * Math.PI / 180), (float) Math.Sin(radians[n] * Math.PI / 180));
+ }
+
+ Point[] points = new Point[3];
+ points[0] = center;
+ points[1] = new(center.X + (int) (innerRadius * degrees[0].Width), center.Y + (int) (innerRadius * degrees[0].Height));
+ points[2] = new(center.X + (int) (innerRadius * degrees[1].Width), center.Y + (int) (innerRadius * degrees[1].Height));
+ using var vec = new VectorOfPoint(points);
+
+ if (topLineLength == 0) topLineLength = PointExtensions.FindLength(points[1], points[2]);
+
+ CvInvoke.FillPoly(layers[2], vec, new MCvScalar(brightness), lineType);
+
+ if (_enableLineDivisions && _lineDivisionThickness > 0)
+ {
+ CvInvoke.Polylines(layers[2], vec, false, new MCvScalar(_lineDivisionBrightness), _lineDivisionThickness, lineType);
+ }
+
+ i++;
+ }
+
+ FontFace fontFace = FontFace.HersheyDuplex;
+ double fontScale = 2;
+ int fontThickness = 5;
+ Point fontPoint = new Point(center.X + radius / 2 + _textXOffset, (int)(center.Y + AngleStep / 1.1));
+
+ var halfAngleStep = AngleStep / 2;
+ var rotatedAngle = halfAngleStep;
+
+
+ layers[2].Rotate(halfAngleStep);
+ for (ushort brightness = _startBrightness; brightness <= _endBrightness; brightness += _brightnessSteps)
+ {
+ CvInvoke.PutText(layers[2], brightness.ToString(), fontPoint, fontFace, fontScale, EmguExtensions.BlackByte, fontThickness, lineType);
+ rotatedAngle += AngleStep;
+ layers[2].Rotate(AngleStep);
+ }
+
+ layers[2].Rotate(-rotatedAngle);
+
+ if (_enableCenterHoleRelief && _centerHoleDiameter > 1)
+ {
+ var holeRadius = Math.Min(radius, _centerHoleDiameter) / 2;
+ if (_innerMargin > 0)
+ {
+ CvInvoke.Circle(layers[2], center, holeRadius + _innerMargin, EmguExtensions.WhiteByte, -1, lineType);
+ }
+
+ foreach (var layer in layers)
+ {
+ CvInvoke.Circle(layer, center, holeRadius, EmguExtensions.BlackByte, -1, lineType);
+ }
+ }
+
+ fontScale = 1.5;
+ fontThickness = 3;
+ CvInvoke.PutText(layers[0], $"{Microns}um at {_bottomExposure}s/{_normalExposure}s",
+ new Point(center.X - radius / 2, center.Y + radius / 2 +40),
+ fontFace, fontScale, EmguExtensions.BlackByte, fontThickness, lineType, true);
+
+ CvInvoke.PutText(layers[0], $"{_startBrightness}-{_endBrightness} S:{_brightnessSteps}",
+ new Point(center.X - radius / 2, center.Y + radius / 2 - 40),
+ fontFace, fontScale, EmguExtensions.BlackByte, fontThickness, lineType, true);
+
+ return layers;
+ }
+
+ public Mat GetThumbnail()
+ {
+ Mat thumbnail = EmguExtensions.InitMat(new Size(400, 200), 3);
+ var fontFace = FontFace.HersheyDuplex;
+ var fontScale = 1;
+ var fontThickness = 2;
+ const byte xSpacing = 45;
+ const byte ySpacing = 45;
+ CvInvoke.PutText(thumbnail, "UVtools", new Point(140, 35), fontFace, fontScale, new MCvScalar(255, 27, 245), fontThickness + 1);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, 0), new Point(xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, ySpacing + 5), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(thumbnail.Width - xSpacing, 0), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.PutText(thumbnail, "Grayscale Cal.", new Point(xSpacing, ySpacing * 2), fontFace, fontScale, new MCvScalar(0, 255, 255), fontThickness);
+ CvInvoke.PutText(thumbnail, $"{Microns}um @ {BottomExposure}s/{NormalExposure}s", new Point(xSpacing, ySpacing * 3), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+ CvInvoke.PutText(thumbnail, $"Divs:{Divisions} Angle:{AngleStep}", new Point(xSpacing, ySpacing * 4), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+
+ return thumbnail;
+ }
+
+ #endregion
+ }
+}
diff --git a/UVtools.Core/Operations/OperationCalibrateTolerance.cs b/UVtools.Core/Operations/OperationCalibrateTolerance.cs
new file mode 100644
index 0000000..82b3dfe
--- /dev/null
+++ b/UVtools.Core/Operations/OperationCalibrateTolerance.cs
@@ -0,0 +1,700 @@
+/*
+ * 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;
+using System.Drawing;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+using Emgu.CV.Structure;
+using UVtools.Core.Extensions;
+using UVtools.Core.Objects;
+
+namespace UVtools.Core.Operations
+{
+ [Serializable]
+ public sealed class OperationCalibrateTolerance : Operation
+ {
+ #region Members
+ private decimal _displayWidth;
+ private decimal _displayHeight;
+ private decimal _layerHeight = 0.05M;
+ private ushort _bottomLayers = 3;
+ private decimal _bottomExposure = 60;
+ private decimal _normalExposure = 12;
+ private decimal _zSize = 10;
+ private ushort _topBottomMargin = 100;
+ private ushort _leftRightMargin = 100;
+ private byte _chamferLayers = 4;
+ private byte _erodeBottomIterations = 4;
+ private Shapes _shape = Shapes.Circle;
+ private ushort _partMargin = 50;
+ private bool _outputSameDiameterPart = true;
+ private bool _fuseParts;
+ private bool _enableAntiAliasing = true;
+
+ private decimal _femaleDiameter = 16;
+ private decimal _femaleHoleDiameter = 10;
+
+ private ushort _maleThinnerModels = 5;
+ private decimal _maleThinnerOffset;
+ private decimal _maleThinnerStep = -0.1M;
+ private ushort _maleThickerModels;
+ private decimal _maleThickerOffset;
+ private decimal _maleThickerStep = 0.1M;
+
+ private decimal _observedXSize;
+ private decimal _observedYSize;
+ private decimal _observedZSize;
+
+ #endregion
+
+ #region Overrides
+
+ public override bool CanROI => false;
+
+ public override bool CanCancel => false;
+
+ public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None;
+
+ public override string Title => "Tolerance";
+ public override string Description =>
+ "Generates test models with various strategies and increments to verify the part tolerances.\n" +
+ "You must repeat this test when change any of the following: printer, LEDs, resin and exposure times.\n" +
+ "Note: The current opened file will be overwritten with this test, use a dummy or a not needed file.";
+
+ public override string ConfirmationText =>
+ $"generate the tolerance test?";
+
+ public override string ProgressTitle =>
+ $"Generating the tolerance test";
+
+ public override string ProgressAction => "Generated";
+
+ public override StringTag Validate(params object[] parameters)
+ {
+ var sb = new StringBuilder();
+
+ if (_displayWidth <= 0)
+ {
+ sb.AppendLine("Display width must be a positive value.");
+ }
+
+ if (_displayHeight <= 0)
+ {
+ sb.AppendLine("Display height must be a positive value.");
+ }
+
+ if (_femaleHoleDiameter >= _femaleDiameter)
+ {
+ sb.AppendLine("Hole diameter must be smaller than female diameter.");
+ }
+
+ if (OutputObjects <= 0)
+ {
+ sb.AppendLine("No objects to output.");
+ }
+
+ return new StringTag(sb.ToString());
+ }
+
+ public override string ToString()
+ {
+ var result = $"[Layer Height: {_layerHeight}] " +
+ $"[Bottom layers: {_bottomLayers}] " +
+ $"[Exposure: {_bottomExposure}/{_normalExposure}] " +
+ $"[Z: {_zSize}] " +
+ $"[TB:{_topBottomMargin} LR:{_leftRightMargin} PM:{_partMargin}] " +
+ $"[Chamfer: {_chamferLayers}] [Erode: {_erodeBottomIterations}] " +
+ $"[OSHD: {_outputSameDiameterPart}] [Fuse: {_fuseParts}] [AA: {_enableAntiAliasing}] " +
+ $"[{_shape}, {_femaleDiameter}/{_femaleHoleDiameter}] " +
+ $"[tM: {_maleThinnerModels} O:{_maleThinnerOffset} S:{_maleThinnerStep}] " +
+ $"[TM: {_maleThickerModels} O:{_maleThickerOffset} S:{_maleThickerStep}] ";
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }
+
+ #endregion
+
+ #region Properties
+
+ [XmlIgnore]
+ public Size Resolution { get; set; } = Size.Empty;
+
+ [XmlIgnore]
+ public decimal DisplayWidth
+ {
+ get => _displayWidth;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _displayWidth, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(Xppmm));
+ }
+ }
+
+ [XmlIgnore]
+ public decimal DisplayHeight
+ {
+ get => _displayHeight;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _displayHeight, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(Yppmm));
+ }
+ }
+
+ public decimal Xppmm => DisplayWidth > 0 ? Math.Round(Resolution.Width / DisplayWidth, 2) : 0;
+ public decimal Yppmm => DisplayHeight > 0 ? Math.Round(Resolution.Height / DisplayHeight, 2) : 0;
+
+ public decimal LayerHeight
+ {
+ get => _layerHeight;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _layerHeight, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(BottomLayersMM));
+ RaisePropertyChanged(nameof(LayerCount));
+ RaisePropertyChanged(nameof(RealZSize));
+ //RaisePropertyChanged(nameof(ObservedZSize));
+ }
+ }
+
+ public ushort Microns => (ushort)(LayerHeight * 1000);
+
+ public ushort BottomLayers
+ {
+ get => _bottomLayers;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _bottomLayers, value)) return;
+ RaisePropertyChanged(nameof(BottomLayersMM));
+ }
+ }
+
+ public decimal BottomLayersMM => Math.Round(LayerHeight * BottomLayers, 2);
+
+ public decimal BottomExposure
+ {
+ get => _bottomExposure;
+ set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2));
+ }
+
+ public decimal NormalExposure
+ {
+ get => _normalExposure;
+ set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2));
+ }
+
+ public decimal ZSize
+ {
+ get => _zSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _zSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(LayerCount));
+ RaisePropertyChanged(nameof(RealZSize));
+ //RaisePropertyChanged(nameof(ObservedZSize));
+ }
+ }
+
+ public uint LayerCount => (uint) Math.Floor(ZSize / LayerHeight);
+
+ public decimal RealZSize => LayerCount * _layerHeight;
+
+ public ushort TopBottomMargin
+ {
+ get => _topBottomMargin;
+ set => RaiseAndSetIfChanged(ref _topBottomMargin, value);
+ }
+
+ public ushort LeftRightMargin
+ {
+ get => _leftRightMargin;
+ set => RaiseAndSetIfChanged(ref _leftRightMargin, value);
+ }
+
+ public byte ChamferLayers
+ {
+ get => _chamferLayers;
+ set => RaiseAndSetIfChanged(ref _chamferLayers, value);
+ }
+
+ public byte ErodeBottomIterations
+ {
+ get => _erodeBottomIterations;
+ set => RaiseAndSetIfChanged(ref _erodeBottomIterations, value);
+ }
+
+ public Shapes Shape
+ {
+ get => _shape;
+ set => RaiseAndSetIfChanged(ref _shape, value);
+ }
+
+ public ushort PartMargin
+ {
+ get => _partMargin;
+ set => RaiseAndSetIfChanged(ref _partMargin, value);
+ }
+
+ public bool OutputSameDiameterPart
+ {
+ get => _outputSameDiameterPart;
+ set => RaiseAndSetIfChanged(ref _outputSameDiameterPart, value);
+ }
+
+ public bool FuseParts
+ {
+ get => _fuseParts;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _fuseParts, value)) return;
+ if (value)
+ {
+ OutputSameDiameterPart = false;
+ MaleThickerModels = 0;
+ }
+ }
+ }
+
+ public bool EnableAntiAliasing
+ {
+ get => _enableAntiAliasing;
+ set => RaiseAndSetIfChanged(ref _enableAntiAliasing, value);
+ }
+
+ public decimal FemaleDiameter
+ {
+ get => _femaleDiameter;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _femaleDiameter, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(FemaleDiameterXPixels));
+ RaisePropertyChanged(nameof(FemaleDiameterYPixels));
+ RaisePropertyChanged(nameof(FemaleDiameterRealXSize));
+ RaisePropertyChanged(nameof(FemaleDiameterRealYSize));
+ }
+ }
+
+ public uint FemaleDiameterXPixels => (uint)Math.Floor(_femaleDiameter * Xppmm);
+ public uint FemaleDiameterYPixels => (uint)Math.Floor(_femaleDiameter * Yppmm);
+
+ public decimal FemaleDiameterRealXSize
+ {
+ get
+ {
+ decimal pixels = _femaleDiameter * Xppmm;
+ return pixels <= 0 ? 0 : Math.Round(_femaleDiameter - (pixels - Math.Truncate(pixels)) / Xppmm, 2);
+ }
+ }
+
+ public decimal FemaleDiameterRealYSize
+ {
+ get
+ {
+ decimal pixels = _femaleDiameter * Yppmm;
+ return pixels <= 0 ? 0 : Math.Round(_femaleDiameter - (pixels - Math.Truncate(pixels)) / Yppmm, 2);
+ }
+ }
+
+ public decimal FemaleHoleDiameter
+ {
+ get => _femaleHoleDiameter;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _femaleHoleDiameter, value)) return;
+ RaisePropertyChanged(nameof(FemaleHoleDiameterXPixels));
+ RaisePropertyChanged(nameof(FemaleHoleDiameterYPixels));
+ RaisePropertyChanged(nameof(FemaleHoleDiameterRealXSize));
+ RaisePropertyChanged(nameof(FemaleHoleDiameterRealYSize));
+ }
+ }
+
+ public uint FemaleHoleDiameterXPixels => (uint)Math.Floor(_femaleHoleDiameter * Xppmm);
+ public uint FemaleHoleDiameterYPixels => (uint)Math.Floor(_femaleHoleDiameter * Yppmm);
+
+ public decimal FemaleHoleDiameterRealXSize
+ {
+ get
+ {
+ decimal pixels = _femaleHoleDiameter * Xppmm;
+ return pixels <= 0 ? 0 : Math.Round(_femaleHoleDiameter - (pixels - Math.Truncate(pixels)) / Xppmm, 2);
+ }
+ }
+
+ public decimal FemaleHoleDiameterRealYSize
+ {
+ get
+ {
+ decimal pixels = _femaleHoleDiameter * Yppmm;
+ return pixels <= 0 ? 0 : Math.Round(_femaleHoleDiameter - (pixels - Math.Truncate(pixels)) / Yppmm, 2);
+ }
+ }
+
+ public ushort MaleThinnerModels
+ {
+ get => _maleThinnerModels;
+ set => RaiseAndSetIfChanged(ref _maleThinnerModels, value);
+ }
+
+ public decimal MaleThinnerOffset
+ {
+ get => _maleThinnerOffset;
+ set => RaiseAndSetIfChanged(ref _maleThinnerOffset, Math.Round(value, 2));
+ }
+
+ public decimal MaleThinnerStep
+ {
+ get => _maleThinnerStep;
+ set => RaiseAndSetIfChanged(ref _maleThinnerStep, Math.Round(value, 2));
+ }
+
+ public ushort MaleThickerModels
+ {
+ get => _maleThickerModels;
+ set => RaiseAndSetIfChanged(ref _maleThickerModels, value);
+ }
+
+ public decimal MaleThickerOffset
+ {
+ get => _maleThickerOffset;
+ set => RaiseAndSetIfChanged(ref _maleThickerOffset, Math.Round(value, 2));
+ }
+
+ public decimal MaleThickerStep
+ {
+ get => _maleThickerStep;
+ set => RaiseAndSetIfChanged(ref _maleThickerStep, Math.Round(value, 2));
+ }
+
+
+ public uint OutputObjects =>
+ (_outputSameDiameterPart ? 1u : 0) +
+ _maleThinnerModels +
+ _maleThickerModels +
+ (_fuseParts ? 0 : 1u);
+
+ /*public decimal ObservedXSize
+ {
+ get => _observedXSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _observedXSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(ScaleXFactor));
+ }
+ }
+
+ public decimal ObservedYSize
+ {
+ get => _observedYSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _observedYSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(ScaleYFactor));
+ }
+ }
+
+ public decimal ObservedZSize
+ {
+ get => _observedZSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _observedZSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(ScaleZFactor));
+ }
+ }
+
+
+ public decimal ScaleXFactor => ObservedXSize > 0 && RealXSize > 0 ? Math.Round(RealXSize * 100 / ObservedXSize, 2) : 100;
+ public decimal ScaleYFactor => ObservedYSize > 0 && RealYSize > 0 ? Math.Round(RealYSize * 100 / ObservedYSize, 2) : 100;
+ public decimal ScaleZFactor => ObservedZSize > 0 && RealZSize > 0 ? Math.Round(RealZSize * 100 / ObservedZSize, 2) : 100;
+ */
+ #endregion
+
+ #region Enums
+
+ public enum Shapes : byte
+ {
+ Circle,
+ Square
+ }
+
+ public static Array ShapesItems => Enum.GetValues(typeof(Shapes));
+ #endregion
+
+
+ #region Equality
+
+ private bool Equals(OperationCalibrateTolerance other)
+ {
+ return _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _zSize == other._zSize && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _chamferLayers == other._chamferLayers && _erodeBottomIterations == other._erodeBottomIterations && _shape == other._shape && _partMargin == other._partMargin && _outputSameDiameterPart == other._outputSameDiameterPart && _fuseParts == other._fuseParts && _enableAntiAliasing == other._enableAntiAliasing && _femaleDiameter == other._femaleDiameter && _femaleHoleDiameter == other._femaleHoleDiameter && _maleThinnerModels == other._maleThinnerModels && _maleThinnerOffset == other._maleThinnerOffset && _maleThinnerStep == other._maleThinnerStep && _maleThickerModels == other._maleThickerModels && _maleThickerOffset == other._maleThickerOffset && _maleThickerStep == other._maleThickerStep;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj) || obj is OperationCalibrateTolerance other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+ hashCode.Add(_layerHeight);
+ hashCode.Add(_bottomLayers);
+ hashCode.Add(_bottomExposure);
+ hashCode.Add(_normalExposure);
+ hashCode.Add(_zSize);
+ hashCode.Add(_topBottomMargin);
+ hashCode.Add(_leftRightMargin);
+ hashCode.Add(_chamferLayers);
+ hashCode.Add(_erodeBottomIterations);
+ hashCode.Add((int) _shape);
+ hashCode.Add(_partMargin);
+ hashCode.Add(_outputSameDiameterPart);
+ hashCode.Add(_fuseParts);
+ hashCode.Add(_enableAntiAliasing);
+ hashCode.Add(_femaleDiameter);
+ hashCode.Add(_femaleHoleDiameter);
+ hashCode.Add(_maleThinnerModels);
+ hashCode.Add(_maleThinnerOffset);
+ hashCode.Add(_maleThinnerStep);
+ hashCode.Add(_maleThickerModels);
+ hashCode.Add(_maleThickerOffset);
+ hashCode.Add(_maleThickerStep);
+ return hashCode.ToHashCode();
+ }
+
+ #endregion
+
+ #region Methods
+
+
+ public Mat[] GetLayers()
+ {
+ var layers = new Mat[LayerCount];
+ var layer = EmguExtensions.InitMat(Resolution);
+
+ ushort startX = Math.Max((ushort)2, _leftRightMargin);
+ ushort startY = Math.Max((ushort)2, _topBottomMargin);
+ int currentX = startX;
+ int currentY = startY;
+
+ const FontFace fontFace = FontFace.HersheyDuplex;
+ const double fontScale = 1;
+ const byte fontThickness = 2;
+ LineType lineType = _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected;
+
+ var anchor = new Point(-1, -1);
+ var kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), anchor);
+
+ var pointTextList = new List<KeyValuePair<Point, string>>();
+
+ if (!_fuseParts)
+ {
+ switch (Shape)
+ {
+ case Shapes.Circle:
+ currentX += (int) FemaleDiameterXPixels / 2;
+ currentY += (int) FemaleDiameterXPixels / 2;
+ CvInvoke.Circle(layer, new Point(currentX, currentY), (int) (FemaleDiameterXPixels / 2), EmguExtensions.WhiteByte, -1, lineType);
+ CvInvoke.Circle(layer, new Point(currentX, currentY), (int) (FemaleHoleDiameterXPixels / 2), EmguExtensions.BlackByte, -1, lineType);
+ currentX += (int) FemaleDiameterXPixels / 2 + PartMargin;
+
+ break;
+ case Shapes.Square:
+ int offsetX = (int) ((FemaleDiameterXPixels - FemaleHoleDiameterXPixels) / 2);
+ int offsetY = (int) ((FemaleDiameterYPixels - FemaleHoleDiameterYPixels) / 2);
+ CvInvoke.Rectangle(layer, new Rectangle(currentX, currentY, (int) FemaleDiameterXPixels, (int) FemaleDiameterXPixels), EmguExtensions.WhiteByte, -1, lineType);
+ CvInvoke.Rectangle(layer, new Rectangle(currentX + offsetX, currentY + offsetY, (int) FemaleHoleDiameterXPixels, (int) FemaleHoleDiameterYPixels), EmguExtensions.BlackByte, -1, lineType);
+ currentX += (int)FemaleDiameterXPixels + PartMargin;
+ currentY = startY + (int) FemaleDiameterYPixels / 2;
+ break;
+ }
+ }
+
+ bool addPart(decimal step)
+ {
+ decimal millimeters = Math.Round(_femaleHoleDiameter + step, 2);
+ if (millimeters <= 0) return false;
+ int xPixels = (int) Math.Floor(millimeters * Xppmm);
+ int yPixels = (int) Math.Floor(millimeters * Yppmm);
+ Point partCenterText;
+
+ if (_fuseParts)
+ {
+ if (xPixels >= FemaleHoleDiameterXPixels || yPixels >= FemaleHoleDiameterYPixels) return false;
+ if (currentX + FemaleDiameterXPixels + _leftRightMargin >= Resolution.Width)
+ {
+ currentX = startX;
+ currentY += (int)FemaleDiameterYPixels + PartMargin;
+ }
+
+ if (currentY + FemaleDiameterYPixels + _topBottomMargin >= Resolution.Height)
+ {
+ return false; // Insufficient size
+ }
+
+ int halfDiameterX = (int)(FemaleDiameterXPixels / 2);
+ int halfDiameterY = (int)(FemaleDiameterYPixels / 2);
+
+ switch (Shape)
+ {
+ case Shapes.Circle:
+ CvInvoke.Circle(layer, new Point(currentX + halfDiameterX, currentY + halfDiameterY), (int)(FemaleDiameterXPixels / 2), EmguExtensions.WhiteByte, -1, lineType);
+ CvInvoke.Circle(layer, new Point(currentX + halfDiameterX, currentY + halfDiameterY), (int)(FemaleHoleDiameterXPixels / 2), EmguExtensions.BlackByte, -1, lineType);
+ CvInvoke.Circle(layer, new Point(currentX + halfDiameterX, currentY + halfDiameterY), xPixels / 2, EmguExtensions.WhiteByte, -1, lineType);
+ break;
+ case Shapes.Square:
+ int offsetX = (int)((FemaleDiameterXPixels - FemaleHoleDiameterXPixels) / 2);
+ int offsetY = (int)((FemaleDiameterYPixels - FemaleHoleDiameterYPixels) / 2);
+ CvInvoke.Rectangle(layer, new Rectangle(currentX, currentY, (int)FemaleDiameterXPixels, (int)FemaleDiameterXPixels), EmguExtensions.WhiteByte, -1, lineType);
+ CvInvoke.Rectangle(layer, new Rectangle(currentX + offsetX, currentY + offsetY, (int)FemaleHoleDiameterXPixels, (int)FemaleHoleDiameterYPixels), EmguExtensions.BlackByte, -1, lineType);
+ offsetX = (int)((FemaleDiameterXPixels - xPixels) / 2);
+ offsetY = (int)((FemaleDiameterYPixels - yPixels) / 2);
+ CvInvoke.Rectangle(layer, new Rectangle(currentX + offsetX, currentY + offsetY, xPixels, yPixels), EmguExtensions.WhiteByte, -1, lineType);
+ break;
+ }
+
+ partCenterText = new Point(currentX + halfDiameterX - 60, currentY + halfDiameterY + 10);
+ currentX += (int)FemaleDiameterXPixels + PartMargin;
+ }
+ else
+ {
+ if (currentX + xPixels + _leftRightMargin >= Resolution.Width)
+ {
+ currentX = startX;
+ currentY += yPixels + PartMargin;
+ }
+
+ if (currentY + yPixels + _topBottomMargin >= Resolution.Height)
+ {
+ return false; // Insufficient size
+ }
+
+ int halfDiameterX = xPixels / 2;
+ int halfDiameterY = yPixels / 2;
+
+ switch (Shape)
+ {
+ case Shapes.Circle:
+ CvInvoke.Circle(layer, new Point(currentX + halfDiameterX, currentY + halfDiameterY), halfDiameterX, EmguExtensions.WhiteByte, -1, lineType);
+ break;
+ case Shapes.Square:
+ CvInvoke.Rectangle(layer, new Rectangle(currentX, currentY, xPixels, yPixels), EmguExtensions.WhiteByte, -1, lineType);
+ break;
+ }
+
+ partCenterText = new Point(currentX + halfDiameterX - 60, currentY + halfDiameterY + 10);
+
+ currentX += xPixels + PartMargin;
+ }
+
+ pointTextList.Add(new KeyValuePair<Point, string>(partCenterText, step > 0 ? $"+{step:0.00}" : $"{step:0.00}"));
+
+ return true;
+ }
+
+ if (!_fuseParts && _outputSameDiameterPart)
+ {
+ addPart(0);
+ }
+
+ for (int i = 1; i <= _maleThinnerModels; i++)
+ {
+ var step = _maleThinnerOffset + _maleThinnerStep * i;
+ if (!addPart(step)) break;
+ }
+
+ for (int i = 1; i <= _maleThickerModels; i++)
+ {
+ var step = _maleThickerOffset + _maleThickerStep * i;
+ if (!addPart(step)) break;
+ }
+
+ Parallel.For(0, layers.Length, layerIndex =>
+ //for (var i = 0; i < layers.Length; i++)
+ {
+ layers[layerIndex] = layer.Clone();
+ });
+
+ if (_erodeBottomIterations > 0)
+ {
+ Parallel.For(0, _bottomLayers, layerIndex =>
+ {
+ CvInvoke.Erode(layers[layerIndex], layers[layerIndex], kernel, anchor, _erodeBottomIterations, BorderType.Reflect101, default);
+ });
+ }
+
+ if (_chamferLayers > 0)
+ {
+ Parallel.For(0, _chamferLayers, layerIndexOffset =>
+ {
+ var iteration = _chamferLayers - layerIndexOffset;
+ CvInvoke.Erode(layers[layerIndexOffset], layers[layerIndexOffset], kernel, anchor, iteration, BorderType.Reflect101, default);
+
+ var layerIndex = layers.Length - 1 - layerIndexOffset;
+ CvInvoke.Erode(layers[layerIndex], layers[layerIndex], kernel, anchor, iteration, BorderType.Reflect101, default);
+ });
+ /*byte iterations = _chamferLayers;
+ var layerIndex = 0;
+ for (; layerIndex < LayerCount && iterations > 0; layerIndex++)
+ {
+ CvInvoke.Erode(layers[layerIndex], layers[layerIndex], kernel, anchor, iterations--, BorderType.Reflect101, default);
+ }
+
+ iterations = _chamferLayers;
+ for (int i = (int) (LayerCount - 1); i >= 0 && i > layerIndex && iterations > 0; i--)
+ {
+ CvInvoke.Erode(layers[i], layers[i], kernel, anchor, iterations--, BorderType.Reflect101, default);
+ }*/
+ }
+
+ Parallel.For(Math.Max(0u, LayerCount - 15), LayerCount, layerIndex =>
+ {
+ foreach (var keyValuePair in pointTextList)
+ {
+ CvInvoke.PutText(layers[layerIndex], keyValuePair.Value, keyValuePair.Key, fontFace, fontScale, EmguExtensions.BlackByte, fontThickness, lineType);
+ }
+ });
+
+ return layers;
+ }
+
+ public Mat GetThumbnail()
+ {
+ Mat thumbnail = EmguExtensions.InitMat(new Size(400, 200), 3);
+ var fontFace = FontFace.HersheyDuplex;
+ var fontScale = 1;
+ var fontThickness = 2;
+ const byte xSpacing = 45;
+ const byte ySpacing = 45;
+ CvInvoke.PutText(thumbnail, "UVtools", new Point(140, 35), fontFace, fontScale, new MCvScalar(255, 27, 245), fontThickness + 1);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, 0), new Point(xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, ySpacing + 5), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(thumbnail.Width - xSpacing, 0), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.PutText(thumbnail, "Tolerance Cal.", new Point(xSpacing, ySpacing * 2), fontFace, fontScale, new MCvScalar(0, 255, 255), fontThickness);
+ CvInvoke.PutText(thumbnail, $"{Microns}um @ {BottomExposure}s/{NormalExposure}s", new Point(xSpacing, ySpacing * 3), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+ CvInvoke.PutText(thumbnail, $"Objects: {OutputObjects}", new Point(xSpacing, ySpacing * 4), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+
+ /*thumbnail.SetTo(EmguExtensions.Black3Byte);
+
+ CvInvoke.Circle(thumbnail, new Point(400/2, 200/2), 200/2, EmguExtensions.White3Byte, -1);
+ for (int angle = 0; angle < 360; angle+=20)
+ {
+ CvInvoke.Line(thumbnail, new Point(400 / 2, 200 / 2), new Point((int)(400 / 2 + 100 * Math.Cos(angle * Math.PI / 180)), (int)(200 / 2 + 100 * Math.Sin(angle * Math.PI / 180))), new MCvScalar(255, 27, 245), 3);
+ }
+
+ thumbnail.Save("D:\\Thumbnail.png");*/
+ return thumbnail;
+ }
+
+ #endregion
+ }
+}
diff --git a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs
new file mode 100644
index 0000000..df0fe59
--- /dev/null
+++ b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs
@@ -0,0 +1,708 @@
+/*
+ * 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.Drawing;
+using System.Text;
+using System.Xml.Serialization;
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+using Emgu.CV.Structure;
+using UVtools.Core.Extensions;
+using UVtools.Core.Objects;
+
+namespace UVtools.Core.Operations
+{
+ [Serializable]
+ public sealed class OperationCalibrateXYZAccuracy : Operation
+ {
+ #region Members
+ private decimal _layerHeight = 0.05M;
+ private ushort _bottomLayers = 3;
+ private decimal _bottomExposure = 60;
+ private decimal _normalExposure = 12;
+ private ushort _topBottomMargin = 100;
+ private ushort _leftRightMargin = 100;
+ private decimal _displayWidth;
+ private decimal _displayHeight;
+ private decimal _xSize = 15;
+ private decimal _ySize = 15;
+ private decimal _zSize = 15;
+ private bool _centerHoleRelief = true;
+ private bool _hollowModel = true;
+ private decimal _wallThickness = 2.5M;
+ private decimal _observedXSize;
+ private decimal _observedYSize;
+ private decimal _observedZSize;
+ private bool _outputTLObject;
+ private bool _outputTCObject;
+ private bool _outputTRObject;
+ private bool _outputMLObject;
+ private bool _outputMCObject = true;
+ private bool _outputMRObject;
+ private bool _outputBLObject;
+ private bool _outputBCObject;
+ private bool _outputBRObject;
+
+ #endregion
+
+ #region Overrides
+
+ public override bool CanROI => false;
+
+ public override bool CanCancel => false;
+
+ public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None;
+
+ public override string Title => "XYZ Accuracy";
+ public override string Description =>
+ "Generates test models with various strategies and increments to verify the XYZ accuracy.\n" +
+ "XYZ are accurate when the printed model match the expected size.\n" +
+ "You must repeat this test when change any of the following: printer, LEDs, resin and exposure times.\n" +
+ "Note: The current opened file will be overwritten with this test, use a dummy or a not needed file.";
+
+ public override string ConfirmationText =>
+ $"generate the XYZ accuracy test?";
+
+ public override string ProgressTitle =>
+ $"Generating the XYZ accuracy test";
+
+ public override string ProgressAction => "Generated";
+
+ public override StringTag Validate(params object[] parameters)
+ {
+ var sb = new StringBuilder();
+
+ if (DisplayWidth <= 0)
+ {
+ sb.AppendLine("Display width must be a positive value.");
+ }
+
+ if (DisplayHeight <= 0)
+ {
+ sb.AppendLine("Display height must be a positive value.");
+ }
+
+ if (OutputObjects <= 0)
+ {
+ sb.AppendLine("No objects to output.");
+ }
+
+ return new StringTag(sb.ToString());
+ }
+
+ public override string ToString()
+ {
+ var result = $"[Layer Height: {_layerHeight}] " +
+ $"[Bottom layers: {_bottomLayers}] " +
+ $"[Exposure: {_bottomExposure}/{_normalExposure}] " +
+ $"[X: {_xSize} Y:{_ySize} Z:{_zSize}] " +
+ $"[TB:{_topBottomMargin} LR:{_leftRightMargin}] " +
+ $"[Model: {_outputTLObject.ToByte()}{_outputTCObject.ToByte()}{_outputTRObject.ToByte()}" +
+ $"|{_outputMLObject.ToByte()}{_outputMCObject.ToByte()}{_outputMRObject.ToByte()}" +
+ $"|{_outputBLObject.ToByte()}{_outputBCObject.ToByte()}{_outputBRObject.ToByte()}] " +
+ $"[Hollow: {_hollowModel} @ {_wallThickness}mm] [Relief: {_centerHoleRelief}]";
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }
+
+ #endregion
+
+ #region Properties
+
+ [XmlIgnore]
+ public Size Resolution { get; set; } = Size.Empty;
+
+ [XmlIgnore]
+ public decimal DisplayWidth
+ {
+ get => _displayWidth;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _displayWidth, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(Xppmm));
+ }
+ }
+
+ [XmlIgnore]
+ public decimal DisplayHeight
+ {
+ get => _displayHeight;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _displayHeight, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(Yppmm));
+ }
+ }
+
+ public decimal Xppmm => DisplayWidth > 0 ? Math.Round(Resolution.Width / DisplayWidth, 2) : 0;
+ public decimal Yppmm => DisplayHeight > 0 ? Math.Round(Resolution.Height / DisplayHeight, 2) : 0;
+
+ public decimal LayerHeight
+ {
+ get => _layerHeight;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _layerHeight, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(BottomLayersMM));
+ RaisePropertyChanged(nameof(LayerCount));
+ RaisePropertyChanged(nameof(RealZSize));
+ RaisePropertyChanged(nameof(ObservedZSize));
+ }
+ }
+
+ public ushort Microns => (ushort)(LayerHeight * 1000);
+
+ public ushort BottomLayers
+ {
+ get => _bottomLayers;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _bottomLayers, value)) return;
+ RaisePropertyChanged(nameof(BottomLayersMM));
+ }
+ }
+
+ public decimal BottomLayersMM => Math.Round(LayerHeight * BottomLayers, 2);
+
+ public decimal BottomExposure
+ {
+ get => _bottomExposure;
+ set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2));
+ }
+
+ public decimal NormalExposure
+ {
+ get => _normalExposure;
+ set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2));
+ }
+
+ public ushort TopBottomMargin
+ {
+ get => _topBottomMargin;
+ set => RaiseAndSetIfChanged(ref _topBottomMargin, value);
+ }
+
+ public ushort LeftRightMargin
+ {
+ get => _leftRightMargin;
+ set => RaiseAndSetIfChanged(ref _leftRightMargin, value);
+ }
+
+ public decimal XSize
+ {
+ get => _xSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _xSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(RealXSize));
+ RaisePropertyChanged(nameof(ObservedXSize));
+ }
+ }
+
+ public decimal YSize
+ {
+ get => _ySize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _ySize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(RealYSize));
+ RaisePropertyChanged(nameof(ObservedYSize));
+ }
+ }
+
+ public decimal ZSize
+ {
+ get => _zSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _zSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(LayerCount));
+ RaisePropertyChanged(nameof(RealZSize));
+ RaisePropertyChanged(nameof(ObservedZSize));
+ }
+ }
+
+ public decimal RealXSize
+ {
+ get
+ {
+ decimal pixels = _xSize * Xppmm;
+ if (pixels <= 0) return 0;
+ return Math.Round(_xSize - (pixels - Math.Truncate(pixels)) / Xppmm, 2);
+ }
+
+ }
+
+ public decimal RealYSize
+ {
+ get
+ {
+ decimal pixels = _ySize * Yppmm;
+ if (pixels <= 0) return 0;
+ return Math.Round(_ySize - (pixels - Math.Truncate(pixels)) / Yppmm, 2);
+ }
+ }
+
+ public decimal RealZSize => LayerCount * _layerHeight;
+
+ public uint XPixels => (uint)Math.Floor(XSize * Xppmm);
+ public uint YPixels => (uint)Math.Floor(YSize * Yppmm);
+
+ public uint LayerCount => (uint) Math.Floor(ZSize / LayerHeight);
+
+ public bool CenterHoleRelief
+ {
+ get => _centerHoleRelief;
+ set => RaiseAndSetIfChanged(ref _centerHoleRelief, value);
+ }
+
+ public bool HollowModel
+ {
+ get => _hollowModel;
+ set => RaiseAndSetIfChanged(ref _hollowModel, value);
+ }
+
+ public decimal WallThickness
+ {
+ get => _wallThickness;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _wallThickness, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(WallThicknessRealXSize));
+ RaisePropertyChanged(nameof(WallThicknessRealYSize));
+ }
+ }
+
+ public decimal WallThicknessRealXSize
+ {
+ get
+ {
+ decimal pixels = _wallThickness * Xppmm;
+ if (pixels <= 0) return 0;
+ return Math.Round(_wallThickness - (pixels - Math.Truncate(pixels)) / Xppmm, 2);
+ }
+ }
+
+ public decimal WallThicknessRealYSize
+ {
+ get
+ {
+ decimal pixels = _wallThickness * Yppmm;
+ if (pixels <= 0) return 0;
+ return Math.Round(_wallThickness - (pixels - Math.Truncate(pixels)) / Yppmm, 2);
+ }
+ }
+
+ public uint WallThicknessXPixels => (uint)Math.Floor(WallThickness * Xppmm);
+ public uint WallThicknessYPixels => (uint)Math.Floor(WallThickness * Yppmm);
+
+ public bool OutputTLObject
+ {
+ get => _outputTLObject;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _outputTLObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputTCObject
+ {
+ get => _outputTCObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputTCObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputTRObject
+ {
+ get => _outputTRObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputTRObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputMLObject
+ {
+ get => _outputMLObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputMLObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputMCObject
+ {
+ get => _outputMCObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputMCObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputMRObject
+ {
+ get => _outputMRObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputMRObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputBLObject
+ {
+ get => _outputBLObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputBLObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputBCObject
+ {
+ get => _outputBCObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputBCObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public bool OutputBRObject
+ {
+ get => _outputBRObject;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _outputBRObject, value)) return;
+ RaisePropertyChanged(nameof(OutputObjects));
+ }
+ }
+
+ public byte OutputObjects => (byte) (_outputTLObject.ToByte() +
+ _outputTCObject.ToByte() +
+ _outputTRObject.ToByte() +
+ _outputMLObject.ToByte() +
+ _outputMCObject.ToByte() +
+ _outputMRObject.ToByte() +
+ _outputBLObject.ToByte() +
+ _outputBCObject.ToByte() +
+ _outputBRObject.ToByte());
+
+ public decimal ObservedXSize
+ {
+ get => _observedXSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _observedXSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(ScaleXFactor));
+ }
+ }
+
+ public decimal ObservedYSize
+ {
+ get => _observedYSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _observedYSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(ScaleYFactor));
+ }
+ }
+
+ public decimal ObservedZSize
+ {
+ get => _observedZSize;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _observedZSize, Math.Round(value, 2))) return;
+ RaisePropertyChanged(nameof(ScaleZFactor));
+ }
+ }
+
+ // 15 - x
+ // 14 - 100
+ public decimal ScaleXFactor => ObservedXSize > 0 && RealXSize > 0 ? Math.Round(RealXSize * 100 / ObservedXSize, 2) : 100;
+ public decimal ScaleYFactor => ObservedYSize > 0 && RealYSize > 0 ? Math.Round(RealYSize * 100 / ObservedYSize, 2) : 100;
+ public decimal ScaleZFactor => ObservedZSize > 0 && RealZSize > 0 ? Math.Round(RealZSize * 100 / ObservedZSize, 2) : 100;
+
+ #endregion
+
+ #region Enums
+
+ #endregion
+
+ #region Properties
+
+
+
+ /*public override string ToString()
+ {
+ var result = $"[{_blurOperation}] [Size: {_size}]" + LayerRangeString;
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }*/
+
+ #endregion
+
+ #region Equality
+
+ private bool Equals(OperationCalibrateXYZAccuracy other)
+ {
+ return _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _xSize == other._xSize && _ySize == other._ySize && _zSize == other._zSize && _centerHoleRelief == other._centerHoleRelief && _hollowModel == other._hollowModel && _wallThickness == other._wallThickness && _observedXSize == other._observedXSize && _observedYSize == other._observedYSize && _observedZSize == other._observedZSize && _outputTLObject == other._outputTLObject && _outputTCObject == other._outputTCObject && _outputTRObject == other._outputTRObject && _outputMLObject == other._outputMLObject && _outputMCObject == other._outputMCObject && _outputMRObject == other._outputMRObject && _outputBLObject == other._outputBLObject && _outputBCObject == other._outputBCObject && _outputBRObject == other._outputBRObject;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj) || obj is OperationCalibrateXYZAccuracy other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+ hashCode.Add(_layerHeight);
+ hashCode.Add(_bottomLayers);
+ hashCode.Add(_bottomExposure);
+ hashCode.Add(_normalExposure);
+ hashCode.Add(_topBottomMargin);
+ hashCode.Add(_leftRightMargin);
+ hashCode.Add(_xSize);
+ hashCode.Add(_ySize);
+ hashCode.Add(_zSize);
+ hashCode.Add(_centerHoleRelief);
+ hashCode.Add(_hollowModel);
+ hashCode.Add(_wallThickness);
+ hashCode.Add(_observedXSize);
+ hashCode.Add(_observedYSize);
+ hashCode.Add(_observedZSize);
+ hashCode.Add(_outputTLObject);
+ hashCode.Add(_outputTCObject);
+ hashCode.Add(_outputTRObject);
+ hashCode.Add(_outputMLObject);
+ hashCode.Add(_outputMCObject);
+ hashCode.Add(_outputMRObject);
+ hashCode.Add(_outputBLObject);
+ hashCode.Add(_outputBCObject);
+ hashCode.Add(_outputBRObject);
+ return hashCode.ToHashCode();
+ }
+
+ #endregion
+
+ #region Methods
+
+ public void SelectNoneObjects()
+ {
+ OutputTLObject = false;
+ OutputTCObject = false;
+ OutputTRObject = false;
+ OutputMLObject = false;
+ OutputMCObject = false;
+ OutputMRObject = false;
+ OutputBLObject = false;
+ OutputBCObject = false;
+ OutputBRObject = false;
+ }
+
+ public void SelectAllObjects()
+ {
+ OutputTLObject = true;
+ OutputTCObject = true;
+ OutputTRObject = true;
+ OutputMLObject = true;
+ OutputMCObject = true;
+ OutputMRObject = true;
+ OutputBLObject = true;
+ OutputBCObject = true;
+ OutputBRObject = true;
+ }
+
+ public void SelectCrossedObjects()
+ {
+ OutputTLObject = false;
+ OutputTCObject = true;
+ OutputTRObject = false;
+ OutputMLObject = true;
+ OutputMCObject = true;
+ OutputMRObject = true;
+ OutputBLObject = false;
+ OutputBCObject = true;
+ OutputBRObject = false;
+ }
+
+ public void SelectCenterObject()
+ {
+ OutputTLObject = false;
+ OutputTCObject = false;
+ OutputTRObject = false;
+ OutputMLObject = false;
+ OutputMCObject = true;
+ OutputMRObject = false;
+ OutputBLObject = false;
+ OutputBCObject = false;
+ OutputBRObject = false;
+ }
+
+ public void Sanitize()
+ {
+ for (ushort i = 0; i < 10000 && (_xSize * Xppmm).DecimalDigits() >= 1; i++)
+ {
+ XSize += 0.01M;
+ }
+
+ for (ushort i = 0; i < 10000 && (_ySize * Yppmm).DecimalDigits() >= 1; i++)
+ {
+ YSize += 0.01M;
+ }
+
+ for (ushort i = 0; i < 10000 && (_zSize / LayerHeight).DecimalDigits() >= 1; i++)
+ {
+ ZSize += 0.01M;
+ }
+ }
+
+ public Mat[] GetLayers()
+ {
+ var layers = new Mat[2];
+ for (byte i = 0; i < layers.Length; i++)
+ {
+ layers[i] = EmguExtensions.InitMat(Resolution);
+ }
+
+
+ int currentX = 0;
+ int currentY = 0;
+ string positionYStr = string.Empty;
+ string positionStr = string.Empty;
+
+ const FontFace fontFace = FontFace.HersheyDuplex;
+ const byte fontStartX = 30;
+ const byte fontStartY = 50;
+ const double fontScale = 1.3;
+ const byte fontThickness = 3;
+
+ for (int y = 0; y < 3; y++)
+ {
+ switch (y)
+ {
+ case 0:
+ currentY = _topBottomMargin;
+ positionYStr = "T";
+ break;
+ case 1:
+ currentY = (int)(Resolution.Height / 2 - YPixels / 2);
+ positionYStr = "M";
+ break;
+ case 2:
+ currentY = (int)(Resolution.Height - YPixels - _topBottomMargin);
+ positionYStr = "B";
+ break;
+ }
+ for (int x = 0; x < 3; x++)
+ {
+ switch (x)
+ {
+ case 0:
+ currentX = _leftRightMargin;
+ positionStr = $"{positionYStr}L";
+ break;
+ case 1:
+ currentX = (int)(Resolution.Width / 2 - XPixels / 2);
+ positionStr = $"{positionYStr}C";
+ break;
+ case 2:
+ currentX = (int)(Resolution.Width - XPixels - _leftRightMargin);
+ positionStr = $"{positionYStr}R";
+ break;
+ }
+
+
+ for (var i = 0; i < layers.Length; i++)
+ {
+ if(y == 0 && x == 0 && !_outputTLObject) continue;
+ if(y == 0 && x == 1 && !_outputTCObject) continue;
+ if(y == 0 && x == 2 && !_outputTRObject) continue;
+ if(y == 1 && x == 0 && !_outputMLObject) continue;
+ if(y == 1 && x == 1 && !_outputMCObject) continue;
+ if(y == 1 && x == 2 && !_outputMRObject) continue;
+ if(y == 2 && x == 0 && !_outputBLObject) continue;
+ if(y == 2 && x == 1 && !_outputBCObject) continue;
+ if(y == 2 && x == 2 && !_outputBRObject) continue;
+ var layer = layers[i];
+ CvInvoke.Rectangle(layer,
+ new Rectangle(currentX, currentY, (int) XPixels, (int) YPixels),
+ EmguExtensions.WhiteByte, -1);
+
+ CvInvoke.PutText(layer, positionStr,
+ new Point(currentX + fontStartX, currentY + fontStartY), fontFace, fontScale,
+ EmguExtensions.BlackByte, fontThickness);
+
+ CvInvoke.PutText(layer, $"{XSize},{YSize},{ZSize}",
+ new Point(currentX + fontStartX, (int) (currentY + YPixels - fontStartY + 25)), fontFace, fontScale,
+ EmguExtensions.BlackByte, fontThickness);
+
+ if (CenterHoleRelief)
+ {
+ CvInvoke.Circle(layer,
+ new Point((int) (currentX + XPixels / 2), (int) (currentY + YPixels / 2)),
+ (int) (Math.Min(XPixels, YPixels) / 4),
+ EmguExtensions.Black3Byte, -1);
+ }
+
+ if (_hollowModel && i != 0 && _wallThickness > 0)
+ {
+ Size rectSize = new Size((int) (XPixels - WallThicknessXPixels * 2), (int) (YPixels - WallThicknessYPixels * 2));
+ Point rectLocation = new Point((int) (currentX + WallThicknessXPixels), (int) (currentY + WallThicknessYPixels));
+ CvInvoke.Rectangle(layers[i], new Rectangle(rectLocation, rectSize),
+ EmguExtensions.Black3Byte, -1);
+ }
+ }
+ }
+ }
+
+ return layers;
+ }
+
+ public Mat GetThumbnail()
+ {
+ Mat thumbnail = EmguExtensions.InitMat(new Size(400, 200), 3);
+ var fontFace = FontFace.HersheyDuplex;
+ var fontScale = 1;
+ var fontThickness = 2;
+ const byte xSpacing = 45;
+ const byte ySpacing = 45;
+ CvInvoke.PutText(thumbnail, "UVtools", new Point(140, 35), fontFace, fontScale, new MCvScalar(255, 27, 245), fontThickness + 1);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, 0), new Point(xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(xSpacing, ySpacing + 5), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.Line(thumbnail, new Point(thumbnail.Width - xSpacing, 0), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3);
+ CvInvoke.PutText(thumbnail, "XYZ Accuracy Cal.", new Point(xSpacing, ySpacing * 2), fontFace, fontScale, new MCvScalar(0, 255, 255), fontThickness);
+ CvInvoke.PutText(thumbnail, $"{Microns}um @ {BottomExposure}s/{NormalExposure}s", new Point(xSpacing, ySpacing * 3), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+ CvInvoke.PutText(thumbnail, $"{XSize} x {YSize} x {ZSize} mm", new Point(xSpacing, ySpacing * 4), fontFace, fontScale, EmguExtensions.White3Byte, fontThickness);
+
+ /*thumbnail.SetTo(EmguExtensions.Black3Byte);
+
+ CvInvoke.Circle(thumbnail, new Point(400/2, 200/2), 200/2, EmguExtensions.White3Byte, -1);
+ for (int angle = 0; angle < 360; angle+=20)
+ {
+ CvInvoke.Line(thumbnail, new Point(400 / 2, 200 / 2), new Point((int)(400 / 2 + 100 * Math.Cos(angle * Math.PI / 180)), (int)(200 / 2 + 100 * Math.Sin(angle * Math.PI / 180))), new MCvScalar(255, 27, 245), 3);
+ }
+
+ thumbnail.Save("D:\\Thumbnail.png");*/
+ return thumbnail;
+ }
+
+ #endregion
+ }
+}
diff --git a/UVtools.Core/Operations/OperationMorph.cs b/UVtools.Core/Operations/OperationMorph.cs
index 116c658..ab603d5 100644
--- a/UVtools.Core/Operations/OperationMorph.cs
+++ b/UVtools.Core/Operations/OperationMorph.cs
@@ -87,7 +87,7 @@ namespace UVtools.Core.Operations
}
[XmlIgnore]
- public Kernel Kernel { get; set; } = new Kernel();
+ public Kernel Kernel { get; set; } = new();
public override string ToString()
{
diff --git a/UVtools.Core/Operations/OperationMove.cs b/UVtools.Core/Operations/OperationMove.cs
index 24337a5..d3141e7 100644
--- a/UVtools.Core/Operations/OperationMove.cs
+++ b/UVtools.Core/Operations/OperationMove.cs
@@ -217,6 +217,14 @@ namespace UVtools.Core.Operations
Anchor = anchor;
}
+ public OperationMove(Rectangle srcRoi, Size resolution, Enumerations.Anchor anchor = Enumerations.Anchor.MiddleCenter)
+ {
+ ROI = srcRoi;
+ ImageWidth = (uint) resolution.Width;
+ ImageHeight = (uint) resolution.Height;
+ Anchor = anchor;
+ }
+
public void Reset()
{
MarginLeft = MarginTop = MarginRight = MarginBottom = 0;
diff --git a/UVtools.Core/Operations/OperationPixelDimming.cs b/UVtools.Core/Operations/OperationPixelDimming.cs
index 182227b..e14a3ba 100644
--- a/UVtools.Core/Operations/OperationPixelDimming.cs
+++ b/UVtools.Core/Operations/OperationPixelDimming.cs
@@ -10,6 +10,7 @@ using System;
using System.Text;
using System.Xml.Serialization;
using Emgu.CV;
+using UVtools.Core.Extensions;
using UVtools.Core.Objects;
namespace UVtools.Core.Operations
@@ -17,6 +18,18 @@ namespace UVtools.Core.Operations
[Serializable]
public class OperationPixelDimming : Operation
{
+ #region Subclasses
+ class StringMatrix
+ {
+ public string Text { get; }
+ public Matrix<byte> Pattern { get; set; }
+
+ public StringMatrix(string text)
+ {
+ Text = text;
+ }
+ }
+ #endregion
private uint _wallThicknessStart = 5;
private uint _wallThicknessEnd = 5;
private bool _wallsOnly;
@@ -24,6 +37,11 @@ namespace UVtools.Core.Operations
private Matrix<byte> _pattern;
private Matrix<byte> _alternatePattern;
private ushort _alternatePatternPerLayers = 1;
+ private string _patternText;
+ private string _alternatePatternText;
+ private byte _brightness = 127;
+ private ushort _infillGenThickness = 10;
+ private ushort _infillGenSpacing = 20;
#region Overrides
public override string Title => "Pixel dimming";
@@ -36,7 +54,7 @@ namespace UVtools.Core.Operations
"NOTE: Run this tool only after repairs and all other transformations.";
public override string ConfirmationText =>
- $"dim pixels from layers {LayerIndexStart} through {LayerIndexEnd}";
+ $"dim pixels from layers {LayerIndexStart} through {LayerIndexEnd}?";
public override string ProgressTitle =>
$"Dimming from layers {LayerIndexStart} through {LayerIndexEnd}";
@@ -51,13 +69,66 @@ namespace UVtools.Core.Operations
sb.AppendLine("Border size must be positive in order to use \"Dim only borders\" function.");
}*/
+ var stringMatrix = new[]
+ {
+ new StringMatrix(PatternText),
+ new StringMatrix(AlternatePatternText),
+ };
+
+ foreach (var item in stringMatrix)
+ {
+ if (string.IsNullOrWhiteSpace(item.Text)) continue;
+ var lines = item.Text.Split('\n');
+ for (var row = 0; row < lines.Length; row++)
+ {
+
+ var bytes = lines[row].Trim().Split(' ');
+ if (row == 0)
+ {
+ item.Pattern = new Matrix<byte>(lines.Length, bytes.Length);
+ }
+ else
+ {
+ if (item.Pattern.Cols != bytes.Length)
+ {
+ sb.AppendLine($"Row {row + 1} have invalid number of pixels, the pattern must have equal pixel count per line, per defined on line 1");
+ return new StringTag(sb.ToString());
+ }
+ }
+
+ for (int col = 0; col < bytes.Length; col++)
+ {
+ if (byte.TryParse(bytes[col], out var value))
+ {
+ item.Pattern[row, col] = value;
+ }
+ else
+ {
+ sb.AppendLine($"{bytes[col]} is a invalid number, use values from 0 to 255");
+ return new StringTag(sb.ToString());
+ }
+ }
+ }
+ }
+
+ Pattern = stringMatrix[0].Pattern;
+ AlternatePattern = stringMatrix[1].Pattern;
+
if (Pattern is null && AlternatePattern is null)
{
sb.AppendLine("Either even or odd pattern must contain a valid matrix.");
+ return new StringTag(sb.ToString());
}
return new StringTag(sb.ToString());
}
+
+ public override string ToString()
+ {
+ var result = $"[Border: {_wallThicknessStart}px to {_wallThicknessEnd}px] [Chamfer: {_chamfer}] [Only borders: {_wallsOnly}] [Alternate every: {_alternatePatternPerLayers}] [B: {_brightness}]" + LayerRangeString;
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }
#endregion
#region Properties
@@ -95,6 +166,18 @@ namespace UVtools.Core.Operations
set => RaiseAndSetIfChanged(ref _alternatePatternPerLayers, Math.Max((ushort)1, value));
}
+ public string PatternText
+ {
+ get => _patternText;
+ set => RaiseAndSetIfChanged(ref _patternText, value);
+ }
+
+ public string AlternatePatternText
+ {
+ get => _alternatePatternText;
+ set => RaiseAndSetIfChanged(ref _alternatePatternText, value);
+ }
+
[XmlIgnore]
public Matrix<byte> Pattern
{
@@ -109,49 +192,340 @@ namespace UVtools.Core.Operations
set => RaiseAndSetIfChanged(ref _alternatePattern, value);
}
- #endregion
-
- #region Methods
-
- public bool IsNormalPattern(uint layerIndex)
+ public byte Brightness
{
- return layerIndex / AlternatePatternPerLayers % 2 == 0;
+ get => _brightness;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _brightness, value)) return;
+ RaisePropertyChanged(nameof(BrightnessPercent));
+ }
}
+
+ public decimal BrightnessPercent => Math.Round(_brightness * 100 / 255M, 2);
- public bool IsAlternatePattern(uint layerIndex) => !IsNormalPattern(layerIndex);
- public override string ToString()
+ public ushort InfillGenThickness
{
- var result = $"[Border: {_wallThicknessStart}px to {_wallThicknessEnd}px] [Chamfer: {_chamfer}] [Only borders: {_wallsOnly}] [Alternate every: {_alternatePatternPerLayers}]" + LayerRangeString;
- if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
- return result;
+ get => _infillGenThickness;
+ set => RaiseAndSetIfChanged(ref _infillGenThickness, value);
}
+ public ushort InfillGenSpacing
+ {
+ get => _infillGenSpacing;
+ set => RaiseAndSetIfChanged(ref _infillGenSpacing, value);
+ }
+
#endregion
#region Equality
protected bool Equals(OperationPixelDimming other)
{
- return _wallThicknessStart == other._wallThicknessStart && _wallsOnly == other._wallsOnly;
+ return _wallThicknessStart == other._wallThicknessStart && _wallThicknessEnd == other._wallThicknessEnd && _wallsOnly == other._wallsOnly && _chamfer == other._chamfer && _alternatePatternPerLayers == other._alternatePatternPerLayers && _patternText == other._patternText && _alternatePatternText == other._alternatePatternText && _brightness == other._brightness && _infillGenThickness == other._infillGenThickness && _infillGenSpacing == other._infillGenSpacing;
}
public override bool Equals(object obj)
{
- if (obj is null) return false;
+ if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != GetType()) return false;
+ if (obj.GetType() != this.GetType()) return false;
return Equals((OperationPixelDimming) obj);
}
public override int GetHashCode()
{
- unchecked
+ var hashCode = new HashCode();
+ hashCode.Add(_wallThicknessStart);
+ hashCode.Add(_wallThicknessEnd);
+ hashCode.Add(_wallsOnly);
+ hashCode.Add(_chamfer);
+ hashCode.Add(_alternatePatternPerLayers);
+ hashCode.Add(_patternText);
+ hashCode.Add(_alternatePatternText);
+ hashCode.Add(_brightness);
+ hashCode.Add(_infillGenThickness);
+ hashCode.Add(_infillGenSpacing);
+ return hashCode.ToHashCode();
+ }
+
+ #endregion
+
+ #region Methods
+ public bool IsNormalPattern(uint layerIndex) => layerIndex / AlternatePatternPerLayers % 2 == 0;
+
+ public bool IsAlternatePattern(uint layerIndex) => !IsNormalPattern(layerIndex);
+
+
+ public void GeneratePixelDimming(string pattern)
+ {
+ if (pattern == "Chessboard")
+ {
+ PatternText = string.Format(
+ "255 {0}{1}" +
+ "{0} 255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "{0} 255{1}" +
+ "255 {0}"
+ , _brightness, "\n");
+
+ return;
+ }
+
+ if (pattern == "Sparse")
{
- return ((int) _wallThicknessStart * 397) ^ _wallsOnly.GetHashCode();
+ PatternText = string.Format(
+ "{0} 255 255 255{1}" +
+ "255 255 {0} 255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 255 {0} 255{1}" +
+ "{0} 255 255 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Crosses")
+ {
+ PatternText = string.Format(
+ "{0} 255 {0} 255{1}" +
+ "255 {0} 255 255{1}" +
+ "{0} 255 {0} 255{1}" +
+ "255 255 255 255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 255 255 255{1}" +
+ "{0} 255 {0} 255{1}" +
+ "255 {0} 255 255{1}" +
+ "{0} 255 {0} 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Strips")
+ {
+ PatternText = string.Format(
+ "{0}{1}" +
+ "255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255{1}" +
+ "{0}"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Pyramid")
+ {
+ PatternText = string.Format(
+ "255 255 {0} 255 255 255{1}" +
+ "255 {0} 255 {0} 255 255{1}" +
+ "{0} 255 {0} 255 {0} 255{1}" +
+ "255 255 255 255 255 255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 {0} 255 {0} 255 {0}{1}" +
+ "255 255 {0} 255 {0} 255{1}" +
+ "255 255 255 {0} 255 255{1}" +
+ "255 255 255 255 255 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Rhombus")
+ {
+ PatternText = string.Format(
+ "255 {0} 255 255{1}" +
+ "{0} 255 {0} 255{1}" +
+ "255 {0} 255 255{1}" +
+ "255 255 255 255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 255 255 255{1}" +
+ "255 {0} 255 255{1}" +
+ "{0} 255 {0} 255{1}" +
+ "255 {0} 255 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Hearts")
+ {
+ PatternText = string.Format(
+ "255 {0} 255 {0} 255 255{1}" +
+ "{0} 255 {0} 255 {0} 255{1}" +
+ "{0} 255 255 255 {0} 255{1}" +
+ "255 {0} 255 {0} 255 255{1}" +
+ "255 255 {0} 255 255 255{1}" +
+ "255 255 255 255 255 255"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 255 255 255 255 255{1}" +
+ "255 255 {0} 255 {0} 255{1}" +
+ "255 {0} 255 {0} 255 {0}{1}" +
+ "255 {0} 255 255 255 {0}{1}" +
+ "255 255 {0} 255 {0} 255{1}" +
+ "255 255 255 {0} 255 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Slashes")
+ {
+ PatternText = string.Format(
+ "{0} 255 255{1}" +
+ "255 {0} 255{1}" +
+ "255 255 {0}"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 255 {0}{1}" +
+ "255 {0} 255{1}" +
+ "{0} 255 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Waves")
+ {
+ PatternText = string.Format(
+ "{0} 255 255{1}" +
+ "255 255 {0}"
+ , _brightness, "\n");
+
+ AlternatePatternText = string.Format(
+ "255 255 {0}{1}" +
+ "{0} 255 255"
+ , _brightness, "\n");
+ return;
+ }
+
+ if (pattern == "Solid")
+ {
+ PatternText = _brightness.ToString();
+ AlternatePatternText = null;
+ return;
}
}
+ public void GenerateInfill(string pattern)
+ {
+ if (pattern == "Rectilinear")
+ {
+ PatternText = ($"0\n".Repeat(_infillGenSpacing) + $"255\n".Repeat(_infillGenSpacing)).Trim('\n', '\r');
+ AlternatePatternText = null;
+ return;
+ }
+
+ if (pattern == "Square grid")
+ {
+ var p1 = "0 ".Repeat(_infillGenSpacing) + "255 ".Repeat(_infillGenThickness);
+ p1 = p1.Trim() + "\n";
+ p1 += p1.Repeat(_infillGenThickness);
+
+
+ var p2 = "255 ".Repeat(_infillGenSpacing) + "255 ".Repeat(_infillGenThickness);
+ p2 = p2.Trim() + '\n';
+ p2 += p2.Repeat(_infillGenThickness);
+
+ p2 = p2.Trim('\n', '\r');
+
+ PatternText = p1 + p2;
+ AlternatePatternText = null;
+ return;
+ }
+
+ if (pattern == "Waves")
+ {
+ var p1 = string.Empty;
+ var pos = 0;
+ for (sbyte dir = 1; dir >= -1; dir -= 2)
+ {
+ while (pos >= 0 && pos <= _infillGenSpacing)
+ {
+ p1 += "0 ".Repeat(pos);
+ p1 += "255 ".Repeat(_infillGenThickness);
+ p1 += "0 ".Repeat(_infillGenSpacing - pos);
+ p1 = p1.Trim() + '\n';
+
+ pos += dir;
+ }
+
+ pos--;
+ }
+
+ PatternText = p1.Trim('\n', '\r');
+ AlternatePatternText = null;
+ return;
+ }
+
+ if (pattern == "Lattice")
+ {
+ var p1 = string.Empty;
+ var p2 = string.Empty;
+
+ var zeros = Math.Max(0, _infillGenSpacing - _infillGenThickness * 2);
+
+ // Pillar
+ for (int i = 0; i < _infillGenThickness; i++)
+ {
+ p1 += "255 ".Repeat(_infillGenThickness);
+ p1 += "0 ".Repeat(zeros);
+ p1 += "255 ".Repeat(_infillGenThickness);
+ p1 = p1.Trim() + '\n';
+ }
+
+ for (int i = 0; i < zeros; i++)
+ {
+ p1 += "0 ".Repeat(_infillGenSpacing);
+ p1 = p1.Trim() + '\n';
+ }
+
+ for (int i = 0; i < _infillGenThickness; i++)
+ {
+ p1 += "255 ".Repeat(_infillGenThickness);
+ p1 += "0 ".Repeat(zeros);
+ p1 += "255 ".Repeat(_infillGenThickness);
+ p1 = p1.Trim() + '\n';
+ }
+
+ // Square
+ for (int i = 0; i < _infillGenThickness; i++)
+ {
+ p2 += "255 ".Repeat(_infillGenSpacing);
+ p2 = p2.Trim() + '\n';
+ }
+
+ for (int i = 0; i < zeros; i++)
+ {
+ p2 += "255 ".Repeat(_infillGenThickness);
+ p2 += "0 ".Repeat(zeros);
+ p2 += "255 ".Repeat(_infillGenThickness);
+ p2 = p2.Trim() + '\n';
+ }
+
+ for (int i = 0; i < _infillGenThickness; i++)
+ {
+ p2 += "255 ".Repeat(_infillGenSpacing);
+ p2 = p2.Trim() + '\n';
+ }
+
+
+
+ PatternText = p1.Trim('\n', '\r');
+ AlternatePatternText = p2.Trim('\n', '\r'); ;
+ return;
+ }
+ }
#endregion
}
}
diff --git a/UVtools.Core/Operations/OperationRaftRelief.cs b/UVtools.Core/Operations/OperationRaftRelief.cs
index 55ce521..4c5a90a 100644
--- a/UVtools.Core/Operations/OperationRaftRelief.cs
+++ b/UVtools.Core/Operations/OperationRaftRelief.cs
@@ -15,7 +15,7 @@ namespace UVtools.Core.Operations
{
private RaftReliefTypes _reliefType = RaftReliefTypes.Relief;
private byte _dilateIterations = 10;
- private byte _wallMargin = 10;
+ private byte _wallMargin = 20;
private byte _holeDiameter = 50;
private byte _holeSpacing = 20;
public override string Title => "Raft relief";
diff --git a/UVtools.Core/PixelEditor/PixelDrainHole.cs b/UVtools.Core/PixelEditor/PixelDrainHole.cs
index 4dd82f9..bf707ab 100644
--- a/UVtools.Core/PixelEditor/PixelDrainHole.cs
+++ b/UVtools.Core/PixelEditor/PixelDrainHole.cs
@@ -20,7 +20,7 @@ namespace UVtools.Core.PixelEditor
set => RaiseAndSetIfChanged(ref _diameter, value);
}
- public PixelDrainHole(){}
+ public PixelDrainHole(){ _pixelBrightness = 0; }
public PixelDrainHole(uint layerIndex, Point location, byte diameter) : base(layerIndex, location)
{
diff --git a/UVtools.Core/PixelEditor/PixelDrawing.cs b/UVtools.Core/PixelEditor/PixelDrawing.cs
index 0e792b9..45cbb9e 100644
--- a/UVtools.Core/PixelEditor/PixelDrawing.cs
+++ b/UVtools.Core/PixelEditor/PixelDrawing.cs
@@ -16,6 +16,7 @@ namespace UVtools.Core.PixelEditor
private BrushShapeType _brushShape = BrushShapeType.Rectangle;
private ushort _brushSize = 1;
private short _thickness = -1;
+ private byte _removePixelBrightness;
public const byte MinRectangleBrush = 1;
public const byte MinCircleBrush = 7;
public enum BrushShapeType : byte
@@ -53,9 +54,21 @@ namespace UVtools.Core.PixelEditor
set => RaiseAndSetIfChanged(ref _thickness, value);
}
+ public byte RemovePixelBrightness
+ {
+ get => _removePixelBrightness;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _removePixelBrightness, value)) return;
+ RaisePropertyChanged(nameof(RemovePixelBrightnessPercent));
+ }
+ }
+
+ public decimal RemovePixelBrightnessPercent => Math.Round(_removePixelBrightness * 100M / 255M, 2);
+
public bool IsAdd { get; }
- public byte Color { get; }
+ public byte Brightness => IsAdd ? _pixelBrightness : _removePixelBrightness;
public Rectangle Rectangle { get; }
@@ -64,15 +77,13 @@ namespace UVtools.Core.PixelEditor
}
- public PixelDrawing(uint layerIndex, Point location, LineType lineType, BrushShapeType brushShape, ushort brushSize, short thickness, bool isAdd) : base(layerIndex, location, lineType)
+ public PixelDrawing(uint layerIndex, Point location, LineType lineType, BrushShapeType brushShape, ushort brushSize, short thickness, byte removePixelBrightness, byte pixelBrightness, bool isAdd) : base(layerIndex, location, lineType, pixelBrightness)
{
- BrushShape = brushShape;
- BrushSize = brushSize;
- Thickness = thickness;
+ _brushShape = brushShape;
+ _brushSize = brushSize;
+ _thickness = thickness;
+ _removePixelBrightness = removePixelBrightness;
IsAdd = isAdd;
-
-
- Color = (byte) (isAdd ? 255 : 0);
int shiftPos = brushSize / 2;
Rectangle = new Rectangle(Math.Max(0, location.X - shiftPos), Math.Max(0, location.Y - shiftPos), brushSize-1, brushSize-1);
diff --git a/UVtools.Core/PixelEditor/PixelEraser.cs b/UVtools.Core/PixelEditor/PixelEraser.cs
index 07d3856..45e9a7f 100644
--- a/UVtools.Core/PixelEditor/PixelEraser.cs
+++ b/UVtools.Core/PixelEditor/PixelEraser.cs
@@ -6,6 +6,7 @@
* of this license document, but changing it is not allowed.
*/
using System.Drawing;
+using Emgu.CV.CvEnum;
namespace UVtools.Core.PixelEditor
{
@@ -17,9 +18,10 @@ namespace UVtools.Core.PixelEditor
public PixelEraser()
{
+ _pixelBrightness = 0;
}
- public PixelEraser(uint layerIndex, Point location) : base(layerIndex, location)
+ public PixelEraser(uint layerIndex, Point location, byte pixelBrightness) : base(layerIndex, location, LineType.AntiAlias, pixelBrightness)
{
Size = new Size(Diameter, Diameter);
}
diff --git a/UVtools.Core/PixelEditor/PixelOperation.cs b/UVtools.Core/PixelEditor/PixelOperation.cs
index 6fb3397..dc6a989 100644
--- a/UVtools.Core/PixelEditor/PixelOperation.cs
+++ b/UVtools.Core/PixelEditor/PixelOperation.cs
@@ -15,7 +15,11 @@ namespace UVtools.Core.PixelEditor
{
public abstract class PixelOperation : BindableBase
{
- private uint _index;
+ private protected uint _index;
+ private protected LineType _lineType = LineType.AntiAlias;
+ private protected byte _pixelBrightness = 255;
+ private protected uint _layersBelow;
+ private protected uint _layersAbove;
public enum PixelOperationType : byte
{
@@ -53,7 +57,11 @@ namespace UVtools.Core.PixelEditor
/// <summary>
/// Gets the <see cref="LineType"/> for the draw operation
/// </summary>
- public LineType LineType { get; set; } = LineType.AntiAlias;
+ public LineType LineType
+ {
+ get => _lineType;
+ set => RaiseAndSetIfChanged(ref _lineType, value);
+ }
public LineType[] LineTypes => new[]
{
@@ -62,24 +70,44 @@ namespace UVtools.Core.PixelEditor
LineType.AntiAlias
};
- public uint LayersBelow { get; set; }
+ public byte PixelBrightness
+ {
+ get => _pixelBrightness;
+ set
+ {
+ if(!RaiseAndSetIfChanged(ref _pixelBrightness, value)) return;
+ RaisePropertyChanged(nameof(PixelBrightnessPercent));
+ }
+ }
+
+ public decimal PixelBrightnessPercent => Math.Round(_pixelBrightness * 100M / 255M, 2);
+
+ public uint LayersBelow
+ {
+ get => _layersBelow;
+ set => RaiseAndSetIfChanged(ref _layersBelow, value);
+ }
- public uint LayersAbove { get; set; }
+ public uint LayersAbove
+ {
+ get => _layersAbove;
+ set => RaiseAndSetIfChanged(ref _layersAbove, value);
+ }
/// <summary>
/// Gets the total size of the operation
/// </summary>
public Size Size { get; private protected set; } = Size.Empty;
- protected PixelOperation()
- {
- }
+ protected PixelOperation() { }
- protected PixelOperation(uint layerIndex, Point location, LineType lineType = LineType.AntiAlias)
+ protected PixelOperation(uint layerIndex, Point location, LineType lineType = LineType.AntiAlias, int pixelBrightness = -1)
{
Location = location;
LayerIndex = layerIndex;
LineType = lineType;
+ if (pixelBrightness > -1)
+ _pixelBrightness = (byte) pixelBrightness;
}
public PixelOperation Clone()
diff --git a/UVtools.Core/PixelEditor/PixelSupport.cs b/UVtools.Core/PixelEditor/PixelSupport.cs
index 31374e5..bbe3392 100644
--- a/UVtools.Core/PixelEditor/PixelSupport.cs
+++ b/UVtools.Core/PixelEditor/PixelSupport.cs
@@ -6,6 +6,7 @@
* of this license document, but changing it is not allowed.
*/
using System.Drawing;
+using Emgu.CV.CvEnum;
namespace UVtools.Core.PixelEditor
{
@@ -36,7 +37,7 @@ namespace UVtools.Core.PixelEditor
public PixelSupport(){}
- public PixelSupport(uint layerIndex, Point location, byte tipDiameter, byte pillarDiameter, byte baseDiameter) : base(layerIndex, location)
+ public PixelSupport(uint layerIndex, Point location, byte tipDiameter, byte pillarDiameter, byte baseDiameter, byte pixelBrightness) : base(layerIndex, location, LineType.AntiAlias, pixelBrightness)
{
TipDiameter = tipDiameter;
PillarDiameter = pillarDiameter;
diff --git a/UVtools.Core/PixelEditor/PixelText.cs b/UVtools.Core/PixelEditor/PixelText.cs
index 5d0f286..27af137 100644
--- a/UVtools.Core/PixelEditor/PixelText.cs
+++ b/UVtools.Core/PixelEditor/PixelText.cs
@@ -19,6 +19,7 @@ namespace UVtools.Core.PixelEditor
private ushort _thickness = 1;
private string _text;
private bool _mirror;
+ private byte _removePixelBrightness;
public override PixelOperationType OperationType => PixelOperationType.Text;
public static FontFace[] FontFaces => (FontFace[]) Enum.GetValues(typeof(FontFace));
@@ -53,24 +54,36 @@ namespace UVtools.Core.PixelEditor
set => RaiseAndSetIfChanged(ref _mirror, value);
}
+ public byte RemovePixelBrightness
+ {
+ get => _removePixelBrightness;
+ set
+ {
+ if (!RaiseAndSetIfChanged(ref _removePixelBrightness, value)) return;
+ RaisePropertyChanged(nameof(RemovePixelBrightnessPercent));
+ }
+ }
+
+ public decimal RemovePixelBrightnessPercent => Math.Round(_removePixelBrightness * 100M / 255M, 2);
+
public bool IsAdd { get; }
- public byte Color { get; }
+ public byte Brightness => IsAdd ? _pixelBrightness : _removePixelBrightness;
public Rectangle Rectangle { get; }
public PixelText(){}
- public PixelText(uint layerIndex, Point location, LineType lineType, FontFace font, double fontScale, ushort thickness, string text, bool mirror, bool isAdd) : base(layerIndex, location, lineType)
+ public PixelText(uint layerIndex, Point location, LineType lineType, FontFace font, double fontScale, ushort thickness, string text, bool mirror, byte removePixelBrightness, byte pixelBrightness, bool isAdd) : base(layerIndex, location, lineType, pixelBrightness)
{
- Font = font;
- FontScale = fontScale;
- Thickness = thickness;
- Text = text;
- Mirror = mirror;
+ _font = font;
+ _fontScale = fontScale;
+ _thickness = thickness;
+ _text = text;
+ _mirror = mirror;
IsAdd = isAdd;
+ _removePixelBrightness = removePixelBrightness;
- Color = (byte) (isAdd ? 255 : 0);
int baseLine = 0;
Size = CvInvoke.GetTextSize(text, font, fontScale, thickness, ref baseLine);
Rectangle = new Rectangle(location, Size);
diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj
index 2d8dfab..72c2fb8 100644
--- a/UVtools.Core/UVtools.Core.csproj
+++ b/UVtools.Core/UVtools.Core.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netstandard2.0</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Company>PTRTECH</Company>
@@ -10,7 +10,7 @@
<RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl>
<PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl>
<Description>MSLA/DLP, file analysis, repair, conversion and manipulation</Description>
- <Version>1.4.0</Version>
+ <Version>2.0.0</Version>
<Copyright>Copyright © 2020 PTRTECH</Copyright>
<PackageIcon>UVtools.png</PackageIcon>
<Platforms>AnyCPU;x64</Platforms>
diff --git a/UVtools.Core/UVtools.Core.csproj.DotSettings b/UVtools.Core/UVtools.Core.csproj.DotSettings
new file mode 100644
index 0000000..6162834
--- /dev/null
+++ b/UVtools.Core/UVtools.Core.csproj.DotSettings
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp90</s:String></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs
index 2d577ec..f56bd8c 100644
--- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs
+++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs
@@ -11,6 +11,9 @@
<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>
+ <Component Id="owcFA31C1307B4F2830BF0AB23B88C6F1C0" Guid="29cf69a8-5aa7-fc7d-dc59-c2406bdbf06e">
+ <File Id="owfFA31C1307B4F2830BF0AB23B88C6F1C0" Source="$(var.SourceDir)\api-ms-win-core-console-l1-2-0.dll" KeyPath="yes" />
+ </Component>
<Component Id="owcB0E3C7251F136710A0F11E0C18525364" Guid="f80e6202-0436-d488-52cf-827e37483096">
<File Id="owfB0E3C7251F136710A0F11E0C18525364" Source="$(var.SourceDir)\api-ms-win-core-datetime-l1-1-0.dll" KeyPath="yes" />
</Component>
@@ -176,15 +179,15 @@
<Component Id="owc2360F1EA9FE0B1355D5D44A01B139070" Guid="5f41891e-50bf-de85-90a5-56cbc18299b8">
<File Id="owf2360F1EA9FE0B1355D5D44A01B139070" Source="$(var.SourceDir)\Avalonia.Markup.Xaml.Loader.dll" KeyPath="yes" />
</Component>
+ <Component Id="owc6834F87221790516C5BC358659FB168A" Guid="a648aa0a-edab-e191-afb0-5744596ea468">
+ <File Id="owf6834F87221790516C5BC358659FB168A" Source="$(var.SourceDir)\Avalonia.MicroCom.dll" KeyPath="yes" />
+ </Component>
<Component Id="owc429F500BA073A0E6269B9341D14183BD" Guid="dcb4ad79-d927-c83e-1938-b2872b68db22">
<File Id="owf429F500BA073A0E6269B9341D14183BD" Source="$(var.SourceDir)\Avalonia.Native.dll" KeyPath="yes" />
</Component>
<Component Id="owc4E185369FC40F6DAFEBB23AA25A98904" Guid="b6be3442-f344-161b-bce4-a08b16f6f69d">
<File Id="owf4E185369FC40F6DAFEBB23AA25A98904" Source="$(var.SourceDir)\Avalonia.OpenGL.dll" KeyPath="yes" />
</Component>
- <Component Id="owc397D62110086E82DFDBC4309C538AA6F" Guid="2eb88c06-e0e9-8e19-92d2-ba888100f24c">
- <File Id="owf397D62110086E82DFDBC4309C538AA6F" Source="$(var.SourceDir)\Avalonia.ReactiveUI.dll" KeyPath="yes" />
- </Component>
<Component Id="owcD69CFCE9EE400CCC3A2DC69ECB3AFB9C" Guid="a4b34c19-87d5-7b3f-61a5-4a897c32202f">
<File Id="owfD69CFCE9EE400CCC3A2DC69ECB3AFB9C" Source="$(var.SourceDir)\Avalonia.Remote.Protocol.dll" KeyPath="yes" />
</Component>
@@ -227,6 +230,9 @@
<Component Id="owc6135A13B8E29883ED57BF3FE20578EED" Guid="cd1aae2e-a4de-1bd1-9680-014bccf80e32">
<File Id="owf6135A13B8E29883ED57BF3FE20578EED" Source="$(var.SourceDir)\coreclr.dll" KeyPath="yes" />
</Component>
+ <Component Id="owc5E8FBF42F9BCF40914849582C19906B8" Guid="f7a08052-6afc-fe5e-291c-07137533ab79">
+ <File Id="owf5E8FBF42F9BCF40914849582C19906B8" Source="$(var.SourceDir)\createdump.exe" KeyPath="yes" />
+ </Component>
<Component Id="owc3883419A2BFFE7013C2194A99728AB11" Guid="dd137337-4b74-c76b-c87e-6b5f42697dd4">
<File Id="owf3883419A2BFFE7013C2194A99728AB11" Source="$(var.SourceDir)\dbgshim.dll" KeyPath="yes" />
</Component>
@@ -278,14 +284,11 @@
<Component Id="owc7609DFEE03B95E830FD72B0699C9CF8C" Guid="c7b987b7-1a3c-0d2c-7831-ad6f4c3871e7">
<File Id="owf7609DFEE03B95E830FD72B0699C9CF8C" Source="$(var.SourceDir)\Microsoft.Win32.SystemEvents.dll" KeyPath="yes" />
</Component>
- <Component Id="owcC64805E458FD0630011C0F519E9CA9E7" Guid="4b534498-a1a1-b38c-3d4b-d63b915c6d88">
- <File Id="owfC64805E458FD0630011C0F519E9CA9E7" Source="$(var.SourceDir)\Microsoft.Windows.SDK.NET.dll" KeyPath="yes" />
- </Component>
<Component Id="owc6F31DEF09608AA645959666A4CB7FBBC" Guid="c94570a5-5bb4-a53f-e12f-9581bddf7d08">
<File Id="owf6F31DEF09608AA645959666A4CB7FBBC" Source="$(var.SourceDir)\mscordaccore.dll" KeyPath="yes" />
</Component>
- <Component Id="owcFEB7513C4748B7C8BC89CF57C2CC59F2" Guid="087d4e59-3c7c-7140-73b8-ab38a91b18b2">
- <File Id="owfFEB7513C4748B7C8BC89CF57C2CC59F2" Source="$(var.SourceDir)\mscordaccore_amd64_amd64_4.700.20.41105.dll" KeyPath="yes" />
+ <Component Id="owc27CF4546F4DEAC5C518305F4CBCBDE8B" Guid="42759aa9-bc58-023f-e7d6-454e105a1d3c">
+ <File Id="owf27CF4546F4DEAC5C518305F4CBCBDE8B" Source="$(var.SourceDir)\mscordaccore_amd64_amd64_5.0.120.57516.dll" KeyPath="yes" />
</Component>
<Component Id="owc5FC34571A1AE47A011FC6C2A95B00DA6" Guid="2591451e-0fe5-ccad-abf6-1d1097251253">
<File Id="owf5FC34571A1AE47A011FC6C2A95B00DA6" Source="$(var.SourceDir)\mscordbi.dll" KeyPath="yes" />
@@ -293,9 +296,6 @@
<Component Id="owc703B511CED6E576F1821E72BCD315C77" Guid="905632ee-e7c7-e961-f7b6-cefe84558e3f">
<File Id="owf703B511CED6E576F1821E72BCD315C77" Source="$(var.SourceDir)\mscorlib.dll" KeyPath="yes" />
</Component>
- <Component Id="owc537429FE657E0F214AF003E728760762" Guid="3c845268-20d3-8867-b25c-1a28f4be795f">
- <File Id="owf537429FE657E0F214AF003E728760762" Source="$(var.SourceDir)\mscorrc.debug.dll" KeyPath="yes" />
- </Component>
<Component Id="owcE382B0CF482F0E7C3AF33E6EEEBD26B0" Guid="7433845c-c763-9b7e-e2c9-7b06986570bf">
<File Id="owfE382B0CF482F0E7C3AF33E6EEEBD26B0" Source="$(var.SourceDir)\mscorrc.dll" KeyPath="yes" />
</Component>
@@ -308,18 +308,9 @@
<Component Id="owcBFCA0BB8CBE4A68414FA088B4F7F3D22" Guid="babeb74f-9861-b222-a49a-ec268c3cd3ca">
<File Id="owfBFCA0BB8CBE4A68414FA088B4F7F3D22" Source="$(var.SourceDir)\ReactiveUI.dll" KeyPath="yes" />
</Component>
- <Component Id="owc6BF307AC8A97E0C4BD18833248972AF7" Guid="d579a123-e5f2-c392-ad0d-92a327f7cb9a">
- <File Id="owf6BF307AC8A97E0C4BD18833248972AF7" Source="$(var.SourceDir)\SharpGen.Runtime.COM.dll" KeyPath="yes" />
- </Component>
- <Component Id="owc4990FD9A89E72BAF9765435F1E74B9F4" Guid="5a419f92-199e-237a-8e8c-853a3a83f069">
- <File Id="owf4990FD9A89E72BAF9765435F1E74B9F4" Source="$(var.SourceDir)\SharpGen.Runtime.dll" KeyPath="yes" />
- </Component>
<Component Id="owc363CAFE6342F1E71AB07D13BAEEE5FF0" Guid="8e946fdc-ea52-7c4b-a8b3-69e1ecf51086">
<File Id="owf363CAFE6342F1E71AB07D13BAEEE5FF0" Source="$(var.SourceDir)\SkiaSharp.dll" KeyPath="yes" />
</Component>
- <Component Id="owc07DD36861474DFA8A7BE8029239D0262" Guid="9c40efa6-5a9a-cfc7-7bc2-05ad801e798f">
- <File Id="owf07DD36861474DFA8A7BE8029239D0262" Source="$(var.SourceDir)\SOS_README.md" KeyPath="yes" />
- </Component>
<Component Id="owcB6B3A58616D1CC4857D00CF30CE2144F" Guid="9e96df83-7e62-ac6c-85b4-2a92ac85ef1e">
<File Id="owfB6B3A58616D1CC4857D00CF30CE2144F" Source="$(var.SourceDir)\Splat.dll" KeyPath="yes" />
</Component>
@@ -425,6 +416,9 @@
<Component Id="owc9A4DD4F6185D06C5A78E8AEDADB2494F" Guid="3249aa9c-9bf9-808a-ad80-94b09eb9ee37">
<File Id="owf9A4DD4F6185D06C5A78E8AEDADB2494F" Source="$(var.SourceDir)\System.Dynamic.Runtime.dll" KeyPath="yes" />
</Component>
+ <Component Id="owc2698635F680AF602A4B363817C363BDB" Guid="343217c8-8437-12c1-741e-84eacfdb2ba3">
+ <File Id="owf2698635F680AF602A4B363817C363BDB" Source="$(var.SourceDir)\System.Formats.Asn1.dll" KeyPath="yes" />
+ </Component>
<Component Id="owcB1B1D5CD5E0525947D05C2530DDA9E96" Guid="43664615-fe63-0e97-cbd8-b599204e0079">
<File Id="owfB1B1D5CD5E0525947D05C2530DDA9E96" Source="$(var.SourceDir)\System.Globalization.Calendars.dll" KeyPath="yes" />
</Component>
@@ -500,6 +494,9 @@
<Component Id="owcB279CD308898A9D5EFCE20D942FB230A" Guid="d983e56b-4031-2045-c1d1-5c2c9be7f564">
<File Id="owfB279CD308898A9D5EFCE20D942FB230A" Source="$(var.SourceDir)\System.Net.Http.dll" KeyPath="yes" />
</Component>
+ <Component Id="owc04A3301773938E6C6F757EFA100024F5" Guid="81d28415-e7aa-2966-544f-d5c2c25450d2">
+ <File Id="owf04A3301773938E6C6F757EFA100024F5" Source="$(var.SourceDir)\System.Net.Http.Json.dll" KeyPath="yes" />
+ </Component>
<Component Id="owc1FF5D15A5BDB1E29D2F481CDF957855A" Guid="c020ff07-701e-3695-86ec-a8484a08f16e">
<File Id="owf1FF5D15A5BDB1E29D2F481CDF957855A" Source="$(var.SourceDir)\System.Net.HttpListener.dll" KeyPath="yes" />
</Component>
@@ -572,9 +569,6 @@
<Component Id="owc784C102062996900FDDFF7A76CCC6BEA" Guid="c0228705-c52a-22a0-45e2-aeea6d7780a0">
<File Id="owf784C102062996900FDDFF7A76CCC6BEA" Source="$(var.SourceDir)\System.Reactive.dll" KeyPath="yes" />
</Component>
- <Component Id="owc659AD7BAD61D23D57810ADB460B5F787" Guid="24c8a539-f0c6-e966-b480-914f2f28b932">
- <File Id="owf659AD7BAD61D23D57810ADB460B5F787" Source="$(var.SourceDir)\System.Reactive.xml" KeyPath="yes" />
- </Component>
<Component Id="owc3113B8ED121A12C42915591BDB9D1908" Guid="13c39e0e-6a8a-6a6c-e70c-464a51dafcee">
<File Id="owf3113B8ED121A12C42915591BDB9D1908" Source="$(var.SourceDir)\System.Reflection.DispatchProxy.dll" KeyPath="yes" />
</Component>
@@ -632,9 +626,6 @@
<Component Id="owc142B759C8549F55E22EAD18CF214B77E" Guid="4e3b53dc-2b0a-9a0f-3956-f346f8b8ebd1">
<File Id="owf142B759C8549F55E22EAD18CF214B77E" Source="$(var.SourceDir)\System.Runtime.InteropServices.RuntimeInformation.dll" KeyPath="yes" />
</Component>
- <Component Id="owc3CA4B705071A600D347C4DD0486AD3BC" Guid="674c2a3e-6a95-d20d-42ac-659354ea5bab">
- <File Id="owf3CA4B705071A600D347C4DD0486AD3BC" Source="$(var.SourceDir)\System.Runtime.InteropServices.WindowsRuntime.dll" KeyPath="yes" />
- </Component>
<Component Id="owc9A9C9FBB06116601690A51EB023CA483" Guid="97913f47-cd75-9dbc-af5e-b2268ffe41f5">
<File Id="owf9A9C9FBB06116601690A51EB023CA483" Source="$(var.SourceDir)\System.Runtime.Intrinsics.dll" KeyPath="yes" />
</Component>
@@ -659,12 +650,6 @@
<Component Id="owcAFF0D75B678B8C04204C4F4BE10DC63D" Guid="962d7785-2bdd-31be-55cc-1ef55fb340f1">
<File Id="owfAFF0D75B678B8C04204C4F4BE10DC63D" Source="$(var.SourceDir)\System.Runtime.Serialization.Xml.dll" KeyPath="yes" />
</Component>
- <Component Id="owc1097CF703360A42944AC2B35A6F9BFE6" Guid="ea57ed6c-2897-5e3e-3123-8d98b98dbd10">
- <File Id="owf1097CF703360A42944AC2B35A6F9BFE6" Source="$(var.SourceDir)\System.Runtime.WindowsRuntime.dll" KeyPath="yes" />
- </Component>
- <Component Id="owcC7317F397E5D1F6B51A848BA3C5670EC" Guid="25c92170-3bc9-34cc-b0f2-03c2d0974abf">
- <File Id="owfC7317F397E5D1F6B51A848BA3C5670EC" Source="$(var.SourceDir)\System.Runtime.WindowsRuntime.UI.Xaml.dll" KeyPath="yes" />
- </Component>
<Component Id="owcE16E2DDD34171BE60D55C8D38E64DFED" Guid="5c9c49b2-ab60-2ada-7c5b-ccaf917fec9e">
<File Id="owfE16E2DDD34171BE60D55C8D38E64DFED" Source="$(var.SourceDir)\System.Security.AccessControl.dll" KeyPath="yes" />
</Component>
@@ -825,10 +810,7 @@
<File Id="owfF7BAF7350DBF794C7897F55847D0B0A6" Source="$(var.SourceDir)\UVtools.dll" KeyPath="yes" />
</Component>
<Component Id="owc21B8C8C0F69E3CD1398B9A0674DF07BD" Guid="1e472543-e636-904d-da5b-cddaac2d36b1">
- <File Id="owf21B8C8C0F69E3CD1398B9A0674DF07BD" Source="$(var.SourceDir)\UVtools.exe" KeyPath="yes">
- <Shortcut Id="sc218BF75801887335D1B30BAFB94BA631" Name="UVtools" Directory="scd220707349D4C8FA275285514283F3E2A" Description="MSLA/DLP, file analysis, repair, conversion and manipulation" WorkingDirectory="MergeRedirectFolder" />
- <Shortcut Id="sc6327849DA5C02D2396E91B8B1892E03C" Name="UVtools" Directory="DesktopFolder" WorkingDirectory="MergeRedirectFolder" Description="MSLA/DLP, file analysis, repair, conversion and manipulation" />
- </File>
+ <File Id="owf21B8C8C0F69E3CD1398B9A0674DF07BD" Source="$(var.SourceDir)\UVtools.exe" KeyPath="yes" />
</Component>
<Component Id="owc0C1077539E2B54F2460D2C0993613EFB" Guid="fc6ae2db-2865-9eb4-5753-4f2f941fdf16">
<File Id="owf0C1077539E2B54F2460D2C0993613EFB" Source="$(var.SourceDir)\UVtools.pdb" KeyPath="yes" />
@@ -842,9 +824,6 @@
<Component Id="owc9AF7D2C52438BF24F3FA3132D7C0E6A9" Guid="892f88e7-cf94-fad1-08d3-b26c1b5913d3">
<File Id="owf9AF7D2C52438BF24F3FA3132D7C0E6A9" Source="$(var.SourceDir)\WindowsBase.dll" KeyPath="yes" />
</Component>
- <Component Id="owcC8A7C6BC010A46EC5B595131EDD0214B" Guid="e125d364-209e-4a33-51df-5b1fff92ce4c">
- <File Id="owfC8A7C6BC010A46EC5B595131EDD0214B" Source="$(var.SourceDir)\winrt.runtime.dll" KeyPath="yes" />
- </Component>
<Directory Id="owd9F9F77C9E3C3E4DA718760164D5303B8" Name="Assets">
<Directory Id="owd09EA423812736FC896B5C82AE8837139" Name="PrusaSlicer">
<Directory Id="owd44E5BD2F3F078D3F66FB8FE12300C21C" Name="printer">
diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs
index 064c8ce..fe21cb8 100644
--- a/UVtools.WPF/App.axaml.cs
+++ b/UVtools.WPF/App.axaml.cs
@@ -26,6 +26,7 @@ using UVtools.Core;
using UVtools.Core.FileFormats;
using UVtools.WPF.Extensions;
using UVtools.WPF.Structures;
+using Size = System.Drawing.Size;
namespace UVtools.WPF
{
@@ -37,6 +38,8 @@ namespace UVtools.WPF
public static AppVersionChecker VersionChecker { get; } = new AppVersionChecker();
+ public static Size MaxWindowSize;
+
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -65,7 +68,7 @@ namespace UVtools.WPF
break;
}
}
-
+
MainWindow = new MainWindow();
try
diff --git a/UVtools.WPF/Assets/Icons/chart-pie-16x16.png b/UVtools.WPF/Assets/Icons/chart-pie-16x16.png
new file mode 100644
index 0000000..8b23bf4
--- /dev/null
+++ b/UVtools.WPF/Assets/Icons/chart-pie-16x16.png
Binary files differ
diff --git a/UVtools.WPF/Assets/Icons/cubes-16x16.png b/UVtools.WPF/Assets/Icons/cubes-16x16.png
new file mode 100644
index 0000000..bfef992
--- /dev/null
+++ b/UVtools.WPF/Assets/Icons/cubes-16x16.png
Binary files differ
diff --git a/UVtools.WPF/Assets/Icons/elephant-foot-16x16.png b/UVtools.WPF/Assets/Icons/elephant-foot-16x16.png
new file mode 100644
index 0000000..bdf1b4e
--- /dev/null
+++ b/UVtools.WPF/Assets/Icons/elephant-foot-16x16.png
Binary files differ
diff --git a/UVtools.WPF/Controls/AdvancedImageBox.cs.old b/UVtools.WPF/Controls/AdvancedImageBox.cs.old
deleted file mode 100644
index 56e064e..0000000
--- a/UVtools.WPF/Controls/AdvancedImageBox.cs.old
+++ /dev/null
@@ -1,1891 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Drawing;
-using System.Runtime.CompilerServices;
-using System.Timers;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Input;
-using Avalonia.Media;
-using Avalonia.Styling;
-using Avalonia.Threading;
-using UVtools.Core.Extensions;
-using UVtools.WPF.Extensions;
-using Bitmap = Avalonia.Media.Imaging.Bitmap;
-using Brushes = Avalonia.Media.Brushes;
-using Color = Avalonia.Media.Color;
-using Pen = Avalonia.Media.Pen;
-using Point = Avalonia.Point;
-using Size = Avalonia.Size;
-
-
-namespace UVtools.WPF.Controls
-{
- public class AdvancedImageBox : ScrollViewer, IStyleable, INotifyPropertyChanged
- {
- #region Bindable Base
- /// <summary>
- /// Multicast event for property change notifications.
- /// </summary>
- private PropertyChangedEventHandler _propertyChanged;
- private List<string> events = new List<string>();
-
- public event PropertyChangedEventHandler PropertyChanged
- {
- add { _propertyChanged += value; events.Add("added"); }
- remove { _propertyChanged -= value; events.Add("removed"); }
- }
- protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
- {
- if (EqualityComparer<T>.Default.Equals(field, value)) return false;
- field = value;
- RaisePropertyChanged(propertyName);
- return true;
- }
-
-
- protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
- {
- }
-
- /// <summary>
- /// Notifies listeners that a property value has changed.
- /// </summary>
- /// <param name="propertyName">
- /// Name of the property used to notify listeners. This
- /// value is optional and can be provided automatically when invoked from compilers
- /// that support <see cref="CallerMemberNameAttribute" />.
- /// </param>
- protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
- {
- var e = new PropertyChangedEventArgs(propertyName);
- OnPropertyChanged(e);
- _propertyChanged?.Invoke(this, e);
- }
- #endregion
-
- #region Sub Classes
-
- /// <summary>
- /// Represents available levels of zoom in an <see cref="ImageBox"/> control
- /// </summary>
- public class ZoomLevelCollection : IList<int>
- {
- #region Public Constructors
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ZoomLevelCollection"/> class.
- /// </summary>
- public ZoomLevelCollection()
- {
- List = new SortedList<int, int>();
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ZoomLevelCollection"/> class.
- /// </summary>
- /// <param name="collection">The default values to populate the collection with.</param>
- /// <exception cref="System.ArgumentNullException">Thrown if the <c>collection</c> parameter is null</exception>
- public ZoomLevelCollection(IEnumerable<int> collection)
- : this()
- {
- if (collection == null)
- {
- throw new ArgumentNullException(nameof(collection));
- }
-
- AddRange(collection);
- }
-
- #endregion
-
- #region Public Class Properties
-
- /// <summary>
- /// Returns the default zoom levels
- /// </summary>
- public static ZoomLevelCollection Default
- {
- get
- {
- return new ZoomLevelCollection(new[]
- {
- 7, 10, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600, 3200
- });
- }
- }
-
- #endregion
-
- #region Public Properties
-
- /// <summary>
- /// Gets the number of elements contained in the <see cref="ZoomLevelCollection" />.
- /// </summary>
- /// <returns>
- /// The number of elements contained in the <see cref="ZoomLevelCollection" />.
- /// </returns>
- public int Count => List.Count;
-
- /// <summary>
- /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1" /> is read-only.
- /// </summary>
- /// <value><c>true</c> if this instance is read only; otherwise, <c>false</c>.</value>
- /// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1" /> is read-only; otherwise, false.
- /// </returns>
- public bool IsReadOnly => false;
-
- /// <summary>
- /// Gets or sets the zoom level at the specified index.
- /// </summary>
- /// <param name="index">The index.</param>
- public int this[int index]
- {
- get => List.Values[index];
- set
- {
- List.RemoveAt(index);
- Add(value);
- }
- }
-
- #endregion
-
- #region Protected Properties
-
- /// <summary>
- /// Gets or sets the backing list.
- /// </summary>
- protected SortedList<int, int> List { get; set; }
-
- #endregion
-
- #region Public Members
-
- /// <summary>
- /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1" />.
- /// </summary>
- /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
- public void Add(int item)
- {
- List.Add(item, item);
- }
-
- /// <summary>
- /// Adds a range of items to the <see cref="ZoomLevelCollection"/>.
- /// </summary>
- /// <param name="collection">The items to add to the collection.</param>
- /// <exception cref="System.ArgumentNullException">Thrown if the <c>collection</c> parameter is null.</exception>
- public void AddRange(IEnumerable<int> collection)
- {
- if (collection == null)
- {
- throw new ArgumentNullException(nameof(collection));
- }
-
- foreach (int value in collection)
- {
- Add(value);
- }
- }
-
- /// <summary>
- /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1" />.
- /// </summary>
- public void Clear()
- {
- List.Clear();
- }
-
- /// <summary>
- /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1" /> contains a specific value.
- /// </summary>
- /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
- /// <returns>true if <paramref name="item" /> is found in the <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false.</returns>
- public bool Contains(int item)
- {
- return List.ContainsKey(item);
- }
-
- /// <summary>
- /// Copies a range of elements this collection into a destination <see cref="Array"/>.
- /// </summary>
- /// <param name="array">The <see cref="Array"/> that receives the data.</param>
- /// <param name="arrayIndex">A 64-bit integer that represents the index in the <see cref="Array"/> at which storing begins.</param>
- public void CopyTo(int[] array, int arrayIndex)
- {
- for (int i = 0; i < this.Count; i++)
- {
- array[arrayIndex + i] = this.List.Values[i];
- }
- }
-
- /// <summary>
- /// Finds the index of a zoom level matching or nearest to the specified value.
- /// </summary>
- /// <param name="zoomLevel">The zoom level.</param>
- public int FindNearest(int zoomLevel)
- {
- int nearestValue = this.List.Values[0];
- int nearestDifference = Math.Abs(nearestValue - zoomLevel);
- for (int i = 1; i < Count; i++)
- {
- int value = List.Values[i];
- int difference = Math.Abs(value - zoomLevel);
- if (difference < nearestDifference)
- {
- nearestValue = value;
- nearestDifference = difference;
- }
- }
- return nearestValue;
- }
-
- /// <summary>
- /// Returns an enumerator that iterates through the collection.
- /// </summary>
- /// <returns>A <see cref="T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.</returns>
- public IEnumerator<int> GetEnumerator()
- {
- return List.Values.GetEnumerator();
- }
-
- /// <summary>
- /// Determines the index of a specific item in the <see cref="T:System.Collections.Generic.IList`1" />.
- /// </summary>
- /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.IList`1" />.</param>
- /// <returns>The index of <paramref name="item" /> if found in the list; otherwise, -1.</returns>
- public int IndexOf(int item)
- {
- return List.IndexOfKey(item);
- }
-
- /// <summary>
- /// Not implemented.
- /// </summary>
- /// <param name="index">The index.</param>
- /// <param name="item">The item.</param>
- /// <exception cref="System.NotImplementedException">Not implemented</exception>
- public void Insert(int index, int item)
- {
- throw new NotImplementedException();
- }
-
- /// <summary>
- /// Returns the next increased zoom level for the given current zoom.
- /// </summary>
- /// <param name="zoomLevel">The current zoom level.</param>
- /// <returns>The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom.</returns>
- public int NextZoom(int zoomLevel)
- {
- var index = IndexOf(this.FindNearest(zoomLevel));
- if (index < this.Count - 1)
- {
- index++;
- }
-
- return this[index];
- }
-
- /// <summary>
- /// Returns the next decreased zoom level for the given current zoom.
- /// </summary>
- /// <param name="zoomLevel">The current zoom level.</param>
- /// <returns>The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom.</returns>
- public int PreviousZoom(int zoomLevel)
- {
- var index = IndexOf(FindNearest(zoomLevel));
- if (index > 0)
- {
- index--;
- }
-
- return this[index];
- }
-
- /// <summary>
- /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1" />.
- /// </summary>
- /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1" />.</param>
- /// <returns>true if <paramref name="item" /> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false. This method also returns false if <paramref name="item" /> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1" />.</returns>
- public bool Remove(int item)
- {
- return List.Remove(item);
- }
-
- /// <summary>
- /// Removes the element at the specified index of the <see cref="ZoomLevelCollection"/>.
- /// </summary>
- /// <param name="index">The zero-based index of the element to remove.</param>
- public void RemoveAt(int index)
- {
- List.RemoveAt(index);
- }
-
- /// <summary>
- /// Copies the elements of the <see cref="ZoomLevelCollection"/> to a new array.
- /// </summary>
- /// <returns>An array containing copies of the elements of the <see cref="ZoomLevelCollection"/>.</returns>
- public int[] ToArray()
- {
- int[] results;
-
- results = new int[Count];
- CopyTo(results, 0);
-
- return results;
- }
-
- #endregion
-
- #region IList<int> Members
-
- /// <summary>
- /// Returns an enumerator that iterates through a collection.
- /// </summary>
- /// <returns>An <see cref="ZoomLevelCollection" /> object that can be used to iterate through the collection.</returns>
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
-
- #endregion
- }
-
- #endregion
-
- #region Enums
-
- /// <summary>
- /// Determines the sizing mode of an image hosted in an <see cref="AdvancedImageBox" /> control.
- /// </summary>
- public enum SizeModes : byte
- {
- /// <summary>
- /// The image is disiplayed according to current zoom and scroll properties.
- /// </summary>
- Normal,
-
- /// <summary>
- /// The image is stretched to fill the client area of the control.
- /// </summary>
- Stretch,
-
- /// <summary>
- /// The image is stretched to fill as much of the client area of the control as possible, whilst retaining the same aspect ratio for the width and height.
- /// </summary>
- //Fit
- }
-
- [Flags]
- public enum MouseButtons : byte
- {
- None = 0,
- LeftButton = 1,
- MiddleButton = 2,
- RightButton = 4
- }
-
- /// <summary>
- /// Describes the zoom action occuring
- /// </summary>
- [Flags]
- public enum ZoomActions : byte
- {
- /// <summary>
- /// No action.
- /// </summary>
- None = 0,
-
- /// <summary>
- /// The control is increasing the zoom.
- /// </summary>
- ZoomIn = 1,
-
- /// <summary>
- /// The control is decreasing the zoom.
- /// </summary>
- ZoomOut = 2,
-
- /// <summary>
- /// The control zoom was reset.
- /// </summary>
- ActualSize = 4
- }
-
- public enum SelectionModes
- {
- /// <summary>
- /// No selection.
- /// </summary>
- None,
-
- /// <summary>
- /// Rectangle selection.
- /// </summary>
- Rectangle,
-
- /// <summary>
- /// Zoom selection.
- /// </summary>
- Zoom
- }
-
- #endregion
-
- #region Constants
- public static readonly int MinZoom = 10;
- public static readonly int MaxZoom = 3500;
- #endregion
-
- public bool CanRender
- {
- get => _canRender;
- set
- {
- if(!RaiseAndSetIfChanged(ref _canRender, value)) return;
- if(_canRender) TriggerRender();
- }
- }
-
- /// <summary>
- /// Gets or sets the basic cell size
- /// </summary>
- public byte GridCellSize
- {
- get => _gridCellSize;
- set => RaiseAndSetIfChanged(ref _gridCellSize, value);
- }
-
- /// <summary>
- /// Gets or sets the color used to create the checkerboard style background
- /// </summary>
- public ISolidColorBrush GridColor
- {
- get => _gridColor;
- set => RaiseAndSetIfChanged(ref _gridColor, value);
- }
-
- /// <summary>
- /// Gets or sets the color used to create the checkerboard style background
- /// </summary>
- public ISolidColorBrush GridColorAlternate
- {
- get => _gridColorAlternate;
- set => RaiseAndSetIfChanged(ref _gridColorAlternate, value);
- }
-
- /// <summary>
- /// Gets or sets the image to be displayed
- /// </summary>
- public Bitmap Image
- {
- get => _image;
- set
- {
- if (!RaiseAndSetIfChanged(ref _image, value)) return;
-
- //SelectNone();
- UpdateViewPort();
- TriggerRender();
- }
- }
-
- public bool IsHorizontalBarVisible
- {
- get
- {
- if (Image is null) return false;
- return ScaledImageWidth > Viewport.Width;
- }
- }
-
- public bool IsVerticalBarVisible
- {
- get
- {
- if (Image is null) return false;
- return ScaledImageHeight > Viewport.Height;
- }
- }
-
- /// <summary>
- /// Gets or sets if the checkerboard background should be displayed
- /// </summary>
- public bool ShowGrid
- {
- get => _showGrid;
- set => RaiseAndSetIfChanged(ref _showGrid, value);
- }
-
- public bool IsPanning
- {
- get => _isPanning;
- protected set
- {
- if (!RaiseAndSetIfChanged(ref _isPanning, value)) return;
- _startScrollPosition = Offset;
-
- if (value)
- {
- Cursor = new Cursor(StandardCursorType.SizeAll);
- //this.OnPanStart(EventArgs.Empty);
- }
- else
- {
- Cursor = Cursor.Default;
- //this.OnPanEnd(EventArgs.Empty);
- }
- }
- }
-
- public bool IsSelecting
- {
- get => _isSelecting;
- protected set => RaiseAndSetIfChanged(ref _isSelecting, value);
- }
-
- public Point CenterPoint
- {
- get
- {
- var viewport = GetImageViewPort();
- return new Point( (viewport.Width / 2), viewport.Height / 2);
- }
- }
-
- public bool AutoPan
- {
- get => _autoPan;
- set => RaiseAndSetIfChanged(ref _autoPan, value);
- }
-
- public MouseButtons PanWithMouseButtons
- {
- get => _panWithMouseButtons;
- set => RaiseAndSetIfChanged(ref _panWithMouseButtons, value);
- }
-
- public bool PanWithArrows
- {
- get => _panWithArrows;
- set => RaiseAndSetIfChanged(ref _panWithArrows, value);
- }
-
- public MouseButtons SelectWithMouseButtons
- {
- get => _selectWithMouseButtons;
- set => RaiseAndSetIfChanged(ref _selectWithMouseButtons, value);
- }
-
- public bool InvertMouse
- {
- get => _invertMouse;
- set => RaiseAndSetIfChanged(ref _invertMouse, value);
- }
-
- public bool AutoCenter
- {
- get => _autoCenter;
- set => RaiseAndSetIfChanged(ref _autoCenter, value);
- }
-
- public SizeModes SizeMode
- {
- get => _sizeMode;
- set => RaiseAndSetIfChanged(ref _sizeMode, value);
- }
-
- private bool _allowZoom = true;
- public virtual bool AllowZoom
- {
- get => _allowZoom;
- set => RaiseAndSetIfChanged(ref _allowZoom, value);
- }
-
- ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default;
- /// <summary>
- /// Gets or sets the zoom levels.
- /// </summary>
- /// <value>The zoom levels.</value>
- public virtual ZoomLevelCollection ZoomLevels
- {
- get => _zoomLevels;
- set => RaiseAndSetIfChanged(ref _zoomLevels, value);
- }
-
- private int _oldZoom = 100;
- private int _zoom = 100;
-
- /// <summary>
- /// Gets or sets the zoom.
- /// </summary>
- /// <value>The zoom.</value>
- public virtual int OldZoom
- {
- get => _oldZoom;
- set => RaiseAndSetIfChanged(ref _oldZoom, value);
- }
-
- /// <summary>
- /// Gets or sets the zoom.
- /// </summary>
- /// <value>The zoom.</value>
- public virtual int Zoom
- {
- get => _zoom;
- set
- {
- var newZoom = value.Clamp(MinZoom, MaxZoom);
-
- if (_zoom == newZoom) return;
- var previousZoom = _zoom;
- _zoom = newZoom;
- UpdateViewPort();
- TriggerRender();
-
- OldZoom = previousZoom;
- RaisePropertyChanged(nameof(Zoom));
- //this.OnZoomChanged(EventArgs.Empty);
- //this.OnZoomed(new ImageBoxZoomEventArgs(actions, source, previousZoom, this.Zoom));
- //SetZoom(value, value > Zoom ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut);
- }
- }
-
- public virtual bool IsActualSize => Zoom == 100;
-
- private ISolidColorBrush _pixelGridColor = Brushes.DimGray;
- /// <summary>
- /// Gets or sets the color of the pixel grid.
- /// </summary>
- /// <value>The color of the pixel grid.</value>
- public virtual ISolidColorBrush PixelGridColor
- {
- get => _pixelGridColor;
- set => RaiseAndSetIfChanged(ref _pixelGridColor, value);
- }
-
- private int _pixelGridThreshold = 5;
- /// <summary>
- /// Gets or sets the minimum size of zoomed pixel's before the pixel grid will be drawn
- /// </summary>
- /// <value>The pixel grid threshold.</value>
-
- public virtual int PixelGridThreshold
- {
- get => _pixelGridThreshold;
- set => RaiseAndSetIfChanged(ref _pixelGridThreshold, value);
- }
-
- public SelectionModes SelectionMode
- {
- get => _selectionMode;
- set => RaiseAndSetIfChanged(ref _selectionMode, value);
- }
-
- public ISolidColorBrush SelectionColor
- {
- get => _selectionColor;
- set => RaiseAndSetIfChanged(ref _selectionColor, value);
- }
-
- public Rect SelectionRegion
- {
- get => _selectionRegion;
- set
- {
- if(!RaiseAndSetIfChanged(ref _selectionRegion, value)) return;
- TriggerRender();
- RaisePropertyChanged(nameof(HaveSelection));
- }
- }
-
- public bool HaveSelection => !SelectionRegion.IsEmpty;
-
-
- //Our render target we compile everything to and present to the user
- private Point _startMousePosition;
- private Vector _startScrollPosition;
- private bool _isPanning;
- private bool _isSelecting;
- private Bitmap _image;
- private byte _gridCellSize;
- private ISolidColorBrush _gridColor = Brushes.Gainsboro;
- private ISolidColorBrush _gridColorAlternate = Brushes.White;
- private bool _showGrid = true;
- private bool _autoPan = true;
- private MouseButtons _panWithMouseButtons = MouseButtons.LeftButton | MouseButtons.MiddleButton | MouseButtons.RightButton;
- private bool _panWithArrows = true;
- private MouseButtons _selectWithMouseButtons = MouseButtons.LeftButton | MouseButtons.RightButton;
- private bool _invertMouse = false;
- private bool _autoCenter = true;
- private SizeModes _sizeMode = SizeModes.Normal;
- private ISolidColorBrush _selectionColor = new SolidColorBrush(new Color(127, 0, 128, 255));
- private Rect _selectionRegion = Rect.Empty;
- private SelectionModes _selectionMode = SelectionModes.None;
- private bool _canRender = true;
-
-
- public ContentControl FillContainer { get; } = new ContentControl
- {
- Background = Brushes.Transparent
- };
-
- public ContentControl SizedContainer { get; private set; } = new ContentControl
- {
-
- };
-
- Type IStyleable.StyleKey => typeof(ScrollViewer);
-
- public AdvancedImageBox()
- {
- Content = FillContainer;
- FillContainer.Content = SizedContainer;
- FillContainer.PointerWheelChanged += FillContainerOnPointerWheelChanged;
-
- HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
-
- //Container.PointerMoved += ScrollViewerOnPointerMoved;
- //Container.PointerPressed += ScrollViewerOnPointerPressed;
- //Container.PointerReleased += ScrollViewerOnPointerReleased;
- }
-
- protected override void OnScrollChanged(ScrollChangedEventArgs e)
- {
- Debug.WriteLine($"ViewportDelta: {e.ViewportDelta} | OffsetDelta: {e.OffsetDelta} | ExtentDelta: {e.ExtentDelta}");
- if (!e.ViewportDelta.IsDefault)
- {
- UpdateViewPort();
- }
-
- TriggerRender();
-
- base.OnScrollChanged(e);
- }
-
- private void FillContainerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
- {
- Debug.WriteLine("mouse whell");
- e.Handled = true;
- if (Image is null) return;
- if (AllowZoom && SizeMode == SizeModes.Normal)
- {
- // The MouseWheel event can contain multiple "spins" of the wheel so we need to adjust accordingly
- //double spins = Math.Abs(e.Delta.Y);
- //Debug.WriteLine(e.GetPosition(this));
- // TODO: Really should update the source method to handle multiple increments rather than calling it multiple times
- /*for (int i = 0; i < spins; i++)
- {*/
- ProcessMouseZoom(e.Delta.Y > 0, e.GetPosition(this));
- //}
- }
- }
-
- public void TriggerRender()
- {
- if (!_canRender) return;
- InvalidateVisual();
- }
-
- private void ProcessMouseZoom(bool isZoomIn, Point cursorPosition)
- => PerformZoom(isZoomIn ? ZoomActions.ZoomIn : ZoomActions.ZoomOut, true, cursorPosition);
-
- /// <summary>
- /// Returns an appropriate zoom level based on the specified action, relative to the current zoom level.
- /// </summary>
- /// <param name="action">The action to determine the zoom level.</param>
- /// <exception cref="System.ArgumentOutOfRangeException">Thrown if an unsupported action is specified.</exception>
- private int GetZoomLevel(ZoomActions action)
- {
- int result;
-
- switch (action)
- {
- case ZoomActions.None:
- result = Zoom;
- break;
- case ZoomActions.ZoomIn:
- result = ZoomLevels.NextZoom(Zoom);
- break;
- case ZoomActions.ZoomOut:
- result = ZoomLevels.PreviousZoom(Zoom);
- break;
- case ZoomActions.ActualSize:
- result = 100;
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(action));
- }
-
- return result;
- }
-
- /// <summary>
- /// Resets the <see cref="SizeModes"/> property whilsts retaining the original <see cref="Zoom"/>.
- /// </summary>
- protected void RestoreSizeMode()
- {
- if (SizeMode != SizeModes.Normal)
- {
- var previousZoom = Zoom;
- SizeMode = SizeModes.Normal;
- Zoom = previousZoom; // Stop the zoom getting reset to 100% before calculating the new zoom
- }
- }
-
- private void PerformZoom(ZoomActions action, bool preservePosition)
- => PerformZoom(action, preservePosition, CenterPoint);
-
- private void PerformZoom(ZoomActions action, bool preservePosition, Point relativePoint)
- {
- Point currentPixel = PointToImage(relativePoint);
- int currentZoom = Zoom;
- int newZoom = GetZoomLevel(action);
-
- if (preservePosition && Zoom != currentZoom)
- CanRender = false;
-
- RestoreSizeMode();
- Zoom = newZoom;
-
- if (preservePosition && Zoom != currentZoom)
- {
- ScrollTo(currentPixel, relativePoint);
- }
- }
-
- /// <summary>
- /// Determines whether the specified point is located within the image view port
- /// </summary>
- /// <param name="point">The point.</param>
- /// <returns>
- /// <c>true</c> if the specified point is located within the image view port; otherwise, <c>false</c>.
- /// </returns>
- public virtual bool IsPointInImage(Point point)
- => GetImageViewPort().Contains(point);
-
- /// <summary>
- /// Determines whether the specified point is located within the image view port
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to check.</param>
- /// <param name="y">The Y co-ordinate of the point to check.</param>
- /// <returns>
- /// <c>true</c> if the specified point is located within the image view port; otherwise, <c>false</c>.
- /// </returns>
- public bool IsPointInImage(int x, int y)
- => IsPointInImage(new Point(x, y));
-
- /// <summary>
- /// Determines whether the specified point is located within the image view port
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to check.</param>
- /// <param name="y">The Y co-ordinate of the point to check.</param>
- /// <returns>
- /// <c>true</c> if the specified point is located within the image view port; otherwise, <c>false</c>.
- /// </returns>
- public bool IsPointInImage(double x, double y)
- => IsPointInImage(new Point(x, y));
-
- /// <summary>
- /// Converts the given client size point to represent a coordinate on the source image.
- /// </summary>
- /// <param name="point">The source point.</param>
- /// <returns><c>Point.Empty</c> if the point could not be matched to the source image, otherwise the new translated point</returns>
- public Point PointToImage(Point point)
- => PointToImage(point, false);
-
- /// <summary>
- /// Converts the given client size point to represent a coordinate on the source image.
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to convert.</param>
- /// <param name="y">The Y co-ordinate of the point to convert.</param>
- /// <param name="fitToBounds">
- /// if set to <c>true</c> and the point is outside the bounds of the source image, it will be mapped to the nearest edge.
- /// </param>
- /// <returns><c>Point.Empty</c> if the point could not be matched to the source image, otherwise the new translated point</returns>
- public Point PointToImage(double x, double y, bool fitToBounds = false)
- => PointToImage(x, y, fitToBounds);
-
- /// <summary>
- /// Converts the given client size point to represent a coordinate on the source image.
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to convert.</param>
- /// <param name="y">The Y co-ordinate of the point to convert.</param>
- /// <param name="fitToBounds">
- /// if set to <c>true</c> and the point is outside the bounds of the source image, it will be mapped to the nearest edge.
- /// </param>
- /// <returns><c>Point.Empty</c> if the point could not be matched to the source image, otherwise the new translated point</returns>
- public Point PointToImage(int x, int y, bool fitToBounds = false)
- {
- return PointToImage(x, y, fitToBounds);
- }
-
- /// <summary>
- /// Converts the given client size point to represent a coordinate on the source image.
- /// </summary>
- /// <param name="point">The source point.</param>
- /// <param name="fitToBounds">
- /// if set to <c>true</c> and the point is outside the bounds of the source image, it will be mapped to the nearest edge.
- /// </param>
- /// <returns><c>Point.Empty</c> if the point could not be matched to the source image, otherwise the new translated point</returns>
- public virtual Point PointToImage(Point point, bool fitToBounds)
- {
- double x;
- double y;
-
- var viewport = GetImageViewPort();
-
- if (!fitToBounds || viewport.Contains(point))
- {
- x = ((point.X + Offset.X - viewport.X) / ZoomFactor);
- y = ((point.Y + Offset.Y - viewport.Y) / ZoomFactor);
-
- if (fitToBounds)
- {
- x = x.Clamp(0, Image.Size.Width);
- y = y.Clamp(0, Image.Size.Height);
- }
- }
- else
- {
- x = 0; // Return Point.Empty if we couldn't match
- y = 0;
- }
-
- return new Point(x, y);
- }
-
- /// <summary>
- /// Scrolls the control to the given point in the image, offset at the specified display point
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to scroll to.</param>
- /// <param name="y">The Y co-ordinate of the point to scroll to.</param>
- /// <param name="relativeX">The X co-ordinate relative to the <c>x</c> parameter.</param>
- /// <param name="relativeY">The Y co-ordinate relative to the <c>y</c> parameter.</param>
- public void ScrollTo(double x, double y, double relativeX, double relativeY)
- => ScrollTo(new Point(x, y), new Point(relativeX, relativeY));
-
- /// <summary>
- /// Scrolls the control to the given point in the image, offset at the specified display point
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to scroll to.</param>
- /// <param name="y">The Y co-ordinate of the point to scroll to.</param>
- /// <param name="relativeX">The X co-ordinate relative to the <c>x</c> parameter.</param>
- /// <param name="relativeY">The Y co-ordinate relative to the <c>y</c> parameter.</param>
- public void ScrollTo(int x, int y, int relativeX, int relativeY)
- => ScrollTo(new Point(x, y), new Point(relativeX, relativeY));
-
- /// <summary>
- /// Scrolls the control to the given point in the image, offset at the specified display point
- /// </summary>
- /// <param name="imageLocation">The point of the image to attempt to scroll to.</param>
- /// <param name="relativeDisplayPoint">The relative display point to offset scrolling by.</param>
- public virtual void ScrollTo(Point imageLocation, Point relativeDisplayPoint)
- {
- CanRender = false;
- var x = imageLocation.X * ZoomFactor - relativeDisplayPoint.X;
- var y = imageLocation.Y * ZoomFactor - relativeDisplayPoint.Y;
-
-
- _canRender = true;
- Offset = new Vector(x, y);
-
-
-
- DispatcherTimer.RunOnce(() =>
- {
- // TODO: Remove this delay?
- //Debug.WriteLine($"1ms delayed viewport: {Viewport}");
- //CenterAt(new Point(cx, cy));
-
- Offset = new Vector(x, y);
- }, TimeSpan.FromTicks(1), DispatcherPriority.MaxValue);
-
-
- /*Timer timer = new Timer(0.1)
- {
- AutoReset = false,
- };
- timer.Elapsed += (sender, args) =>
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- Offset = new Vector(x, y);
- CanRender = true;
- timer.Dispose();
- });
- };
- timer.Start();*/
-
-
- Debug.WriteLine(
- $"X/Y: {x},{y} | \n" +
- $"Offset: {Offset} | \n" +
- $"ZoomFactor: {ZoomFactor} | \n" +
- $"Image Location: {imageLocation}\n" +
- $"MAX: {HorizontalScrollBarMaximum},{VerticalScrollBarMaximum} \n" +
- $"ViewPort: {Viewport.Width},{Viewport.Height} \n" +
- $"Container: {SizedContainer.Width},{SizedContainer.Height} \n" +
- $"Relative: {relativeDisplayPoint}");
- }
-
- /// <summary>
- /// Zooms into the image
- /// </summary>
- public virtual void ZoomIn()
- => ZoomIn(true);
-
- /// <summary>
- /// Zooms into the image
- /// </summary>
- /// <param name="preservePosition"><c>true</c> if the current scrolling position should be preserved relative to the new zoom level, <c>false</c> to reset.</param>
- public virtual void ZoomIn(bool preservePosition)
- {
- PerformZoom(ZoomActions.ZoomIn, preservePosition);
- }
-
- /// <summary>
- /// Zooms out of the image
- /// </summary>
- public virtual void ZoomOut()
- => ZoomOut(true);
-
- /// <summary>
- /// Zooms out of the image
- /// </summary>
- /// <param name="preservePosition"><c>true</c> if the current scrolling position should be preserved relative to the new zoom level, <c>false</c> to reset.</param>
- public virtual void ZoomOut(bool preservePosition)
- {
- PerformZoom(ZoomActions.ZoomOut, preservePosition);
- }
-
- /// <summary>
- /// Zooms to the maximum size for displaying the entire image within the bounds of the control.
- /// </summary>
- public virtual void ZoomToFit()
- {
- if (Image is null) return;
-
- double zoom;
- double aspectRatio;
-
- if (Image.Size.Width > Image.Size.Height)
- {
- aspectRatio = Viewport.Width / Image.Size.Width;
- zoom = aspectRatio * 100.0;
-
- if (Viewport.Height < Image.Size.Height * zoom / 100.0)
- {
- aspectRatio = Viewport.Height / Image.Size.Height;
- zoom = aspectRatio * 100.0;
- }
- }
- else
- {
- aspectRatio = Viewport.Height / Image.Size.Height;
- zoom = aspectRatio * 100.0;
-
- if (Viewport.Width < Image.Size.Width * zoom / 100.0)
- {
- aspectRatio = Viewport.Width / Image.Size.Width;
- zoom = aspectRatio * 100.0;
- }
- }
-
- Zoom = (int)Math.Round(Math.Floor(zoom));
- }
-
- /// <summary>
- /// Adjusts the view port to fit the given region
- /// </summary>
- /// <param name="x">The X co-ordinate of the selection region.</param>
- /// <param name="y">The Y co-ordinate of the selection region.</param>
- /// <param name="width">The width of the selection region.</param>
- /// <param name="height">The height of the selection region.</param>
- /// <param name="margin">Give a margin to rectangle by a value to zoom-out that pixel value</param>
- public void ZoomToRegion(double x, double y, double width, double height, double margin = 0)
- {
- ZoomToRegion(new Rect(x, y, width, height), margin);
- }
-
- /// <summary>
- /// Adjusts the view port to fit the given region
- /// </summary>
- /// <param name="x">The X co-ordinate of the selection region.</param>
- /// <param name="y">The Y co-ordinate of the selection region.</param>
- /// <param name="width">The width of the selection region.</param>
- /// <param name="height">The height of the selection region.</param>
- /// <param name="margin">Give a margin to rectangle by a value to zoom-out that pixel value</param>
- public void ZoomToRegion(int x, int y, int width, int height, double margin = 0)
- {
- ZoomToRegion(new Rect(x, y, width, height), margin);
- }
-
- /// <summary>
- /// Adjusts the view port to fit the given region
- /// </summary>
- /// <param name="rectangle">The rectangle to fit the view port to.</param>
- /// <param name="margin">Give a margin to rectangle by a value to zoom-out that pixel value</param>
- public virtual void ZoomToRegion(Rectangle rectangle, double margin = 0) => ZoomToRegion(rectangle.ToAvalonia(), margin);
-
- /// <summary>
- /// Adjusts the view port to fit the given region
- /// </summary>
- /// <param name="rectangle">The rectangle to fit the view port to.</param>
- /// <param name="margin">Give a margin to rectangle by a value to zoom-out that pixel value</param>
- public virtual void ZoomToRegion(Rect rectangle, double margin = 0)
- {
- if(margin > 0) rectangle = rectangle.Inflate(margin);
- var ratioX = Viewport.Width / rectangle.Width;
- var ratioY = Viewport.Height / rectangle.Height;
- var zoomFactor = Math.Min(ratioX, ratioY);
- var cx = rectangle.X + rectangle.Width / 2;
- var cy = rectangle.Y + rectangle.Height / 2;
-
- CanRender = false;
- Zoom = (int) (zoomFactor * 100); // This function sets the zoom so viewport will change
- CenterAt(new Point(cx, cy)); // If i call this here, it will move to the wrong position due wrong viewport
- }
-
- /// <summary>
- /// Zooms to current selection region
- /// </summary>
- public void ZoomToSelectionRegion(double margin = 0)
- {
- if (!HaveSelection) return;
- ZoomToRegion(SelectionRegion, margin);
- }
-
- /// <summary>
- /// Centers the given point in the image in the center of the control
- /// </summary>
- /// <param name="imageLocation">The point of the image to attempt to center.</param>
- public virtual void CenterAt(System.Drawing.Point imageLocation)
- => ScrollTo(new Point(imageLocation.X, imageLocation.Y), new Point(Viewport.Width / 2, Viewport.Height / 2));
-
- /// <summary>
- /// Centers the given point in the image in the center of the control
- /// </summary>
- /// <param name="imageLocation">The point of the image to attempt to center.</param>
- public virtual void CenterAt(Point imageLocation)
- => ScrollTo(imageLocation, new Point(Viewport.Width / 2, Viewport.Height / 2));
-
- /// <summary>
- /// Centers the given point in the image in the center of the control
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to center.</param>
- /// <param name="y">The Y co-ordinate of the point to center.</param>
- public void CenterAt(int x, int y)
- => CenterAt(new Point(x, y));
-
- /// <summary>
- /// Centers the given point in the image in the center of the control
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to center.</param>
- /// <param name="y">The Y co-ordinate of the point to center.</param>
- public void CenterAt(double x, double y)
- => CenterAt(new Point(x, y));
-
- /// <summary>
- /// Resets the viewport to show the center of the image.
- /// </summary>
- public virtual void CenterToImage()
- {
- Offset = new Vector(HorizontalScrollBarMaximum / 2, VerticalScrollBarMaximum / 2);
- }
-
- private bool UpdateViewPort()
- {
- if (Image is null)
- {
- SizedContainer.Width = 0;
- SizedContainer.Height = 0;
- return true;
- }
-
- var scaledImageWidth = ScaledImageWidth;
- var scaledImageHeight = ScaledImageHeight;
- var width = scaledImageWidth <= Viewport.Width ? Viewport.Width : scaledImageWidth;
- var height = scaledImageHeight <= Viewport.Height ? Viewport.Height : scaledImageHeight;
-
-
-
- bool changed = false;
- if (SizedContainer.Width != width)
- {
- SizedContainer.Width = width;
- changed = true;
- }
-
- if (SizedContainer.Height != height)
- {
- SizedContainer.Height = height;
- changed = true;
- }
-
- /*if (changed)
- {
- var newContainer = new ContentControl
- {
- Width = width,
- Height = height
- };
- FillContainer.Content = SizedContainer = newContainer;
- Debug.WriteLine($"Updated ViewPort: {DateTime.Now.Ticks}");
- //TriggerRender();
- }*/
-
- return changed;
- }
-
- /// <summary>
- /// Resets the zoom to 100%.
- /// </summary>
- /// <param name="source">The source that initiated the action.</param>
- private void PerformActualSize()
- {
- SizeMode = SizeModes.Normal;
- //SetZoom(100, ImageZoomActions.ActualSize | (Zoom < 100 ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut));
- Zoom = 100;
- }
-
- #region Overrides
-
-
- #endregion
-
- #region Methods
-
- #region Selection
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Point" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to scale.</param>
- /// <param name="y">The Y co-ordinate of the point to scale.</param>
- /// <returns>A <see cref="Point"/> which has been scaled to match the current zoom level</returns>
- public Point GetScaledPoint(int x, int y)
- {
- return GetScaledPoint(new Point(x, y));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Point" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="x">The X co-ordinate of the point to scale.</param>
- /// <param name="y">The Y co-ordinate of the point to scale.</param>
- /// <returns>A <see cref="Point"/> which has been scaled to match the current zoom level</returns>
- public PointF GetScaledPoint(float x, float y)
- {
- return GetScaledPoint(new PointF(x, y));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Point" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="Point"/> to scale.</param>
- /// <returns>A <see cref="Point"/> which has been scaled to match the current zoom level</returns>
- public virtual Point GetScaledPoint(Point source)
- {
- return new Point(source.X * ZoomFactor, source.Y * ZoomFactor);
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.PointF" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="PointF"/> to scale.</param>
- /// <returns>A <see cref="PointF"/> which has been scaled to match the current zoom level</returns>
- public virtual PointF GetScaledPoint(PointF source)
- {
- return new PointF((float)(source.X * this.ZoomFactor), (float)(source.Y * this.ZoomFactor));
- }
-
- /// <summary>
- /// Returns the source rectangle scaled according to the current zoom level
- /// </summary>
- /// <param name="x">The X co-ordinate of the source rectangle.</param>
- /// <param name="y">The Y co-ordinate of the source rectangle.</param>
- /// <param name="width">The width of the rectangle.</param>
- /// <param name="height">The height of the rectangle.</param>
- /// <returns>A <see cref="Rectangle"/> which has been scaled to match the current zoom level</returns>
- public Rect GetScaledRectangle(int x, int y, int width, int height)
- {
- return GetScaledRectangle(new Rect(x, y, width, height));
- }
-
- /// <summary>
- /// Returns the source rectangle scaled according to the current zoom level
- /// </summary>
- /// <param name="x">The X co-ordinate of the source rectangle.</param>
- /// <param name="y">The Y co-ordinate of the source rectangle.</param>
- /// <param name="width">The width of the rectangle.</param>
- /// <param name="height">The height of the rectangle.</param>
- /// <returns>A <see cref="RectangleF"/> which has been scaled to match the current zoom level</returns>
- public RectangleF GetScaledRectangle(float x, float y, float width, float height)
- {
- return GetScaledRectangle(new RectangleF(x, y, width, height));
- }
-
- /// <summary>
- /// Returns the source rectangle scaled according to the current zoom level
- /// </summary>
- /// <param name="location">The location of the source rectangle.</param>
- /// <param name="size">The size of the source rectangle.</param>
- /// <returns>A <see cref="Rectangle"/> which has been scaled to match the current zoom level</returns>
- public Rect GetScaledRectangle(Point location, Size size)
- {
- return GetScaledRectangle(new Rect(location, size));
- }
-
- /// <summary>
- /// Returns the source rectangle scaled according to the current zoom level
- /// </summary>
- /// <param name="location">The location of the source rectangle.</param>
- /// <param name="size">The size of the source rectangle.</param>
- /// <returns>A <see cref="Rectangle"/> which has been scaled to match the current zoom level</returns>
- public RectangleF GetScaledRectangle(PointF location, SizeF size)
- {
- return GetScaledRectangle(new RectangleF(location, size));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Rectangle" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="Rectangle"/> to scale.</param>
- /// <returns>A <see cref="Rectangle"/> which has been scaled to match the current zoom level</returns>
- public virtual Rect GetScaledRectangle(Rect source)
- {
- return new Rect(source.Left * ZoomFactor, source.Top * ZoomFactor, source.Width * ZoomFactor, source.Height * ZoomFactor);
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.RectangleF" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="RectangleF"/> to scale.</param>
- /// <returns>A <see cref="RectangleF"/> which has been scaled to match the current zoom level</returns>
- public virtual RectangleF GetScaledRectangle(RectangleF source)
- {
- return new RectangleF((float)(source.Left * ZoomFactor), (float)(source.Top * ZoomFactor), (float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor));
- }
-
- /// <summary>
- /// Returns the source size scaled according to the current zoom level
- /// </summary>
- /// <param name="width">The width of the size to scale.</param>
- /// <param name="height">The height of the size to scale.</param>
- /// <returns>A <see cref="SizeF"/> which has been resized to match the current zoom level</returns>
- public SizeF GetScaledSize(float width, float height)
- {
- return this.GetScaledSize(new SizeF(width, height));
- }
-
- /// <summary>
- /// Returns the source size scaled according to the current zoom level
- /// </summary>
- /// <param name="width">The width of the size to scale.</param>
- /// <param name="height">The height of the size to scale.</param>
- /// <returns>A <see cref="Size"/> which has been resized to match the current zoom level</returns>
- public Size GetScaledSize(int width, int height)
- {
- return this.GetScaledSize(new Size(width, height));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.SizeF" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="SizeF"/> to scale.</param>
- /// <returns>A <see cref="SizeF"/> which has been resized to match the current zoom level</returns>
- public virtual SizeF GetScaledSize(SizeF source)
- {
- return new SizeF((float)(source.Width * this.ZoomFactor), (float)(source.Height * this.ZoomFactor));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Size" /> scaled according to the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="Size"/> to scale.</param>
- /// <returns>A <see cref="Size"/> which has been resized to match the current zoom level</returns>
- public virtual Size GetScaledSize(Size source)
- {
- return new Size(source.Width * ZoomFactor, source.Height * ZoomFactor);
- }
-
- /// <summary>
- /// Creates a selection region which encompasses the entire image
- /// </summary>
- /// <exception cref="System.InvalidOperationException">Thrown if no image is currently set</exception>
- public virtual void SelectAll()
- {
- if (Image is null) return;
- SelectionRegion = new Rect(0, 0, Image.Size.Width, Image.Size.Height);
- }
-
- /// <summary>
- /// Clears any existing selection region
- /// </summary>
- public virtual void SelectNone()
- {
- SelectionRegion = Rect.Empty;
- }
-
- #endregion
-
- public void LoadImage(string path)
- {
- Image = new Bitmap(path);
- }
-
- public override void Render(DrawingContext context)
- {
- Debug.WriteLine($"Render: {DateTime.Now.Ticks}");
- base.Render(context);
-
- // Draw Grid
- if (ShowGrid)
- {
- // draw the background
- var currentColor = GridColor;
- for (int y = 0; y < Viewport.Height; y += GridCellSize)
- {
- var firstRowColor = currentColor;
- for (int x = 0; x < Viewport.Width; x += GridCellSize)
- {
- context.FillRectangle(currentColor, new Rect(x, y, GridCellSize, GridCellSize));
- currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor;
- }
-
- if (firstRowColor == currentColor)
- currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor;
- }
-
- }
- /*else
- {
- context.FillRectangle(Background, new Rect(0, 0, Viewport.Width, Viewport.Height));
- }*/
-
- if (Image is null) return;
- // Draw iamge
- context.DrawImage(Image,
- GetSourceImageRegion(),
- GetImageViewPort()
- );
- //SkiaContext.SkCanvas.dr
- // Draw pixel grid
- var pixelSize = ZoomFactor;
- if (pixelSize > PixelGridThreshold)
- {
- var viewport = GetImageViewPort();
- var offsetX = Offset.X % pixelSize;
- var offsetY = Offset.Y % pixelSize;
-
- Pen pen = new Pen(PixelGridColor);
- for (double x = viewport.X + pixelSize - offsetX; x < viewport.Right; x += pixelSize)
- {
- context.DrawLine(pen, new Avalonia.Point(x, viewport.X), new Avalonia.Point(x, viewport.Bottom));
- }
-
- for (double y = viewport.Y + pixelSize - offsetY; y < viewport.Bottom; y += pixelSize)
- {
- context.DrawLine(pen, new Avalonia.Point(viewport.Y, y), new Avalonia.Point(viewport.Right, y));
- }
-
- context.DrawRectangle(pen, viewport);
- }
-
- if (!SelectionRegion.IsEmpty)
- {
- var rect = GetOffsetRectangle(SelectionRegion);
- context.FillRectangle(SelectionColor, rect);
- Color solidColor = Color.FromArgb(255, SelectionColor.Color.R, SelectionColor.Color.G, SelectionColor.Color.B);
- context.DrawRectangle(new Pen(solidColor.ToUint32()), rect);
- }
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Point" /> repositioned to include the current image offset and scaled by the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="Point"/> to offset.</param>
- /// <returns>A <see cref="Point"/> which has been repositioned to match the current zoom level and image offset</returns>
- public virtual Point GetOffsetPoint(System.Drawing.Point source)
- {
- var offset = GetOffsetPoint(new Point(source.X, source.Y));
-
- return new Point((int)offset.X, (int)offset.Y);
- }
-
- /// <summary>
- /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level
- /// </summary>
- /// <param name="x">The source X co-ordinate.</param>
- /// <param name="y">The source Y co-ordinate.</param>
- /// <returns>A <see cref="Point"/> which has been repositioned to match the current zoom level and image offset</returns>
- public Point GetOffsetPoint(int x, int y)
- {
- return GetOffsetPoint(new System.Drawing.Point(x, y));
- }
-
- /// <summary>
- /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level
- /// </summary>
- /// <param name="x">The source X co-ordinate.</param>
- /// <param name="y">The source Y co-ordinate.</param>
- /// <returns>A <see cref="Point"/> which has been repositioned to match the current zoom level and image offset</returns>
- public Point GetOffsetPoint(double x, double y)
- {
- return GetOffsetPoint(new Point(x, y));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.PointF" /> repositioned to include the current image offset and scaled by the current zoom level
- /// </summary>
- /// <param name="source">The source <see cref="PointF"/> to offset.</param>
- /// <returns>A <see cref="PointF"/> which has been repositioned to match the current zoom level and image offset</returns>
- public virtual Point GetOffsetPoint(Point source)
- {
- Rect viewport = GetImageViewPort();
- var scaled = GetScaledPoint(source);
- var offsetX = viewport.Left + Offset.X;
- var offsetY = viewport.Top + Offset.Y;
-
- return new Point(scaled.X + offsetX, scaled.Y + offsetY);
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.RectangleF" /> scaled according to the current zoom level and repositioned to include the current image offset
- /// </summary>
- /// <param name="source">The source <see cref="RectangleF"/> to offset.</param>
- /// <returns>A <see cref="RectangleF"/> which has been resized and repositioned to match the current zoom level and image offset</returns>
- public virtual Rect GetOffsetRectangle(Rect source)
- {
- var viewport = GetImageViewPort();
- var scaled = GetScaledRectangle(source);
- var offsetX = viewport.Left - Offset.X;
- var offsetY = viewport.Top - Offset.Y;
-
- return new Rect(new Point(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size);
- }
-
- /// <summary>
- /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset
- /// </summary>
- /// <param name="x">The X co-ordinate of the source rectangle.</param>
- /// <param name="y">The Y co-ordinate of the source rectangle.</param>
- /// <param name="width">The width of the rectangle.</param>
- /// <param name="height">The height of the rectangle.</param>
- /// <returns>A <see cref="Rectangle"/> which has been resized and repositioned to match the current zoom level and image offset</returns>
- public Rectangle GetOffsetRectangle(int x, int y, int width, int height)
- {
- return this.GetOffsetRectangle(new Rectangle(x, y, width, height));
- }
-
- /// <summary>
- /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset
- /// </summary>
- /// <param name="x">The X co-ordinate of the source rectangle.</param>
- /// <param name="y">The Y co-ordinate of the source rectangle.</param>
- /// <param name="width">The width of the rectangle.</param>
- /// <param name="height">The height of the rectangle.</param>
- /// <returns>A <see cref="RectangleF"/> which has been resized and repositioned to match the current zoom level and image offset</returns>
- public Rect GetOffsetRectangle(double x, double y, double width, double height)
- {
- return GetOffsetRectangle(new Rect(x, y, width, height));
- }
-
- /// <summary>
- /// Returns the source <see cref="T:System.Drawing.Rectangle" /> scaled according to the current zoom level and repositioned to include the current image offset
- /// </summary>
- /// <param name="source">The source <see cref="Rectangle"/> to offset.</param>
- /// <returns>A <see cref="Rectangle"/> which has been resized and repositioned to match the current zoom level and image offset</returns>
- public virtual Rectangle GetOffsetRectangle(Rectangle source)
- {
- var viewport = GetImageViewPort();
- var scaled = GetScaledRectangle(source);
- var offsetX = viewport.Left + Offset.X;
- var offsetY = viewport.Top + Offset.Y;
-
- return new Rectangle(new System.Drawing.Point((int) (scaled.Left + offsetX), (int) (scaled.Top + offsetY)),
- new System.Drawing.Size((int) scaled.Size.Width, (int) scaled.Size.Height));
- }
-
- /// <summary>
- /// Fits a given <see cref="T:System.Drawing.Rectangle" /> to match image boundaries
- /// </summary>
- /// <param name="rectangle">The rectangle.</param>
- /// <returns>
- /// A <see cref="T:System.Drawing.Rectangle" /> structure remapped to fit the image boundaries
- /// </returns>
- public Rectangle FitRectangle(Rectangle rectangle)
- {
- if (Image is null) return Rectangle.Empty;
- var x = rectangle.X;
- var y = rectangle.Y;
- var w = rectangle.Width;
- var h = rectangle.Height;
-
- if (x < 0)
- {
- x = 0;
- }
-
- if (y < 0)
- {
- y = 0;
- }
-
- if (x + w > Image.Size.Width)
- {
- w = (int) (Image.Size.Width - x);
- }
-
- if (y + h > Image.Size.Height)
- {
- h = (int) (Image.Size.Height - y);
- }
-
- return new Rectangle(x, y, w, h);
- }
-
- /// <summary>
- /// Fits a given <see cref="T:System.Drawing.RectangleF" /> to match image boundaries
- /// </summary>
- /// <param name="rectangle">The rectangle.</param>
- /// <returns>
- /// A <see cref="T:System.Drawing.RectangleF" /> structure remapped to fit the image boundaries
- /// </returns>
- public Rect FitRectangle(Rect rectangle)
- {
- if(Image is null) return Rect.Empty;
- var x = rectangle.X;
- var y = rectangle.Y;
- var w = rectangle.Width;
- var h = rectangle.Height;
-
- if (x < 0)
- {
- w -= -x;
- x = 0;
- }
-
- if (y < 0)
- {
- h -= -y;
- y = 0;
- }
-
- if (x + w > Image.Size.Width)
- {
- w = Image.Size.Width - x;
- }
-
- if (y + h > Image.Size.Height)
- {
- h = Image.Size.Height - y;
- }
-
- return new Rect(x, y, w, h);
- }
-
- /// <summary>
- /// Gets the source image region.
- /// </summary>
- /// <returns></returns>
- public virtual Rect GetSourceImageRegion()
- {
- if (Image is null) return Rect.Empty;
-
- if (SizeMode != SizeModes.Stretch)
- {
- var viewPort = GetImageViewPort();
- double sourceLeft = (Offset.X / ZoomFactor);
- double sourceTop = (Offset.Y / ZoomFactor);
- double sourceWidth = (viewPort.Width / ZoomFactor);
- double sourceHeight = (viewPort.Height / ZoomFactor);
-
- return new Rect(sourceLeft, sourceTop, sourceWidth, sourceHeight);
- }
-
- return new Rect(0, 0, Image.Size.Width, Image.Size.Height);
- }
-
- /// <summary>
- /// Gets the image view port.
- /// </summary>
- /// <returns></returns>
- public virtual Rect GetImageViewPort()
- {
- if (Viewport.Width == 0 && Viewport.Height == 0) return Rect.Empty;
-
- double xOffset = 0;
- double yOffset = 0;
- double width;
- double height;
-
- if (SizeMode != SizeModes.Stretch)
- {
- if (AutoCenter)
- {
- xOffset = (!IsHorizontalBarVisible ? (Viewport.Width - ScaledImageWidth) / 2 : 0);
- yOffset = (!IsVerticalBarVisible ? (Viewport.Height - ScaledImageHeight) / 2 : 0);
- }
-
- width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), Viewport.Width);
- height = Math.Min(ScaledImageHeight - Math.Abs(Offset.Y), Viewport.Height);
- }
- else
- {
- width = Viewport.Width;
- height = Viewport.Height;
- }
-
- return new Rect(xOffset, yOffset, width, height);
- }
-
- /// <summary>
- /// Gets the width of the scaled image.
- /// </summary>
- /// <value>The width of the scaled image.</value>
- protected virtual double ScaledImageWidth => Image.Size.Width * ZoomFactor;
-
- /// <summary>
- /// Gets the height of the scaled image.
- /// </summary>
- /// <value>The height of the scaled image.</value>
- protected virtual double ScaledImageHeight => Image.Size.Height * ZoomFactor;
-
- public double ZoomFactor => _zoom / 100.0;
-
- protected override void OnPointerPressed(PointerPressedEventArgs e)
- {
- base.OnPointerPressed(e);
- if (e.Handled
- || IsPanning
- || IsSelecting
- || Image is null) return;
-
- var pointer = e.GetCurrentPoint(this);
-
- if (SelectionMode != SelectionModes.None)
- {
- if (!(
- pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 ||
- pointer.Properties.IsMiddleButtonPressed && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 ||
- pointer.Properties.IsRightButtonPressed && (SelectWithMouseButtons & MouseButtons.RightButton) != 0
- )
- ) return;
- IsSelecting = true;
- }
- else
- {
- if (!(
- pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 ||
- pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 ||
- pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0
- )
- || !AutoPan
-
- ) return;
-
- IsPanning = true;
- }
-
- var location = pointer.Position;
-
- if (location.X > Viewport.Width) return;
- if (location.Y > Viewport.Height) return;
- _startMousePosition = location;
- }
-
- protected override void OnPointerReleased(PointerReleasedEventArgs e)
- {
- base.OnPointerReleased(e);
- if (e.Handled) return;
-
- IsPanning = false;
- IsSelecting = false;
- }
-
- protected override void OnPointerMoved(PointerEventArgs e)
- {
- base.OnPointerMoved(e);
- if (e.Handled) return;
-
- if (!IsPanning && !IsSelecting) return;
- var pointer = e.GetCurrentPoint(this);
- var location = pointer.Position;
-
-
- if (IsPanning)
- {
- double x;
- double y;
-
- if (!InvertMouse)
- {
- x = _startScrollPosition.X + (_startMousePosition.X - location.X);
- y = _startScrollPosition.Y + (_startMousePosition.Y - location.Y);
- }
- else
- {
- x = (_startScrollPosition.X - (_startMousePosition.X - location.X));
- y = (_startScrollPosition.Y - (_startMousePosition.Y - location.Y));
- }
-
- Offset = new Vector(x, y);
- }
- else if (IsSelecting)
- {
- double x;
- double y;
- double w;
- double h;
-
- var imageOffset = GetImageViewPort().Position;
-
- if (location.X < _startMousePosition.X)
- {
- x = location.X;
- w = _startMousePosition.X - location.X;
- }
- else
- {
- x = _startMousePosition.X;
- w = location.X - _startMousePosition.X;
- }
-
- if (location.Y < _startMousePosition.Y)
- {
- y = location.Y;
- h = _startMousePosition.Y - location.Y;
- }
- else
- {
- y = _startMousePosition.Y;
- h = location.Y - _startMousePosition.Y;
- }
-
- x -= imageOffset.X - Offset.X;
- y -= imageOffset.Y - Offset.Y;
-
- x /= ZoomFactor;
- y /= ZoomFactor;
- w /= ZoomFactor;
- h /= ZoomFactor;
-
- if (w != 0 && h != 0)
- {
- SelectionRegion = FitRectangle(new Rect(x, y, w, h));
- }
- }
-
- e.Handled = true;
- }
- #endregion
- }
-}
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml b/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml
new file mode 100644
index 0000000..84f9e33
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml
@@ -0,0 +1,335 @@
+<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"
+ xmlns:controls="clr-namespace:UVtools.WPF.Controls"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
+ x:Class="UVtools.WPF.Controls.Calibrators.CalibrateElephantFootControl">
+
+ <Grid ColumnDefinitions="Auto,10,350">
+ <StackPanel Spacing="10">
+ <Grid
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,100,5,Auto,20,Auto,10,100,5,Auto"
+ >
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Layer height:"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="0.01"
+ Maximum="0.30"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.LayerHeight}"
+ />
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <CheckBox Grid.Row="0" Grid.Column="8"
+ Content="Sync layers"
+ ToolTip.Tip="Set bottom and normal layers equally"
+ IsChecked="{Binding Operation.SyncLayers}" />
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom layers:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="1"
+ Maximum="1000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.BottomLayers}"
+ />
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.BottomHeight, StringFormat=\{0:0.00\}mm}"/>
+
+
+
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding !Operation.SyncLayers}"
+ Text="Normal layers:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="8"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding !Operation.SyncLayers}"
+ Increment="1"
+ Minimum="1"
+ Maximum="1000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.NormalLayers}"
+ />
+ <TextBlock Grid.Row="2" Grid.Column="10"
+ IsEnabled="{Binding !Operation.SyncLayers}"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.NormalHeight, StringFormat=\{0:0.00\}mm}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom exposure:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ Value="{Binding Operation.BottomExposure}"/>
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Normal exposure:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ Value="{Binding Operation.NormalExposure}"
+ />
+ <TextBlock Grid.Row="4" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Margin:"/>
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="255"
+ Value="{Binding Operation.Margin}"
+ />
+ <TextBlock Grid.Row="6" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <StackPanel Grid.Row="6" Grid.Column="6"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total layers:"/>
+
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total objects:"/>
+
+ </StackPanel>
+
+ <StackPanel Grid.Row="6" Grid.Column="8"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock FontWeight="Bold">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="{}{0} / {1:0.00}mm">
+ <Binding Path="Operation.LayerCount"/>
+ <Binding Path="Operation.TotalHeight"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.ObjectCount}"/>
+ </StackPanel>
+
+ <CheckBox Grid.Row="8" Grid.Column="0"
+ Grid.ColumnSpan="6"
+ IsChecked="{Binding Operation.OutputOriginalPart}"
+ Content="Output an unmodified part (The original)" />
+
+ <CheckBox Grid.Row="8" Grid.Column="6"
+ Grid.ColumnSpan="5"
+ IsChecked="{Binding Operation.EnableAntiAliasing}"
+ Content="Enable Anti-Aliasing" />
+ </Grid>
+
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="{Binding Operation.ErodeObjects, StringFormat=Morph - Erode [\{0\} objects]}"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <StackPanel>
+ <Grid
+ Margin="0,10,0,0"
+ RowDefinitions="Auto,10,Auto"
+ ColumnDefinitions="Auto,10,80,5,Auto,5,80,5,Auto,40,Auto,10,80"
+ >
+
+ <CheckBox Grid.Row="0" Grid.Column="2"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.IsErodeEnabled}"
+ Content="Enable"/>
+
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Iterations range:"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsErodeEnabled}"
+ Increment="1"
+ Minimum="1"
+ Maximum="30"
+ Value="{Binding Operation.ErodeStartIteration}"/>
+
+
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="-"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="6"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsErodeEnabled}"
+ Increment="1"
+ Minimum="1"
+ Maximum="30"
+ Value="{Binding Operation.ErodeEndIteration}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="8"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="Step increment(s):"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="12"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsErodeEnabled}"
+ Increment="1"
+ Minimum="1"
+ Maximum="20"
+ Value="{Binding Operation.ErodeIterationSteps}"/>
+
+ </Grid>
+
+ <Border
+ Margin="0,10,0,0">
+ <Expander>
+ <Expander.Header>
+ <TextBlock Text="Kernel - Advanced options (Click to expand)"
+ FontWeight="Bold"
+ Cursor="Hand"
+ />
+ </Expander.Header>
+ <controls:KernelControl
+ Name="KernelCtrl"
+ Margin="0,10,0,0"
+ />
+ </Expander>
+ </Border>
+
+ </StackPanel>
+
+ </Expander>
+ </Border>
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="{Binding Operation.DimmingObjects, StringFormat=Wall dimming [\{0\} objects]}"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <Grid
+ Margin="0,10,0,0"
+ RowDefinitions="Auto,10,Auto,10,Auto,5,Auto"
+ ColumnDefinitions="Auto,10,80,5,Auto,5,80,5,Auto,20,Auto,10,80"
+ >
+
+ <CheckBox Grid.Row="0" Grid.Column="2"
+ Grid.ColumnSpan="11"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.IsDimmingEnabled}"
+ Content="Enable - Requires a compatible anti-aliased file format and printer"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Wall thickness:"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsDimmingEnabled}"
+ Increment="1"
+ Minimum="1"
+ Maximum="255"
+ Value="{Binding Operation.DimmingWallThickness}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Brightness range:"/>
+
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsDimmingEnabled}"
+ Increment="1"
+ Minimum="1"
+ Maximum="254"
+ Value="{Binding Operation.DimmingStartBrightness}"/>
+
+
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="-"/>
+
+ <NumericUpDown Grid.Row="4" Grid.Column="6"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsDimmingEnabled}"
+ Increment="1"
+ Minimum="2"
+ Maximum="254"
+ Value="{Binding Operation.DimmingEndBrightness}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="Step increment(s):"/>
+
+ <NumericUpDown Grid.Row="4" Grid.Column="12"
+ ClipValueToMinMax="True"
+ IsEnabled="{Binding Operation.IsDimmingEnabled}"
+ Increment="1"
+ Minimum="2"
+ Maximum="254"
+ Value="{Binding Operation.DimmingBrightnessSteps}"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="2"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="{Binding Operation.DimmingStartBrightnessPercent, StringFormat=(\{0\}%)}"/>
+ <TextBlock Grid.Row="6" Grid.Column="6"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="{Binding Operation.DimmingEndBrightnessPercent, StringFormat=(\{0\}%)}"/>
+ </Grid>
+
+ </Expander>
+ </Border>
+
+ </StackPanel>
+
+ <Image Grid.Column="2"
+ Stretch="Uniform"
+ Source="{Binding PreviewImage}"/>
+ </Grid>
+
+</UserControl>
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs b/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs
new file mode 100644
index 0000000..f854c65
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs
@@ -0,0 +1,99 @@
+using System.Timers;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using UVtools.Core.Operations;
+using UVtools.WPF.Controls.Tools;
+using UVtools.WPF.Extensions;
+using UVtools.WPF.Windows;
+
+namespace UVtools.WPF.Controls.Calibrators
+{
+ public class CalibrateElephantFootControl : ToolControl
+ {
+ public OperationCalibrateElephantFoot Operation => BaseOperation as OperationCalibrateElephantFoot;
+
+ private readonly Timer _timer;
+
+ private Bitmap _previewImage;
+ public Bitmap PreviewImage
+ {
+ get => _previewImage;
+ set => RaiseAndSetIfChanged(ref _previewImage, value);
+ }
+
+ private KernelControl _kernelCtrl;
+
+
+ public CalibrateElephantFootControl()
+ {
+ this.InitializeComponent();
+ BaseOperation = new OperationCalibrateElephantFoot();
+
+ if(App.SlicerFile is not null)
+ {
+ Operation.LayerHeight = (decimal) App.SlicerFile.LayerHeight;
+ Operation.BottomExposure = (decimal) App.SlicerFile.BottomExposureTime;
+ Operation.NormalExposure = (decimal) App.SlicerFile.ExposureTime;
+ }
+
+ _kernelCtrl = this.Find<KernelControl>("KernelCtrl");
+
+ _timer = new Timer(20)
+ {
+ AutoReset = false
+ };
+ _timer.Elapsed += (sender, e) => Dispatcher.UIThread.InvokeAsync(UpdatePreview);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ if (App.SlicerFile is null) return;
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.Init:
+ case ToolWindow.Callbacks.ProfileLoaded:
+ Operation.Resolution = App.SlicerFile.Resolution;
+ Operation.PropertyChanged += (sender, e) =>
+ {
+ _timer.Stop();
+ _timer.Start();
+ if (e.PropertyName == nameof(Operation.ObjectCount))
+ {
+ ParentWindow.ButtonOkEnabled = Operation.ObjectCount > 0;
+ return;
+ }
+ };
+ ParentWindow.ButtonOkEnabled = Operation.ObjectCount > 0;
+ _timer.Stop();
+ _timer.Start();
+ break;
+ }
+ }
+
+ public override bool UpdateOperation()
+ {
+ Operation.ErodeKernel.Matrix = _kernelCtrl.GetMatrix();
+ Operation.ErodeKernel.Anchor = _kernelCtrl.Anchor;
+ return !(Operation.ErodeKernel.Matrix is null);
+ }
+
+ public void UpdatePreview()
+ {
+ var layers = Operation.GetLayers();
+ _previewImage?.Dispose();
+ PreviewImage = layers[0].ToBitmap();
+ foreach (var layer in layers)
+ {
+ layer.Dispose();
+ }
+ }
+ }
+}
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml b/UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml
new file mode 100644
index 0000000..bf65952
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml
@@ -0,0 +1,311 @@
+<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="800"
+ x:Class="UVtools.WPF.Controls.Calibrators.CalibrateGrayscaleControl">
+ <Grid ColumnDefinitions="Auto,10,350">
+ <StackPanel Spacing="10">
+ <Grid
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,100,5,Auto,20,Auto,10,100,5,Auto"
+ >
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Layer height:"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="0.01"
+ Maximum="0.30"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.LayerHeight}"
+ />
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <StackPanel Grid.Row="0" Grid.Column="6"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total layers:"/>
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total height:"/>
+ </StackPanel>
+
+ <StackPanel Grid.Row="0" Grid.Column="8"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.LayerCount}"/>
+
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.TotalHeight, StringFormat=\{0:0.00\}mm}"/>
+
+
+ </StackPanel>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom layers:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="1"
+ Maximum="1000"
+ Value="{Binding Operation.BottomLayers}"
+ />
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.BottomHeight, StringFormat=\{0:0.00\}mm}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Number of layers at normal exposure between bottom and normal layers"
+ Text="Interface layers:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="1000"
+ Value="{Binding Operation.InterfaceLayers}"/>
+ <TextBlock Grid.Row="2" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.InterfaceHeight, StringFormat=\{0:0.00\}mm}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Normal layers:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="1"
+ Maximum="1000"
+ Value="{Binding Operation.NormalLayers}"/>
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.NormalHeight, StringFormat=\{0:0.00\}mm}"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom exposure:"/>
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.BottomExposure}"/>
+ <TextBlock Grid.Row="6" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Normal exposure:"/>
+ <NumericUpDown Grid.Row="6" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.NormalExposure}"/>
+ <TextBlock Grid.Row="6" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Outer margin:"/>
+ <NumericUpDown Grid.Row="8" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.OuterMargin}"/>
+ <TextBlock Grid.Row="8" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Inner margin:"/>
+ <NumericUpDown Grid.Row="8" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.InnerMargin}"
+ />
+ <TextBlock Grid.Row="8" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="px"/>
+ </Grid>
+
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <StackPanel>
+ <TextBlock FontWeight="Bold">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="Pie settings [{0} divisions with {1:0.00}º steps]">
+ <Binding Path="Operation.Divisions"/>
+ <Binding Path="Operation.AngleStep"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+
+ <Grid
+ Margin="0,10,0,0"
+ RowDefinitions="Auto,0,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,80,5,Auto,5,80,5,Auto,20,Auto,10,80"
+ >
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Brightness range:"/>
+
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="1"
+ Maximum="254"
+ Value="{Binding Operation.StartBrightness}"/>
+
+
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="-"/>
+
+ <NumericUpDown Grid.Row="0" Grid.Column="6"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="2"
+ Maximum="255"
+ Value="{Binding Operation.EndBrightness}"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="Step increment(s):"/>
+
+ <NumericUpDown Grid.Row="0" Grid.Column="12"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="2"
+ Maximum="254"
+ Value="{Binding Operation.BrightnessSteps}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="2"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="{Binding Operation.StartBrightnessPercent, StringFormat=(\{0\}%)}"/>
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Text="{Binding Operation.EndBrightnessPercent, StringFormat=(\{0\}%)}"/>
+
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.EnableCenterHoleRelief}"
+ Text="Center hole diameter:"/>
+
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="2"
+ Maximum="10000"
+ IsEnabled="{Binding Operation.EnableCenterHoleRelief}"
+ Value="{Binding Operation.CenterHoleDiameter}"/>
+
+ <CheckBox Grid.Row="4" Grid.Column="6" Grid.ColumnSpan="4"
+ VerticalAlignment="Center"
+ Content="Enable center hole relief"
+ IsChecked="{Binding Operation.EnableCenterHoleRelief}"/>
+
+
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="4"
+ VerticalAlignment="Center"
+ Content="Enable division lines"
+ IsChecked="{Binding Operation.EnableLineDivisions}"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Text="Division thickness:"/>
+
+ <NumericUpDown Grid.Row="8" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="255"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Value="{Binding Operation.LineDivisionThickness}"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="4"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Text="px"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="6"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Text="Div. brightness:"/>
+
+ <NumericUpDown Grid.Row="8" Grid.Column="9"
+ Grid.ColumnSpan="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="255"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Value="{Binding Operation.LineDivisionBrightness}"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="12"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Text="{Binding Operation.LineDivisionBrightnessPercent, StringFormat=(\{0\}%)}"/>
+
+ <TextBlock Grid.Row="10" Grid.Column="0"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.EnableLineDivisions}"
+ Text="X text offset:"/>
+
+ <NumericUpDown Grid.Row="10" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="-10000"
+ Maximum="10000"
+ Value="{Binding Operation.TextXOffset}"/>
+
+ <TextBlock Grid.Row="10" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="12" Grid.Column="2" Grid.ColumnSpan="11"
+ VerticalAlignment="Center"
+ Content="Enable Anti-Aliasing"
+ IsChecked="{Binding Operation.EnableAntiAliasing}"/>
+ </Grid>
+ </StackPanel>
+ </Border>
+
+ </StackPanel>
+
+ <Image Grid.Column="2"
+ Stretch="Uniform"
+ Source="{Binding PreviewImage}"/>
+ </Grid>
+</UserControl>
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml.cs b/UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml.cs
new file mode 100644
index 0000000..b60ce83
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml.cs
@@ -0,0 +1,86 @@
+using System.Timers;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using UVtools.Core.Operations;
+using UVtools.WPF.Controls.Tools;
+using UVtools.WPF.Extensions;
+using UVtools.WPF.Windows;
+
+namespace UVtools.WPF.Controls.Calibrators
+{
+ public class CalibrateGrayscaleControl : ToolControl
+ {
+ public OperationCalibrateGrayscale Operation => BaseOperation as OperationCalibrateGrayscale;
+
+ private Bitmap _previewImage;
+ public Bitmap PreviewImage
+ {
+ get => _previewImage;
+ set => RaiseAndSetIfChanged(ref _previewImage, value);
+ }
+
+ private readonly Timer _timer;
+
+ public CalibrateGrayscaleControl()
+ {
+ this.InitializeComponent();
+
+ BaseOperation = new OperationCalibrateGrayscale();
+
+ if (App.SlicerFile is not null)
+ {
+ Operation.LayerHeight = (decimal)App.SlicerFile.LayerHeight;
+ Operation.BottomLayers = App.SlicerFile.BottomLayerCount;
+ Operation.BottomExposure = (decimal)App.SlicerFile.BottomExposureTime;
+ Operation.NormalExposure = (decimal)App.SlicerFile.ExposureTime;
+ }
+
+ _timer = new Timer(20)
+ {
+ AutoReset = false
+ };
+ _timer.Elapsed += (sender, e) => Dispatcher.UIThread.InvokeAsync(UpdatePreview);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ if (App.SlicerFile is null) return;
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.Init:
+ case ToolWindow.Callbacks.ProfileLoaded:
+ Operation.Resolution = App.SlicerFile.Resolution;
+ Operation.PropertyChanged += (sender, e) =>
+ {
+ _timer.Stop();
+ _timer.Start();
+ if (e.PropertyName == nameof(Operation.Divisions))
+ {
+ ParentWindow.ButtonOkEnabled = Operation.Divisions > 0;
+ return;
+ }
+ };
+ ParentWindow.ButtonOkEnabled = Operation.Divisions > 0;
+ _timer.Stop();
+ _timer.Start();
+ break;
+ }
+ }
+ public void UpdatePreview()
+ {
+ var layers = Operation.GetLayers();
+ _previewImage?.Dispose();
+ PreviewImage = layers[2].ToBitmap();
+ foreach (var layer in layers)
+ {
+ layer.Dispose();
+ }
+ }
+ }
+}
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml b/UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml
new file mode 100644
index 0000000..715b995
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml
@@ -0,0 +1,553 @@
+<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="800"
+ x:Class="UVtools.WPF.Controls.Calibrators.CalibrateToleranceControl">
+ <Grid ColumnDefinitions="Auto,10,380">
+ <StackPanel Spacing="10">
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 1 - Commun properties"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <Grid
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,100,5,Auto,20,Auto,10,100,5,Auto"
+ >
+
+ <TextBlock
+ Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="The printer display width. Required to calculate the pixels per mm."
+ Text="Display width:"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.1"
+ Minimum="0"
+ Maximum="10000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.DisplayWidth}"/>
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="6"
+ VerticalAlignment="Center"
+ ToolTip.Tip="The printer display height. Required to calculate the pixels per mm."
+ Text="Display height:"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.1"
+ Minimum="0"
+ Maximum="10000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.DisplayHeight}"/>
+ <TextBlock Grid.Row="0" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Layer height:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="0.01"
+ Maximum="0.30"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.LayerHeight}"
+ />
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Bottom layers:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="1"
+ Maximum="1000"
+ Value="{Binding Operation.BottomLayers}"/>
+ <TextBlock Grid.Row="2" Grid.Column="8"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.BottomHeight, StringFormat=\{0:0.00\}mm}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom exposure:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.BottomExposure}"/>
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Normal exposure:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.NormalExposure}"/>
+ <TextBlock Grid.Row="4" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Z height:"/>
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="5"
+ Maximum="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ZSize}"/>
+ <TextBlock Grid.Row="6" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <StackPanel Grid.Row="6" Grid.Column="6"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total layers:"/>
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total height:"/>
+ </StackPanel>
+
+ <StackPanel Grid.Row="6" Grid.Column="8"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.LayerCount}"/>
+
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.RealZSize, StringFormat=\{0:0.00\}mm}"/>
+
+
+ </StackPanel>
+
+
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Top/bottom margin:"/>
+ <NumericUpDown Grid.Row="8" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.TopBottomMargin}"/>
+ <TextBlock Grid.Row="8" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Left/right margin:"/>
+ <NumericUpDown Grid.Row="8" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.LeftRightMargin}"/>
+ <TextBlock Grid.Row="8" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="10" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Chamfer the bottom and top layers"
+ Text="Chamfer layers:"/>
+ <NumericUpDown Grid.Row="10" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="255"
+ IsEnabled="{Binding Operation.ChamferModel}"
+ Value="{Binding Operation.ChamferLayers}"/>
+
+ <TextBlock Grid.Row="10" Grid.Column="6"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Erode bottom iterations to counter the elephant foot"
+ Text="Erode bottom iter.:"/>
+ <NumericUpDown Grid.Row="10" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="255"
+ Value="{Binding Operation.ErodeBottomIterations}"/>
+
+ <TextBlock Grid.Row="12" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Shape:"/>
+ <ComboBox Grid.Row="12" Grid.Column="2"
+ Items="{Binding Operation.ShapesItems}"
+ SelectedItem="{Binding Operation.Shape}"/>
+
+ <TextBlock Grid.Row="12" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Part margin:"/>
+ <NumericUpDown Grid.Row="12" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.PartMargin}"/>
+ <TextBlock Grid.Row="10" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="14" Grid.Column="2" Grid.ColumnSpan="5"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.OutputSameDiameterPart}"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ ToolTip.Tip="Output the same hole diameter part (+0mm)"
+ Content="Output same hole diameter part"/>
+
+ <CheckBox Grid.Row="14" Grid.Column="8"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.FuseParts}"
+ ToolTip.Tip="Output the male counter-part in female"
+ Content="Fuse parts"/>
+
+ <CheckBox Grid.Row="16" Grid.Column="2" Grid.ColumnSpan="5"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.EnableAntiAliasing}"
+ Content="Enable Anti-Aliasing"/>
+
+ </Grid>
+ </Expander>
+ </Border>
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 2 - Female part"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+
+ </Expander.Header>
+
+ <Grid RowDefinitions="Auto,5,Auto"
+ ColumnDefinitions="Auto,10,100,5,Auto,20,Auto,10,100,5,Auto">
+
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ Text="Diameter:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1.0"
+ Minimum="2"
+ Maximum="1000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.FemaleDiameter}"/>
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="6"
+ Text="Hole diameter:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1.0"
+ Minimum="2"
+ Maximum="1000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.FemaleHoleDiameter}"/>
+ <TextBlock Grid.Row="0" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ Text="Expected:"
+ FontWeight="Bold"
+ VerticalAlignment="Center"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="2"
+ Text="{Binding Operation.FemaleDiameterRealXSize, StringFormat=\{0:0.00\}mm}"
+ FontWeight="Bold"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="8"
+ Text="{Binding Operation.FemaleHoleDiameterRealXSize, StringFormat=\{0:0.00\}mm}"
+ FontWeight="Bold"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"/>
+ </Grid>
+
+ </Expander>
+ </Border>
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 3 - Male parts"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+
+ </Expander.Header>
+
+ <Grid RowDefinitions="Auto,10,Auto"
+ ColumnDefinitions="Auto,10,80,5,Auto,20,Auto,10,80,5,Auto,20,Auto,10,80,5,Auto">
+
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ Text="Thinner models:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="1000"
+ Value="{Binding Operation.MaleThinnerModels}"/>
+ <TextBlock Grid.Row="0" Grid.Column="6"
+ Text="-Offset:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="-1000"
+ Maximum="0"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.MaleThinnerOffset}"/>
+ <TextBlock Grid.Row="0" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="12"
+ Text="-Step:"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="14"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="-1000"
+ Maximum="-0.01"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.MaleThinnerStep}"/>
+ <TextBlock Grid.Row="0" Grid.Column="16"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ Text="Thicker models:"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="1000"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ Value="{Binding Operation.MaleThickerModels}"/>
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ Text="+Offset:"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="0"
+ Maximum="1000"
+ FormatString="\{0:0.00\}"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ Value="{Binding Operation.MaleThickerOffset}"/>
+ <TextBlock Grid.Row="2" Grid.Column="10"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="12"
+ Text="+Step:"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ VerticalAlignment="Center"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="14"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="0.01"
+ Maximum="1000"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.MaleThickerStep}"/>
+ <TextBlock Grid.Row="2" Grid.Column="16"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding !Operation.FuseParts}"
+ Text="mm"/>
+
+
+
+
+ </Grid>
+
+ </Expander>
+ </Border>
+
+ <!--
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 4: Validate the printed model with your measures"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <StackPanel Spacing="10">
+
+ <StackPanel Orientation="Horizontal" Spacing="5">
+ <TextBlock VerticalAlignment="Center" Text="X:"/>
+ <NumericUpDown
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="100"
+ Width="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ObservedXSize}"/>
+ <TextBlock VerticalAlignment="Center" Text="mm"/>
+
+ <TextBlock VerticalAlignment="Center" Text="Y:"
+ Margin="20,0,0,0"/>
+ <NumericUpDown
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="100"
+ Width="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ObservedYSize}"/>
+ <TextBlock VerticalAlignment="Center" Text="mm"/>
+
+ <TextBlock VerticalAlignment="Center" Text="Z:"
+ Margin="20,0,0,0"/>
+ <NumericUpDown
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="100"
+ Width="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ObservedZSize}"/>
+ <TextBlock VerticalAlignment="Center" Text="mm"/>
+ </StackPanel>
+
+ <Grid RowDefinitions="Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,Auto">
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ FontWeight="Bold"
+ ToolTip.Tip="The calculated expected size for the part based on your input"
+ Text="Expected size:"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold"
+ ToolTip.Tip="The calculated expected size for the part based on your input">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="\{0\}mm x \{1\}mm x \{2\}mm">
+ <Binding Path="Operation.RealXSize"/>
+ <Binding Path="Operation.RealYSize"/>
+ <Binding Path="Operation.RealZSize"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ IsVisible="{Binding Operation.HollowModel}"
+ FontWeight="Bold"
+ ToolTip.Tip="The calculated expected wall thickness size for the part based on your input"
+ Text="Expected wall size:"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="2" FontWeight="Bold"
+ IsVisible="{Binding Operation.HollowModel}"
+ ToolTip.Tip="The calculated expected wall thickness size for the part based on your input">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="\{0\}mm x \{1\}mm">
+ <Binding Path="Operation.WallThicknessRealXSize"/>
+ <Binding Path="Operation.WallThicknessRealYSize"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ FontWeight="Bold"
+ ToolTip.Tip="The resultant scale factor you should resize your model with"
+ Text="Resultant scale factor:"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="2" FontWeight="Bold"
+ ToolTip.Tip="The resultant scale factor you should resize your model with">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="\{0\}% x \{1\}% x \{2\}%">
+ <Binding Path="Operation.ScaleXFactor"/>
+ <Binding Path="Operation.ScaleYFactor"/>
+ <Binding Path="Operation.ScaleZFactor"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+ </Grid>
+ </StackPanel>
+
+ </Expander>
+ </Border>
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 5: Save a resize profile with the results"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <Grid ColumnDefinitions="Auto,10,*,5,Auto,5,Auto">
+ <TextBlock VerticalAlignment="Center"
+ Text="Profile name:"/>
+ <TextBox Grid.Column="2" VerticalAlignment="Center"
+ IsEnabled="{Binding IsProfileAddEnabled}"
+ Text="{Binding ProfileName}"/>
+ <Button Grid.Column="4" VerticalAlignment="Center"
+ IsEnabled="{Binding IsProfileAddEnabled}"
+ Command="{Binding AutoNameProfile}"
+ ToolTip.Tip="Auto name the profile with the input values.
+&#x0a;Rename MyPrinterX to your printer name and MyResinX to your resin name."
+ Content="Auto name"/>
+ <Button Grid.Column="6" VerticalAlignment="Center"
+ IsEnabled="{Binding IsProfileAddEnabled}"
+ Command="{Binding AddProfile}">
+ <Image Source="/Assets/Icons/plus-16x16.png"/>
+ </Button>
+ </Grid>
+ </Expander>
+ </Border>
+ !-->
+
+
+ </StackPanel>
+
+ <Image Grid.Column="2"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ Stretch="Uniform"
+ Source="{Binding PreviewImage}"/>
+ </Grid>
+</UserControl>
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml.cs b/UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml.cs
new file mode 100644
index 0000000..285f9fc
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml.cs
@@ -0,0 +1,131 @@
+using System.Diagnostics;
+using System.Timers;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using UVtools.Core.Operations;
+using UVtools.WPF.Controls.Tools;
+using UVtools.WPF.Extensions;
+using UVtools.WPF.Windows;
+
+namespace UVtools.WPF.Controls.Calibrators
+{
+ public class CalibrateToleranceControl : ToolControl
+ {
+ public OperationCalibrateTolerance Operation => BaseOperation as OperationCalibrateTolerance;
+
+ private Timer _timer;
+
+ /*public string ProfileName
+ {
+ get => _profileName;
+ set => RaiseAndSetIfChanged(ref _profileName, value);
+ }*/
+
+ //public bool IsProfileAddEnabled => Operation.ScaleXFactor != 100 || Operation.ScaleYFactor != 100;
+
+ private Bitmap _previewImage;
+ /*private string _profileName;
+ private bool _isProfileNameEnabled;*/
+
+ public Bitmap PreviewImage
+ {
+ get => _previewImage;
+ set => RaiseAndSetIfChanged(ref _previewImage, value);
+ }
+
+ public bool IsDisplaySizeVisible => App.SlicerFile.DisplayWidth <= 0 && App.SlicerFile.DisplayHeight <= 0;
+
+ public CalibrateToleranceControl()
+ {
+ InitializeComponent();
+ BaseOperation = new OperationCalibrateTolerance();
+
+ if (App.SlicerFile is not null)
+ {
+ Operation.LayerHeight = (decimal)App.SlicerFile.LayerHeight;
+ Operation.BottomLayers = App.SlicerFile.BottomLayerCount;
+ Operation.BottomExposure = (decimal)App.SlicerFile.BottomExposureTime;
+ Operation.NormalExposure = (decimal)App.SlicerFile.ExposureTime;
+ }
+
+ _timer = new Timer(100)
+ {
+ AutoReset = false
+ };
+ _timer.Elapsed += (sender, e) => Dispatcher.UIThread.InvokeAsync(UpdatePreview);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ if (App.SlicerFile is null) return;
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.Init:
+ case ToolWindow.Callbacks.ProfileLoaded:
+ Operation.Resolution = App.SlicerFile.Resolution;
+ Operation.DisplayWidth = (decimal)App.SlicerFile.DisplayWidth;
+ Operation.DisplayHeight = (decimal)App.SlicerFile.DisplayHeight;
+ Operation.PropertyChanged += (sender, e) =>
+ {
+ _timer.Stop();
+ _timer.Start();
+ /*if (e.PropertyName == nameof(Operation.ScaleXFactor) || e.PropertyName == nameof(Operation.ScaleYFactor))
+ {
+ RaisePropertyChanged(nameof(IsProfileAddEnabled));
+ return;
+ }*/
+ };
+ _timer.Stop();
+ _timer.Start();
+ break;
+ }
+ }
+
+ public void UpdatePreview()
+ {
+ var layers = Operation.GetLayers();
+ _previewImage?.Dispose();
+ PreviewImage = layers[^1].ToBitmap();
+ foreach (var layer in layers)
+ {
+ layer.Dispose();
+ }
+ }
+
+ /*public async void AddProfile()
+ {
+ OperationResize resize = new OperationResize
+ {
+ ProfileName = ProfileName,
+ X = Operation.ScaleXFactor,
+ Y = Operation.ScaleYFactor
+ };
+ var find = OperationProfiles.FindByName(resize, ProfileName);
+ if (find is not null)
+ {
+ if (await ParentWindow.MessageBoxQuestion(
+ $"A profile with same name and/or values already exists, do you want to overwrite:\n{find}\nwith:\n{resize}\n?") != ButtonResult.Yes) return;
+
+ OperationProfiles.RemoveProfile(resize, false);
+ }
+
+ OperationProfiles.AddProfile(resize);
+ await ParentWindow.MessageBoxInfo($"The resize profile has been added.\nGo to Tools - Resize and select the saved profile to load it in.\n{resize}");
+ }
+
+ public void AutoNameProfile()
+ {
+ var printerName = string.IsNullOrEmpty(App.SlicerFile.MachineName)
+ ? "MyPrinterX"
+ : App.SlicerFile.MachineName;
+ ProfileName = $"{printerName}, MyResinX, {Operation.Microns}µm, {Operation.BottomExposure}s/{Operation.NormalExposure}s";
+ }
+ */
+ }
+}
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml b/UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml
new file mode 100644
index 0000000..f865e56
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml
@@ -0,0 +1,478 @@
+<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="800"
+ x:Class="UVtools.WPF.Controls.Calibrators.CalibrateXYZAccuracyControl">
+ <Grid ColumnDefinitions="Auto,10,380">
+ <StackPanel Spacing="10">
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 1: Generate and print test model"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <Grid
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,100,5,Auto,20,Auto,10,100,5,Auto"
+ >
+
+ <TextBlock
+ Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="The printer display width. Required to calculate the pixels per mm."
+ Text="Display width:"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.1"
+ Minimum="0"
+ Maximum="10000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.DisplayWidth}"/>
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="6"
+ VerticalAlignment="Center"
+ ToolTip.Tip="The printer display height. Required to calculate the pixels per mm."
+ Text="Display height:"/>
+ <NumericUpDown Grid.Row="0" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.1"
+ Minimum="0"
+ Maximum="10000"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.DisplayHeight}"/>
+ <TextBlock Grid.Row="0" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Layer height:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.01"
+ Minimum="0.01"
+ Maximum="0.30"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.LayerHeight}"
+ />
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Bottom layers:"/>
+ <NumericUpDown Grid.Row="2" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="1"
+ Maximum="1000"
+ Value="{Binding Operation.BottomLayers}"/>
+ <TextBlock Grid.Row="2" Grid.Column="8"
+ VerticalAlignment="Center"
+ Text="{Binding Operation.BottomHeight, StringFormat=\{0:0.00\}mm}"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Bottom exposure:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.BottomExposure}"/>
+ <TextBlock Grid.Row="4" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Normal exposure:"/>
+ <NumericUpDown Grid.Row="4" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0.1"
+ Maximum="200"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.NormalExposure}"/>
+ <TextBlock Grid.Row="4" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="s"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="X length:"/>
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="5"
+ Maximum="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.XSize}"/>
+ <TextBlock Grid.Row="6" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="6" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Y length:"/>
+ <NumericUpDown Grid.Row="6" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="5"
+ Maximum="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.YSize}"/>
+ <TextBlock Grid.Row="6" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <TextBlock Grid.Row="8" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Z height:"/>
+ <NumericUpDown Grid.Row="8" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="5"
+ Maximum="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ZSize}"/>
+ <TextBlock Grid.Row="8" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="mm"/>
+
+ <StackPanel Grid.Row="8" Grid.Column="6"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total layers:"/>
+ <TextBlock
+ FontWeight="Bold"
+ Text="Total height:"/>
+ </StackPanel>
+
+ <StackPanel Grid.Row="8" Grid.Column="8"
+ VerticalAlignment="Center"
+ Spacing="0">
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.LayerCount}"/>
+
+ <TextBlock
+ FontWeight="Bold"
+ Text="{Binding Operation.RealZSize, StringFormat=\{0:0.00\}mm}"/>
+
+
+ </StackPanel>
+
+ <TextBlock Grid.Row="10" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Top/bottom margin:"/>
+ <NumericUpDown Grid.Row="10" Grid.Column="2"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.TopBottomMargin}"/>
+ <TextBlock Grid.Row="10" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="10" Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="Left/right margin:"/>
+ <NumericUpDown Grid.Row="10" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="10000"
+ Value="{Binding Operation.LeftRightMargin}"/>
+ <TextBlock Grid.Row="10" Grid.Column="10"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <CheckBox Grid.Row="12" Grid.Column="2" Grid.ColumnSpan="2"
+ Content="Hollow model"
+ IsChecked="{Binding Operation.HollowModel}"/>
+
+ <TextBlock Grid.Row="12" Grid.Column="6" Grid.ColumnSpan="3"
+ Text="Wall thickness:"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.HollowModel}"/>
+ <NumericUpDown Grid.Row="12" Grid.Column="8"
+ ClipValueToMinMax="True"
+ Increment="0.5"
+ Minimum="0"
+ Maximum="100"
+ FormatString="\{0:0.00\}"
+ IsEnabled="{Binding Operation.HollowModel}"
+ Value="{Binding Operation.WallThickness}"/>
+ <TextBlock Grid.Row="12" Grid.Column="10"
+ VerticalAlignment="Center"
+ IsEnabled="{Binding Operation.HollowModel}"
+ Text="mm"/>
+
+ <CheckBox Grid.Row="14" Grid.Column="2" Grid.ColumnSpan="9"
+ VerticalAlignment="Center"
+ IsChecked="{Binding Operation.CenterHoleRelief}"
+ Content="Relief with a center hole"/>
+
+ <TextBlock Grid.Row="16" Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Objects to output:"/>
+
+ <Grid Grid.Row="16" Grid.Column="2"
+ RowDefinitions="Auto,Auto,Auto"
+ ColumnDefinitions="Auto,Auto,Auto"
+ >
+
+ <CheckBox
+ Grid.Row="0" Grid.Column="0"
+ ToolTip.Tip="Top Left"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputTLObject}"
+ />
+ <CheckBox
+ Grid.Row="0" Grid.Column="1"
+ ToolTip.Tip="Top Center"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputTCObject}"
+ />
+ <CheckBox
+ Grid.Row="0" Grid.Column="2"
+ ToolTip.Tip="Top Right"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputTRObject}"
+ />
+
+ <CheckBox
+ Grid.Row="1" Grid.Column="0"
+ ToolTip.Tip="Middle Left"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputMLObject}"
+ />
+ <CheckBox
+ Grid.Row="1" Grid.Column="1"
+ ToolTip.Tip="Middle Center"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Margin="5"
+ IsChecked="{Binding Operation.OutputMCObject}"
+ />
+ <CheckBox
+ Grid.Row="1" Grid.Column="2"
+ ToolTip.Tip="Middle Right"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputMRObject}"
+ />
+
+ <CheckBox
+ Grid.Row="2" Grid.Column="0"
+ ToolTip.Tip="Bottom Left"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputBLObject}"
+ />
+ <CheckBox
+ Grid.Row="2" Grid.Column="1"
+ ToolTip.Tip="Bottom Center"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputBCObject}"
+ />
+ <CheckBox
+ Grid.Row="2" Grid.Column="2"
+ ToolTip.Tip="Bottom Right"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ IsChecked="{Binding Operation.OutputBRObject}"
+ />
+
+
+ </Grid>
+
+ <StackPanel Grid.Row="16" Grid.Column="4" Grid.ColumnSpan="3"
+ VerticalAlignment="Center"
+ Spacing="5">
+ <Button
+ Command="{Binding Operation.SelectNoneObjects}"
+ Content="Select none"/>
+ <Button
+ Command="{Binding Operation.SelectAllObjects}"
+ Content="Select all"/>
+ </StackPanel>
+
+ <StackPanel Grid.Row="16" Grid.Column="8" Grid.ColumnSpan="3"
+ VerticalAlignment="Center"
+ Spacing="5">
+ <Button
+ Command="{Binding Operation.SelectCrossedObjects}"
+ Content="Select crossed"/>
+ <Button
+ Command="{Binding Operation.SelectCenterObject}"
+ Content="Select center"/>
+ </StackPanel>
+
+ </Grid>
+ </Expander>
+ </Border>
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 2: Validate the printed model with your measures"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <StackPanel Spacing="10">
+
+ <StackPanel Orientation="Horizontal" Spacing="5">
+ <TextBlock VerticalAlignment="Center" Text="X:"/>
+ <NumericUpDown
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="100"
+ Width="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ObservedXSize}"/>
+ <TextBlock VerticalAlignment="Center" Text="mm"/>
+
+ <TextBlock VerticalAlignment="Center" Text="Y:"
+ Margin="20,0,0,0"/>
+ <NumericUpDown
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="100"
+ Width="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ObservedYSize}"/>
+ <TextBlock VerticalAlignment="Center" Text="mm"/>
+
+ <TextBlock VerticalAlignment="Center" Text="Z:"
+ Margin="20,0,0,0"/>
+ <NumericUpDown
+ ClipValueToMinMax="True"
+ Increment="1"
+ Minimum="0"
+ Maximum="100"
+ Width="100"
+ FormatString="\{0:0.00\}"
+ Value="{Binding Operation.ObservedZSize}"/>
+ <TextBlock VerticalAlignment="Center" Text="mm"/>
+ </StackPanel>
+
+ <Grid RowDefinitions="Auto,10,Auto,10,Auto"
+ ColumnDefinitions="Auto,10,Auto">
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ FontWeight="Bold"
+ ToolTip.Tip="The calculated expected size for the part based on your input"
+ Text="Expected size:"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" FontWeight="Bold"
+ ToolTip.Tip="The calculated expected size for the part based on your input">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="\{0\}mm x \{1\}mm x \{2\}mm">
+ <Binding Path="Operation.RealXSize"/>
+ <Binding Path="Operation.RealYSize"/>
+ <Binding Path="Operation.RealZSize"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ IsVisible="{Binding Operation.HollowModel}"
+ FontWeight="Bold"
+ ToolTip.Tip="The calculated expected wall thickness size for the part based on your input"
+ Text="Expected wall size:"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="2" FontWeight="Bold"
+ IsVisible="{Binding Operation.HollowModel}"
+ ToolTip.Tip="The calculated expected wall thickness size for the part based on your input">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="\{0\}mm x \{1\}mm">
+ <Binding Path="Operation.WallThicknessRealXSize"/>
+ <Binding Path="Operation.WallThicknessRealYSize"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+
+ <TextBlock Grid.Row="4" Grid.Column="0"
+ VerticalAlignment="Center"
+ FontWeight="Bold"
+ ToolTip.Tip="The resultant scale factor you should resize your model with"
+ Text="Resultant scale factor:"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="2" FontWeight="Bold"
+ ToolTip.Tip="The resultant scale factor you should resize your model with">
+ <TextBlock.Text>
+ <MultiBinding StringFormat="\{0\}% x \{1\}% x \{2\}%">
+ <Binding Path="Operation.ScaleXFactor"/>
+ <Binding Path="Operation.ScaleYFactor"/>
+ <Binding Path="Operation.ScaleZFactor"/>
+ </MultiBinding>
+ </TextBlock.Text>
+ </TextBlock>
+ </Grid>
+ </StackPanel>
+
+ </Expander>
+ </Border>
+
+ <Border BorderBrush="Black" BorderThickness="1" Padding="5">
+ <Expander IsExpanded="True">
+ <Expander.Header>
+ <TextBlock Text="Step 3: Save a resize profile with the results"
+ FontWeight="Bold"
+ Cursor="Hand"/>
+ </Expander.Header>
+
+ <Grid ColumnDefinitions="Auto,10,*,5,Auto,5,Auto">
+ <TextBlock VerticalAlignment="Center"
+ Text="Profile name:"/>
+ <TextBox Grid.Column="2" VerticalAlignment="Center"
+ IsEnabled="{Binding IsProfileAddEnabled}"
+ Text="{Binding ProfileName}"/>
+ <Button Grid.Column="4" VerticalAlignment="Center"
+ IsEnabled="{Binding IsProfileAddEnabled}"
+ Command="{Binding AutoNameProfile}"
+ ToolTip.Tip="Auto name the profile with the input values.
+&#x0a;Rename MyPrinterX to your printer name and MyResinX to your resin name."
+ Content="Auto name"/>
+ <Button Grid.Column="6" VerticalAlignment="Center"
+ IsEnabled="{Binding IsProfileAddEnabled}"
+ Command="{Binding AddProfile}">
+ <Image Source="/Assets/Icons/plus-16x16.png"/>
+ </Button>
+ </Grid>
+ </Expander>
+ </Border>
+
+
+ </StackPanel>
+
+ <Image Grid.Column="2"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ Stretch="Uniform"
+ Source="{Binding PreviewImage}"/>
+ </Grid>
+</UserControl>
diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml.cs b/UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml.cs
new file mode 100644
index 0000000..de538ab
--- /dev/null
+++ b/UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml.cs
@@ -0,0 +1,132 @@
+using System.Timers;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using MessageBox.Avalonia.Enums;
+using UVtools.Core.Operations;
+using UVtools.WPF.Controls.Tools;
+using UVtools.WPF.Extensions;
+using UVtools.WPF.Structures;
+using UVtools.WPF.Windows;
+
+namespace UVtools.WPF.Controls.Calibrators
+{
+ public class CalibrateXYZAccuracyControl : ToolControl
+ {
+ public OperationCalibrateXYZAccuracy Operation => BaseOperation as OperationCalibrateXYZAccuracy;
+
+ private Timer _timer;
+
+ public string ProfileName
+ {
+ get => _profileName;
+ set => RaiseAndSetIfChanged(ref _profileName, value);
+ }
+
+ public bool IsProfileAddEnabled => Operation.ScaleXFactor != 100 || Operation.ScaleYFactor != 100;
+
+ private Bitmap _previewImage;
+ private string _profileName;
+ private bool _isProfileNameEnabled;
+
+ public Bitmap PreviewImage
+ {
+ get => _previewImage;
+ set => RaiseAndSetIfChanged(ref _previewImage, value);
+ }
+
+ public bool IsDisplaySizeVisible => App.SlicerFile.DisplayWidth <= 0 && App.SlicerFile.DisplayHeight <= 0;
+
+ public CalibrateXYZAccuracyControl()
+ {
+ InitializeComponent();
+ BaseOperation = new OperationCalibrateXYZAccuracy();
+
+ if (App.SlicerFile is not null)
+ {
+ Operation.LayerHeight = (decimal)App.SlicerFile.LayerHeight;
+ Operation.BottomLayers = App.SlicerFile.BottomLayerCount;
+ Operation.BottomExposure = (decimal)App.SlicerFile.BottomExposureTime;
+ Operation.NormalExposure = (decimal)App.SlicerFile.ExposureTime;
+ }
+
+ _timer = new Timer(20)
+ {
+ AutoReset = false
+ };
+ _timer.Elapsed += (sender, e) => Dispatcher.UIThread.InvokeAsync(UpdatePreview);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ if (App.SlicerFile is null) return;
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.Init:
+ case ToolWindow.Callbacks.ProfileLoaded:
+ Operation.Resolution = App.SlicerFile.Resolution;
+ Operation.DisplayWidth = (decimal)App.SlicerFile.DisplayWidth;
+ Operation.DisplayHeight = (decimal)App.SlicerFile.DisplayHeight;
+ Operation.PropertyChanged += (sender, e) =>
+ {
+ _timer.Stop();
+ _timer.Start();
+ if (e.PropertyName == nameof(Operation.ScaleXFactor) || e.PropertyName == nameof(Operation.ScaleYFactor))
+ {
+ RaisePropertyChanged(nameof(IsProfileAddEnabled));
+ return;
+ }
+ };
+ //ParentWindow.ButtonOkEnabled = Operation.ObjectCount > 0;
+ _timer.Stop();
+ _timer.Start();
+ break;
+ }
+ }
+
+ public void UpdatePreview()
+ {
+ var layers = Operation.GetLayers();
+ _previewImage?.Dispose();
+ PreviewImage = layers[1].ToBitmap();
+ foreach (var layer in layers)
+ {
+ layer.Dispose();
+ }
+ }
+
+ public async void AddProfile()
+ {
+ OperationResize resize = new OperationResize
+ {
+ ProfileName = ProfileName,
+ X = Operation.ScaleXFactor,
+ Y = Operation.ScaleYFactor
+ };
+ var find = OperationProfiles.FindByName(resize, ProfileName);
+ if (find is not null)
+ {
+ if (await ParentWindow.MessageBoxQuestion(
+ $"A profile with same name and/or values already exists, do you want to overwrite:\n{find}\nwith:\n{resize}\n?") != ButtonResult.Yes) return;
+
+ OperationProfiles.RemoveProfile(resize, false);
+ }
+
+ OperationProfiles.AddProfile(resize);
+ await ParentWindow.MessageBoxInfo($"The resize profile has been added.\nGo to Tools - Resize and select the saved profile to load it in.\n{resize}");
+ }
+
+ public void AutoNameProfile()
+ {
+ var printerName = string.IsNullOrEmpty(App.SlicerFile.MachineName)
+ ? "MyPrinterX"
+ : App.SlicerFile.MachineName;
+ ProfileName = $"{printerName}, MyResinX, {Operation.Microns}µm, {Operation.BottomExposure}s/{Operation.NormalExposure}s";
+ }
+ }
+}
diff --git a/UVtools.WPF/Controls/Tools/ToolBlurControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolBlurControl.axaml.cs
index 88058ec..c9ea8c3 100644
--- a/UVtools.WPF/Controls/Tools/ToolBlurControl.axaml.cs
+++ b/UVtools.WPF/Controls/Tools/ToolBlurControl.axaml.cs
@@ -1,6 +1,7 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using UVtools.Core.Operations;
+using UVtools.WPF.Windows;
namespace UVtools.WPF.Controls.Tools
{
@@ -55,5 +56,15 @@ namespace UVtools.WPF.Controls.Tools
Operation.Kernel.Anchor = _kernelCtrl.Anchor;
return !(Operation.Kernel.Matrix is null);
}
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.ProfileLoaded:
+ SelectedAlgorithmIndex = Operation.BlurTypeIndex;
+ break;
+ }
+ }
}
}
diff --git a/UVtools.WPF/Controls/Tools/ToolCalculatorControl.axaml b/UVtools.WPF/Controls/Tools/ToolCalculatorControl.axaml
index ecd84b5..85e03db 100644
--- a/UVtools.WPF/Controls/Tools/ToolCalculatorControl.axaml
+++ b/UVtools.WPF/Controls/Tools/ToolCalculatorControl.axaml
@@ -83,7 +83,7 @@
Minimum="0"
Maximum="100000"
Increment="0.01"
- FormatString="{}{0:#,0.00}"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcMillimetersToPixels.DisplayWidth}"
/>
<TextBlock
@@ -105,7 +105,7 @@
Minimum="0"
Maximum="100000"
Increment="0.01"
- FormatString="{}{0:#,0.00}"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcMillimetersToPixels.DisplayHeight}"
/>
<TextBlock
@@ -133,8 +133,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="100000"
- Increment="0.01"
- FormatString="{}{0:#,0.00}"
+ Increment="0.5"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcMillimetersToPixels.Millimeters}"
/>
<TextBlock
@@ -266,7 +266,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="1.0"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.LiftHeight}"/>
<TextBlock
Grid.Row="0"
@@ -286,7 +287,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="1.0"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.LiftSpeed}"/>
<TextBlock
Grid.Row="2"
@@ -306,7 +308,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="1.0"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.RetractSpeed}"/>
<TextBlock
Grid.Row="4"
@@ -326,7 +329,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="0.5"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.WaitTime}"/>
<TextBlock
Grid.Row="6"
@@ -404,7 +408,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="1.0"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.BottomLiftHeight}"/>
<TextBlock
Grid.Row="0"
@@ -424,7 +429,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="1.0"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.BottomLiftSpeed}"/>
<TextBlock
Grid.Row="2"
@@ -432,27 +438,6 @@
VerticalAlignment="Center"
Text="mm/min"/>
- <!--
- <TextBlock
- Grid.Row="4"
- Grid.Column="6"
- VerticalAlignment="Center"
- HorizontalAlignment="Right"
- Text="Retract speed:"/>
- <NumericUpDown
- Grid.Row="4"
- Grid.Column="8"
- VerticalAlignment="Center"
- Minimum="0"
- Maximum="1000"
- Increment="0.01"
- Value="{Binding Operation.CalcLightOffDelay.RetractSpeed}"/>
- <TextBlock
- Grid.Row="4"
- Grid.Column="10"
- VerticalAlignment="Center"
- Text="mm/min"/>
- -->
<TextBlock
Grid.Row="6"
Grid.Column="6"
@@ -465,7 +450,8 @@
VerticalAlignment="Center"
Minimum="0"
Maximum="1000"
- Increment="0.01"
+ Increment="0.5"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.CalcLightOffDelay.BottomWaitTime}"/>
<TextBlock
Grid.Row="6"
diff --git a/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs
index 9f45a89..d0ac7e1 100644
--- a/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs
+++ b/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs
@@ -62,14 +62,14 @@ namespace UVtools.WPF.Controls.Tools
VerticalAlignment = VerticalAlignment.Center,
Minimum = (double) modifier.Minimum,
Maximum = (double) modifier.Maximum,
- Increment = modifier.DecimalPlates == 0 ? 1 : 0.01,
+ Increment = modifier.DecimalPlates == 0 ? 1 : 0.5,
Value = (double)modifier.NewValue,
Tag = this,
Width = 100,
};
if (modifier.DecimalPlates > 0)
{
- NewValue.FormatString = "{0:#,0.00}";
+ NewValue.FormatString = "{0:0.00}";
}
Unit = new TextBlock
diff --git a/UVtools.WPF/Controls/Tools/ToolMorphControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolMorphControl.axaml.cs
index 7dac93e..bd5c176 100644
--- a/UVtools.WPF/Controls/Tools/ToolMorphControl.axaml.cs
+++ b/UVtools.WPF/Controls/Tools/ToolMorphControl.axaml.cs
@@ -43,5 +43,15 @@ namespace UVtools.WPF.Controls.Tools
Operation.Kernel.Anchor = _kernelCtrl.Anchor;
return !(Operation.Kernel.Matrix is null);
}
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.ProfileLoaded:
+ MorphSelectedIndex = Operation.MorphOperationIndex;
+ break;
+ }
+ }
}
}
diff --git a/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml b/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml
index 9119b55..88924e3 100644
--- a/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml
+++ b/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml
@@ -77,7 +77,7 @@
Watermark="Pattern"
UseFloatingWatermark="True"
TextWrapping="NoWrap"
- Text="{Binding PatternText}"
+ Text="{Binding Operation.PatternText}"
/>
<TextBox
Grid.Column="2"
@@ -85,7 +85,7 @@
Watermark="Alternate pattern (Optional)"
UseFloatingWatermark="True"
TextWrapping="NoWrap"
- Text="{Binding AlternatePatternText}"
+ Text="{Binding Operation.AlternatePatternText}"
/>
<Border
@@ -106,8 +106,11 @@
Minimum="0"
Maximum="254"
Width="80"
- Value="{Binding DimGenBrightness}"
- />
+ Value="{Binding Operation.Brightness}"/>
+
+ <TextBlock
+ VerticalAlignment="Center"
+ Text="{Binding Operation.BrightnessPercent, StringFormat=(\{0\}%)}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="10">
@@ -115,28 +118,28 @@
Padding="10"
Content="Chessboard"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Chessboard"
/>
<Button
Padding="10"
Content="Sparse"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Sparse"
/>
<Button
Padding="10"
Content="Crosses"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Crosses"
/>
<Button
Padding="10"
Content="Strips"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Strips"
/>
</StackPanel>
@@ -146,7 +149,7 @@
Padding="10"
Content="Pyramid"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Pyramid"
/>
@@ -154,21 +157,21 @@
Padding="10"
Content="Rhombus"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Rhombus"
/>
<Button
Padding="10"
Content="Waves"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Waves"
/>
<Button
Padding="10"
Content="Slashes"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Slashes"
/>
</StackPanel>
@@ -178,14 +181,14 @@
Padding="10"
Content="Hearts"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Hearts"
/>
<Button
Padding="10"
Content="Solid"
Width="100"
- Command="{Binding GeneratePixelDimming}"
+ Command="{Binding Operation.GeneratePixelDimming}"
CommandParameter="Solid"
/>
</StackPanel>
@@ -194,8 +197,8 @@
</Border>
<Border
- Grid.Row="3"
- Grid.Column="3"
+ Grid.Row="2"
+ Grid.Column="2"
BorderBrush="LightGray"
BorderThickness="1"
Padding="5"
@@ -217,7 +220,7 @@
Minimum="5"
Maximum="10000"
Width="80"
- Value="{Binding InfillGenThickness}"
+ Value="{Binding Operation.InfillGenThickness}"
/>
<TextBlock
VerticalAlignment="Center"
@@ -231,7 +234,7 @@
Minimum="5"
Maximum="10000"
Width="80"
- Value="{Binding InfillGenSpacing}"
+ Value="{Binding Operation.InfillGenSpacing}"
/>
<TextBlock
VerticalAlignment="Center"
@@ -243,28 +246,28 @@
Padding="10"
Content="Rectilinear"
Width="100"
- Command="{Binding GenerateInfill}"
+ Command="{Binding Operation.GenerateInfill}"
CommandParameter="Rectilinear"
/>
<Button
Padding="10"
Content="Square grid"
Width="100"
- Command="{Binding GenerateInfill}"
+ Command="{Binding Operation.GenerateInfill}"
CommandParameter="Square grid"
/>
<Button
Padding="10"
Content="Waves"
Width="100"
- Command="{Binding GenerateInfill}"
+ Command="{Binding Operation.GenerateInfill}"
CommandParameter="Waves"
/>
<Button
Padding="10"
Content="Lattice"
Width="100"
- Command="{Binding GenerateInfill}"
+ Command="{Binding Operation.GenerateInfill}"
CommandParameter="Lattice"
/>
</StackPanel>
diff --git a/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml.cs
index e4d35bf..3ec06b9 100644
--- a/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml.cs
+++ b/UVtools.WPF/Controls/Tools/ToolPixelDimmingControl.axaml.cs
@@ -1,9 +1,7 @@
using System;
-using System.Drawing;
using Avalonia.Markup.Xaml;
using Emgu.CV;
using UVtools.Core.Extensions;
-using UVtools.Core.Objects;
using UVtools.Core.Operations;
using UVtools.WPF.Extensions;
@@ -11,386 +9,21 @@ namespace UVtools.WPF.Controls.Tools
{
public class ToolPixelDimmingControl : ToolControl
{
- #region Subclasses
- class StringMatrix
- {
- public string Text { get; }
- public Matrix<byte> Pattern { get; set; }
-
- public StringMatrix(string text)
- {
- Text = text;
- }
- }
- #endregion
-
- private string _patternText;
- private string _alternatePatternText;
- private byte _dimGenBrightness = 127;
- private ushort _infillGenThickness = 10;
- private ushort _infillGenSpacing = 20;
public OperationPixelDimming Operation => BaseOperation as OperationPixelDimming;
- public string PatternText
- {
- get => _patternText;
- set => RaiseAndSetIfChanged(ref _patternText, value);
- }
-
- public string AlternatePatternText
- {
- get => _alternatePatternText;
- set => RaiseAndSetIfChanged(ref _alternatePatternText, value);
- }
-
- public byte DimGenBrightness
- {
- get => _dimGenBrightness;
- set => RaiseAndSetIfChanged(ref _dimGenBrightness, value);
- }
-
- public ushort InfillGenThickness
- {
- get => _infillGenThickness;
- set => RaiseAndSetIfChanged(ref _infillGenThickness, value);
- }
-
- public ushort InfillGenSpacing
- {
- get => _infillGenSpacing;
- set => RaiseAndSetIfChanged(ref _infillGenSpacing, value);
- }
-
+
public ToolPixelDimmingControl()
{
InitializeComponent();
BaseOperation = new OperationPixelDimming();
- GeneratePixelDimming("Chessboard");
+ Operation.GeneratePixelDimming("Chessboard");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
-
- public override bool UpdateOperation()
- {
- var stringMatrix = new[]
- {
- new StringMatrix(PatternText),
- new StringMatrix(AlternatePatternText),
- };
-
- foreach (var item in stringMatrix)
- {
- if (string.IsNullOrWhiteSpace(item.Text)) continue;
- var lines = item.Text.Split('\n');
- for (var row = 0; row < lines.Length; row++)
- {
-
- var bytes = lines[row].Trim().Split(' ');
- if (row == 0)
- {
- item.Pattern = new Matrix<byte>(lines.Length, bytes.Length);
- }
- else
- {
- if (item.Pattern.Cols != bytes.Length)
- {
- ParentWindow.MessageBoxError($"Row {row + 1} have invalid number of pixels, the pattern must have equal pixel count per line, per defined on line 1");
- return false;
- }
- }
-
- for (int col = 0; col < bytes.Length; col++)
- {
- if (byte.TryParse(bytes[col], out var value))
- {
- item.Pattern[row, col] = value;
- }
- else
- {
- ParentWindow.MessageBoxError($"{bytes[col]} is a invalid number, use values from 0 to 255");
- return false;
- }
- }
- }
- }
-
- Operation.Pattern = stringMatrix[0].Pattern;
- Operation.AlternatePattern = stringMatrix[1].Pattern;
-
- return true;
- }
-
- public void GeneratePixelDimming(string pattern)
- {
- if (pattern == "Chessboard")
- {
- PatternText = string.Format(
- "255 {0}{1}" +
- "{0} 255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "{0} 255{1}" +
- "255 {0}"
- , _dimGenBrightness, "\n");
-
- return;
- }
-
- if (pattern == "Sparse")
- {
- PatternText = string.Format(
- "{0} 255 255 255{1}" +
- "255 255 {0} 255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 255 {0} 255{1}" +
- "{0} 255 255 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Crosses")
- {
- PatternText = string.Format(
- "{0} 255 {0} 255{1}" +
- "255 {0} 255 255{1}" +
- "{0} 255 {0} 255{1}" +
- "255 255 255 255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 255 255 255{1}" +
- "{0} 255 {0} 255{1}" +
- "255 {0} 255 255{1}" +
- "{0} 255 {0} 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Strips")
- {
- PatternText = string.Format(
- "{0}{1}" +
- "255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255{1}" +
- "{0}"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Pyramid")
- {
- PatternText = string.Format(
- "255 255 {0} 255 255 255{1}" +
- "255 {0} 255 {0} 255 255{1}" +
- "{0} 255 {0} 255 {0} 255{1}" +
- "255 255 255 255 255 255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 {0} 255 {0} 255 {0}{1}" +
- "255 255 {0} 255 {0} 255{1}" +
- "255 255 255 {0} 255 255{1}" +
- "255 255 255 255 255 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Rhombus")
- {
- PatternText = string.Format(
- "255 {0} 255 255{1}" +
- "{0} 255 {0} 255{1}" +
- "255 {0} 255 255{1}" +
- "255 255 255 255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 255 255 255{1}" +
- "255 {0} 255 255{1}" +
- "{0} 255 {0} 255{1}" +
- "255 {0} 255 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Hearts")
- {
- PatternText = string.Format(
- "255 {0} 255 {0} 255 255{1}" +
- "{0} 255 {0} 255 {0} 255{1}" +
- "{0} 255 255 255 {0} 255{1}" +
- "255 {0} 255 {0} 255 255{1}" +
- "255 255 {0} 255 255 255{1}" +
- "255 255 255 255 255 255"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 255 255 255 255 255{1}" +
- "255 255 {0} 255 {0} 255{1}" +
- "255 {0} 255 {0} 255 {0}{1}" +
- "255 {0} 255 255 255 {0}{1}" +
- "255 255 {0} 255 {0} 255{1}" +
- "255 255 255 {0} 255 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Slashes")
- {
- PatternText = string.Format(
- "{0} 255 255{1}" +
- "255 {0} 255{1}" +
- "255 255 {0}"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 255 {0}{1}" +
- "255 {0} 255{1}" +
- "{0} 255 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Waves")
- {
- PatternText = string.Format(
- "{0} 255 255{1}" +
- "255 255 {0}"
- , _dimGenBrightness, "\n");
-
- AlternatePatternText = string.Format(
- "255 255 {0}{1}" +
- "{0} 255 255"
- , _dimGenBrightness, "\n");
- return;
- }
-
- if (pattern == "Solid")
- {
- PatternText = _dimGenBrightness.ToString();
- AlternatePatternText = null;
- return;
- }
- }
-
- public void GenerateInfill(string pattern)
- {
- if (pattern == "Rectilinear")
- {
- PatternText = ($"0\n".Repeat(_infillGenSpacing) + $"255\n".Repeat(_infillGenSpacing)).Trim('\n', '\r');
- AlternatePatternText = null;
- return;
- }
-
- if (pattern == "Square grid")
- {
- var p1 = "0 ".Repeat(_infillGenSpacing) + "255 ".Repeat(_infillGenThickness);
- p1 = p1.Trim() + "\n";
- p1 += p1.Repeat(_infillGenThickness);
-
-
- var p2 = "255 ".Repeat(_infillGenSpacing) + "255 ".Repeat(_infillGenThickness);
- p2 = p2.Trim() + '\n';
- p2 += p2.Repeat(_infillGenThickness);
-
- p2 = p2.Trim('\n', '\r');
-
- PatternText = p1 + p2;
- AlternatePatternText = null;
- return;
- }
-
- if (pattern == "Waves")
- {
- var p1 = string.Empty;
- var pos = 0;
- for (sbyte dir = 1; dir >= -1; dir -= 2)
- {
- while (pos >= 0 && pos <= _infillGenSpacing)
- {
- p1 += "0 ".Repeat(pos);
- p1 += "255 ".Repeat(_infillGenThickness);
- p1 += "0 ".Repeat(_infillGenSpacing - pos);
- p1 = p1.Trim() + '\n';
-
- pos += dir;
- }
-
- pos--;
- }
-
- PatternText = p1.Trim('\n', '\r');
- AlternatePatternText = null;
- return;
- }
-
- if (pattern == "Lattice")
- {
- var p1 = string.Empty;
- var p2 = string.Empty;
-
- var zeros = Math.Max(0, _infillGenSpacing - _infillGenThickness * 2);
-
- // Pillar
- for (int i = 0; i < _infillGenThickness; i++)
- {
- p1 += "255 ".Repeat(_infillGenThickness);
- p1 += "0 ".Repeat(zeros);
- p1 += "255 ".Repeat(_infillGenThickness);
- p1 = p1.Trim() + '\n';
- }
-
- for (int i = 0; i < zeros; i++)
- {
- p1 += "0 ".Repeat(_infillGenSpacing);
- p1 = p1.Trim() + '\n';
- }
-
- for (int i = 0; i < _infillGenThickness; i++)
- {
- p1 += "255 ".Repeat(_infillGenThickness);
- p1 += "0 ".Repeat(zeros);
- p1 += "255 ".Repeat(_infillGenThickness);
- p1 = p1.Trim() + '\n';
- }
-
- // Square
- for (int i = 0; i < _infillGenThickness; i++)
- {
- p2 += "255 ".Repeat(_infillGenSpacing);
- p2 = p2.Trim() + '\n';
- }
-
- for (int i = 0; i < zeros; i++)
- {
- p2 += "255 ".Repeat(_infillGenThickness);
- p2 += "0 ".Repeat(zeros);
- p2 += "255 ".Repeat(_infillGenThickness);
- p2 = p2.Trim() + '\n';
- }
-
- for (int i = 0; i < _infillGenThickness; i++)
- {
- p2 += "255 ".Repeat(_infillGenSpacing);
- p2 = p2.Trim() + '\n';
- }
-
-
-
- PatternText = p1.Trim('\n', '\r');
- AlternatePatternText = p2.Trim('\n', '\r'); ;
- return;
- }
- }
+
}
}
diff --git a/UVtools.WPF/Controls/Tools/ToolRaftReliefControl.axaml b/UVtools.WPF/Controls/Tools/ToolRaftReliefControl.axaml
index 3acf2cc..fb3a6f4 100644
--- a/UVtools.WPF/Controls/Tools/ToolRaftReliefControl.axaml
+++ b/UVtools.WPF/Controls/Tools/ToolRaftReliefControl.axaml
@@ -8,7 +8,7 @@
<Grid
ColumnDefinitions="Auto,10,Auto,5,Auto"
- RowDefinitions="Auto,10,Auto,Auto,10,Auto,10,Auto"
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto"
>
<TextBlock Text="Relief type:" VerticalAlignment="Center"/>
@@ -20,73 +20,75 @@
Items="{Binding Operation.RaftReliefItems}"/>
<TextBlock
+ Grid.Row="2"
+ IsVisible="{Binding !#ReliefType.SelectedIndex}"
+ Text="Supports margin:" VerticalAlignment="Center"/>
+
+ <TextBlock
Grid.Row="2"
- ToolTip.Tip="Raft will be replaced by the present supports and then dilated by this value.
-&#x0a;Use large numbers with tiny supports for best adhesion."
- Text="Dilate supports by:" VerticalAlignment="Center"
IsVisible="{Binding #ReliefType.SelectedIndex}"
- />
+ ToolTip.Tip="Raft will be replaced by the present supports and then dilated by this value to thicken the supports and increase the adhesion.
+&#x0a;Use large numbers with tiny supports for best adhesion."
+ Text="Dilate supports by:" VerticalAlignment="Center"/>
<NumericUpDown Grid.Row="2" Grid.Column="2"
Width="100"
Minimum="0"
Maximum="255"
- Value="{Binding Operation.DilateIterations}"
- IsVisible="{Binding #ReliefType.SelectedIndex}"/>
+ Value="{Binding Operation.DilateIterations}"/>
<TextBlock
Grid.Row="2" Grid.Column="4"
- Text="px" VerticalAlignment="Center"
- IsVisible="{Binding #ReliefType.SelectedIndex}"/>
+ Text="px" VerticalAlignment="Center"/>
<TextBlock
- Grid.Row="3"
+ Grid.Row="4"
Text="Wall margin:" VerticalAlignment="Center"
IsVisible="{Binding !#ReliefType.SelectedIndex}"
/>
- <NumericUpDown Grid.Row="3" Grid.Column="2"
+ <NumericUpDown Grid.Row="4" Grid.Column="2"
Width="100"
Minimum="1"
Maximum="255"
Value="{Binding Operation.WallMargin}"
IsVisible="{Binding !#ReliefType.SelectedIndex}"/>
<TextBlock
- Grid.Row="3" Grid.Column="4"
+ Grid.Row="4" Grid.Column="4"
Text="px" VerticalAlignment="Center"
IsVisible="{Binding !#ReliefType.SelectedIndex}"/>
<TextBlock
- Grid.Row="5"
+ Grid.Row="6"
Text="Hole diameter:" VerticalAlignment="Center"
IsVisible="{Binding !#ReliefType.SelectedIndex}"
/>
- <NumericUpDown Grid.Row="5" Grid.Column="2"
+ <NumericUpDown Grid.Row="6" Grid.Column="2"
Width="100"
Minimum="10"
Maximum="255"
Value="{Binding Operation.HoleDiameter}"
IsVisible="{Binding !#ReliefType.SelectedIndex}"/>
<TextBlock
- Grid.Row="5" Grid.Column="4"
+ Grid.Row="6" Grid.Column="4"
Text="px" VerticalAlignment="Center"
IsVisible="{Binding !#ReliefType.SelectedIndex}"/>
<TextBlock
- Grid.Row="7"
+ Grid.Row="8"
Text="Hole spacing:" VerticalAlignment="Center"
IsVisible="{Binding !#ReliefType.SelectedIndex}"
/>
- <NumericUpDown Grid.Row="7" Grid.Column="2"
+ <NumericUpDown Grid.Row="8" Grid.Column="2"
Width="100"
Minimum="10"
Maximum="255"
Value="{Binding Operation.HoleSpacing}"
IsVisible="{Binding !#ReliefType.SelectedIndex}"/>
<TextBlock
- Grid.Row="7" Grid.Column="4"
+ Grid.Row="8" Grid.Column="4"
Text="px" VerticalAlignment="Center"
IsVisible="{Binding !#ReliefType.SelectedIndex}"/>
diff --git a/UVtools.WPF/Controls/Tools/ToolResizeControl.axaml b/UVtools.WPF/Controls/Tools/ToolResizeControl.axaml
index 31f49f1..300ecc8 100644
--- a/UVtools.WPF/Controls/Tools/ToolResizeControl.axaml
+++ b/UVtools.WPF/Controls/Tools/ToolResizeControl.axaml
@@ -13,8 +13,8 @@
MaxWidth="150"
Minimum="1"
Maximum="10000"
- Increment="0.01"
- FormatString="{}{0:#,0.00} %"
+ Increment="0.1"
+ FormatString="{}{0:0.00} %"
Value="{Binding Operation.X}"
/>
@@ -29,8 +29,8 @@
MaxWidth="150"
Minimum="1"
Maximum="10000"
- Increment="0.01"
- FormatString="{}{0:#,0.00} %"
+ Increment="0.1"
+ FormatString="{}{0:0.00} %"
Value="{Binding Operation.Y}"
IsEnabled="{Binding !#ConstrainXY.IsChecked}"
/>
diff --git a/UVtools.WPF/Controls/Tools/ToolRotateControl.axaml b/UVtools.WPF/Controls/Tools/ToolRotateControl.axaml
index 832c0e6..293c620 100644
--- a/UVtools.WPF/Controls/Tools/ToolRotateControl.axaml
+++ b/UVtools.WPF/Controls/Tools/ToolRotateControl.axaml
@@ -11,8 +11,8 @@
MaxWidth="150"
Minimum="-359.99"
Maximum="359.99"
- Increment="0.01"
- FormatString="{}{0:#,0.00}"
+ Increment="1.0"
+ FormatString="{}{0:0.00}"
Value="{Binding Operation.AngleDegrees}"
/>
<TextBlock VerticalAlignment="Center" Text="degrees"/>
diff --git a/UVtools.WPF/Extensions/WindowExtensions.cs b/UVtools.WPF/Extensions/WindowExtensions.cs
index 98bc5a3..8ad4405 100644
--- a/UVtools.WPF/Extensions/WindowExtensions.cs
+++ b/UVtools.WPF/Extensions/WindowExtensions.cs
@@ -28,9 +28,11 @@ namespace UVtools.WPF.Extensions
ContentMessage = message,
Icon = icon,
Style = style,
- //WindowIcon = new WindowIcon(App.GetAsset("/Assets/Icons/UVtools.ico")),
+ WindowIcon = new WindowIcon(App.GetAsset("/Assets/Icons/UVtools.ico")),
WindowStartupLocation = location,
- CanResize = false
+ CanResize = false,
+ MaxWidth = App.MaxWindowSize.Width - UserSettings.Instance.General.WindowsHorizontalMargin,
+ ShowInCenter = true
});
return await messageBoxStandardWindow.ShowDialog(window);
@@ -74,5 +76,6 @@ namespace UVtools.WPF.Extensions
window.DataContext = new object();
window.DataContext = old;
}
+
}
}
diff --git a/UVtools.WPF/MainWindow.Clipboard.cs b/UVtools.WPF/MainWindow.Clipboard.cs
index b2a4f11..6ef670c 100644
--- a/UVtools.WPF/MainWindow.Clipboard.cs
+++ b/UVtools.WPF/MainWindow.Clipboard.cs
@@ -10,6 +10,7 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Threading;
using MessageBox.Avalonia.Enums;
using UVtools.WPF.Extensions;
@@ -48,6 +49,32 @@ namespace UVtools.WPF
}
}
+ public void ClipboardUndo()
+ {
+ if ((_globalModifiers & KeyModifiers.Shift) != 0)
+ {
+ ClipboardUndo(true);
+ return;
+ }
+ Clipboard.Undo();
+ }
+
+ public async void ClipboardUndo(bool rerun)
+ {
+ var clip = Clipboard.CurrentClip;
+ Clipboard.Undo();
+ if (!rerun)
+ {
+ return;
+ }
+ if (clip?.Operation is null) return;
+ var operation = await ShowRunOperation(clip.Operation.GetType(), clip.Operation);
+ if (operation is null)
+ {
+ Clipboard.Redo();
+ }
+ }
+
public async void ClipboardClear()
{
if (await this.MessageBoxQuestion("Are you sure you want to clear the clipboard?\n" +
diff --git a/UVtools.WPF/MainWindow.GCode.cs b/UVtools.WPF/MainWindow.GCode.cs
index b46f03a..01f4793 100644
--- a/UVtools.WPF/MainWindow.GCode.cs
+++ b/UVtools.WPF/MainWindow.GCode.cs
@@ -42,7 +42,6 @@ namespace UVtools.WPF
Directory = Path.GetDirectoryName(SlicerFile.FileFullPath),
InitialFileName = $"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_gcode.txt"
};
-
var file = await dialog.ShowAsync(this);
if (string.IsNullOrEmpty(file)) return;
diff --git a/UVtools.WPF/MainWindow.Information.cs b/UVtools.WPF/MainWindow.Information.cs
index 594a698..7a8864d 100644
--- a/UVtools.WPF/MainWindow.Information.cs
+++ b/UVtools.WPF/MainWindow.Information.cs
@@ -330,7 +330,7 @@ namespace UVtools.WPF
#region Current Layer
- public void AddCurrentLayerData()
+ public void RefreshCurrentLayerData()
{
var layer = LayerCache.Layer;
CurrentLayerProperties.Clear();
diff --git a/UVtools.WPF/MainWindow.Issues.cs b/UVtools.WPF/MainWindow.Issues.cs
index e06b098..53cef90 100644
--- a/UVtools.WPF/MainWindow.Issues.cs
+++ b/UVtools.WPF/MainWindow.Issues.cs
@@ -270,14 +270,16 @@ namespace UVtools.WPF
var issueList = Issues.ToList();
- foreach (var layerIndex in islandConfig.WhiteListLayers)
+ issueList.RemoveAll(issue =>
+ islandConfig.WhiteListLayers.Contains(issue.LayerIndex) && (issue.Type == LayerIssue.IssueType.Island ||
+ issue.Type == LayerIssue.IssueType.Overhang));
+ /*foreach (var layerIndex in islandConfig.WhiteListLayers)
{
- foreach (var issue in Issues)
- {
- if (issue.LayerIndex != layerIndex && (issue.Type == LayerIssue.IssueType.Island || issue.Type == LayerIssue.IssueType.Overhang)) continue;
- issueList.Remove(issue);
- }
- }
+ issueList.RemoveAll(issue =>
+ issue.LayerIndex == layerIndex && (issue.Type == LayerIssue.IssueType.Island ||
+ issue.Type == LayerIssue.IssueType.Overhang));
+
+ }*/
var resultIssues = await Task.Factory.StartNew(() =>
{
@@ -307,15 +309,15 @@ namespace UVtools.WPF
IsGUIEnabled = true;
- if (resultIssues is null || resultIssues.Count == 0) return;
-
- issueList.AddRange(resultIssues);
+ if (resultIssues is not null && resultIssues.Count > 0) issueList.AddRange(resultIssues);
+
issueList = issueList.OrderBy(issue => issue.Type)
.ThenBy(issue => issue.LayerIndex)
.ThenBy(issue => issue.PixelsCount).ToList();
Issues.Clear();
Issues.AddRange(issueList);
+ UpdateLayerTrackerHighlightIssues();
}
public int IssueSelectedIndex
diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs
index 984bd60..5f3c67b 100644
--- a/UVtools.WPF/MainWindow.LayerPreview.cs
+++ b/UVtools.WPF/MainWindow.LayerPreview.cs
@@ -10,7 +10,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -19,7 +18,6 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.Platform;
using Avalonia.Threading;
using Emgu.CV;
using Emgu.CV.CvEnum;
@@ -27,7 +25,6 @@ using Emgu.CV.Structure;
using Emgu.CV.Util;
using UVtools.Core;
using UVtools.Core.Extensions;
-using UVtools.Core.Objects;
using UVtools.Core.PixelEditor;
using UVtools.WPF.Controls;
using UVtools.WPF.Extensions;
@@ -776,7 +773,7 @@ namespace UVtools.WPF
LayerImageBox.Image = LayerCache.Bitmap = LayerCache.ImageBgr.ToBitmap();
- AddCurrentLayerData();
+ RefreshCurrentLayerData();
watch.Stop();
ShowLayerRenderMs = watch.ElapsedMilliseconds;
@@ -1313,7 +1310,7 @@ namespace UVtools.WPF
if (DrawingPixelDrawing.BrushSize > 1)
{
- cursor = EmguExtensions.InitMat(new System.Drawing.Size(DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.BrushSize), DepthType.Cv8U, 4);
+ cursor = EmguExtensions.InitMat(new System.Drawing.Size(DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.BrushSize), 4);
switch (DrawingPixelDrawing.BrushShape)
{
case PixelDrawing.BrushShapeType.Rectangle:
@@ -1351,7 +1348,7 @@ namespace UVtools.WPF
int baseLine = 0;
var size = CvInvoke.GetTextSize(text, DrawingPixelText.Font, DrawingPixelText.FontScale, DrawingPixelText.Thickness, ref baseLine);
- cursor = EmguExtensions.InitMat(size, DepthType.Cv8U, 4);
+ cursor = EmguExtensions.InitMat(size, 4);
CvInvoke.PutText(cursor, text, new Point(0, 0), DrawingPixelText.Font, DrawingPixelText.FontScale, _pixelEditorCursorColor, DrawingPixelText.Thickness, DrawingPixelText.LineType, DrawingPixelText.Mirror);
break;
case PixelOperation.PixelOperationType.Supports:
@@ -1361,7 +1358,7 @@ namespace UVtools.WPF
if (diameter >= _pixelEditorCursorMinDiamater)
{
- cursor = EmguExtensions.InitMat(new System.Drawing.Size(diameter, diameter), DepthType.Cv8U, 4);
+ cursor = EmguExtensions.InitMat(new System.Drawing.Size(diameter, diameter), 4);
var center = new Point(diameter / 2, diameter / 2);
CvInvoke.Circle(cursor,
center,
diff --git a/UVtools.WPF/MainWindow.PixelEditor.cs b/UVtools.WPF/MainWindow.PixelEditor.cs
index 2800516..8f0d32e 100644
--- a/UVtools.WPF/MainWindow.PixelEditor.cs
+++ b/UVtools.WPF/MainWindow.PixelEditor.cs
@@ -177,7 +177,7 @@ namespace UVtools.WPF
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
{
var operationDrawing = new PixelDrawing(layerIndex, realLocation, DrawingPixelDrawing.LineType,
- DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.Thickness, isAdd);
+ DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.RemovePixelBrightness, DrawingPixelDrawing.PixelBrightness, isAdd);
//if (PixelHistory.Contains(operation)) continue;
Drawings.Add(operationDrawing);
@@ -260,7 +260,7 @@ namespace UVtools.WPF
{
var operationText = new PixelText(layerIndex, realLocation, DrawingPixelText.LineType,
DrawingPixelText.Font, DrawingPixelText.FontScale, DrawingPixelText.Thickness,
- DrawingPixelText.Text, DrawingPixelText.Mirror, isAdd);
+ DrawingPixelText.Text, DrawingPixelText.Mirror, DrawingPixelText.RemovePixelBrightness, DrawingPixelText.PixelBrightness, isAdd);
//if (PixelHistory.Contains(operation)) continue;
//PixelHistory.Add(operation);
@@ -289,7 +289,7 @@ namespace UVtools.WPF
ActualLayer + DrawingPixelEraser.LayersAbove);
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
{
- var operationEraser = new PixelEraser(layerIndex, realLocation);
+ var operationEraser = new PixelEraser(layerIndex, realLocation, DrawingPixelEraser.PixelBrightness);
//if (PixelHistory.Contains(operation)) continue;
Drawings.Add(operationEraser);
@@ -317,7 +317,7 @@ namespace UVtools.WPF
if (_actualLayer == 0) return;
var operationSupport = new PixelSupport(ActualLayer, realLocation,
DrawingPixelSupport.TipDiameter, DrawingPixelSupport.PillarDiameter,
- DrawingPixelSupport.BaseDiameter);
+ DrawingPixelSupport.BaseDiameter, DrawingPixelSupport.PixelBrightness);
//if (PixelHistory.Contains(operation)) return;
Drawings.Add(operationSupport);
@@ -420,12 +420,12 @@ namespace UVtools.WPF
List<uint> whiteListLayers = new List<uint>();
foreach (var item in Drawings)
{
- if (item.OperationType != PixelOperation.PixelOperationType.Drawing &&
+ /*if (item.OperationType != PixelOperation.PixelOperationType.Drawing &&
item.OperationType != PixelOperation.PixelOperationType.Text &&
item.OperationType != PixelOperation.PixelOperationType.Eraser &&
- item.OperationType != PixelOperation.PixelOperationType.Supports) continue;
- if (whiteListLayers.Contains(item.LayerIndex)) continue;
- whiteListLayers.Add(item.LayerIndex);
+ item.OperationType != PixelOperation.PixelOperationType.Supports) continue;*/
+ if (!whiteListLayers.Contains(item.LayerIndex))
+ whiteListLayers.Add(item.LayerIndex);
uint nextLayer = item.LayerIndex + 1;
if (nextLayer < SlicerFile.LayerCount &&
diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml
index b5cd862..1036ce3 100644
--- a/UVtools.WPF/MainWindow.axaml
+++ b/UVtools.WPF/MainWindow.axaml
@@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:manager="clr-namespace:Avalonia.ThemeManager;assembly=Avalonia.ThemeManager"
xmlns:uc="clr-namespace:UVtools.WPF.Controls"
- xmlns:idc="clr-namespace:Dock.Avalonia.Controls;assembly=Dock.Avalonia"
mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="600"
x:Class="UVtools.WPF.MainWindow"
Title="UVtools"
@@ -135,6 +134,13 @@
IsEnabled="{Binding IsFileLoaded}"
Items="{Binding MenuTools}">
</MenuItem>
+
+ <MenuItem
+ Header="_Calibration"
+ IsVisible="{Binding IsFileLoaded}"
+ IsEnabled="{Binding IsFileLoaded}"
+ Items="{Binding MenuCalibration}">
+ </MenuItem>
<MenuItem Header="_Help">
@@ -375,6 +381,7 @@
</StackPanel>
+ <!-- Preview image -->
<Image
IsVisible="{Binding SlicerFile.CreatedThumbnailsCount}"
Stretch="Uniform"
@@ -413,9 +420,12 @@
VerticalAlignment="Center">
<Button
- IsEnabled="{Binding SlicerFile.CreatedThumbnailsCount}"
- ToolTip.Tip="Save properties to a file or clipboard"
- Command="{Binding #PropertiesSaveContextMenu.Open}">
+ Name="PropertiesSaveButton"
+ IsEnabled="{Binding SlicerFile.CreatedThumbnailsCount}"
+ ToolTip.Tip="Save properties to a file or clipboard"
+ Command="{Binding OpenContextMenu}"
+ CommandParameter="PropertiesSave"
+ >
<Button.ContextMenu>
<ContextMenu Name="PropertiesSaveContextMenu" PlacementMode="Bottom">
<MenuItem
@@ -468,7 +478,8 @@
</DataGrid.Columns>
</DataGrid>
-
+
+ <!-- Layer data -->
<TextBlock Grid.Row="5"
Text="Layer data"
ToolTip.Tip="Shows the properties for the current selected layer"
@@ -543,9 +554,10 @@
<Image Source="/Assets/Icons/refresh-16x16.png"/>
</Button>
- <Button
+ <Button Name="GcodeSaveButton"
ToolTip.Tip="Save gcode to a file or clipboard"
- Command="{Binding #GcodeSaveContextMenu.Open}">
+ Command="{Binding OpenContextMenu}"
+ CommandParameter="GcodeSave">
<Button.ContextMenu>
<ContextMenu Name="GcodeSaveContextMenu" PlacementMode="Bottom">
<MenuItem
@@ -780,8 +792,8 @@
<Grid
- RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,Auto"
- ColumnDefinitions="Auto,10,Auto,10,Auto,10,Auto">
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,Auto"
+ ColumnDefinitions="Auto,10,Auto,10,Auto,5,Auto">
<TextBlock
Grid.Row="0"
@@ -847,33 +859,71 @@
Text="px" />
<TextBlock
- Grid.Row="8"
+ Grid.Row="8"
+ Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Remove pixel brightness:" />
+ <NumericUpDown
+ Grid.Row="8"
+ Grid.Column="2"
+ Grid.ColumnSpan="3"
+ Minimum="0"
+ Maximum="255"
+ ClipValueToMinMax="True"
+ Value="{Binding DrawingPixelDrawing.RemovePixelBrightness}"/>
+ <TextBlock
+ Grid.Row="8"
+ Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding DrawingPixelDrawing.RemovePixelBrightnessPercent, StringFormat=\{0:0.00\}%}" />
+
+ <TextBlock
+ Grid.Row="10"
+ Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Add pixel brightness:" />
+ <NumericUpDown
+ Grid.Row="10"
+ Grid.Column="2"
+ Grid.ColumnSpan="3"
+ Minimum="1"
+ Maximum="255"
+ ClipValueToMinMax="True"
+ Value="{Binding DrawingPixelDrawing.PixelBrightness}"/>
+ <TextBlock
+ Grid.Row="10"
+ Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding DrawingPixelDrawing.PixelBrightnessPercent, StringFormat=\{0:0.00\}%}" />
+
+ <TextBlock
+ Grid.Row="12"
Grid.Column="0"
VerticalAlignment="Center"
Text="Layers depth:" />
<NumericUpDown
- Grid.Row="8"
+ Grid.Row="12"
Grid.Column="2"
Minimum="0"
Width="80"
Value="{Binding DrawingPixelDrawing.LayersBelow}"
/>
<TextBlock
- Grid.Row="9"
+ Grid.Row="13"
Grid.Column="2"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="Below" />
<NumericUpDown
- Grid.Row="8"
+ Grid.Row="12"
Grid.Column="4"
Minimum="0"
Width="80"
Value="{Binding DrawingPixelDrawing.LayersAbove}"
/>
<TextBlock
- Grid.Row="9"
+ Grid.Row="13"
Grid.Column="4"
VerticalAlignment="Center"
HorizontalAlignment="Center"
@@ -906,8 +956,8 @@
</Border>
<Grid
- RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,Auto"
- ColumnDefinitions="Auto,10,Auto,10,Auto">
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,Auto"
+ ColumnDefinitions="Auto,10,Auto,10,Auto,5,Auto">
<TextBlock
Grid.Row="0"
@@ -979,35 +1029,73 @@
Content="Flip text Vertically"
IsChecked="{Binding DrawingPixelText.Mirror}"/>
+ <TextBlock
+ Grid.Row="12"
+ Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Remove pixel brightness:" />
+ <NumericUpDown
+ Grid.Row="12"
+ Grid.Column="2"
+ Grid.ColumnSpan="3"
+ Minimum="0"
+ Maximum="255"
+ ClipValueToMinMax="True"
+ Value="{Binding DrawingPixelText.RemovePixelBrightness}"/>
+ <TextBlock
+ Grid.Row="12"
+ Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding DrawingPixelText.RemovePixelBrightnessPercent, StringFormat=\{0:0.00\}%}" />
<TextBlock
- Grid.Row="12"
+ Grid.Row="14"
+ Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Add pixel brightness:" />
+ <NumericUpDown
+ Grid.Row="14"
+ Grid.Column="2"
+ Grid.ColumnSpan="3"
+ Minimum="1"
+ Maximum="255"
+ ClipValueToMinMax="True"
+ Value="{Binding DrawingPixelText.PixelBrightness}"/>
+ <TextBlock
+ Grid.Row="14"
+ Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding DrawingPixelText.PixelBrightnessPercent, StringFormat=\{0:0.00\}%}" />
+
+
+ <TextBlock
+ Grid.Row="16"
Grid.Column="0"
VerticalAlignment="Center"
Text="Layers depth:" />
<NumericUpDown
- Grid.Row="12"
+ Grid.Row="16"
Grid.Column="2"
Minimum="0"
Width="80"
Value="{Binding DrawingPixelText.LayersBelow}"
/>
<TextBlock
- Grid.Row="14"
+ Grid.Row="17"
Grid.Column="2"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="Below" />
<NumericUpDown
- Grid.Row="12"
+ Grid.Row="16"
Grid.Column="4"
Minimum="0"
Width="80"
Value="{Binding DrawingPixelText.LayersAbove}"
/>
<TextBlock
- Grid.Row="14"
+ Grid.Row="17"
Grid.Column="4"
VerticalAlignment="Center"
HorizontalAlignment="Center"
@@ -1039,37 +1127,56 @@
</Border>
<Grid
- RowDefinitions="Auto,Auto"
- ColumnDefinitions="Auto,10,Auto,10,Auto">
-
- <TextBlock
- Grid.Row="0"
+ RowDefinitions="Auto,10,Auto,Auto"
+ ColumnDefinitions="Auto,10,Auto,10,Auto,5,Auto">
+
+ <TextBlock
+ Grid.Row="0"
+ Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Pixel brightness:" />
+ <NumericUpDown
+ Grid.Row="0"
+ Grid.Column="2"
+ Grid.ColumnSpan="3"
+ Minimum="0"
+ Maximum="255"
+ ClipValueToMinMax="True"
+ Value="{Binding DrawingPixelEraser.PixelBrightness}"/>
+ <TextBlock
+ Grid.Row="0"
+ Grid.Column="6"
+ VerticalAlignment="Center"
+ Text="{Binding DrawingPixelEraser.PixelBrightnessPercent, StringFormat=\{0:0.00\}%}" />
+
+ <TextBlock
+ Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"
Text="Layers depth:" />
<NumericUpDown
- Grid.Row="0"
+ Grid.Row="2"
Grid.Column="2"
Minimum="0"
Width="80"
Value="{Binding DrawingPixelEraser.LayersBelow}"
/>
<TextBlock
- Grid.Row="1"
+ Grid.Row="3"
Grid.Column="2"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="Below" />
<NumericUpDown
- Grid.Row="0"
+ Grid.Row="2"
Grid.Column="4"
Minimum="0"
Width="80"
Value="{Binding DrawingPixelEraser.LayersAbove}"
/>
<TextBlock
- Grid.Row="1"
+ Grid.Row="3"
Grid.Column="4"
VerticalAlignment="Center"
HorizontalAlignment="Center"
@@ -1101,7 +1208,7 @@
&#x0a;Note: this operation can't be previewed."/>
</Border>
<Grid
- RowDefinitions="Auto,10,Auto,10,Auto"
+ RowDefinitions="Auto,10,Auto,10,Auto,10,Auto"
ColumnDefinitions="Auto,10,100,5,Auto">
<TextBlock
Grid.Row="0"
@@ -1154,6 +1261,24 @@
VerticalAlignment="Center"
Text="px" />
+ <TextBlock
+ Grid.Row="6"
+ Grid.Column="0"
+ VerticalAlignment="Center"
+ Text="Pixel brightness:" />
+ <NumericUpDown
+ Grid.Row="6"
+ Grid.Column="2"
+ Minimum="0"
+ Maximum="255"
+ ClipValueToMinMax="True"
+ Value="{Binding DrawingPixelSupport.PixelBrightness}"/>
+ <TextBlock
+ Grid.Row="6"
+ Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="{Binding DrawingPixelSupport.PixelBrightnessPercent, StringFormat=\{0:0.00\}%}" />
+
</Grid>
</StackPanel>
@@ -1306,9 +1431,10 @@
<Button
IsEnabled="{Binding Clipboard.CanUndo}"
- Command="{Binding Clipboard.Undo}"
+ Command="{Binding ClipboardUndo}"
HotKey="Ctrl + Z"
- ToolTip.Tip="Undo [Ctrl + Z]">
+ ToolTip.Tip="Undo [Ctrl + Z]
+&#x0a;Shift + Click to Undo and edit last operation [Ctrl + Shift + Z]">
<Image Source="/Assets/Icons/undo-16x16.png" Width="16"/>
</Button>
@@ -1618,9 +1744,10 @@
</StackPanel>
</ToggleButton>
- <Button
+ <Button Name="LayerPreviewOutlineButton"
ToolTip.Tip="Click to access the various outlines."
- Command="{Binding #LayerPreviewOutlineContextMenu.Open}"
+ Command="{Binding OpenContextMenu}"
+ CommandParameter="LayerPreviewOutline"
Margin="1,0,0,0"
>
<Button.ContextMenu>
@@ -1661,9 +1788,9 @@
<WrapPanel HorizontalAlignment="Right" Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
- <Button
- Command="{Binding #LayerActionsContextMenu.Open}"
- >
+ <Button Name="LayerActionsButton"
+ Command="{Binding OpenContextMenu}"
+ CommandParameter="LayerActions">
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/Icons/layers-alt-16x16.png"/>
<TextBlock Margin="5,0,5,0" Text="Actions ⮟"/>
diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs
index 9123232..51c2286 100644
--- a/UVtools.WPF/MainWindow.axaml.cs
+++ b/UVtools.WPF/MainWindow.axaml.cs
@@ -19,6 +19,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
+using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -28,6 +29,7 @@ using UVtools.Core.FileFormats;
using UVtools.Core.Managers;
using UVtools.Core.Operations;
using UVtools.WPF.Controls;
+using UVtools.WPF.Controls.Calibrators;
using UVtools.WPF.Controls.Tools;
using UVtools.WPF.Extensions;
using UVtools.WPF.Structures;
@@ -35,6 +37,7 @@ using UVtools.WPF.Windows;
using Bitmap = Avalonia.Media.Imaging.Bitmap;
using Helpers = UVtools.WPF.Controls.Helpers;
using Path = System.IO.Path;
+using Point = Avalonia.Point;
namespace UVtools.WPF
{
@@ -208,9 +211,45 @@ namespace UVtools.WPF
},
};
+ public static MenuItem[] MenuCalibration { get; } =
+ {
+ new()
+ {
+ Tag = new OperationCalibrateElephantFoot(),
+ Icon = new Avalonia.Controls.Image
+ {
+ Source = new Bitmap(App.GetAsset("/Assets/Icons/elephant-foot-16x16.png"))
+ }
+ },
+ new()
+ {
+ Tag = new OperationCalibrateXYZAccuracy(),
+ Icon = new Avalonia.Controls.Image
+ {
+ Source = new Bitmap(App.GetAsset("/Assets/Icons/cubes-16x16.png"))
+ }
+ },
+ new()
+ {
+ Tag = new OperationCalibrateTolerance(),
+ Icon = new Avalonia.Controls.Image
+ {
+ Source = new Bitmap(App.GetAsset("/Assets/Icons/dot-circle-16x16.png"))
+ }
+ },
+ new()
+ {
+ Tag = new OperationCalibrateGrayscale(),
+ Icon = new Avalonia.Controls.Image
+ {
+ Source = new Bitmap(App.GetAsset("/Assets/Icons/chart-pie-16x16.png"))
+ }
+ },
+ };
+
public static MenuItem[] LayerActionsMenu { get; } =
{
- new MenuItem
+ new()
{
Tag = new OperationLayerImport(),
Icon = new Avalonia.Controls.Image
@@ -218,7 +257,7 @@ namespace UVtools.WPF
Source = new Bitmap(App.GetAsset("/Assets/Icons/file-import-16x16.png"))
}
},
- new MenuItem
+ new()
{
Tag = new OperationLayerClone(),
Icon = new Avalonia.Controls.Image
@@ -226,7 +265,7 @@ namespace UVtools.WPF
Source = new Bitmap(App.GetAsset("/Assets/Icons/copy-16x16.png"))
}
},
- new MenuItem
+ new()
{
Tag = new OperationLayerRemove(),
Icon = new Avalonia.Controls.Image
@@ -255,7 +294,7 @@ namespace UVtools.WPF
private PointerEventArgs _globalPointerEventArgs;
private PointerPoint _globalPointerPoint;
- private KeyModifiers _globalModifiers;
+ private KeyModifiers _globalModifiers = KeyModifiers.None;
private TabItem _selectedTabItem;
private TabItem _lastSelectedTab;
private TabItem _lastSelectedTabItem;
@@ -361,6 +400,9 @@ namespace UVtools.WPF
#if DEBUG
//this.AttachDevTools();
#endif
+
+ UpdateMaxWindowsSize();
+
App.ThemeSelector?.EnableThemes(this);
InitInformation();
InitIssues();
@@ -376,7 +418,7 @@ namespace UVtools.WPF
TabLog = this.FindControl<TabItem>("TabLog");
- foreach (var menuItem in new[] { MenuTools, LayerActionsMenu })
+ foreach (var menuItem in new[] { MenuTools, MenuCalibration, LayerActionsMenu })
{
foreach (var menuTool in menuItem)
{
@@ -405,8 +447,8 @@ namespace UVtools.WPF
UpdateTitle();
if (Settings.General.StartMaximized
- || ClientSize.Width > Screens.Primary.Bounds.Width / Screens.Primary.PixelDensity
- || ClientSize.Height > Screens.Primary.Bounds.Height / Screens.Primary.PixelDensity)
+ || ClientSize.Width > App.MaxWindowSize.Width
+ || ClientSize.Height > App.MaxWindowSize.Height)
{
WindowState = WindowState.Maximized;
}
@@ -419,6 +461,12 @@ namespace UVtools.WPF
});
}
+ public void UpdateMaxWindowsSize()
+ {
+ App.MaxWindowSize = new System.Drawing.Size(Settings.General.WindowsTakeIntoAccountScreenScaling ? (int)(Screens.Primary.WorkingArea.Width / Screens.Primary.PixelDensity) : Screens.Primary.WorkingArea.Width,
+ Settings.General.WindowsTakeIntoAccountScreenScaling ? (int)(Screens.Primary.WorkingArea.Height / Screens.Primary.PixelDensity) : Screens.Primary.WorkingArea.Height);
+ }
+
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
@@ -543,6 +591,17 @@ namespace UVtools.WPF
{
base.OnKeyUp(e);
_globalModifiers = e.KeyModifiers;
+ if ((e.Key == Key.LeftShift ||
+ e.Key == Key.RightShift ||
+ (e.KeyModifiers & KeyModifiers.Shift) != 0) &&
+ (e.KeyModifiers & KeyModifiers.Control) != 0 &&
+ e.Key == Key.Z)
+ {
+ e.Handled = true;
+ ClipboardUndo(true);
+ return;
+ }
+
if (e.Key == Key.LeftShift ||
e.Key == Key.RightShift ||
(e.KeyModifiers & KeyModifiers.Shift) == 0 ||
@@ -556,8 +615,17 @@ namespace UVtools.WPF
e.Handled = true;
}
}
-
+ public void OpenContextMenu(string name)
+ {
+ var menu = this.FindControl<ContextMenu>($"{name}ContextMenu");
+ if (menu is null) return;
+ var parent = this.FindControl<Button>($"{name}Button");
+ if (parent is null) return;
+ menu.Open(parent);
+ }
+
+
#endregion
@@ -681,6 +749,7 @@ namespace UVtools.WPF
{
SettingsWindow settingsWindow = new SettingsWindow();
await settingsWindow.ShowDialog(this);
+ UpdateMaxWindowsSize();
}
public void OpenWebsite()
@@ -945,8 +1014,16 @@ namespace UVtools.WPF
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
- ProgressWindow.SetTitle(title);
- await ProgressWindow.ShowDialog(this);
+ try
+ {
+ ProgressWindow.SetTitle(title);
+ await ProgressWindow.ShowDialog(this);
+ }
+ catch (Exception e)
+ {
+ Debug.WriteLine(e);
+ }
+
});
}
}
@@ -1078,6 +1155,11 @@ namespace UVtools.WPF
}
catch (Exception ex)
{
+ SlicerFile.FileFullPath = oldFile;
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
Dispatcher.UIThread.InvokeAsync(async () =>
await this.MessageBoxError(ex.ToString(), "Error while saving the file"));
}
@@ -1150,24 +1232,27 @@ namespace UVtools.WPF
}
#region Operations
- public async Task<Operation> ShowRunOperation(Type type)
+ public async Task<Operation> ShowRunOperation(Type type, Operation loadOperation = null)
{
- var operation = await ShowOperation(type);
+ var operation = await ShowOperation(type, loadOperation);
await RunOperation(operation);
return operation;
}
- public async Task<Operation> ShowOperation(Type type)
+ public async Task<Operation> ShowOperation(Type type, Operation loadOperation = null)
{
- var typeBase = typeof(ToolControl);
- var classname = $"{typeBase.Namespace}.Tool{type.Name.Remove(0, Operation.ClassNameLength)}Control";
+ var toolTypeBase = typeof(ToolControl);
+ var calibrateTypeBase = typeof(CalibrateElephantFootControl);
+ var classname = type.Name.StartsWith("OperationCalibrate") ?
+ $"{calibrateTypeBase.Namespace}.{type.Name.Remove(0, Operation.ClassNameLength)}Control" :
+ $"{toolTypeBase.Namespace}.Tool{type.Name.Remove(0, Operation.ClassNameLength)}Control"; ;
var controlType = Type.GetType(classname);
ToolControl control;
bool removeContent = false;
if (controlType is null)
{
- controlType = typeBase;
+ controlType = toolTypeBase;
removeContent = true;
control = new ToolControl(type.CreateInstance<Operation>());
}
@@ -1177,6 +1262,9 @@ namespace UVtools.WPF
if (control is null) return null;
}
+ if(loadOperation is not null)
+ control.BaseOperation = loadOperation;
+
if (!control.CanRun)
{
return null;
@@ -1208,6 +1296,7 @@ namespace UVtools.WPF
}*/
SlicerFile.EditPrintParameters(operation);
RefreshProperties();
+ RefreshCurrentLayerData();
ResetDataContext();
CanSave = true;
@@ -1318,6 +1407,20 @@ namespace UVtools.WPF
SlicerFile.LayerManager.RemoveLayers(operation, ProgressWindow.RestartProgress(operation.CanCancel));
break;
+ // Calibrators
+ case OperationCalibrateElephantFoot operation:
+ SlicerFile.LayerManager.CalibrateElephantFoot(operation, ProgressWindow.RestartProgress(operation.CanCancel));
+ break;
+ case OperationCalibrateXYZAccuracy operation:
+ SlicerFile.LayerManager.CalibrateXYZAccuracy(operation, ProgressWindow.RestartProgress(operation.CanCancel));
+ break;
+ case OperationCalibrateTolerance operation:
+ SlicerFile.LayerManager.CalibrateTolerance(operation, ProgressWindow.RestartProgress(operation.CanCancel));
+ break;
+ case OperationCalibrateGrayscale operation:
+ SlicerFile.LayerManager.CalibrateGrayscale(operation, ProgressWindow.RestartProgress(operation.CanCancel));
+ break;
+
default:
throw new NotImplementedException();
}
@@ -1347,9 +1450,7 @@ namespace UVtools.WPF
if (result)
{
- string description = baseOperation.ToString();
- if (!description.StartsWith(baseOperation.Title)) description = $"{baseOperation.Title}: {description}";
- ClipboardManager.Instance.Clip(description, backup);
+ ClipboardManager.Instance.Clip(baseOperation, backup);
ShowLayer();
RefreshProperties();
diff --git a/UVtools.WPF/Structures/OperationProfiles.cs b/UVtools.WPF/Structures/OperationProfiles.cs
index 1ce9503..57e5c72 100644
--- a/UVtools.WPF/Structures/OperationProfiles.cs
+++ b/UVtools.WPF/Structures/OperationProfiles.cs
@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
+using DynamicData;
using UVtools.Core.Operations;
namespace UVtools.WPF.Structures
@@ -39,6 +40,9 @@ namespace UVtools.WPF.Structures
[XmlElement(typeof(OperationResize))]
[XmlElement(typeof(OperationRotate))]
[XmlElement(typeof(OperationThreshold))]
+ [XmlElement(typeof(OperationCalibrateElephantFoot))]
+ [XmlElement(typeof(OperationCalibrateXYZAccuracy))]
+ [XmlElement(typeof(OperationCalibrateGrayscale))]
public List<Operation> Operations { get; internal set; } = new List<Operation>();
[XmlIgnore]
diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj
index 22d8777..15aaf26 100644
--- a/UVtools.WPF/UVtools.WPF.csproj
+++ b/UVtools.WPF/UVtools.WPF.csproj
@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<AssemblyName>UVtools</AssemblyName>
<ApplicationIcon>UVtools.ico</ApplicationIcon>
<Authors>Tiago Conceição</Authors>
<Company>PTRTECH</Company>
- <Description>MSLA/DLP, file analysis, repair, conversion and manipulation</Description>
+ <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description>
<Copyright>Copyright © 2020 PTRTECH</Copyright>
<PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
- <Version>1.4.0</Version>
+ <Version>2.0.0</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -24,15 +24,14 @@
<NoWarn>1701;1702;</NoWarn>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Avalonia" Version="0.10.0-preview6" />
+ <PackageReference Include="Avalonia" Version="0.10.0-rc1" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
- <PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.0-preview6" />
- <PackageReference Include="Avalonia.Desktop" Version="0.10.0-preview6" />
- <PackageReference Include="Avalonia.ThemeManager" Version="0.10.0-preview6" />
- <PackageReference Include="Emgu.CV.runtime.ubuntu" Version="4.4.0.4099" />
+ <PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.0-rc1" />
+ <PackageReference Include="Avalonia.Desktop" Version="0.10.0-rc1" />
+ <PackageReference Include="Avalonia.ThemeManager" Version="0.10.0-rc1" />
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.4.0.4099" />
- <PackageReference Include="MessageBox.Avalonia" Version="0.10.0-prev2" />
- <PackageReference Include="ThemeEditor.Controls.ColorPicker" Version="0.10.0-preview6" />
+ <PackageReference Include="MessageBox.Avalonia" Version="0.10.7-rc1" />
+ <PackageReference Include="ThemeEditor.Controls.ColorPicker" Version="0.10.0-rc1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\UVtools.Core\UVtools.Core.csproj" />
diff --git a/UVtools.WPF/UVtools.WPF.csproj.DotSettings b/UVtools.WPF/UVtools.WPF.csproj.DotSettings
new file mode 100644
index 0000000..6162834
--- /dev/null
+++ b/UVtools.WPF/UVtools.WPF.csproj.DotSettings
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp90</s:String></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs
index a27c687..613f2d4 100644
--- a/UVtools.WPF/UserSettings.cs
+++ b/UVtools.WPF/UserSettings.cs
@@ -35,6 +35,9 @@ namespace UVtools.WPF
private bool _startMaximized = true;
private bool _checkForUpdatesOnStartup = true;
private bool _loadDemoFileOnStartup = true;
+ private bool _windowsTakeIntoAccountScreenScaling = true;
+ private ushort _windowsHorizontalMargin = 200;
+ private ushort _windowsVerticalMargin = 250;
private byte _defaultOpenFileExtensionIndex;
private string _defaultDirectoryOpenFile;
private string _defaultDirectorySaveFile;
@@ -44,6 +47,7 @@ namespace UVtools.WPF
private string _fileSaveNamePrefix;
private string _fileSaveNameSuffix = "_copy";
private int _maxDegreeOfParallelism;
+
public bool StartMaximized
{
@@ -63,6 +67,24 @@ namespace UVtools.WPF
set => RaiseAndSetIfChanged(ref _loadDemoFileOnStartup, value);
}
+ public bool WindowsTakeIntoAccountScreenScaling
+ {
+ get => _windowsTakeIntoAccountScreenScaling;
+ set => RaiseAndSetIfChanged(ref _windowsTakeIntoAccountScreenScaling, value);
+ }
+
+ public ushort WindowsHorizontalMargin
+ {
+ get => _windowsHorizontalMargin;
+ set => RaiseAndSetIfChanged(ref _windowsHorizontalMargin, value);
+ }
+
+ public ushort WindowsVerticalMargin
+ {
+ get => _windowsVerticalMargin;
+ set => RaiseAndSetIfChanged(ref _windowsVerticalMargin, value);
+ }
+
public byte DefaultOpenFileExtensionIndex
{
get => _defaultOpenFileExtensionIndex;
diff --git a/UVtools.WPF/Windows/AboutWindow.axaml.cs b/UVtools.WPF/Windows/AboutWindow.axaml.cs
index 740d0f5..6926d67 100644
--- a/UVtools.WPF/Windows/AboutWindow.axaml.cs
+++ b/UVtools.WPF/Windows/AboutWindow.axaml.cs
@@ -22,7 +22,7 @@ namespace UVtools.WPF.Windows
public int ScreenCount => Screens.ScreenCount;
public string ScreenResolution => $"{Screens.Primary.Bounds.Width} x {Screens.Primary.Bounds.Height} @ {Screens.Primary.PixelDensity*100}%";
public string WorkingArea => $"{Screens.Primary.WorkingArea.Width} x {Screens.Primary.WorkingArea.Height}";
- public string RealWorkingArea => $"{Math.Round(Screens.Primary.WorkingArea.Width / Screens.Primary.PixelDensity)} x {Math.Round(Screens.Primary.WorkingArea.Height / Screens.Primary.PixelDensity)}";
+ public string RealWorkingArea => $"{App.MaxWindowSize.Width} x {App.MaxWindowSize.Height}";
public string ScreensDescription
{
diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml b/UVtools.WPF/Windows/SettingsWindow.axaml
index f0d20ff..5a15288 100644
--- a/UVtools.WPF/Windows/SettingsWindow.axaml
+++ b/UVtools.WPF/Windows/SettingsWindow.axaml
@@ -35,6 +35,61 @@
</StackPanel>
</Border>
+ <Border
+ Margin="5"
+ BorderBrush="LightBlue"
+ BorderThickness="4"
+ >
+
+ <StackPanel Orientation="Vertical">
+ <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" Text="Windows / dialogs"/>
+ <StackPanel Margin="15" Orientation="Vertical" Spacing="15">
+ <CheckBox IsChecked="{Binding Settings.General.WindowsTakeIntoAccountScreenScaling}"
+ Content="Take into account the screen scale factor to limit the dialogs windows maximum size"/>
+ <Grid RowDefinitions="Auto,10,Auto"
+ ColumnDefinitions="Auto,10,100,5,Auto">
+
+ <TextBlock Grid.Row="0" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Limits the windows and dialogs maximum width to the screen resolution less this margin"
+ Text="Horizontal limiting margin:"/>
+
+ <NumericUpDown Grid.Row="0" Grid.Column="2"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Limits the windows and dialogs maximum width to the screen resolution less this margin"
+ ClipValueToMinMax="True"
+ Minimum="0"
+ Maximum="1000"
+ Increment="1"
+ Value="{Binding Settings.General.WindowsHorizontalMargin}"/>
+ <TextBlock Grid.Row="0" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Limits windows and dialogs maximum height to the screen resolution less this margin"
+ Text="Vertical limiting margin:"/>
+
+ <NumericUpDown Grid.Row="2" Grid.Column="2"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Limits windows and dialogs maximum height to the screen resolution less this margin"
+ ClipValueToMinMax="True"
+ Minimum="0"
+ Maximum="1000"
+ Increment="1"
+ Value="{Binding Settings.General.WindowsVerticalMargin}"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="4"
+ VerticalAlignment="Center"
+ Text="px"/>
+
+ </Grid>
+ </StackPanel>
+
+ </StackPanel>
+ </Border>
+
<Border
Margin="5"
BorderBrush="LightBlue"
diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml.cs b/UVtools.WPF/Windows/SettingsWindow.axaml.cs
index f3522d5..3eac5ea 100644
--- a/UVtools.WPF/Windows/SettingsWindow.axaml.cs
+++ b/UVtools.WPF/Windows/SettingsWindow.axaml.cs
@@ -60,8 +60,7 @@ namespace UVtools.WPF.Windows
s => Convert.ToString(s / 100, CultureInfo.InvariantCulture) + "x").ToArray();
- //MaxHeight = Screens.Primary.WorkingArea.Height - 50;
- ScrollViewerMaxHeight = Screens.Primary.WorkingArea.Height / Screens.Primary.PixelDensity - 300;
+ ScrollViewerMaxHeight = App.MaxWindowSize.Height - Settings.General.WindowsVerticalMargin;
DataContext = this;
diff --git a/UVtools.WPF/Windows/ToolWindow.axaml b/UVtools.WPF/Windows/ToolWindow.axaml
index 4c37dda..e548105 100644
--- a/UVtools.WPF/Windows/ToolWindow.axaml
+++ b/UVtools.WPF/Windows/ToolWindow.axaml
@@ -94,17 +94,18 @@
Value="{Binding LayerIndexEnd}"
/>
- <Button
+ <Button Name="LayerSelectPresetButton"
Grid.Row="0"
Grid.Column="4"
Margin="10,0,0,0"
VerticalAlignment="Stretch"
Padding="10,0,10,0"
Content="Select ⮟"
- Command="{Binding #LayerSelectPreset.Open}"
+ Command="{Binding OpenContextMenu}"
+ CommandParameter="LayerSelectPreset"
>
<Button.ContextMenu>
- <ContextMenu Name="LayerSelectPreset" PlacementMode="Bottom">
+ <ContextMenu Name="LayerSelectPresetContextMenu" PlacementMode="Bottom">
<MenuItem
Header="_All layers"
HotKey="Ctrl + Shift + A" InputGesture="Ctrl + Shift + A"
@@ -240,7 +241,7 @@
<Grid
RowDefinitions="Auto,Auto"
- ColumnDefinitions="*,5,Auto"
+ ColumnDefinitions="*,5,Auto,5,Auto,5,Auto"
Margin="15"
>
@@ -252,8 +253,34 @@
Items="{Binding Profiles}" />
<Button
+ Grid.Row="0"
+ Grid.Column="2"
+ Margin="0,0,0,10"
+ VerticalAlignment="Center"
+ ToolTip.Tip="Deselect the current profile"
+ IsEnabled="{Binding SelectedProfileItem, Converter={x:Static ObjectConverters.IsNotNull}}"
+ IsVisible="{Binding Profiles.Count}"
+ Command="{Binding DeselectProfile}">
+ <Image Source="/Assets/Icons/checkbox-unmarked-16x16.png" />
+ </Button>
+
+ <Button
+ Grid.Row="0"
+ Grid.Column="4"
+ Margin="0,0,0,10"
+ Width="24"
+ VerticalAlignment="Center"
+ FontWeight="Bold"
+ ToolTip.Tip="Set the selected profile as default to load in with this dialog.
+&#x0a;Shift + click to clear the default profile."
+ IsEnabled="{Binding SelectedProfileItem, Converter={x:Static ObjectConverters.IsNotNull}}"
+ IsVisible="{Binding Profiles.Count}"
+ Command="{Binding SetDefaultProfile}"
+ Content="D"/>
+
+ <Button
Grid.Row="0"
- Grid.Column="2"
+ Grid.Column="6"
Margin="0,0,0,10"
VerticalAlignment="Center"
ToolTip.Tip="Remove the selected profile"
@@ -269,8 +296,7 @@
Grid.Column="0"
IsEnabled="{Binding ButtonOkEnabled}"
Text="{Binding ProfileText}"
- Watermark="Profile name or leave empty for auto name"
- />
+ Watermark="Profile name or leave empty for auto name"/>
<Button
Grid.Row="1"
diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs
index 8f2e9fa..fafd25b 100644
--- a/UVtools.WPF/Windows/ToolWindow.axaml.cs
+++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Drawing;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using DynamicData;
@@ -24,9 +25,11 @@ namespace UVtools.WPF.Windows
{
Init,
ClearROI,
+ ProfileLoaded,
Button1, // Reset to defaults
Checkbox1, // Show Advanced
}
+ private KeyModifiers _globalModifiers;
public ToolControl ToolControl;
private string _description;
private double _descriptionMaxWidth;
@@ -285,19 +288,13 @@ namespace UVtools.WPF.Windows
set
{
if(!RaiseAndSetIfChanged(ref _selectedProfileItem, value) || value is null) return;
+ if (ToolControl is null) return;
var operation = _selectedProfileItem.Clone();
operation.ProfileName = null;
ToolControl.BaseOperation = operation;
SelectLayers(operation.LayerRangeSelection);
-
- if (operation is OperationMorph operationMorph && ToolControl is ToolMorphControl toolMorphControl)
- {
- toolMorphControl.MorphSelectedIndex = operationMorph.MorphOperationIndex;
- }
- else if (operation is OperationBlur operationBlur && ToolControl is ToolBlurControl toolBlurControl)
- {
- toolBlurControl.SelectedAlgorithmIndex = operationBlur.BlurTypeIndex;
- }
+ ToolControl.Callback(Callbacks.ProfileLoaded);
+ ToolControl.ResetDataContext();
}
}
@@ -336,6 +333,7 @@ namespace UVtools.WPF.Windows
public async void RemoveSelectedProfile()
{
if (_selectedProfileItem is null) return;
+
if (await this.MessageBoxQuestion(
$"Are you sure you want to remove the selected profile?\n{_selectedProfileItem}",
"Remove selected profile?") != ButtonResult.Yes) return;
@@ -343,6 +341,7 @@ namespace UVtools.WPF.Windows
OperationProfiles.RemoveProfile(_selectedProfileItem);
Profiles.Remove(_selectedProfileItem);
SelectedProfileItem = null;
+
}
public async void ClearProfiles()
@@ -356,6 +355,44 @@ namespace UVtools.WPF.Windows
Profiles.Clear();
}
+ public void DeselectProfile()
+ {
+ SelectedProfileItem = null;
+ }
+
+ public async void SetDefaultProfile()
+ {
+ if (_selectedProfileItem is null) return;
+
+ if ((_globalModifiers & KeyModifiers.Shift) != 0)
+ {
+ if (await this.MessageBoxQuestion(
+ $"Are you sure you want to clear the selected profile as default settings for this dialog?",
+ "Clear the default profile?") != ButtonResult.Yes) return;
+
+ foreach (var operation in Profiles)
+ {
+ operation.ProfileIsDefault = false;
+ }
+ }
+ else
+ {
+ if (await this.MessageBoxQuestion(
+ $"Are you sure you want to set the selected profile as default settings for this dialog?\n{_selectedProfileItem}",
+ "Set as default profile?") != ButtonResult.Yes) return;
+
+ foreach (var operation in Profiles)
+ {
+ operation.ProfileIsDefault = false;
+ }
+
+ _selectedProfileItem.ProfileIsDefault = true;
+ }
+
+ OperationProfiles.Save();
+
+ }
+
#endregion
#region Content
@@ -427,10 +464,11 @@ namespace UVtools.WPF.Windows
set => RaiseAndSetIfChanged(ref _buttonOkText, value);
}
-
+
#endregion
+ #region Constructors
public ToolWindow()
{
InitializeComponent();
@@ -473,16 +511,25 @@ namespace UVtools.WPF.Windows
IsProfilesVisible = true;
}
+ foreach (var operation in Profiles)
+ {
+ if (operation.ProfileIsDefault)
+ {
+ SelectedProfileItem = operation;
+ break;
+ }
+ }
+
// Ensure the description don't stretch window
DispatcherTimer.Run(() =>
{
if (Bounds.Width == 0) return true;
- ScrollViewerMaxHeight = Screens.Primary.WorkingArea.Height / Screens.Primary.PixelDensity - Bounds.Height + ToolControl.Bounds.Height - 250;
+ ScrollViewerMaxHeight = App.MaxWindowSize.Height - Bounds.Height + ToolControl.Bounds.Height - UserSettings.Instance.General.WindowsVerticalMargin;
DescriptionMaxWidth = Math.Max(Bounds.Width, ToolControl.Bounds.Width) - 40;
Description = toolControl.BaseOperation.Description;
return false;
}, TimeSpan.FromMilliseconds(1));
-
+
toolControl.Callback(Callbacks.Init);
toolControl.DataContext = toolControl;
DataContext = this;
@@ -492,6 +539,7 @@ namespace UVtools.WPF.Windows
{
AvaloniaXamlLoader.Load(this);
}
+ #endregion
/*protected override void OnOpened(EventArgs e)
{
@@ -504,6 +552,28 @@ namespace UVtools.WPF.Windows
DataContext = this;
}*/
+ #region Overrides
+
+ protected override void OnPointerMoved(PointerEventArgs e)
+ {
+ base.OnPointerMoved(e);
+ _globalModifiers = e.KeyModifiers;
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ _globalModifiers = e.KeyModifiers;
+ }
+
+ protected override void OnKeyUp(KeyEventArgs e)
+ {
+ base.OnKeyUp(e);
+ _globalModifiers = e.KeyModifiers;
+ }
+
+ #endregion
+
public async void Process()
{
if (LayerIndexStart > LayerIndexEnd)
@@ -524,8 +594,10 @@ namespace UVtools.WPF.Windows
if (!await ToolControl.ValidateForm()) return;
if (!string.IsNullOrEmpty(ToolControl.BaseOperation.ConfirmationText))
{
- if (await this.MessageBoxQuestion($"Are you sure you want to {ToolControl.BaseOperation.ConfirmationText}") !=
- ButtonResult.Yes) return;
+ var result =
+ await this.MessageBoxQuestion(
+ $"Are you sure you want to {ToolControl.BaseOperation.ConfirmationText}");
+ if (result != ButtonResult.Yes) return;
}
}
@@ -537,5 +609,14 @@ namespace UVtools.WPF.Windows
DialogResult = DialogResults.OK;
Close(DialogResult);
}
+
+ public void OpenContextMenu(string name)
+ {
+ var menu = this.FindControl<ContextMenu>($"{name}ContextMenu");
+ if (menu is null) return;
+ var parent = this.FindControl<Button>($"{name}Button");
+ if (parent is null) return;
+ menu.Open(parent);
+ }
}
}