From b2617d149f1528c5ad292dacfae73de245ab07a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Fri, 25 Dec 2020 04:38:28 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 42 + CREDITS.md | 3 +- CreateRelease.WPF.ps1 | 44 +- README.md | 11 +- UVtools.Cmd/UVtools.Cmd.csproj | 2 +- UVtools.Core/Extensions/EmguExtensions.cs | 16 +- UVtools.Core/Extensions/PointExtensions.cs | 5 +- UVtools.Core/Extensions/TypeExtensions.cs | 2 + UVtools.Core/FileFormats/ChituboxFile.cs | 10 +- UVtools.Core/FileFormats/FileFormat.cs | 52 +- .../FileFormats/PhotonWorkshopFile.cs | 20 +- UVtools.Core/Layer/Layer.cs | 65 +- UVtools.Core/Layer/LayerManager.cs | 297 ++- UVtools.Core/Managers/ClipboardManager.cs | 38 +- UVtools.Core/Operations/Operation.cs | 11 +- .../OperationCalibrateElephantFoot.cs | 615 ++++++ .../Operations/OperationCalibrateGrayscale.cs | 442 ++++ .../Operations/OperationCalibrateTolerance.cs | 700 ++++++ .../OperationCalibrateXYZAccuracy.cs | 708 ++++++ UVtools.Core/Operations/OperationMorph.cs | 2 +- UVtools.Core/Operations/OperationMove.cs | 8 + .../Operations/OperationPixelDimming.cs | 416 +++- .../Operations/OperationRaftRelief.cs | 2 +- UVtools.Core/PixelEditor/PixelDrainHole.cs | 2 +- UVtools.Core/PixelEditor/PixelDrawing.cs | 27 +- UVtools.Core/PixelEditor/PixelEraser.cs | 4 +- UVtools.Core/PixelEditor/PixelOperation.cs | 44 +- UVtools.Core/PixelEditor/PixelSupport.cs | 3 +- UVtools.Core/PixelEditor/PixelText.cs | 29 +- UVtools.Core/UVtools.Core.csproj | 4 +- UVtools.Core/UVtools.Core.csproj.DotSettings | 2 + UVtools.InstallerMM/UVtools.InstallerMM.wxs | 57 +- UVtools.WPF/App.axaml.cs | 5 +- UVtools.WPF/Assets/Icons/chart-pie-16x16.png | Bin 0 -> 226 bytes UVtools.WPF/Assets/Icons/cubes-16x16.png | Bin 0 -> 268 bytes .../Assets/Icons/elephant-foot-16x16.png | Bin 0 -> 335 bytes UVtools.WPF/Controls/AdvancedImageBox.cs.old | 1891 ----------------- .../CalibrateElephantFootControl.axaml | 335 +++ .../CalibrateElephantFootControl.axaml.cs | 99 + .../CalibrateGrayscaleControl.axaml | 311 +++ .../CalibrateGrayscaleControl.axaml.cs | 86 + .../CalibrateToleranceControl.axaml | 553 +++++ .../CalibrateToleranceControl.axaml.cs | 131 ++ .../CalibrateXYZAccuracyControl.axaml | 478 +++++ .../CalibrateXYZAccuracyControl.axaml.cs | 132 ++ .../Controls/Tools/ToolBlurControl.axaml.cs | 11 + .../Tools/ToolCalculatorControl.axaml | 50 +- .../Tools/ToolEditParametersControl.axaml.cs | 4 +- .../Controls/Tools/ToolMorphControl.axaml.cs | 10 + .../Tools/ToolPixelDimmingControl.axaml | 47 +- .../Tools/ToolPixelDimmingControl.axaml.cs | 373 +--- .../Tools/ToolRaftReliefControl.axaml | 38 +- .../Controls/Tools/ToolResizeControl.axaml | 8 +- .../Controls/Tools/ToolRotateControl.axaml | 4 +- UVtools.WPF/Extensions/WindowExtensions.cs | 7 +- UVtools.WPF/MainWindow.Clipboard.cs | 27 + UVtools.WPF/MainWindow.GCode.cs | 1 - UVtools.WPF/MainWindow.Information.cs | 2 +- UVtools.WPF/MainWindow.Issues.cs | 22 +- UVtools.WPF/MainWindow.LayerPreview.cs | 11 +- UVtools.WPF/MainWindow.PixelEditor.cs | 16 +- UVtools.WPF/MainWindow.axaml | 201 +- UVtools.WPF/MainWindow.axaml.cs | 139 +- UVtools.WPF/Structures/OperationProfiles.cs | 4 + UVtools.WPF/UVtools.WPF.csproj | 19 +- UVtools.WPF/UVtools.WPF.csproj.DotSettings | 2 + UVtools.WPF/UserSettings.cs | 22 + UVtools.WPF/Windows/AboutWindow.axaml.cs | 2 +- UVtools.WPF/Windows/SettingsWindow.axaml | 55 + UVtools.WPF/Windows/SettingsWindow.axaml.cs | 3 +- UVtools.WPF/Windows/ToolWindow.axaml | 40 +- UVtools.WPF/Windows/ToolWindow.axaml.cs | 109 +- 72 files changed, 6258 insertions(+), 2673 deletions(-) create mode 100644 UVtools.Core/Operations/OperationCalibrateElephantFoot.cs create mode 100644 UVtools.Core/Operations/OperationCalibrateGrayscale.cs create mode 100644 UVtools.Core/Operations/OperationCalibrateTolerance.cs create mode 100644 UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs create mode 100644 UVtools.Core/UVtools.Core.csproj.DotSettings create mode 100644 UVtools.WPF/Assets/Icons/chart-pie-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/cubes-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/elephant-foot-16x16.png delete mode 100644 UVtools.WPF/Controls/AdvancedImageBox.cs.old create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateGrayscaleControl.axaml.cs create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateToleranceControl.axaml.cs create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml create mode 100644 UVtools.WPF/Controls/Calibrators/CalibrateXYZAccuracyControl.axaml.cs create mode 100644 UVtools.WPF/UVtools.WPF.csproj.DotSettings 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,28 +146,30 @@ $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" + + 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 "Coping $runtime MSI to: $msiTargetFile" + Copy-Item $msiSourceFile $msiTargetFile + + 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 @@ Exe - netcoreapp3.1 + net5.0 UVtoolsCmd UVtools.ico 0.1 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(2, 3)) + { + CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, 1.0, translateTransform); + CvInvoke.WarpAffine(src, src, translateTransform, src.Size); + } + } + /// /// Scale image from it center, preserving src bounds /// https://stackoverflow.com/a/62543674/933976 @@ -188,7 +200,7 @@ namespace UVtools.Core.Extensions /// Blanked 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(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; - - foreach (var layer in this) + bool computeGeneral = LayerManager is null; + 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; + foreach (var layer in this) + { + if (layer is null) + { + computeGeneral = true; + break; + } + + 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) + { + 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); + } } /// @@ -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); + } } /// @@ -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); + } } /// @@ -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); + } } /// @@ -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); + } } /// @@ -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); + } } /// @@ -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(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; + /// /// Layers List /// - 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(); var layerHollowAreas = new ConcurrentDictionary>(); @@ -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 { #region Properties - private readonly List _layers = new List(); + private readonly List _layers = new(); /// /// Gets the LayerCount for this clip @@ -25,9 +25,20 @@ namespace UVtools.Core.Managers /// 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 /// /// Collect differences and create a clip /// - 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; + } + + /// + /// Collect differences and create a clip + /// + 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); + } + /// /// Gets or sets an ROI to process this operation /// @@ -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. + * 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 + + /// + /// Gets the bottom and normal layers, 0 = bottom | 1 = normal + /// + /// + 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 { 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. + * 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 + + /// + /// Gets the bottom and normal layers, 0 = bottom | 1 = normal + /// + /// + 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. + * 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>(); + + 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(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. + * 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 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 _pattern; private Matrix _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(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 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 ushort InfillGenThickness + { + get => _infillGenThickness; + set => RaiseAndSetIfChanged(ref _infillGenThickness, value); } - public bool IsAlternatePattern(uint layerIndex) => !IsNormalPattern(layerIndex); - - public override string ToString() + public ushort InfillGenSpacing { - 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 => _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 - { - return ((int) _wallThicknessStart * 397) ^ _wallsOnly.GetHashCode(); - } + 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") + { + 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 /// /// Gets the for the draw operation /// - 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 uint LayersAbove { get; set; } + public decimal PixelBrightnessPercent => Math.Round(_pixelBrightness * 100M / 255M, 2); + + public uint LayersBelow + { + get => _layersBelow; + set => RaiseAndSetIfChanged(ref _layersBelow, value); + } + + public uint LayersAbove + { + get => _layersAbove; + set => RaiseAndSetIfChanged(ref _layersAbove, value); + } /// /// Gets the total size of the operation /// 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 @@ - netstandard2.0 + net5.0 true LICENSE PTRTECH @@ -10,7 +10,7 @@ https://github.com/sn4k3/UVtools https://github.com/sn4k3/UVtools MSLA/DLP, file analysis, repair, conversion and manipulation - 1.4.0 + 2.0.0 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 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 @@ + + CSharp90 \ 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 @@ + + + @@ -176,15 +179,15 @@ + + + - - - @@ -227,6 +230,9 @@ + + + @@ -278,14 +284,11 @@ - - - - - + + @@ -293,9 +296,6 @@ - - - @@ -308,18 +308,9 @@ - - - - - - - - - @@ -425,6 +416,9 @@ + + + @@ -500,6 +494,9 @@ + + + @@ -572,9 +569,6 @@ - - - @@ -632,9 +626,6 @@ - - - @@ -659,12 +650,6 @@ - - - - - - @@ -825,10 +810,7 @@ - - - - + @@ -842,9 +824,6 @@ - - - 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 0000000000000000000000000000000000000000..8b23bf40baf61a8323a1359ba0b27855db6e7f5d GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6R(iTPhEy=to@R7&brd=Nke`uX zL&1rWX_10}e-pa{N0u<-88#;tT?H?f1|g;>2PYP$jsV6JojiTUufDTCzn9NrdV8A0 z?Ct5IA9ous^;R@-xnKP^*3sg+wGiu*+aZ5A^Bz2r|G@a2b&JgporD*Pjp^B8Hp}$= zO(uTJx|~#1a=h=v=d`SYx%>GZY?+{MEPVaY#(SZP_qUzN3^LZ|jEOKa*_e2LVeIQ5 Z`=hrf1nukH%@1@mgQu&X%Q~loCIDN%Q>*|0 literal 0 HcmV?d00001 diff --git a/UVtools.WPF/Assets/Icons/cubes-16x16.png b/UVtools.WPF/Assets/Icons/cubes-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..bfef99217bc9294a418007d2909922579f510ec4 GIT binary patch literal 268 zcmV+n0rUQeP)>!f)D|*=>djce_YqD zpw8jTm*YR|x)dVq9>2M@4Rk$Oi?m%dj4TCF_~}c} zuzcax;bxFS8H6SXJ~n~_kCouWJ8hF9xDzgtA?e7si8j!YBAIPSn+X#gf8huAF>pUk SezhF{0000;n>CUaIH9yr4hfqelkd*x zB7_p!CaZ?Cerw~0yJS>OHkmZUG4Fkp&r9WG(#ap?8khKfpG*bdAUrUL+qx-PT@in%B3)JsJfW!A)44}@6&RFeV{`s%a)men!>KrX{9 haoB6`T(wFwfPW1!XjhSn(U1TD002ovPDHLkV1oHWjvN30 literal 0 HcmV?d00001 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 - /// - /// Multicast event for property change notifications. - /// - private PropertyChangedEventHandler _propertyChanged; - private List events = new List(); - - public event PropertyChangedEventHandler PropertyChanged - { - add { _propertyChanged += value; events.Add("added"); } - remove { _propertyChanged -= value; events.Add("removed"); } - } - protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) - { - if (EqualityComparer.Default.Equals(field, value)) return false; - field = value; - RaisePropertyChanged(propertyName); - return true; - } - - - protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) - { - } - - /// - /// Notifies listeners that a property value has changed. - /// - /// - /// Name of the property used to notify listeners. This - /// value is optional and can be provided automatically when invoked from compilers - /// that support . - /// - protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) - { - var e = new PropertyChangedEventArgs(propertyName); - OnPropertyChanged(e); - _propertyChanged?.Invoke(this, e); - } - #endregion - - #region Sub Classes - - /// - /// Represents available levels of zoom in an control - /// - public class ZoomLevelCollection : IList - { - #region Public Constructors - - /// - /// Initializes a new instance of the class. - /// - public ZoomLevelCollection() - { - List = new SortedList(); - } - - /// - /// Initializes a new instance of the class. - /// - /// The default values to populate the collection with. - /// Thrown if the collection parameter is null - public ZoomLevelCollection(IEnumerable collection) - : this() - { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } - - AddRange(collection); - } - - #endregion - - #region Public Class Properties - - /// - /// Returns the default zoom levels - /// - 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 - - /// - /// Gets the number of elements contained in the . - /// - /// - /// The number of elements contained in the . - /// - public int Count => List.Count; - - /// - /// Gets a value indicating whether the is read-only. - /// - /// true if this instance is read only; otherwise, false. - /// true if the is read-only; otherwise, false. - /// - public bool IsReadOnly => false; - - /// - /// Gets or sets the zoom level at the specified index. - /// - /// The index. - public int this[int index] - { - get => List.Values[index]; - set - { - List.RemoveAt(index); - Add(value); - } - } - - #endregion - - #region Protected Properties - - /// - /// Gets or sets the backing list. - /// - protected SortedList List { get; set; } - - #endregion - - #region Public Members - - /// - /// Adds an item to the . - /// - /// The object to add to the . - public void Add(int item) - { - List.Add(item, item); - } - - /// - /// Adds a range of items to the . - /// - /// The items to add to the collection. - /// Thrown if the collection parameter is null. - public void AddRange(IEnumerable collection) - { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } - - foreach (int value in collection) - { - Add(value); - } - } - - /// - /// Removes all items from the . - /// - public void Clear() - { - List.Clear(); - } - - /// - /// Determines whether the contains a specific value. - /// - /// The object to locate in the . - /// true if is found in the ; otherwise, false. - public bool Contains(int item) - { - return List.ContainsKey(item); - } - - /// - /// Copies a range of elements this collection into a destination . - /// - /// The that receives the data. - /// A 64-bit integer that represents the index in the at which storing begins. - public void CopyTo(int[] array, int arrayIndex) - { - for (int i = 0; i < this.Count; i++) - { - array[arrayIndex + i] = this.List.Values[i]; - } - } - - /// - /// Finds the index of a zoom level matching or nearest to the specified value. - /// - /// The zoom level. - 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; - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// A that can be used to iterate through the collection. - public IEnumerator GetEnumerator() - { - return List.Values.GetEnumerator(); - } - - /// - /// Determines the index of a specific item in the . - /// - /// The object to locate in the . - /// The index of if found in the list; otherwise, -1. - public int IndexOf(int item) - { - return List.IndexOfKey(item); - } - - /// - /// Not implemented. - /// - /// The index. - /// The item. - /// Not implemented - public void Insert(int index, int item) - { - throw new NotImplementedException(); - } - - /// - /// Returns the next increased zoom level for the given current zoom. - /// - /// The current zoom level. - /// The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom. - public int NextZoom(int zoomLevel) - { - var index = IndexOf(this.FindNearest(zoomLevel)); - if (index < this.Count - 1) - { - index++; - } - - return this[index]; - } - - /// - /// Returns the next decreased zoom level for the given current zoom. - /// - /// The current zoom level. - /// The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom. - public int PreviousZoom(int zoomLevel) - { - var index = IndexOf(FindNearest(zoomLevel)); - if (index > 0) - { - index--; - } - - return this[index]; - } - - /// - /// Removes the first occurrence of a specific object from the . - /// - /// The object to remove from the . - /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . - public bool Remove(int item) - { - return List.Remove(item); - } - - /// - /// Removes the element at the specified index of the . - /// - /// The zero-based index of the element to remove. - public void RemoveAt(int index) - { - List.RemoveAt(index); - } - - /// - /// Copies the elements of the to a new array. - /// - /// An array containing copies of the elements of the . - public int[] ToArray() - { - int[] results; - - results = new int[Count]; - CopyTo(results, 0); - - return results; - } - - #endregion - - #region IList Members - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - } - - #endregion - - #region Enums - - /// - /// Determines the sizing mode of an image hosted in an control. - /// - public enum SizeModes : byte - { - /// - /// The image is disiplayed according to current zoom and scroll properties. - /// - Normal, - - /// - /// The image is stretched to fill the client area of the control. - /// - Stretch, - - /// - /// 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. - /// - //Fit - } - - [Flags] - public enum MouseButtons : byte - { - None = 0, - LeftButton = 1, - MiddleButton = 2, - RightButton = 4 - } - - /// - /// Describes the zoom action occuring - /// - [Flags] - public enum ZoomActions : byte - { - /// - /// No action. - /// - None = 0, - - /// - /// The control is increasing the zoom. - /// - ZoomIn = 1, - - /// - /// The control is decreasing the zoom. - /// - ZoomOut = 2, - - /// - /// The control zoom was reset. - /// - ActualSize = 4 - } - - public enum SelectionModes - { - /// - /// No selection. - /// - None, - - /// - /// Rectangle selection. - /// - Rectangle, - - /// - /// Zoom selection. - /// - 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(); - } - } - - /// - /// Gets or sets the basic cell size - /// - public byte GridCellSize - { - get => _gridCellSize; - set => RaiseAndSetIfChanged(ref _gridCellSize, value); - } - - /// - /// Gets or sets the color used to create the checkerboard style background - /// - public ISolidColorBrush GridColor - { - get => _gridColor; - set => RaiseAndSetIfChanged(ref _gridColor, value); - } - - /// - /// Gets or sets the color used to create the checkerboard style background - /// - public ISolidColorBrush GridColorAlternate - { - get => _gridColorAlternate; - set => RaiseAndSetIfChanged(ref _gridColorAlternate, value); - } - - /// - /// Gets or sets the image to be displayed - /// - 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; - } - } - - /// - /// Gets or sets if the checkerboard background should be displayed - /// - 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; - /// - /// Gets or sets the zoom levels. - /// - /// The zoom levels. - public virtual ZoomLevelCollection ZoomLevels - { - get => _zoomLevels; - set => RaiseAndSetIfChanged(ref _zoomLevels, value); - } - - private int _oldZoom = 100; - private int _zoom = 100; - - /// - /// Gets or sets the zoom. - /// - /// The zoom. - public virtual int OldZoom - { - get => _oldZoom; - set => RaiseAndSetIfChanged(ref _oldZoom, value); - } - - /// - /// Gets or sets the zoom. - /// - /// The zoom. - 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; - /// - /// Gets or sets the color of the pixel grid. - /// - /// The color of the pixel grid. - public virtual ISolidColorBrush PixelGridColor - { - get => _pixelGridColor; - set => RaiseAndSetIfChanged(ref _pixelGridColor, value); - } - - private int _pixelGridThreshold = 5; - /// - /// Gets or sets the minimum size of zoomed pixel's before the pixel grid will be drawn - /// - /// The pixel grid threshold. - - 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); - - /// - /// Returns an appropriate zoom level based on the specified action, relative to the current zoom level. - /// - /// The action to determine the zoom level. - /// Thrown if an unsupported action is specified. - 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; - } - - /// - /// Resets the property whilsts retaining the original . - /// - 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); - } - } - - /// - /// Determines whether the specified point is located within the image view port - /// - /// The point. - /// - /// true if the specified point is located within the image view port; otherwise, false. - /// - public virtual bool IsPointInImage(Point point) - => GetImageViewPort().Contains(point); - - /// - /// Determines whether the specified point is located within the image view port - /// - /// The X co-ordinate of the point to check. - /// The Y co-ordinate of the point to check. - /// - /// true if the specified point is located within the image view port; otherwise, false. - /// - public bool IsPointInImage(int x, int y) - => IsPointInImage(new Point(x, y)); - - /// - /// Determines whether the specified point is located within the image view port - /// - /// The X co-ordinate of the point to check. - /// The Y co-ordinate of the point to check. - /// - /// true if the specified point is located within the image view port; otherwise, false. - /// - public bool IsPointInImage(double x, double y) - => IsPointInImage(new Point(x, y)); - - /// - /// Converts the given client size point to represent a coordinate on the source image. - /// - /// The source point. - /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point - public Point PointToImage(Point point) - => PointToImage(point, false); - - /// - /// Converts the given client size point to represent a coordinate on the source image. - /// - /// The X co-ordinate of the point to convert. - /// The Y co-ordinate of the point to convert. - /// - /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. - /// - /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point - public Point PointToImage(double x, double y, bool fitToBounds = false) - => PointToImage(x, y, fitToBounds); - - /// - /// Converts the given client size point to represent a coordinate on the source image. - /// - /// The X co-ordinate of the point to convert. - /// The Y co-ordinate of the point to convert. - /// - /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. - /// - /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point - public Point PointToImage(int x, int y, bool fitToBounds = false) - { - return PointToImage(x, y, fitToBounds); - } - - /// - /// Converts the given client size point to represent a coordinate on the source image. - /// - /// The source point. - /// - /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. - /// - /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point - 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); - } - - /// - /// Scrolls the control to the given point in the image, offset at the specified display point - /// - /// The X co-ordinate of the point to scroll to. - /// The Y co-ordinate of the point to scroll to. - /// The X co-ordinate relative to the x parameter. - /// The Y co-ordinate relative to the y parameter. - public void ScrollTo(double x, double y, double relativeX, double relativeY) - => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); - - /// - /// Scrolls the control to the given point in the image, offset at the specified display point - /// - /// The X co-ordinate of the point to scroll to. - /// The Y co-ordinate of the point to scroll to. - /// The X co-ordinate relative to the x parameter. - /// The Y co-ordinate relative to the y parameter. - public void ScrollTo(int x, int y, int relativeX, int relativeY) - => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); - - /// - /// Scrolls the control to the given point in the image, offset at the specified display point - /// - /// The point of the image to attempt to scroll to. - /// The relative display point to offset scrolling by. - 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}"); - } - - /// - /// Zooms into the image - /// - public virtual void ZoomIn() - => ZoomIn(true); - - /// - /// Zooms into the image - /// - /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. - public virtual void ZoomIn(bool preservePosition) - { - PerformZoom(ZoomActions.ZoomIn, preservePosition); - } - - /// - /// Zooms out of the image - /// - public virtual void ZoomOut() - => ZoomOut(true); - - /// - /// Zooms out of the image - /// - /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. - public virtual void ZoomOut(bool preservePosition) - { - PerformZoom(ZoomActions.ZoomOut, preservePosition); - } - - /// - /// Zooms to the maximum size for displaying the entire image within the bounds of the control. - /// - 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)); - } - - /// - /// Adjusts the view port to fit the given region - /// - /// The X co-ordinate of the selection region. - /// The Y co-ordinate of the selection region. - /// The width of the selection region. - /// The height of the selection region. - /// Give a margin to rectangle by a value to zoom-out that pixel value - public void ZoomToRegion(double x, double y, double width, double height, double margin = 0) - { - ZoomToRegion(new Rect(x, y, width, height), margin); - } - - /// - /// Adjusts the view port to fit the given region - /// - /// The X co-ordinate of the selection region. - /// The Y co-ordinate of the selection region. - /// The width of the selection region. - /// The height of the selection region. - /// Give a margin to rectangle by a value to zoom-out that pixel value - public void ZoomToRegion(int x, int y, int width, int height, double margin = 0) - { - ZoomToRegion(new Rect(x, y, width, height), margin); - } - - /// - /// Adjusts the view port to fit the given region - /// - /// The rectangle to fit the view port to. - /// Give a margin to rectangle by a value to zoom-out that pixel value - public virtual void ZoomToRegion(Rectangle rectangle, double margin = 0) => ZoomToRegion(rectangle.ToAvalonia(), margin); - - /// - /// Adjusts the view port to fit the given region - /// - /// The rectangle to fit the view port to. - /// Give a margin to rectangle by a value to zoom-out that pixel value - 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 - } - - /// - /// Zooms to current selection region - /// - public void ZoomToSelectionRegion(double margin = 0) - { - if (!HaveSelection) return; - ZoomToRegion(SelectionRegion, margin); - } - - /// - /// Centers the given point in the image in the center of the control - /// - /// The point of the image to attempt to center. - public virtual void CenterAt(System.Drawing.Point imageLocation) - => ScrollTo(new Point(imageLocation.X, imageLocation.Y), new Point(Viewport.Width / 2, Viewport.Height / 2)); - - /// - /// Centers the given point in the image in the center of the control - /// - /// The point of the image to attempt to center. - public virtual void CenterAt(Point imageLocation) - => ScrollTo(imageLocation, new Point(Viewport.Width / 2, Viewport.Height / 2)); - - /// - /// Centers the given point in the image in the center of the control - /// - /// The X co-ordinate of the point to center. - /// The Y co-ordinate of the point to center. - public void CenterAt(int x, int y) - => CenterAt(new Point(x, y)); - - /// - /// Centers the given point in the image in the center of the control - /// - /// The X co-ordinate of the point to center. - /// The Y co-ordinate of the point to center. - public void CenterAt(double x, double y) - => CenterAt(new Point(x, y)); - - /// - /// Resets the viewport to show the center of the image. - /// - 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; - } - - /// - /// Resets the zoom to 100%. - /// - /// The source that initiated the action. - 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 - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The X co-ordinate of the point to scale. - /// The Y co-ordinate of the point to scale. - /// A which has been scaled to match the current zoom level - public Point GetScaledPoint(int x, int y) - { - return GetScaledPoint(new Point(x, y)); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The X co-ordinate of the point to scale. - /// The Y co-ordinate of the point to scale. - /// A which has been scaled to match the current zoom level - public PointF GetScaledPoint(float x, float y) - { - return GetScaledPoint(new PointF(x, y)); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The source to scale. - /// A which has been scaled to match the current zoom level - public virtual Point GetScaledPoint(Point source) - { - return new Point(source.X * ZoomFactor, source.Y * ZoomFactor); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The source to scale. - /// A which has been scaled to match the current zoom level - public virtual PointF GetScaledPoint(PointF source) - { - return new PointF((float)(source.X * this.ZoomFactor), (float)(source.Y * this.ZoomFactor)); - } - - /// - /// Returns the source rectangle scaled according to the current zoom level - /// - /// The X co-ordinate of the source rectangle. - /// The Y co-ordinate of the source rectangle. - /// The width of the rectangle. - /// The height of the rectangle. - /// A which has been scaled to match the current zoom level - public Rect GetScaledRectangle(int x, int y, int width, int height) - { - return GetScaledRectangle(new Rect(x, y, width, height)); - } - - /// - /// Returns the source rectangle scaled according to the current zoom level - /// - /// The X co-ordinate of the source rectangle. - /// The Y co-ordinate of the source rectangle. - /// The width of the rectangle. - /// The height of the rectangle. - /// A which has been scaled to match the current zoom level - public RectangleF GetScaledRectangle(float x, float y, float width, float height) - { - return GetScaledRectangle(new RectangleF(x, y, width, height)); - } - - /// - /// Returns the source rectangle scaled according to the current zoom level - /// - /// The location of the source rectangle. - /// The size of the source rectangle. - /// A which has been scaled to match the current zoom level - public Rect GetScaledRectangle(Point location, Size size) - { - return GetScaledRectangle(new Rect(location, size)); - } - - /// - /// Returns the source rectangle scaled according to the current zoom level - /// - /// The location of the source rectangle. - /// The size of the source rectangle. - /// A which has been scaled to match the current zoom level - public RectangleF GetScaledRectangle(PointF location, SizeF size) - { - return GetScaledRectangle(new RectangleF(location, size)); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The source to scale. - /// A which has been scaled to match the current zoom level - public virtual Rect GetScaledRectangle(Rect source) - { - return new Rect(source.Left * ZoomFactor, source.Top * ZoomFactor, source.Width * ZoomFactor, source.Height * ZoomFactor); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The source to scale. - /// A which has been scaled to match the current zoom level - 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)); - } - - /// - /// Returns the source size scaled according to the current zoom level - /// - /// The width of the size to scale. - /// The height of the size to scale. - /// A which has been resized to match the current zoom level - public SizeF GetScaledSize(float width, float height) - { - return this.GetScaledSize(new SizeF(width, height)); - } - - /// - /// Returns the source size scaled according to the current zoom level - /// - /// The width of the size to scale. - /// The height of the size to scale. - /// A which has been resized to match the current zoom level - public Size GetScaledSize(int width, int height) - { - return this.GetScaledSize(new Size(width, height)); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The source to scale. - /// A which has been resized to match the current zoom level - public virtual SizeF GetScaledSize(SizeF source) - { - return new SizeF((float)(source.Width * this.ZoomFactor), (float)(source.Height * this.ZoomFactor)); - } - - /// - /// Returns the source scaled according to the current zoom level - /// - /// The source to scale. - /// A which has been resized to match the current zoom level - public virtual Size GetScaledSize(Size source) - { - return new Size(source.Width * ZoomFactor, source.Height * ZoomFactor); - } - - /// - /// Creates a selection region which encompasses the entire image - /// - /// Thrown if no image is currently set - public virtual void SelectAll() - { - if (Image is null) return; - SelectionRegion = new Rect(0, 0, Image.Size.Width, Image.Size.Height); - } - - /// - /// Clears any existing selection region - /// - 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); - } - } - - /// - /// Returns the source repositioned to include the current image offset and scaled by the current zoom level - /// - /// The source to offset. - /// A which has been repositioned to match the current zoom level and image offset - 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); - } - - /// - /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level - /// - /// The source X co-ordinate. - /// The source Y co-ordinate. - /// A which has been repositioned to match the current zoom level and image offset - public Point GetOffsetPoint(int x, int y) - { - return GetOffsetPoint(new System.Drawing.Point(x, y)); - } - - /// - /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level - /// - /// The source X co-ordinate. - /// The source Y co-ordinate. - /// A which has been repositioned to match the current zoom level and image offset - public Point GetOffsetPoint(double x, double y) - { - return GetOffsetPoint(new Point(x, y)); - } - - /// - /// Returns the source repositioned to include the current image offset and scaled by the current zoom level - /// - /// The source to offset. - /// A which has been repositioned to match the current zoom level and image offset - 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); - } - - /// - /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset - /// - /// The source to offset. - /// A which has been resized and repositioned to match the current zoom level and image offset - 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); - } - - /// - /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset - /// - /// The X co-ordinate of the source rectangle. - /// The Y co-ordinate of the source rectangle. - /// The width of the rectangle. - /// The height of the rectangle. - /// A which has been resized and repositioned to match the current zoom level and image offset - public Rectangle GetOffsetRectangle(int x, int y, int width, int height) - { - return this.GetOffsetRectangle(new Rectangle(x, y, width, height)); - } - - /// - /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset - /// - /// The X co-ordinate of the source rectangle. - /// The Y co-ordinate of the source rectangle. - /// The width of the rectangle. - /// The height of the rectangle. - /// A which has been resized and repositioned to match the current zoom level and image offset - public Rect GetOffsetRectangle(double x, double y, double width, double height) - { - return GetOffsetRectangle(new Rect(x, y, width, height)); - } - - /// - /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset - /// - /// The source to offset. - /// A which has been resized and repositioned to match the current zoom level and image offset - 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)); - } - - /// - /// Fits a given to match image boundaries - /// - /// The rectangle. - /// - /// A structure remapped to fit the image boundaries - /// - 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); - } - - /// - /// Fits a given to match image boundaries - /// - /// The rectangle. - /// - /// A structure remapped to fit the image boundaries - /// - 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); - } - - /// - /// Gets the source image region. - /// - /// - 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); - } - - /// - /// Gets the image view port. - /// - /// - 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); - } - - /// - /// Gets the width of the scaled image. - /// - /// The width of the scaled image. - protected virtual double ScaledImageWidth => Image.Size.Width * ZoomFactor; - - /// - /// Gets the height of the scaled image. - /// - /// The height of the scaled image. - 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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("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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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}" /> - 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}" /> + Value="{Binding Operation.Brightness}"/> + + @@ -115,28 +118,28 @@ Padding="10" Content="Chessboard" Width="100" - Command="{Binding GeneratePixelDimming}" + Command="{Binding Operation.GeneratePixelDimming}" CommandParameter="Chessboard" /> - @@ -1618,9 +1744,10 @@ - + +