diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml index 2f2e615a..6cb987b2 100644 --- a/.github/workflows/package-publish.yml +++ b/.github/workflows/package-publish.yml @@ -78,10 +78,10 @@ jobs: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/master - - name: 🟣 Setup .NET 7.0 + - name: 🟣 Setup .NET 9.0 uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 9.0.x #- name: Set VERSION variable from tag # run: | @@ -95,3 +95,6 @@ jobs: - name: 🚀 Push nuget.org run: dotnet nuget push UVtools*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} --skip-duplicate + + - name: 🚀 Push Github + run: dotnet nuget push UVtools*.nupkg --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --api-key ${{ github.token }} --skip-duplicate diff --git a/CHANGELOG.md b/CHANGELOG.md index 2489d2ef..0d5536ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 18/12/2024 - v5.0.0 + +- **File formats:** + - (Add) Anycubic PWSZ Zip file format (#892) + - (Add) Support for Anycubic Photon Mono 4 (pm4n) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono 4 Ultra (pm4u) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono M7 (pm7) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono M7 Max (pm7m) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono M7 Pro (pwsz) and corresponding PrusaSlicer profile +- (Add) PrusaSlicer printer: Phrozen Sonic Mighty Revo (#950) +- (Add) Litophane tool: Option to enable or disable the separation of grayscale pixels (#954) +- (Add) Exposure time finder: Re-arrange exposure text layout and allow to change it font (#955) +- (Add) Setting: Available RAM lower limit - Sets a lower limiter for the available memory RAM the program is allowed to run operations. + When meet the threshold, a stopping action will be queued to relief pressure and maintain the system stability. + In cases where is unable to pause or cancel the operation, the program will be forced to close and trigger an exception to ensure the system stability. + Note: This limiter will check the RAM every 2 seconds while operations are running, if you have set a very low limit there is a chance to consume more RAM in the time and cause system instability. + System can also reserve some RAM to prevent depletion and start to use SWAP memory, doing such with a low limit and is possible that the limiter never trigger. + 0: Ignore the RAM limiter. + Default: 1 GB +- (Improvement) Change the gif animation library to a more efficient and compatible one +- (Fix) Contour traverse function was duplicating the contours, causing wrong calculations +- (Fix) Blur: Prevent stack blur from use even values +- (Upgrade) AvaloniaUI from 11.1.3 to 11.2.2 +- (Upgrade) .NET from 6.0.33 to 9.0.0 + - .NET 6.0 is end of life and will no longer supported + - This represents three major upgrades and will increase the system os version requirements, see more here: https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md + - With this upgrade the software will be able to take advantage of the new features and improvements of the .NET 9.0, including a significant performance boost + ## 04/10/2024 - v4.4.3 - **File formats:** diff --git a/Directory.Build.props b/Directory.Build.props index 2bb72c03..501e5112 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,13 +1,17 @@ - net6.0 + + net9.0 AnyCPU;x64;ARM64 + enable + Tiago Conceição, sn4k3 PTRTECH Copyright 2020-$([System.DateTime]::Now.ToString(`yyyy`)) © PTRTECH MSLA/DLP, file analysis, calibration, repair, conversion and manipulation + $(MSBuildThisFileDirectory)UVtools.CAD\UVtools.ico UVtools.png README.md @@ -19,20 +23,29 @@ msla, dlp, resin, printer, slicer, 3d printing, image processing, layers Git - enable - + True $(MSBuildThisFileDirectory)build\UVtools.snk + $(MSBuildProjectDirectory)=$(MSBuildProjectName) + $(MSBuildThisFileDirectory)artifacts + $(MSBuildThisFileDirectory)publish + true - $(MSBuildThisFileDirectory)publish + + 5.0.0 + 11.2.2 + + + false + - 4.4.3 - 11.1.3 + + true diff --git a/PrusaSlicer/printer/AnyCubic Photon Mono 4 Ultra.ini b/PrusaSlicer/printer/AnyCubic Photon Mono 4 Ultra.ini new file mode 100644 index 00000000..252d7e2b --- /dev/null +++ b/PrusaSlicer/printer/AnyCubic Photon Mono 4 Ultra.ini @@ -0,0 +1,43 @@ +# generated by PrusaSlicer 2.8.1+win64 on 2024-12-15 at 23:55:19 UTC +absolute_correction = 0 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,153.41x0,153.41x87.04,0x87.04 +default_sla_material_profile = Prusa Orange Tough 0.05 +default_sla_print_profile = 0.05 Normal +display_height = 87.04 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 9024 +display_pixels_y = 5120 +display_width = 153.408 +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +fast_tilt_time = 5 +gamma_correction = 1 +high_viscosity_tilt_time = 10 +host_type = octoprint +inherits = Original Prusa SL1 +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 165 +min_exposure_time = 1 +min_initial_exposure_time = 1 +print_host = +printer_model = SL1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_SL1\nPRINTER_VENDOR_ANYCUBIC\nPRINTER_MODEL_PHOTON_MONO_4_ULTRA\nFILEFORMAT_PM4U\n\nSTART_CUSTOM_VALUES\nWaitTimeBeforeCure_2.5\nBottomLiftHeight_8\nLiftHeight_8\nBottomLiftSpeed_120\nLiftSpeed_120\nRetractSpeed_120\nBottomLightPWM_255\nLightPWM_255\nEND_CUSTOM_VALUES +printer_settings_id = +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +sla_archive_format = SL1 +sla_output_precision = 0.001 +slow_tilt_time = 8 +thumbnails = 224x168/PNG diff --git a/PrusaSlicer/printer/AnyCubic Photon Mono 4.ini b/PrusaSlicer/printer/AnyCubic Photon Mono 4.ini new file mode 100644 index 00000000..bee0ee7e --- /dev/null +++ b/PrusaSlicer/printer/AnyCubic Photon Mono 4.ini @@ -0,0 +1,43 @@ +# generated by PrusaSlicer 2.8.1+win64 on 2024-12-15 at 23:56:57 UTC +absolute_correction = 0 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,153.41x0,153.41x87.04,0x87.04 +default_sla_material_profile = Prusa Orange Tough 0.05 +default_sla_print_profile = 0.05 Normal +display_height = 87.04 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 9024 +display_pixels_y = 5120 +display_width = 153.408 +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +fast_tilt_time = 5 +gamma_correction = 1 +high_viscosity_tilt_time = 10 +host_type = octoprint +inherits = Original Prusa SL1 +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 165 +min_exposure_time = 1 +min_initial_exposure_time = 1 +print_host = +printer_model = SL1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_SL1\nPRINTER_VENDOR_ANYCUBIC\nPRINTER_MODEL_PHOTON_MONO_4\nFILEFORMAT_PM4N\n\nSTART_CUSTOM_VALUES\nWaitTimeBeforeCure_2.5\nBottomLiftHeight_8\nLiftHeight_8\nBottomLiftSpeed_120\nLiftSpeed_120\nRetractSpeed_120\nBottomLightPWM_255\nLightPWM_255\nEND_CUSTOM_VALUES +printer_settings_id = +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +sla_archive_format = SL1 +sla_output_precision = 0.001 +slow_tilt_time = 8 +thumbnails = 224x168/PNG diff --git a/PrusaSlicer/printer/AnyCubic Photon Mono M7 Max.ini b/PrusaSlicer/printer/AnyCubic Photon Mono M7 Max.ini new file mode 100644 index 00000000..c8d8bed9 --- /dev/null +++ b/PrusaSlicer/printer/AnyCubic Photon Mono M7 Max.ini @@ -0,0 +1,43 @@ +# generated by PrusaSlicer 2.8.1+win64 on 2024-12-15 at 23:53:58 UTC +absolute_correction = 0 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,298.08x0,298.08x165.6,0x165.6 +default_sla_material_profile = Prusa Orange Tough 0.05 +default_sla_print_profile = 0.05 Normal +display_height = 165.6 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 6480 +display_pixels_y = 3600 +display_width = 298.08 +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +fast_tilt_time = 5 +gamma_correction = 1 +high_viscosity_tilt_time = 10 +host_type = octoprint +inherits = Original Prusa SL1 +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 300 +min_exposure_time = 1 +min_initial_exposure_time = 1 +print_host = +printer_model = SL1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_SL1\nPRINTER_VENDOR_ANYCUBIC\nPRINTER_MODEL_PHOTON_MONO_M7_MAX\nFILEFORMAT_PM5M\n\nSTART_CUSTOM_VALUES\nWaitTimeBeforeCure_2.5\nBottomLiftHeight_8\nLiftHeight_8\nBottomLiftSpeed_120\nLiftSpeed_120\nRetractSpeed_120\nBottomLightPWM_255\nLightPWM_255\nEND_CUSTOM_VALUES +printer_settings_id = +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +sla_archive_format = SL1 +sla_output_precision = 0.001 +slow_tilt_time = 8 +thumbnails = 224x168/PNG diff --git a/PrusaSlicer/printer/AnyCubic Photon Mono M7 Pro.ini b/PrusaSlicer/printer/AnyCubic Photon Mono M7 Pro.ini new file mode 100644 index 00000000..c77ffc45 --- /dev/null +++ b/PrusaSlicer/printer/AnyCubic Photon Mono M7 Pro.ini @@ -0,0 +1,43 @@ +# generated by PrusaSlicer 2.8.1+win64 on 2024-12-15 at 23:53:01 UTC +absolute_correction = 0 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,223.642x0,223.642x126.976,0x126.976 +default_sla_material_profile = Prusa Orange Tough 0.05 +default_sla_print_profile = 0.05 Normal +display_height = 126.976 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 13312 +display_pixels_y = 5120 +display_width = 223.642 +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +fast_tilt_time = 5 +gamma_correction = 0 +high_viscosity_tilt_time = 10 +host_type = octoprint +inherits = Original Prusa SL1 +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 230 +min_exposure_time = 1 +min_initial_exposure_time = 1 +print_host = +printer_model = SL1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_SL1\nPRINTER_VENDOR_ANYCUBIC\nPRINTER_MODEL_PHOTON_MONO_M7_PRO\nFILEFORMAT_PWSZ\n\nSTART_CUSTOM_VALUES\nWaitTimeBeforeCure_2.5\nBottomLiftHeight_8\nLiftHeight_8\nBottomLiftSpeed_120\nLiftSpeed_120\nRetractSpeed_120\nBottomLightPWM_255\nLightPWM_255\nEND_CUSTOM_VALUES +printer_settings_id = +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +sla_archive_format = SL1 +sla_output_precision = 0.001 +slow_tilt_time = 8 +thumbnails = 224x168/PNG diff --git a/PrusaSlicer/printer/AnyCubic Photon Mono M7.ini b/PrusaSlicer/printer/AnyCubic Photon Mono M7.ini new file mode 100644 index 00000000..7bbe0d14 --- /dev/null +++ b/PrusaSlicer/printer/AnyCubic Photon Mono M7.ini @@ -0,0 +1,43 @@ +# generated by PrusaSlicer 2.8.1+win64 on 2024-12-15 at 23:53:05 UTC +absolute_correction = 0 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,223.642x0,223.642x126.976,0x126.976 +default_sla_material_profile = Prusa Orange Tough 0.05 +default_sla_print_profile = 0.05 Normal +display_height = 126.976 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 13312 +display_pixels_y = 5120 +display_width = 223.642 +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +fast_tilt_time = 5 +gamma_correction = 0 +high_viscosity_tilt_time = 10 +host_type = octoprint +inherits = Original Prusa SL1 +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 230 +min_exposure_time = 1 +min_initial_exposure_time = 1 +print_host = +printer_model = SL1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_SL1\nPRINTER_VENDOR_ANYCUBIC\nPRINTER_MODEL_PHOTON_MONO_M7\nFILEFORMAT_PM7\n\nSTART_CUSTOM_VALUES\nWaitTimeBeforeCure_2.5\nBottomLiftHeight_8\nLiftHeight_8\nBottomLiftSpeed_120\nLiftSpeed_120\nRetractSpeed_120\nBottomLightPWM_255\nLightPWM_255\nEND_CUSTOM_VALUES +printer_settings_id = +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +sla_archive_format = SL1 +sla_output_precision = 0.001 +slow_tilt_time = 8 +thumbnails = 224x168/PNG diff --git a/README.md b/README.md index 06e3c699..7406dd56 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ But also, I need victims for test subject. Proceed at your own risk! - SL1, SL1S (PrusaSlicer) - Photon, Photons, CBDDLP, CTB, PHZ, FDG, ZIP (Chitubox) -- PWS, PW0, PWX, DLP, DL2P, PWMO, PWMA, PWMS, PWMX, PMX2, PWMB, PWSQ, PX6S, PM3, PM3N, PM3M, PM3R, PM5, PM5S, PWC (Photon Workshop) +- PWS, PW0, PWX, DLP, DL2P, PWMO, PWMA, PWMS, PWMX, PMX2, PWMB, PWSQ, PX6S, PM3, PM3N, PM3M, PM4M, PM4U, PM3R, PM5, PM5S, PWC, PM7, PM7M, PWSZ (Photon Workshop) - JXS (GKone Slicer) - ZCode (UnizMaker) - ZCodex (Z-Suite) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b4d23e58..9359112b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,9 +1,26 @@ - **File formats:** - - (Change) Rename file and class from `PhotonSFile` to `AnycubicPhotonSFile` - - (Change) Rename file and class from `PhotonWorkshopFile` to `AnycubicFile` - - (Change) Rename file and class from `CXDLPFile` to `CrealityCXDLPFile` - - (Change) Rename convert menu group from `CXDLP` to `Creality CXDLP` - - (Fix) CTB (Version 5): `NullReferenceException` when trying to convert from a file with a `null` MaterialName (#857) - - (Fix) Sanitize file version before convert the file to ensure capabilities (#934) - - (Fix) Unable to set the format version when converting from files with a version that match it own default version (#857) + - (Add) Anycubic PWSZ Zip file format (#892) + - (Add) Support for Anycubic Photon Mono 4 (pm4n) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono 4 Ultra (pm4u) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono M7 (pm7) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono M7 Max (pm7m) and corresponding PrusaSlicer profile + - (Add) Support for Anycubic Photon Mono M7 Pro (pwsz) and corresponding PrusaSlicer profile +- (Add) PrusaSlicer printer: Phrozen Sonic Mighty Revo (#950) +- (Add) Litophane tool: Option to enable or disable the separation of grayscale pixels (#954) +- (Add) Exposure time finder: Re-arrange exposure text layout and allow to change it font (#955) +- (Add) Setting: Available RAM lower limit - Sets a lower limiter for the available memory RAM the program is allowed to run operations. + When meet the threshold, a stopping action will be queued to relief pressure and maintain the system stability. + In cases where is unable to pause or cancel the operation, the program will be forced to close and trigger an exception to ensure the system stability. + Note: This limiter will check the RAM every 2 seconds while operations are running, if you have set a very low limit there is a chance to consume more RAM in the time and cause system instability. + System can also reserve some RAM to prevent depletion and start to use SWAP memory, doing such with a low limit and is possible that the limiter never trigger. + 0: Ignore the RAM limiter. + Default: 1 GB +- (Improvement) Change the gif animation library to a more efficient and compatible one +- (Fix) Contour traverse function was duplicating the contours, causing wrong calculations +- (Fix) Blur: Prevent stack blur from use even values +- (Upgrade) AvaloniaUI from 11.1.3 to 11.2.2 +- (Upgrade) .NET from 6.0.33 to 9.0.0 + - .NET 6.0 is end of life and will no longer supported + - This represents three major upgrades and will increase the system os version requirements, see more here: https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md + - With this upgrade the software will be able to take advantage of the new features and improvements of the .NET 9.0, including a significant performance boost diff --git a/Scripts/010 Editor/pwsz.bt b/Scripts/010 Editor/pwsz.bt index ef0ed401..d63197b5 100644 --- a/Scripts/010 Editor/pwsz.bt +++ b/Scripts/010 Editor/pwsz.bt @@ -16,15 +16,15 @@ struct BOUNDING_BOX { float EndXOffsetFromCenter ; // in mm float EndYOffsetFromCenter ; // in mm float Padding ; // 0 - uint CountourCount ; // + uint ObjectCount ; // } BoundingBox; char ImageStartMarker[4] ; // [-- uint Padding ; // 0 -uint CoordinateCount ; // +uint LineCount ; // uint Unknown ; // So far always 1 -struct COORDINATE { +struct LINE { float StartXOffsetFromCenter ; // in mm float StartYOffsetFromCenter ; // in mm float EndXOffsetFromCenter ; // in mm @@ -32,12 +32,12 @@ struct COORDINATE { ubyte Unknown ; // 0 or 1 }; -struct COUNTOURS{ +struct LINES{ local uint i; - for( i = 0; i < CoordinateCount; i++ ){ - COORDINATE coordinate; + for( i = 0; i < LineCount; i++ ){ + LINE line; } - } Countours; + } lines; char ImageEndMarker[4] ; // --] char FileEndMarker[4] ; // ==} diff --git a/Scripts/010 Editor/pwszScene.bt b/Scripts/010 Editor/pwszScene.bt index 94611f66..468d93f6 100644 --- a/Scripts/010 Editor/pwszScene.bt +++ b/Scripts/010 Editor/pwszScene.bt @@ -18,11 +18,11 @@ struct HEADER { uint ModelUnit ; // 0: mm; 1: cm; 2: m; Currently fixed at 0 float PointRatio ; // Currently fixed at 1.0f uint LayerCount ; - float MinX ; // - float MinY ; // + float XStartBoundingRectangleOffsetFromCenter ; // + float YStartBoundingRectangleOffsetFromCenter ; // float MinZ ; // - float MaxX ; // - float MaxY ; // + float XEndBoundingRectangleOffsetFromCenter ; // + float YEndBoundingRectangleOffsetFromCenter ; // float MaxZ ; // uint ModelStats ; // Some status flags of the scene model uint Padding[64] ; // @@ -33,10 +33,10 @@ struct HEADER { struct LAYER_DEF { float Height ; float Area ; // mm^2 - float XMin ; - float YMin ; - float XMax ; - float YMax ; + float XStartBoundingRectangleOffsetFromCenter ; + float YStartBoundingRectangleOffsetFromCenter ; + float YEndBoundingRectangleOffsetFromCenter ; + float YEndOffsetFromCenter ; uint ContourCount ; float MaxContourArea ; // mm^2 uint Padding[8] ; diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index 7a6f99a8..090efa5f 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -1,13 +1,14 @@  - UVtools Avalonia Controls + net8.0;net9.0 + UVtools Avalonia Controls AvaloniaUI Controls - AdvancedImageBox: Pan, zoom, cursor, pixel grid and selections image box - ExtendedNumericUpDown: Initial value with a reset button and value unit label - IndexSelector: Allow to choose an index from a collection count and display the selected number - GroupBox: Similar to GroupBox of WinForms, it contain an Header and Content - 3.1.0 + 4.0.0 MIT https://github.com/sn4k3/UVtools/tree/master/UVtools.AvaloniaControls diff --git a/UVtools.Core/EmguCV/EmguContour.cs b/UVtools.Core/EmguCV/EmguContour.cs index 327a1519..54b31790 100644 --- a/UVtools.Core/EmguCV/EmguContour.cs +++ b/UVtools.Core/EmguCV/EmguContour.cs @@ -97,6 +97,16 @@ public double Perimeter } } + /// + /// Gets if the contour is closed + /// + public bool IsClosed => IsConvex || Area > Perimeter; + + /// + /// Gets if the contour is open + /// + public bool IsOpen => !IsClosed; + public Moments Moments => _moments ??= CvInvoke.Moments(_vector); /// diff --git a/UVtools.Core/EmguCV/EmguContourFamily.cs b/UVtools.Core/EmguCV/EmguContourFamily.cs index af429016..ce6b86d2 100644 --- a/UVtools.Core/EmguCV/EmguContourFamily.cs +++ b/UVtools.Core/EmguCV/EmguContourFamily.cs @@ -127,7 +127,7 @@ public IEnumerable TraverseTree() yield return currentFamily; foreach (var child in currentFamily) { - yield return child; + //yield return child; queue.Enqueue(child); } } diff --git a/UVtools.Core/Enumerations.cs b/UVtools.Core/Enumerations.cs index f5d0efce..e5675664 100644 --- a/UVtools.Core/Enumerations.cs +++ b/UVtools.Core/Enumerations.cs @@ -204,4 +204,15 @@ public static RotateFlags ToOpenCVRotateFlags(RotateDirection rotate) _ => throw new ArgumentOutOfRangeException(nameof(rotate), rotate, null) }; } -}*/ \ No newline at end of file +}*/ + +/// +/// The action to take when the RAM hit the limit. +/// +public enum RamLimitAction : byte +{ + [Description("Pause the current operation")] + Pause, + [Description("Cancel the current operation")] + Cancel, +} \ No newline at end of file diff --git a/UVtools.Core/Exceptions/MessageException.cs b/UVtools.Core/Exceptions/MessageException.cs index 11b5bdb5..302f6167 100644 --- a/UVtools.Core/Exceptions/MessageException.cs +++ b/UVtools.Core/Exceptions/MessageException.cs @@ -18,9 +18,6 @@ public class MessageException : Exception { public string? Title { get; } - protected MessageException(SerializationInfo info, StreamingContext context) : base(info, context) - { } - public MessageException(string? message, string? title = null) : base(message) { Title = title; diff --git a/UVtools.Core/Extensions/BitExtensions.cs b/UVtools.Core/Extensions/BitExtensions.cs index fdbfbb47..d117e26c 100644 --- a/UVtools.Core/Extensions/BitExtensions.cs +++ b/UVtools.Core/Extensions/BitExtensions.cs @@ -5,28 +5,14 @@ * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ + +using System; +using System.Buffers.Binary; + namespace UVtools.Core.Extensions; public static class BitExtensions { - public static ushort ToUShortLittleEndian(byte byte1, byte byte2) => (ushort)(byte1 + (byte2 << 8)); - public static ushort ToUShortBigEndian(byte byte1, byte byte2) => (ushort)((byte1 << 8) + byte2); - - public static ushort ToUShortLittleEndian(byte[] buffer, int offset = 0) - => (ushort)(buffer[offset] + (buffer[offset+1] << 8)); - public static ushort ToUShortBigEndian(byte[] buffer, int offset = 0) - => (ushort)((buffer[offset] << 8) + buffer[offset+1]); - - public static uint ToUIntLittleEndian(byte byte1, byte byte2, byte byte3, byte byte4) - => (uint)(byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); - public static uint ToUIntBigEndian(byte byte1, byte byte2, byte byte3, byte byte4) - => (uint)((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4); - - public static uint ToUIntLittleEndian(byte[] buffer, int offset = 0) - => (uint)(buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24)); - public static uint ToUIntBigEndian(byte[] buffer, int offset = 0) - => (uint)((buffer[offset] << 24) + (buffer[offset+1] << 16) + (buffer[offset+2] << 8) + buffer[offset+3]); - public static byte[] ToBytesLittleEndian(ushort value) { var bytes = new byte[2]; @@ -34,10 +20,11 @@ public static byte[] ToBytesLittleEndian(ushort value) return bytes; } - public static void ToBytesLittleEndian(ushort value, byte[] buffer, uint offset = 0) + public static void ToBytesLittleEndian(ushort value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)value; - buffer[offset + 1] = (byte)(value >> 8); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(offset, 2), value); + //buffer[offset] = (byte)value; + //buffer[offset + 1] = (byte)(value >> 8); } public static byte[] ToBytesBigEndian(ushort value) @@ -47,10 +34,11 @@ public static byte[] ToBytesBigEndian(ushort value) return bytes; } - public static void ToBytesBigEndian(ushort value, byte[] buffer, uint offset = 0) + public static void ToBytesBigEndian(ushort value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)(value >> 8); - buffer[offset + 1] = (byte)value; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset, 2), value); + //buffer[offset] = (byte)(value >> 8); + //buffer[offset + 1] = (byte)value; } public static byte[] ToBytesLittleEndian(uint value) @@ -60,12 +48,13 @@ public static byte[] ToBytesLittleEndian(uint value) return bytes; } - public static void ToBytesLittleEndian(uint value, byte[] buffer, uint offset = 0) + public static void ToBytesLittleEndian(uint value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)value; + BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), value); + /*buffer[offset] = (byte)value; buffer[offset + 1] = (byte)(value >> 8); buffer[offset + 2] = (byte)(value >> 16); - buffer[offset + 3] = (byte)(value >> 24); + buffer[offset + 3] = (byte)(value >> 24);*/ } public static byte[] ToBytesBigEndian(uint value) @@ -75,12 +64,13 @@ public static byte[] ToBytesBigEndian(uint value) return bytes; } - public static void ToBytesBigEndian(uint value, byte[] buffer, uint offset = 0) + public static void ToBytesBigEndian(uint value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)(value >> 24); + BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(offset, 4), value); + /*buffer[offset] = (byte)(value >> 24); buffer[offset + 1] = (byte)(value >> 16); buffer[offset + 2] = (byte)(value >> 8); - buffer[offset + 3] = (byte)value; + buffer[offset + 3] = (byte)value;*/ } public static byte[] ToBytesLittleEndian(int value) @@ -90,12 +80,13 @@ public static byte[] ToBytesLittleEndian(int value) return bytes; } - public static void ToBytesLittleEndian(int value, byte[] buffer, uint offset = 0) + public static void ToBytesLittleEndian(int value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)value; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(offset, 4), value); + /*buffer[offset] = (byte)value; buffer[offset + 1] = (byte)(value >> 8); buffer[offset + 2] = (byte)(value >> 16); - buffer[offset + 3] = (byte)(value >> 24); + buffer[offset + 3] = (byte)(value >> 24);*/ } public static byte[] ToBytesBigEndian(int value) @@ -105,12 +96,37 @@ public static byte[] ToBytesBigEndian(int value) return bytes; } - public static void ToBytesBigEndian(int value, byte[] buffer, uint offset = 0) + public static void ToBytesBigEndian(int value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)(value >> 24); + BinaryPrimitives.WriteInt32BigEndian(buffer.AsSpan(offset, 4), value); + /*buffer[offset] = (byte)(value >> 24); buffer[offset + 1] = (byte)(value >> 16); buffer[offset + 2] = (byte)(value >> 8); - buffer[offset + 3] = (byte)value; + buffer[offset + 3] = (byte)value;*/ + } + + public static byte[] ToBytesLittleEndian(float value) + { + var bytes = new byte[4]; + ToBytesLittleEndian(value, bytes); + return bytes; + } + + public static void ToBytesLittleEndian(float value, byte[] buffer, uint offset = 0) + { + BinaryPrimitives.WriteSingleLittleEndian(buffer.AsSpan((int)offset, 4), value); + } + + public static byte[] ToBytesBigEndian(float value) + { + var bytes = new byte[4]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(float value, byte[] buffer, uint offset = 0) + { + BinaryPrimitives.WriteSingleBigEndian(buffer.AsSpan((int)offset, 4), value); } @@ -121,9 +137,10 @@ public static byte[] ToBytesLittleEndian(ulong value) return bytes; } - public static void ToBytesLittleEndian(ulong value, byte[] buffer, ulong offset = 0) + public static void ToBytesLittleEndian(ulong value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)value; + BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(offset, 8), value); + /*buffer[offset] = (byte)value; buffer[offset + 1] = (byte)(value >> 8); buffer[offset + 2] = (byte)(value >> 16); buffer[offset + 3] = (byte)(value >> 24); @@ -131,7 +148,7 @@ public static void ToBytesLittleEndian(ulong value, byte[] buffer, ulong offset buffer[offset + 4] = (byte)(value >> 32); buffer[offset + 5] = (byte)(value >> 40); buffer[offset + 6] = (byte)(value >> 48); - buffer[offset + 7] = (byte)(value >> 56); + buffer[offset + 7] = (byte)(value >> 56);*/ } public static byte[] ToBytesBigEndian(ulong value) @@ -141,17 +158,97 @@ public static byte[] ToBytesBigEndian(ulong value) return bytes; } - public static void ToBytesBigEndian(ulong value, byte[] buffer, ulong offset = 0) + public static void ToBytesBigEndian(ulong value, byte[] buffer, int offset = 0) { - buffer[offset] = (byte)(value >> 56); + BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(offset, 8), value); + /*buffer[offset] = (byte)(value >> 56); buffer[offset + 1] = (byte)(value >> 48); buffer[offset + 2] = (byte)(value >> 40); buffer[offset + 3] = (byte)(value >> 32); buffer[offset + 4] = (byte)(value >> 24); buffer[offset + 5] = (byte)(value >> 16); buffer[offset + 6] = (byte)(value >> 8); - buffer[offset + 7] = (byte)value; + buffer[offset + 7] = (byte)value;*/ + } + + public static ushort ToUShortLittleEndian(byte byte1, byte byte2) + { + return BinaryPrimitives.ReadUInt16LittleEndian(new ReadOnlySpan(new []{ byte1, byte2})); + //return (ushort)(byte1 + (byte2 << 8)); + } + + public static ushort ToUShortBigEndian(byte byte1, byte byte2) + { + return BinaryPrimitives.ReadUInt16BigEndian(new ReadOnlySpan(new[] { byte1, byte2 })); + //return (ushort)((byte1 << 8) + byte2); + } + + public static ushort ToUShortLittleEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadUInt16LittleEndian(new ReadOnlySpan(buffer, offset, 2)); + //return (ushort)(buffer[offset] + (buffer[offset + 1] << 8)); + } + + public static ushort ToUShortBigEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadUInt16BigEndian(new ReadOnlySpan(buffer, offset, 2)); + //return (ushort)((buffer[offset] << 8) + buffer[offset + 1]); + } + + public static uint ToUIntLittleEndian(byte byte1, byte byte2, byte byte3, byte byte4) + { + return BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan(new[] { byte1, byte2, byte3, byte4 })); + //return (uint)(byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); + } + + public static uint ToUIntBigEndian(byte byte1, byte byte2, byte byte3, byte byte4) + { + return BinaryPrimitives.ReadUInt32BigEndian(new ReadOnlySpan(new[] { byte1, byte2, byte3, byte4 })); + //return (uint)((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4); + } + + public static uint ToUIntLittleEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan(buffer, offset, 4)); + //return (uint)(buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24)); + } + + public static uint ToUIntBigEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadUInt32BigEndian(new ReadOnlySpan(buffer, offset, 4)); + //return (uint)((buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + buffer[offset + 3]); + } + + public static int ToIntLittleEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan(buffer, offset, 4)); + //return buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24); + } + + public static int ToIntBigEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadInt32BigEndian(new ReadOnlySpan(buffer, offset, 4)); + //return (buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + buffer[offset + 3]; } + public static float ToSingleLittleEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadSingleLittleEndian(buffer.AsSpan(offset, 4)); + } + + public static float ToSingleBigEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadSingleBigEndian(buffer.AsSpan(offset, 4)); + } + + public static double ToDoubleLittleEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadDoubleLittleEndian(buffer.AsSpan(offset, 8)); + } + + public static double ToDoubleBigEndian(byte[] buffer, int offset = 0) + { + return BinaryPrimitives.ReadDoubleBigEndian(buffer.AsSpan(offset, 8)); + } } \ No newline at end of file diff --git a/UVtools.Core/Extensions/CompressionExtensions.cs b/UVtools.Core/Extensions/CompressionExtensions.cs index 844c812b..664768de 100644 --- a/UVtools.Core/Extensions/CompressionExtensions.cs +++ b/UVtools.Core/Extensions/CompressionExtensions.cs @@ -27,7 +27,7 @@ public static int GetGzipUncompressedLength(Stream stream) { Span uncompressedLength = stackalloc byte[4]; stream.Seek(-4, SeekOrigin.End); - stream.Read(uncompressedLength); + stream.ReadExactly(uncompressedLength); stream.Seek(0, SeekOrigin.Begin); return BitConverter.ToInt32(uncompressedLength); } diff --git a/UVtools.Core/Extensions/CryptExtensions.cs b/UVtools.Core/Extensions/CryptExtensions.cs index ae848207..158d22a7 100644 --- a/UVtools.Core/Extensions/CryptExtensions.cs +++ b/UVtools.Core/Extensions/CryptExtensions.cs @@ -61,7 +61,7 @@ public static byte[] AesCryptBytes(byte[] data, byte[] key, CipherMode mode, Pad using var msDecrypt = new MemoryStream(data); using var csDecrypt = new CryptoStream(msDecrypt, cryptor, CryptoStreamMode.Read); var outputBuffer = new byte[data.Length]; - csDecrypt.Read(outputBuffer, 0, data.Length); + csDecrypt.ReadExactly(outputBuffer.AsSpan()); return outputBuffer; } diff --git a/UVtools.Core/Extensions/FileStreamExtensions.cs b/UVtools.Core/Extensions/FileStreamExtensions.cs index 2994c17d..72e2aaef 100644 --- a/UVtools.Core/Extensions/FileStreamExtensions.cs +++ b/UVtools.Core/Extensions/FileStreamExtensions.cs @@ -22,7 +22,7 @@ public static uint ReadBytes(this FileStream fs, byte[] bytes, int offset = 0) public static byte[] ReadBytes(this FileStream fs, int length, int offset = 0) { var buffer = new byte[length]; - fs.Read(buffer, offset, length); + fs.ReadExactly(buffer, offset, length); return buffer; } diff --git a/UVtools.Core/Extensions/StreamExtensions.cs b/UVtools.Core/Extensions/StreamExtensions.cs index 7d906e76..0fb0d066 100644 --- a/UVtools.Core/Extensions/StreamExtensions.cs +++ b/UVtools.Core/Extensions/StreamExtensions.cs @@ -22,23 +22,6 @@ public static class StreamExtensions //public const int DefaultCopyBufferSize = 512000; // 512 kilobytes public const int DefaultCopyBufferSize = 1048576; // 1 MB - public static void ReadExactly(this Stream input, byte[] buffer, int bytesToRead) - { - int index = 0; - while (index < bytesToRead) - { - int read = input.Read(buffer, index, bytesToRead - index); - if (read == 0) - { - throw new EndOfStreamException - (string.Format("End of stream reached with {0} byte{1} left to read.", - bytesToRead - index, - bytesToRead - index == 1 ? 's' : string.Empty)); - } - index += read; - } - } - /// /// Converts stream into byte array /// diff --git a/UVtools.Core/FileFormats/AnycubicFile.cs b/UVtools.Core/FileFormats/AnycubicFile.cs index 82142bbd..0694e3f9 100644 --- a/UVtools.Core/FileFormats/AnycubicFile.cs +++ b/UVtools.Core/FileFormats/AnycubicFile.cs @@ -90,6 +90,7 @@ public enum AnyCubicMachine : byte PhotonD2, PhotonMono, PhotonMono2, + PhotonMono4, PhotonMonoSE, PhotonMono4K, PhotonMonoX, @@ -717,7 +718,7 @@ public void CopyTo(Layer layer) public Mat Decode(AnycubicFile slicerFile, bool consumeData = true) { - var result = slicerFile.RleFormat == PhotonRleFormat.PWS ? DecodePWS(slicerFile) : DecodePW0(slicerFile); + var result = slicerFile.RleFormat == PhotonRleFormat.PWS ? DecodePWS(slicerFile) : DecodePW0(slicerFile.CreateMat(), EncodedRle); if (consumeData) EncodedRle = null!; @@ -727,6 +728,7 @@ public Mat Decode(AnycubicFile slicerFile, bool consumeData = true) public byte[] Encode(AnycubicFile slicerFile, Mat image) { EncodedRle = slicerFile.RleFormat == PhotonRleFormat.PWS ? EncodePWS(slicerFile, image) : EncodePW0(image); + DataLength = (uint)EncodedRle.Length; return EncodedRle; } @@ -857,152 +859,6 @@ void AddRep() return rawData.ToArray(); } - private Mat DecodePW0(AnycubicFile slicerFile) - { - var mat = slicerFile.CreateMat(); - var imageLength = mat.GetLength(); - - int pixelPos = 0; - for (int i = 0; i < EncodedRle.Length; i++) - { - byte b = EncodedRle[i]; - int code = b >> 4; - int repeat = b & 0xf; - byte color; - switch (code) - { - case 0x0: - color = 0; - i++; - //reps = reps * 256 + EncodedRle[i]; - if (i >= EncodedRle.Length) - { - repeat = imageLength - pixelPos; - break; - } - - repeat = (repeat << 8) + EncodedRle[i]; - break; - case 0xf: - color = 255; - i++; - //reps = reps * 256 + EncodedRle[i]; - if (i >= EncodedRle.Length) - { - repeat = imageLength - pixelPos; - break; - } - - repeat = (repeat << 8) + EncodedRle[i]; - break; - default: - color = (byte) ((code << 4) | code); - if (i >= EncodedRle.Length) - { - repeat = imageLength - pixelPos; - } - break; - } - - //color &= 0xff; - - if (pixelPos + repeat > imageLength) - { - mat.Dispose(); - throw new FileLoadException($"Image ran off the end: {pixelPos} + {repeat} = {pixelPos + repeat}, expecting: {imageLength}"); - } - - // We only need to set the non-zero pixels - mat.FillSpan(ref pixelPos, repeat, color); - - - if (pixelPos == imageLength) - { - //i++; - break; - } - } - - if (pixelPos > 0 && pixelPos != imageLength) - { - mat.Dispose(); - throw new FileLoadException($"Image ended short: {pixelPos}, expecting: {imageLength}"); - } - - return mat; - } - - public unsafe byte[] EncodePW0(Mat image) - { - List rawData = new(); - var span = image.GetBytePointer(); - var imageLength = image.GetLength(); - - int lastColor = -1; - int reps = 0; - - void PutReps() - { - while (reps > 0) - { - int done = reps; - - if (lastColor is 0 or 0xf) - { - if (done > RLE4EncodingLimit) - { - done = RLE4EncodingLimit; - } - //more:= []byte{ 0, 0} - //binary.BigEndian.PutUint16(more, uint16(done | (color << 12))) - - //rle = append(rle, more...) - - ushort more = (ushort)(done | (lastColor << 12)); - rawData.Add((byte)(more >> 8)); - rawData.Add((byte)more); - } - else - { - if (done > 0xf) - { - done = 0xf; - } - rawData.Add((byte)(done | lastColor << 4)); - } - - reps -= done; - } - } - - for (int i = 0; i < imageLength; i++) - { - int color = span[i] >> 4; - - if (color == lastColor) - { - reps++; - } - else - { - PutReps(); - lastColor = color; - reps = 1; - } - } - - PutReps(); - - EncodedRle = rawData.ToArray(); - DataLength = (uint)rawData.Count; - - ushort crc = CRCRle4(EncodedRle); - rawData.Add((byte)(crc >> 8)); - rawData.Add((byte)crc); - - return EncodedRle; - } - public static ushort CRCRle4(byte[] data) { ushort crc16 = 0; @@ -1196,6 +1052,7 @@ public override string ToString() new(typeof(AnycubicFile), "px6s", "Photon Mono X 6Ks (PX6S)"), new(typeof(AnycubicFile), "pwmo", "Photon Mono (PWMO)"), new(typeof(AnycubicFile), "pm3n", "Photon Mono 2 (PM3N)"), + new(typeof(AnycubicFile), "pm4n", "Photon Mono 4 (PM4N)"), new(typeof(AnycubicFile), "pwms", "Photon Mono SE (PWMS)"), new(typeof(AnycubicFile), "pwma", "Photon Mono 4K (PWMA)"), new(typeof(AnycubicFile), "pmsq", "Photon Mono SQ (PMSQ)"), @@ -1366,6 +1223,7 @@ public override float DisplayWidth AnyCubicMachine.PhotonD2 => 130.56f, AnyCubicMachine.PhotonMono => 82.62f, AnyCubicMachine.PhotonMono2 => 143.36f, + AnyCubicMachine.PhotonMono4 => 153.408f, AnyCubicMachine.PhotonMonoSE => 82.62f, AnyCubicMachine.PhotonMono4K => 134.40f, AnyCubicMachine.PhotonMonoX => 192, @@ -1398,6 +1256,7 @@ public override float DisplayHeight AnyCubicMachine.PhotonD2 => 73.44f, AnyCubicMachine.PhotonMono => 130.56f, AnyCubicMachine.PhotonMono2 => 89.60f, + AnyCubicMachine.PhotonMono4 => 87.040f, AnyCubicMachine.PhotonMonoSE => 130.56f, AnyCubicMachine.PhotonMono4K => 84, AnyCubicMachine.PhotonMonoX => 120, @@ -1431,6 +1290,7 @@ public override float MachineZ AnyCubicMachine.PhotonD2 => 165, AnyCubicMachine.PhotonMono => 165, AnyCubicMachine.PhotonMono2 => 165, + AnyCubicMachine.PhotonMono4 => 165, AnyCubicMachine.PhotonMonoSE => 160, AnyCubicMachine.PhotonMono4K => 165, AnyCubicMachine.PhotonMonoX => 245, @@ -1729,6 +1589,7 @@ public override string MachineName AnyCubicMachine.PhotonD2 => "Photon D2", AnyCubicMachine.PhotonMono => "Photon Mono", AnyCubicMachine.PhotonMono2 => "Photon Mono 2", + AnyCubicMachine.PhotonMono4 => "Photon Mono 4", AnyCubicMachine.PhotonMonoSE => "Photon Mono SE", AnyCubicMachine.PhotonMono4K => "Photon Mono 4K", AnyCubicMachine.PhotonMonoX => "Photon Mono X", @@ -1830,6 +1691,11 @@ public AnyCubicMachine PrinterModel return AnyCubicMachine.PhotonMono2; } + if (FileEndsWith(".pm4n")) + { + return AnyCubicMachine.PhotonMono4; + } + if (FileEndsWith(".pwms")) { return AnyCubicMachine.PhotonMonoSE; @@ -2343,5 +2209,147 @@ protected override void PartialSaveInternally(OperationProgress progress) outputFile.WriteSerialize(LayersDefinition); } + + public static Mat DecodePW0(Mat mat, byte[] encodedRle) + { + var imageLength = mat.GetLength(); + + int pixelPos = 0; + for (int i = 0; i < encodedRle.Length; i++) + { + byte b = encodedRle[i]; + int code = b >> 4; + int repeat = b & 0xf; + byte color; + switch (code) + { + case 0x0: + color = 0; + i++; + //reps = reps * 256 + EncodedRle[i]; + if (i >= encodedRle.Length) + { + repeat = imageLength - pixelPos; + break; + } + + repeat = (repeat << 8) + encodedRle[i]; + break; + case 0xf: + color = 255; + i++; + //reps = reps * 256 + EncodedRle[i]; + if (i >= encodedRle.Length) + { + repeat = imageLength - pixelPos; + break; + } + + repeat = (repeat << 8) + encodedRle[i]; + break; + default: + color = (byte)((code << 4) | code); + if (i >= encodedRle.Length) + { + repeat = imageLength - pixelPos; + } + break; + } + + //color &= 0xff; + + if (pixelPos + repeat > imageLength) + { + mat.Dispose(); + throw new FileLoadException($"Image ran off the end: {pixelPos} + {repeat} = {pixelPos + repeat}, expecting: {imageLength}"); + } + + // We only need to set the non-zero pixels + mat.FillSpan(ref pixelPos, repeat, color); + + + if (pixelPos == imageLength) + { + //i++; + break; + } + } + + if (pixelPos > 0 && pixelPos != imageLength) + { + mat.Dispose(); + throw new FileLoadException($"Image ended short: {pixelPos}, expecting: {imageLength}"); + } + + return mat; + } + + public static byte[] EncodePW0(Mat image) + { + List rawData = new(); + var span = image.GetDataByteSpan(); + + int lastColor = -1; + int reps = 0; + + void PutReps() + { + while (reps > 0) + { + int done = reps; + + if (lastColor is 0 or 0xf) + { + if (done > RLE4EncodingLimit) + { + done = RLE4EncodingLimit; + } + + ushort more = (ushort)(done | (lastColor << 12)); + rawData.Add((byte)(more >> 8)); + rawData.Add((byte)more); + } + else + { + if (done > 0xf) + { + done = 0xf; + } + rawData.Add((byte)(done | lastColor << 4)); + } + + reps -= done; + } + } + + for (int i = 0; i < span.Length; i++) + { + int color = span[i] >> 4; + + if (color == lastColor) + { + reps++; + } + else + { + PutReps(); + lastColor = color; + reps = 1; + } + } + + PutReps(); + + return rawData.ToArray(); + /*EncodedRle = rawData.ToArray(); + DataLength = (uint)rawData.Count; + + ushort crc = CRCRle4(EncodedRle); + rawData.Add((byte)(crc >> 8)); + rawData.Add((byte)crc); + + return EncodedRle;*/ + } + #endregion } \ No newline at end of file diff --git a/UVtools.Core/FileFormats/AnycubicZipFile.cs b/UVtools.Core/FileFormats/AnycubicZipFile.cs index e6e8faeb..6210daab 100644 --- a/UVtools.Core/FileFormats/AnycubicZipFile.cs +++ b/UVtools.Core/FileFormats/AnycubicZipFile.cs @@ -10,19 +10,21 @@ using Emgu.CV; using Emgu.CV.CvEnum; using System; +using System.Collections.Generic; using System.Drawing; using System.IO; using System.IO.Compression; +using System.Linq; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; +using Emgu.CV.Util; using UVtools.Core.Converters; using UVtools.Core.EmguCV; using UVtools.Core.Extensions; using UVtools.Core.Layers; using UVtools.Core.Operations; -using static UVtools.Core.FileFormats.AnycubicFile; namespace UVtools.Core.FileFormats; @@ -102,7 +104,7 @@ public sealed class SettingsMachineType public float PixelHeightMicrons { get; set; } [JsonPropertyName("max_samples")] - public byte AntiAliasing { get; set; } + public byte MaxSamples { get; set; } = 16; [JsonPropertyName("property")] public ushort Properties { get; set; } = 119; @@ -144,7 +146,7 @@ public sealed class SettingsMachineType public uint RasterSegmentsCapacity { get; set; } = 100000; [JsonPropertyName("raster_antialiasing")] - public byte RasterAntialiasing { get; set; } = 4; + public byte RasterAntialiasing { get; set; } = 8; [JsonPropertyName("cloudprev_back_color")] public float[] CloudBackgroundColor { get; set; } = { 0.00f, 0.28f, 0.39f }; @@ -155,7 +157,7 @@ public sealed class SettingsMachineType public override string ToString() { return - $"{nameof(Version)}: {Version}, {nameof(Name)}: {Name}, {nameof(KeySuffix)}: {KeySuffix}, {nameof(KeyImageFormat)}: {KeyImageFormat}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(PixelWidthMicrons)}: {PixelWidthMicrons}, {nameof(PixelHeightMicrons)}: {PixelHeightMicrons}, {nameof(AntiAliasing)}: {AntiAliasing}, {nameof(Properties)}: {Properties}, {nameof(DisplayWidth)}: {DisplayWidth}, {nameof(DisplayHeight)}: {DisplayHeight}, {nameof(MachineZ)}: {MachineZ}, {nameof(MaxFileVersion)}: {MaxFileVersion}, {nameof(PreviewBackgroundColor)}: {PreviewBackgroundColor}, {nameof(ModelBackgroundColor)}: {ModelBackgroundColor}, {nameof(SupportsBackgroundColor)}: {SupportsBackgroundColor}, {nameof(PreviewImageSize)}: {PreviewImageSize}, {nameof(Preview2BackgroundColor)}: {Preview2BackgroundColor}, {nameof(Preview2ImageSize)}: {Preview2ImageSize}, {nameof(RasterSegmentsCapacity)}: {RasterSegmentsCapacity}, {nameof(RasterAntialiasing)}: {RasterAntialiasing}, {nameof(CloudBackgroundColor)}: {CloudBackgroundColor}, {nameof(CloudImageSize)}: {CloudImageSize}"; + $"{nameof(Version)}: {Version}, {nameof(Name)}: {Name}, {nameof(KeySuffix)}: {KeySuffix}, {nameof(KeyImageFormat)}: {KeyImageFormat}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(PixelWidthMicrons)}: {PixelWidthMicrons}, {nameof(PixelHeightMicrons)}: {PixelHeightMicrons}, {nameof(MaxSamples)}: {MaxSamples}, {nameof(Properties)}: {Properties}, {nameof(DisplayWidth)}: {DisplayWidth}, {nameof(DisplayHeight)}: {DisplayHeight}, {nameof(MachineZ)}: {MachineZ}, {nameof(MaxFileVersion)}: {MaxFileVersion}, {nameof(PreviewBackgroundColor)}: {PreviewBackgroundColor}, {nameof(ModelBackgroundColor)}: {ModelBackgroundColor}, {nameof(SupportsBackgroundColor)}: {SupportsBackgroundColor}, {nameof(PreviewImageSize)}: {PreviewImageSize}, {nameof(Preview2BackgroundColor)}: {Preview2BackgroundColor}, {nameof(Preview2ImageSize)}: {Preview2ImageSize}, {nameof(RasterSegmentsCapacity)}: {RasterSegmentsCapacity}, {nameof(RasterAntialiasing)}: {RasterAntialiasing}, {nameof(CloudBackgroundColor)}: {CloudBackgroundColor}, {nameof(CloudImageSize)}: {CloudImageSize}"; } } @@ -293,15 +295,15 @@ public sealed class SceneLayerDef [FieldOrder(1)] public float Area { get; set; } - [FieldOrder(2)] public float XMin { get; set; } + [FieldOrder(2)] public float XStartBoundingRectangleOffsetFromCenter { get; set; } - [FieldOrder(3)] public float YMin { get; set; } + [FieldOrder(3)] public float YStartBoundingRectangleOffsetFromCenter { get; set; } - [FieldOrder(4)] public float XMax { get; set; } + [FieldOrder(4)] public float XEndBoundingRectangleOffsetFromCenter { get; set; } - [FieldOrder(5)] public float YMax { get; set; } + [FieldOrder(5)] public float YEndBoundingRectangleOffsetFromCenter { get; set; } - [FieldOrder(6)] public uint ContourCount { get; set; } + [FieldOrder(6)] public uint ObjectCount { get; set; } [FieldOrder(7)] public float MaxContourArea { get; set; } @@ -310,7 +312,7 @@ public sealed class SceneLayerDef public override string ToString() { return - $"{nameof(Height)}: {Height}, {nameof(Area)}: {Area}, {nameof(XMin)}: {XMin}, {nameof(YMin)}: {YMin}, {nameof(XMax)}: {XMax}, {nameof(YMax)}: {YMax}, {nameof(ContourCount)}: {ContourCount}, {nameof(MaxContourArea)}: {MaxContourArea}, {nameof(Padding)}: {Padding}"; + $"{nameof(Height)}: {Height}, {nameof(Area)}: {Area}, {nameof(XStartBoundingRectangleOffsetFromCenter)}: {XStartBoundingRectangleOffsetFromCenter}, {nameof(YStartBoundingRectangleOffsetFromCenter)}: {YStartBoundingRectangleOffsetFromCenter}, {nameof(XEndBoundingRectangleOffsetFromCenter)}: {XEndBoundingRectangleOffsetFromCenter}, {nameof(YEndBoundingRectangleOffsetFromCenter)}: {YEndBoundingRectangleOffsetFromCenter}, {nameof(ObjectCount)}: {ObjectCount}, {nameof(MaxContourArea)}: {MaxContourArea}, {nameof(Padding)}: {Padding}"; } } @@ -350,15 +352,15 @@ public sealed class SceneManifest [FieldOrder(7)] public uint LayerCount { get; set; } - [FieldOrder(8)] public float XMin { get; set; } + [FieldOrder(8)] public float XStartBoundingRectangleOffsetFromCenter { get; set; } - [FieldOrder(9)] public float YMin { get; set; } + [FieldOrder(9)] public float YStartBoundingRectangleOffsetFromCenter { get; set; } [FieldOrder(10)] public float ZMin { get; set; } - [FieldOrder(11)] public float XMax { get; set; } = 1; + [FieldOrder(11)] public float XEndBoundingRectangleOffsetFromCenter { get; set; } - [FieldOrder(12)] public float YMax { get; set; } + [FieldOrder(12)] public float YEndBoundingRectangleOffsetFromCenter { get; set; } [FieldOrder(13)] public float ZMax { get; set; } @@ -382,10 +384,10 @@ public void Update(FileFormat slicerFile) Software = About.SoftwareWithVersionArch; var rect = slicerFile.BoundingRectangleMillimeters; rect.Offset(slicerFile.DisplayWidth / -2f, slicerFile.DisplayHeight / -2f); - XMin = (float)Math.Round(rect.X, 4); - YMin = (float)Math.Round(rect.Y, 4); - XMax = (float)Math.Round(rect.Right, 4); - YMax = (float)Math.Round(rect.Bottom, 4); + XStartBoundingRectangleOffsetFromCenter = RoundDisplaySize(rect.X); + YStartBoundingRectangleOffsetFromCenter = RoundDisplaySize(rect.Y); + XEndBoundingRectangleOffsetFromCenter = RoundDisplaySize(rect.Right); + YEndBoundingRectangleOffsetFromCenter = RoundDisplaySize(rect.Bottom); ZMin = 0; ZMax = slicerFile.PrintHeight; @@ -394,11 +396,19 @@ public void Update(FileFormat slicerFile) public override string ToString() { return - $"{nameof(Magic)}: {Magic}, {nameof(Software)}: {Software}, {nameof(BinaryType)}: {BinaryType}, {nameof(Version)}: {Version}, {nameof(SliceType)}: {SliceType}, {nameof(ModelUnit)}: {ModelUnit}, {nameof(PointRatio)}: {PointRatio}, {nameof(LayerCount)}: {LayerCount}, {nameof(XMin)}: {XMin}, {nameof(YMin)}: {YMin}, {nameof(ZMin)}: {ZMin}, {nameof(XMax)}: {XMax}, {nameof(YMax)}: {YMax}, {nameof(ZMax)}: {ZMax}, {nameof(ModelStats)}: {ModelStats}, {nameof(Padding)}: {Padding}, {nameof(Separator)}: {Separator}, {nameof(LayerDefCount)}: {LayerDefCount}, {nameof(LayersDef)}: {LayersDef}, {nameof(EndMarker)}: {EndMarker}"; + $"{nameof(Magic)}: {Magic}, {nameof(Software)}: {Software}, {nameof(BinaryType)}: {BinaryType}, {nameof(Version)}: {Version}, {nameof(SliceType)}: {SliceType}, {nameof(ModelUnit)}: {ModelUnit}, {nameof(PointRatio)}: {PointRatio}, {nameof(LayerCount)}: {LayerCount}, {nameof(XStartBoundingRectangleOffsetFromCenter)}: {XStartBoundingRectangleOffsetFromCenter}, {nameof(YStartBoundingRectangleOffsetFromCenter)}: {YStartBoundingRectangleOffsetFromCenter}, {nameof(ZMin)}: {ZMin}, {nameof(XEndBoundingRectangleOffsetFromCenter)}: {XEndBoundingRectangleOffsetFromCenter}, {nameof(YEndBoundingRectangleOffsetFromCenter)}: {YEndBoundingRectangleOffsetFromCenter}, {nameof(ZMax)}: {ZMax}, {nameof(ModelStats)}: {ModelStats}, {nameof(Padding)}: {Padding}, {nameof(Separator)}: {Separator}, {nameof(LayerDefCount)}: {LayerDefCount}, {nameof(LayersDef)}: {LayersDef}, {nameof(EndMarker)}: {EndMarker}"; } } #endregion + #region Enums + public enum AnycubicZipRleFormat + { + PW0, + PWSZ + } + #endregion + #region Constants private const string SettingsFileName = "anycubic_photon_resins.pwsp"; private const string LayersFileName = "layers_controller.conf"; @@ -448,8 +458,11 @@ public override PrintParameterModifier[] PrintParameterModifiers public override string ConvertMenuGroup => "Anycubic Photon Workshop"; public override FileExtension[] FileExtensions { get; } = { + new(typeof(AnycubicZipFile), "pm4u", "Photon Mono 4 Ultra (PM4U)"), new(typeof(AnycubicZipFile), "pm7", "Photon Mono M7 (PM7)"), - new(typeof(AnycubicZipFile), "pwsz", "Photon Mono M7 Pro (PWSZ)") + new(typeof(AnycubicZipFile), "pm7m", "Photon Mono M7 Max (PM7M)"), + new(typeof(AnycubicZipFile), "pwsz", "Photon Mono M7 Pro (PWSZ)"), + }; /*public override uint[] AvailableVersions { get; } = { 1 }; @@ -512,6 +525,12 @@ public override string MachineName } } + public override byte AntiAliasing + { + get => Settings.MachineType.RasterAntialiasing; + set => base.AntiAliasing = Settings.MachineType.RasterAntialiasing = Math.Clamp(value, (byte)1, (byte)16); + } + public override Size[] ThumbnailsOriginalSize { get; } = { new(224, 168), @@ -532,80 +551,209 @@ public AnycubicZipFile() #region Methods - // PW0 - private Mat DecodeLayerRle(byte[] encodedRle) + private Mat DecodeLayerRle(AnycubicZipRleFormat format, byte[] encodedRle) { var mat = CreateMat(); - var imageLength = mat.GetLength(); - int pixelPos = 0; - for (int i = 0; i < encodedRle.Length; i++) + if (format == AnycubicZipRleFormat.PW0) + { + return AnycubicFile.DecodePW0(mat, encodedRle); + } + + if (format == AnycubicZipRleFormat.PWSZ) { - byte b = encodedRle[i]; - int code = b >> 4; - int repeat = b & 0xf; - byte color; - switch (code) + if (encodedRle.Length < 56) throw new FileLoadException("Invalid RLE data is shorter than 56 bytes."); + + + if (encodedRle[0] != '{' || encodedRle[1] != '=' || encodedRle[2] != '=' || encodedRle[3] != '\0') + throw new FileLoadException($"Invalid RLE file start marker, expecting: {{==\0 got: {System.Text.Encoding.Default.GetString(encodedRle, 0, 4)}."); + + + int index = 4; + var area = BitExtensions.ToSingleLittleEndian(encodedRle, index); index += 4; + var xMin = BitExtensions.ToSingleLittleEndian(encodedRle, index); index += 4; + var yMin = BitExtensions.ToSingleLittleEndian(encodedRle, index); index += 4; + var xMax = BitExtensions.ToSingleLittleEndian(encodedRle, index); index += 4; + var yMax = BitExtensions.ToSingleLittleEndian(encodedRle, index); index += 4; + var padding1 = BitExtensions.ToUIntLittleEndian(encodedRle, index); index += 4; + var objectCount = BitExtensions.ToUIntLittleEndian(encodedRle, index); index += 4; + + if (encodedRle[index] != '[' || encodedRle[index+1] != '-' || encodedRle[index+2] != '-' || encodedRle[index+3] != '\0') + throw new FileLoadException($"Invalid RLE coordinates start marker, expecting: [--\0 got: {System.Text.Encoding.Default.GetString(encodedRle, index, 4)}."); + + index += 4; + + var padding2 = BitExtensions.ToUIntLittleEndian(encodedRle, index); index += 4; + var lineCount = BitExtensions.ToUIntLittleEndian(encodedRle, index); index += 4; + var unknown1 = BitExtensions.ToUIntLittleEndian(encodedRle, index); index += 4; + + float halfDisplayX = DisplayWidth / 2f; + float halfDisplayY = DisplayHeight / 2f; + + if (lineCount > 0) { - case 0x0: - color = 0; - i++; - //reps = reps * 256 + EncodedRle[i]; - if (i >= encodedRle.Length) - { - repeat = imageLength - pixelPos; - break; - } + for (int i = 0; i < lineCount; i++) + { + var startX = BitExtensions.ToSingleLittleEndian(encodedRle, index) + halfDisplayX; index += 4; + var startY = BitExtensions.ToSingleLittleEndian(encodedRle, index) + halfDisplayY; index += 4; + var endX = BitExtensions.ToSingleLittleEndian(encodedRle, index) + halfDisplayX; index += 4; + var endY = BitExtensions.ToSingleLittleEndian(encodedRle, index) + halfDisplayY; index += 4; + var cw = encodedRle[index++]; - repeat = (repeat << 8) + encodedRle[i]; - break; - case 0xf: - color = 255; - i++; - //reps = reps * 256 + EncodedRle[i]; - if (i >= encodedRle.Length) - { - repeat = imageLength - pixelPos; - break; - } + var startPoint = DisplayToPixelPosition(startX, startY); + var endPoint = DisplayToPixelPosition(endX, endY); - repeat = (repeat << 8) + encodedRle[i]; - break; - default: - color = (byte)((code << 4) | code); - if (i >= encodedRle.Length) + CvInvoke.Line(mat, startPoint, endPoint, EmguExtensions.WhiteColor); + } + + var boundingRectangle = CvInvoke.BoundingRectangle(mat); + using var matRoi = new Mat(mat, boundingRectangle); + using var contours = new EmguContours(matRoi, RetrType.Tree, ChainApproxMethod.ChainApproxSimple, boundingRectangle.Location); + + if (!contours.IsEmpty) + { + using var newContours = new VectorOfVectorOfPoint(); + foreach (var family in contours.Families) { - repeat = imageLength - pixelPos; + newContours.Push(family.TraverseTree() + .Where(traverseFamily => traverseFamily.Depth % 2 == 0) // Ignore all non-welcomers + .Select(traverseFamily => traverseFamily.Self.Vector).ToArray()); } - break; + + CvInvoke.DrawContours(mat, newContours, -1, EmguExtensions.WhiteColor, -1); + } } - //color &= 0xff; + if (encodedRle[index] != '-' || encodedRle[index + 1] != '-' || encodedRle[index + 2] != ']' || encodedRle[index + 3] != '\0') + throw new FileLoadException($"Invalid RLE coordinates end marker, expecting: --]\0 got: {System.Text.Encoding.Default.GetString(encodedRle, index, 4)}."); + + index += 4; + + if (encodedRle[index] != '=' || encodedRle[index + 1] != '=' || encodedRle[index + 2] != '}' || encodedRle[index + 3] != '\0') + throw new FileLoadException($"Invalid RLE file end marker, expecting: ==}}\0 got: {System.Text.Encoding.Default.GetString(encodedRle, index, 4)}."); + + return mat; + } + + throw new NotSupportedException($"Unsupported RLE format: {format}"); + } - if (pixelPos + repeat > imageLength) + private byte[] EncodeLayerRle(AnycubicZipRleFormat format, uint layerIndex) + { + if (format == AnycubicZipRleFormat.PW0) + { + using var mat = this[layerIndex].LayerMat; + return AnycubicFile.EncodePW0(mat); + } + + if (format == AnycubicZipRleFormat.PWSZ) + { + var layer = this[layerIndex]; + var rle = new List(); + + rle.AddRange(new byte[] { - mat.Dispose(); - throw new FileLoadException($"Image ran off the end: {pixelPos} + {repeat} = {pixelPos + repeat}, expecting: {imageLength}"); - } + (byte)'{', + (byte)'=', + (byte)'=', + 0 + }); - // We only need to set the non-zero pixels - mat.FillSpan(ref pixelPos, repeat, color); + var zeroUintArray = new byte[4]; + rle.AddRange(BitExtensions.ToBytesLittleEndian(SceneSettings.LayersDef[layerIndex].Area)); + rle.AddRange(BitExtensions.ToBytesLittleEndian(SceneSettings.LayersDef[layerIndex].XStartBoundingRectangleOffsetFromCenter)); + rle.AddRange(BitExtensions.ToBytesLittleEndian(SceneSettings.LayersDef[layerIndex].YStartBoundingRectangleOffsetFromCenter)); + rle.AddRange(BitExtensions.ToBytesLittleEndian(SceneSettings.LayersDef[layerIndex].XEndBoundingRectangleOffsetFromCenter)); + rle.AddRange(BitExtensions.ToBytesLittleEndian(SceneSettings.LayersDef[layerIndex].YEndBoundingRectangleOffsetFromCenter)); + rle.AddRange(zeroUintArray); + rle.AddRange(BitExtensions.ToBytesLittleEndian(SceneSettings.LayersDef[layerIndex].ObjectCount)); - if (pixelPos == imageLength) + rle.AddRange(new byte[] { - //i++; - break; + (byte)'[', + (byte)'-', + (byte)'-', + 0 + }); + + rle.AddRange(zeroUintArray); + + float halfDisplayX = DisplayWidth / 2f; + float halfDisplayY = DisplayHeight / 2f; + + uint lines = 0; + + var linesRle = new List(); + foreach (var family in layer.Contours.Families) + { + foreach (var contour in family.TraverseTreeAsEmguContour()) + { + if (contour.Count == 1) + { + var startPoint = PixelToDisplayPosition(contour[0]); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.X - halfDisplayX))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.Y - halfDisplayY))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.X - halfDisplayX))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.Y - halfDisplayY))); + linesRle.Add(1); + lines++; + } + else + { + for (int i = 1; i < contour.Count; i++) + { + var startPoint = PixelToDisplayPosition(contour[i-1]); + var endPoint = PixelToDisplayPosition(contour[i]); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.X - halfDisplayX))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.Y - halfDisplayY))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(endPoint.X - halfDisplayX))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(endPoint.Y - halfDisplayY))); + linesRle.Add(1); + lines++; + } + + // Closing line + if (lines >= 3 && contour.IsClosed) + { + var startPoint = PixelToDisplayPosition(contour[^1]); + var endPoint = PixelToDisplayPosition(contour[0]); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.X - halfDisplayX))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(startPoint.Y - halfDisplayY))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(endPoint.X - halfDisplayX))); + linesRle.AddRange(BitExtensions.ToBytesLittleEndian(RoundDisplaySize(endPoint.Y - halfDisplayY))); + linesRle.Add(1); + lines++; + } + } + } } - } + + rle.AddRange(BitExtensions.ToBytesLittleEndian(lines)); + rle.AddRange(BitExtensions.ToBytesLittleEndian(1u)); // Unknown - if (pixelPos > 0 && pixelPos != imageLength) - { - mat.Dispose(); - throw new FileLoadException($"Image ended short: {pixelPos}, expecting: {imageLength}"); + rle.AddRange(linesRle); + + rle.AddRange(new byte[] + { + (byte)'-', + (byte)'-', + (byte)']', + 0 + }); + + rle.AddRange(new byte[] + { + (byte)'=', + (byte)'=', + (byte)'}', + 0 + }); + + return rle.ToArray(); } - return mat; + throw new NotSupportedException($"Unsupported RLE format: {format}"); } protected override void OnBeforeEncode(bool isPartialEncode) @@ -671,6 +819,8 @@ protected override void EncodeInternally(OperationProgress progress) SceneSettings.LayersDef = new SceneLayerDef[LayerCount]; var pixelArea = PixelArea; + var encodeWith = AnycubicZipRleFormat.PW0; // pw0 better than my pmsz implementation + var encodedRle = new byte[LayerCount][]; progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); foreach (var batch in BatchLayersIndexes()) { @@ -679,30 +829,30 @@ protected override void EncodeInternally(OperationProgress progress) progress.PauseIfRequested(); var layer = this[layerIndex]; - using var mat = layer.LayerMatModelBoundingRectangle; - var rect = layer.BoundingRectangleMillimeters; rect.Offset(DisplayWidth / -2f, DisplayHeight / -2f); - using var contours = new EmguContours(mat.RoiMat, RetrType.External); - SceneSettings.LayersDef[layerIndex] = new SceneLayerDef { Height = this[layerIndex].PositionZ, - Area = layer.GetArea(), - XMin = (float)Math.Round(rect.X, 4), - YMin = (float)Math.Round(rect.Y, 4), - XMax = (float)Math.Round(rect.Right, 4), - YMax = (float)Math.Round(rect.Bottom, 4), - ContourCount = (uint)contours.ExternalContoursCount, - MaxContourArea = (float)Math.Round(contours.MaxSolidArea * pixelArea, 4) + Area = (float)Math.Round(layer.Contours.TotalSolidArea * pixelArea, 4), + XStartBoundingRectangleOffsetFromCenter = (float)Math.Round(rect.X, 4), + YStartBoundingRectangleOffsetFromCenter = (float)Math.Round(rect.Y, 4), + XEndBoundingRectangleOffsetFromCenter = (float)Math.Round(rect.Right, 4), + YEndBoundingRectangleOffsetFromCenter = (float)Math.Round(rect.Bottom, 4), + ObjectCount = (uint)layer.Contours.ExternalContoursCount, + MaxContourArea = (float)Math.Round(layer.Contours.MaxSolidArea * pixelArea, 4) }; + encodedRle[layerIndex] = EncodeLayerRle(encodeWith, (uint)layerIndex); + progress.LockAndIncrement(); }); foreach (var layerIndex in batch) { + outputFile.CreateEntryFromContent($"layer_images/layer_{layerIndex}.{encodeWith.ToString().ToLowerInvariant()}Img", encodedRle[layerIndex], ZipArchiveMode.Create); + encodedRle[layerIndex] = null!; progress.PauseOrCancelIfRequested(); } } @@ -825,20 +975,31 @@ protected override void DecodeInternally(OperationProgress progress) { progress.PauseIfRequested(); byte[] encodedRle; + AnycubicZipRleFormat rleFormat; lock (Mutex) { entry = inputFile.GetEntry($"layer_images/layer_{layerIndex}.pwszImg"); if (entry is null) { - Clear(); - throw new FileLoadException($"Layer image {layerIndex} is missing in the file.", FileFullPath); + entry = inputFile.GetEntry($"layer_images/layer_{layerIndex}.pw0Img"); + if (entry is null) + { + Clear(); + throw new FileLoadException($"Layer image {layerIndex} is missing in the file.", FileFullPath); + } + + rleFormat = AnycubicZipRleFormat.PW0; + } + else + { + rleFormat = AnycubicZipRleFormat.PWSZ; } using var stream = entry.Open(); encodedRle = stream.ToArray(); } - this[layerIndex].LayerMat = DecodeLayerRle(encodedRle); + this[layerIndex].LayerMat = DecodeLayerRle(rleFormat, encodedRle); progress.LockAndIncrement(); }); } diff --git a/UVtools.Core/FileFormats/CTBEncryptedFile.cs b/UVtools.Core/FileFormats/CTBEncryptedFile.cs index 2a3765c7..cf4da2aa 100644 --- a/UVtools.Core/FileFormats/CTBEncryptedFile.cs +++ b/UVtools.Core/FileFormats/CTBEncryptedFile.cs @@ -1142,8 +1142,8 @@ protected override void DecodeInternally(OperationProgress progress) Debug.WriteLine(Previews[i]); inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin); - byte[] rawImageData = new byte[Previews[i].ImageLength]; - inputFile.Read(rawImageData, 0, (int)Previews[i].ImageLength); + var rawImageData = new byte[Previews[i].ImageLength]; + inputFile.ReadExactly(rawImageData.AsSpan()); Thumbnails.Add(DecodeChituImageRGB15Rle(rawImageData, Previews[i].ResolutionX, Previews[i].ResolutionY)); progress++; diff --git a/UVtools.Core/FileFormats/ChituboxFile.cs b/UVtools.Core/FileFormats/ChituboxFile.cs index 9a6269c0..a757bdeb 100644 --- a/UVtools.Core/FileFormats/ChituboxFile.cs +++ b/UVtools.Core/FileFormats/ChituboxFile.cs @@ -2012,8 +2012,8 @@ protected override void DecodeInternally(OperationProgress progress) Debug.WriteLine(Previews[i]); inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin); - byte[] rawImageData = new byte[Previews[i].ImageLength]; - inputFile.Read(rawImageData, 0, (int)Previews[i].ImageLength); + var rawImageData = new byte[Previews[i].ImageLength]; + inputFile.ReadExactly(rawImageData.AsSpan()); Thumbnails.Add(DecodeChituImageRGB15Rle(rawImageData, Previews[i].ResolutionX, Previews[i].ResolutionY)); diff --git a/UVtools.Core/FileFormats/CrealityCXDLPv4File.cs b/UVtools.Core/FileFormats/CrealityCXDLPv4File.cs index eff2a86c..1b5319c9 100644 --- a/UVtools.Core/FileFormats/CrealityCXDLPv4File.cs +++ b/UVtools.Core/FileFormats/CrealityCXDLPv4File.cs @@ -1249,8 +1249,8 @@ protected override void DecodeInternally(OperationProgress progress) Debug.WriteLine(Previews[i]); inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin); - byte[] rawImageData = new byte[Previews[i].ImageLength]; - inputFile.Read(rawImageData, 0, (int)Previews[i].ImageLength); + var rawImageData = new byte[Previews[i].ImageLength]; + inputFile.ReadExactly(rawImageData.AsSpan()); Thumbnails.Add(DecodeChituImageRGB15Rle(rawImageData, Previews[i].ResolutionX, Previews[i].ResolutionY)); progress++; diff --git a/UVtools.Core/FileFormats/FDGFile.cs b/UVtools.Core/FileFormats/FDGFile.cs index 9409b4c7..b2ca9c04 100644 --- a/UVtools.Core/FileFormats/FDGFile.cs +++ b/UVtools.Core/FileFormats/FDGFile.cs @@ -941,8 +941,8 @@ protected override void DecodeInternally(OperationProgress progress) Debug.WriteLine(Previews[i]); inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin); - byte[] rawImageData = new byte[Previews[i].ImageLength]; - inputFile.Read(rawImageData, 0, (int)Previews[i].ImageLength); + var rawImageData = new byte[Previews[i].ImageLength]; + inputFile.ReadExactly(rawImageData.AsSpan()); Thumbnails.Add(DecodeChituImageRGB15Rle(rawImageData, Previews[i].ResolutionX, Previews[i].ResolutionY)); progress++; @@ -952,7 +952,7 @@ protected override void DecodeInternally(OperationProgress progress) { inputFile.Seek(HeaderSettings.MachineNameAddress, SeekOrigin.Begin); var buffer = new byte[HeaderSettings.MachineNameSize]; - inputFile.Read(buffer, 0, (int) HeaderSettings.MachineNameSize); + inputFile.ReadExactly(buffer.AsSpan()); HeaderSettings.MachineName = Encoding.ASCII.GetString(buffer); } diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index a312542e..f6ab0a66 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -6722,7 +6722,7 @@ public float MillimetersToPixelsF(float millimeters, uint fallbackToPixels = 0) /// X position in pixels /// Decimal precision /// Display position in millimeters - public float PixelToDisplayPositionX(int x, byte precision = 3) => (float)Math.Round(PixelWidth * x, precision); + public float PixelToDisplayPositionX(int x, byte precision = DisplayFloatPrecision) => (float)Math.Round(PixelWidth * x, precision); /// /// From a pixel position get the equivalent position on the display @@ -6730,7 +6730,7 @@ public float MillimetersToPixelsF(float millimeters, uint fallbackToPixels = 0) /// Y position in pixels /// Decimal precision /// Display position in millimeters - public float PixelToDisplayPositionY(int y, byte precision = 3) => (float)Math.Round(PixelHeight * y, precision); + public float PixelToDisplayPositionY(int y, byte precision = DisplayFloatPrecision) => (float)Math.Round(PixelHeight * y, precision); /// /// From a pixel position get the equivalent position on the display @@ -6739,8 +6739,8 @@ public float MillimetersToPixelsF(float millimeters, uint fallbackToPixels = 0) /// Y position in pixels /// Decimal precision /// Resolution position in pixels - public PointF PixelToDisplayPosition(int x, int y, byte precision = 3) =>new(PixelToDisplayPositionX(x, precision), PixelToDisplayPositionY(y, precision)); - public PointF PixelToDisplayPosition(Point point, byte precision = 3) => new(PixelToDisplayPositionX(point.X, precision), PixelToDisplayPositionY(point.Y, precision)); + public PointF PixelToDisplayPosition(int x, int y, byte precision = DisplayFloatPrecision) =>new(PixelToDisplayPositionX(x, precision), PixelToDisplayPositionY(y, precision)); + public PointF PixelToDisplayPosition(Point point, byte precision = DisplayFloatPrecision) => new(PixelToDisplayPositionX(point.X, precision), PixelToDisplayPositionY(point.Y, precision)); /// /// From a pixel position get the equivalent position on the display @@ -6757,12 +6757,17 @@ public float MillimetersToPixelsF(float millimeters, uint fallbackToPixels = 0) public int DisplayToPixelPositionY(float y) => (int)(y * Yppmm); /// - /// From a pixel position get the equivalent position on the display + /// From a display position get the equivalent position on the pixel /// /// X position in millimeters /// Y position in millimeters /// Resolution position in pixels public Point DisplayToPixelPosition(float x, float y) => new(DisplayToPixelPositionX(x), DisplayToPixelPositionY(y)); + /// + /// From a display position get the equivalent position on the pixel + /// + /// + /// public Point DisplayToPixelPosition(PointF point) => new(DisplayToPixelPositionX(point.X), DisplayToPixelPositionY(point.Y)); public bool SanitizeBoundingRectangle(ref Rectangle rectangle) diff --git a/UVtools.Core/FileFormats/PHZFile.cs b/UVtools.Core/FileFormats/PHZFile.cs index a60aab69..86ad67e2 100644 --- a/UVtools.Core/FileFormats/PHZFile.cs +++ b/UVtools.Core/FileFormats/PHZFile.cs @@ -963,8 +963,8 @@ protected override void DecodeInternally(OperationProgress progress) Debug.WriteLine(Previews[i]); inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin); - byte[] rawImageData = new byte[Previews[i].ImageLength]; - inputFile.Read(rawImageData, 0, (int)Previews[i].ImageLength); + var rawImageData = new byte[Previews[i].ImageLength]; + inputFile.ReadExactly(rawImageData.AsSpan()); Thumbnails.Add(DecodeChituImageRGB15Rle(rawImageData, Previews[i].ResolutionX, Previews[i].ResolutionY)); progress++; @@ -974,7 +974,7 @@ protected override void DecodeInternally(OperationProgress progress) { inputFile.Seek(HeaderSettings.MachineNameAddress, SeekOrigin.Begin); byte[] buffer = new byte[HeaderSettings.MachineNameSize]; - inputFile.Read(buffer, 0, (int) HeaderSettings.MachineNameSize); + inputFile.ReadExactly(buffer); HeaderSettings.MachineName = Encoding.ASCII.GetString(buffer); } diff --git a/UVtools.Core/FileFormats/SL1File.cs b/UVtools.Core/FileFormats/SL1File.cs index 61bc64dd..8c9065c9 100644 --- a/UVtools.Core/FileFormats/SL1File.cs +++ b/UVtools.Core/FileFormats/SL1File.cs @@ -198,6 +198,7 @@ public class Material public float MaterialCorrectionX { get; set; } = 1; public float MaterialCorrectionY { get; set; } = 1; public float MaterialCorrectionZ { get; set; } = 1; + public int ZcorrectionLayers { get; set; } #endregion diff --git a/UVtools.Core/IO/ReverseLineReader.cs b/UVtools.Core/IO/ReverseLineReader.cs index 6dddf5a2..d84b1ded 100644 --- a/UVtools.Core/IO/ReverseLineReader.cs +++ b/UVtools.Core/IO/ReverseLineReader.cs @@ -169,7 +169,7 @@ private IEnumerator GetEnumeratorImpl(Stream stream) position -= bytesToRead; stream.Position = position; - stream.ReadExactly(buffer, bytesToRead); + stream.ReadExactly(buffer, 0, bytesToRead); // If we haven't read a full buffer, but we had bytes left // over from before, copy them to the end of the buffer if (leftOverData > 0 && bytesToRead != _bufferSize) diff --git a/UVtools.Core/Operations/OperationBlur.cs b/UVtools.Core/Operations/OperationBlur.cs index 929b666d..e6d63fcc 100644 --- a/UVtools.Core/Operations/OperationBlur.cs +++ b/UVtools.Core/Operations/OperationBlur.cs @@ -49,7 +49,7 @@ public sealed class OperationBlur : Operation { var sb = new StringBuilder(); - if (BlurOperation is BlurAlgorithm.GaussianBlur or BlurAlgorithm.MedianBlur) + if (BlurOperation is BlurAlgorithm.StackBlur or BlurAlgorithm.GaussianBlur or BlurAlgorithm.MedianBlur) { if (Size % 2 != 1) { diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index 5d2059d4..d562cd00 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -1287,25 +1287,36 @@ public void GenerateExposureTable() ExposureTable = new(list); } - public Mat[] GetLayers(bool isPreview = false) + public Mat[] GetLayers(out Point markingTextPositivePosition, out Point markingTextNegativePosition, bool isPreview = false) { var holes = Holes; var bars = Bars; var bulleyes = BullsEyes; var textSize = TextSize; - + + int baseLine = 0; + var markingTextSize = EmguExtensions.GetTextSizeExtended("100u\n20.00s\n3.00s", + _textFont, _textScale, _textThickness, 10, ref baseLine); + markingTextPositivePosition = Point.Empty; + markingTextNegativePosition = Point.Empty; + int featuresMarginX = (int)(Xppmm * _featuresMargin); int featuresMarginY = (int)(Yppmm * _featuresMargin); ushort startCaseThickness = StaircaseThickness; int holePanelWidth = holes.Length > 0 ? featuresMarginX * 2 + holes[^1] : 0; + if (holePanelWidth > 0) + { + holePanelWidth = Math.Max(holePanelWidth, markingTextSize.Width + featuresMarginX * 2); + } + int holePanelHeight = GetHolesHeight(holes); int barsPanelHeight = GetBarsLength(bars); int bulleyesDiameter = GetBullsEyeMaxDiameter(bulleyes); int bulleyesPanelDiameter = GetBullsEyeMaxPanelDiameter(bulleyes); int bulleyesRadius = bulleyesDiameter / 2; int yLeftMaxSize = startCaseThickness + featuresMarginY + Math.Max(barsPanelHeight, textSize.Width) + bulleyesPanelDiameter; - int yRightMaxSize = startCaseThickness + holePanelHeight + featuresMarginY * 2; + int yRightMaxSize = startCaseThickness + holePanelHeight + markingTextSize.Height + featuresMarginY * 2; int xSize = featuresMarginX; int ySize = TextMarkingSpacing + featuresMarginY; @@ -1335,21 +1346,29 @@ public Mat[] GetLayers(bool isPreview = false) } int bullseyeYPos = yLeftMaxSize - bulleyesPanelDiameter / 2; - + markingTextPositivePosition.Y = (int)(bullseyeYPos - markingTextSize.Height / 3.5); + if (bulleyes.Length > 0) { - xSize = Math.Max(xSize, bulleyesPanelDiameter + featuresMarginX * 2); + xSize = Math.Max(xSize, markingTextSize.Width + bulleyesPanelDiameter + featuresMarginX * 3); + yLeftMaxSize += featuresMarginY + 24; + markingTextPositivePosition.X = featuresMarginX; + } + else + { + xSize = Math.Max(xSize, markingTextSize.Width + featuresMarginX * 2); yLeftMaxSize += featuresMarginY + 24; + markingTextPositivePosition.X = xSize / 2 - markingTextSize.Width / 2; } - int bullseyeXPos = xSize / 2; + + int bullseyeXPos = (int)(xSize / 1.5); if (holePanelWidth > 0) { - xSize -= featuresMarginX; + xSize += featuresMarginX + holes[^1]; } - xSize += holePanelWidth; int negativeSideWidth = xSize; xSize += holePanelWidth; @@ -1391,18 +1410,17 @@ public Mat[] GetLayers(bool isPreview = false) { var diameter = holes[i]; var radius = diameter / 2; - xPos = layers[0].Width - holePanelWidth - featuresMarginX; + xPos = layers[0].Width - holePanelWidth - featuresMarginX - holes[^1] / 2; - CalibrateExposureFinderShapes effectiveShape = _holeShape == CalibrateExposureFinderShapes.Square || diameter < 6 ? + var effectiveShape = _holeShape == CalibrateExposureFinderShapes.Square || diameter < 6 ? CalibrateExposureFinderShapes.Square : CalibrateExposureFinderShapes.Circle; switch (effectiveShape) { case CalibrateExposureFinderShapes.Square: - xPos -= diameter; + xPos -= radius; break; case CalibrateExposureFinderShapes.Circle: - xPos -= radius; yPos += radius; break; } @@ -1440,10 +1458,11 @@ public Mat[] GetLayers(bool isPreview = false) switch (effectiveShape) { case CalibrateExposureFinderShapes.Square: - xPos = layers[0].Width - rect.X - featuresMarginX - holes[^1]; + //xPos = layers[0].Width - rect.X - featuresMarginX - holes[^1]; + xPos = layers[0].Width - holePanelWidth / 2 - radius; break; case CalibrateExposureFinderShapes.Circle: - xPos = layers[0].Width - rect.X - featuresMarginX - holes[^1] + radius; + xPos = layers[0].Width - holePanelWidth / 2; break; } @@ -1576,17 +1595,21 @@ public Mat[] GetLayers(bool isPreview = false) yPos += bulleyesRadius; } + if (holes.Length > 0) + { + markingTextNegativePosition.X = layers[1].Width - holePanelWidth / 2 - markingTextSize.Width / 3; + markingTextNegativePosition.Y = layers[1].Height - featuresMarginY - markingTextSize.Height; + } + if (isPreview) { - var textHeightStart = layers[1].Height - featuresMarginY - TextMarkingSpacing; - CvInvoke.PutText(layers[1], $"{Microns}u", new Point(TextMarkingStartX, textHeightStart), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(layers[1], $"{_bottomExposure}s", new Point(TextMarkingStartX, textHeightStart + TextMarkingLineBreak), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(layers[1], $"{_normalExposure}s", new Point(TextMarkingStartX, textHeightStart + TextMarkingLineBreak * 2), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + layers[1].PutTextExtended($"{Microns}u\n{_bottomExposure}s\n{_normalExposure}s", markingTextPositivePosition, + _textFont, _textScale, EmguExtensions.WhiteColor, _textThickness, 10, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + if (holes.Length > 0) { - CvInvoke.PutText(layers[1], $"{Microns}u", new Point(layers[1].Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(layers[1], $"{_bottomExposure}s", new Point(layers[1].Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart + TextMarkingLineBreak), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(layers[1], $"{_normalExposure}s", new Point(layers[1].Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart + TextMarkingLineBreak * 2), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + layers[1].PutTextExtended($"{Microns}u\n{_bottomExposure}s\n{_normalExposure}s", markingTextNegativePosition, + _textFont, _textScale, EmguExtensions.BlackColor, _textThickness, 10, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); } } @@ -1777,6 +1800,10 @@ protected override bool ExecuteInternally(OperationProgress progress) int xHalf = boundingRectangle.Width / 2; int yHalf = boundingRectangle.Height / 2; + var baseLine = 0; + var markingTextSize = EmguExtensions.GetTextSizeExtended("100u\n20.00s\n3.00s", + _textFont, _textScale, _textThickness, 10, ref baseLine); + var brightnesses = MultipleBrightnessValuesArray; var multipleExposures = _exposureTable.Where(item => item.IsValid && item.LayerHeight == (decimal) SlicerFile.LayerHeight).ToArray(); if (brightnesses.Length == 0 || !_multipleBrightness) brightnesses = new[] { byte.MaxValue }; @@ -1857,13 +1884,9 @@ protected override bool ExecuteInternally(OperationProgress progress) if(_patternModelTextEnabled) { - if (_multipleBrightness) - { - CvInvoke.PutText(newMatRoi, brightness.ToString(), new(xHalf - 60, yHalf + 20 - TextMarkingLineBreak * 4), TextMarkingFontFace, 2, EmguExtensions.BlackColor, 3, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - } - CvInvoke.PutText(newMatRoi, $"{microns}u", new(xHalf - 60, yHalf + 20 - TextMarkingLineBreak * 2), TextMarkingFontFace, 2, EmguExtensions.BlackColor, 3, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(newMatRoi, $"{group.Key.BottomExposure}s", new(xHalf - 60, yHalf + 20), TextMarkingFontFace, 2, EmguExtensions.BlackColor, 3, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(newMatRoi, $"{group.Key.Exposure}s", new(xHalf - 60, yHalf + 20 + TextMarkingLineBreak * 2), TextMarkingFontFace, 2, EmguExtensions.BlackColor, 3, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + newMatRoi.PutTextExtended((_multipleBrightness ? $"{brightness.ToString()}\n" : string.Empty) + + $"{microns}u\n{group.Key.BottomExposure}s\n{group.Key.Exposure}s", new Point(xHalf - markingTextSize.Width / 2, yHalf - markingTextSize.Height / 2), + _textFont, _textScale, EmguExtensions.BlackColor, _textThickness, 10, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); } } @@ -1928,7 +1951,7 @@ protected override bool ExecuteInternally(OperationProgress progress) } else // No patterned { - var layers = GetLayers(); + var layers = GetLayers(out var markingTextPositivePosition, out var markingTextNegativePosition); progress.ItemCount = 0; //SanitizeExposureTable(); if (layers[0].Width+sideMarginPx > SlicerFile.ResolutionX || layers[0].Height+topBottomMarginPx > SlicerFile.ResolutionY) @@ -2061,17 +2084,15 @@ lastExposureItem is not null && } } - var textHeightStart = matRoi.Height - featuresMarginY - TextMarkingSpacing; - CvInvoke.PutText(matRoi, $"{microns}u", new Point(TextMarkingStartX, textHeightStart), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(matRoi, $"{bottomExposureTemp}s", new Point(TextMarkingStartX, textHeightStart + TextMarkingLineBreak), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(matRoi, $"{normalExposureTemp}s", new Point(TextMarkingStartX, textHeightStart + TextMarkingLineBreak * 2), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + matRoi.PutTextExtended($"{Microns}u\n{bottomExposureTemp}s\n{normalExposureTemp}s", markingTextPositivePosition, + _textFont, _textScale, EmguExtensions.WhiteColor, _textThickness, 10, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); if (holes.Length > 0) { - CvInvoke.PutText(matRoi, $"{microns}u", new Point(matRoi.Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(matRoi, $"{bottomExposureTemp}s", new Point(matRoi.Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart + TextMarkingLineBreak), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); - CvInvoke.PutText(matRoi, $"{normalExposureTemp}s", new Point(matRoi.Width - featuresMarginX * 2 - holes[^1] + TextMarkingStartX, textHeightStart + TextMarkingLineBreak * 2), TextMarkingFontFace, TextMarkingScale, EmguExtensions.BlackColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); + matRoi.PutTextExtended($"{Microns}u\n{bottomExposureTemp}s\n{normalExposureTemp}s", markingTextNegativePosition, + _textFont, _textScale, EmguExtensions.BlackColor, _textThickness, 10, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); } + if (_multipleBrightness) { CvInvoke.PutText(matRoi, brightness.ToString(), new Point(matRoi.Width / 3, 35), TextMarkingFontFace, TextMarkingScale, EmguExtensions.WhiteColor, TextMarkingThickness, _enableAntiAliasing ? LineType.AntiAlias : LineType.EightConnected); diff --git a/UVtools.Core/Operations/OperationLayerExportGif.cs b/UVtools.Core/Operations/OperationLayerExportGif.cs index e788b166..8fa06f70 100644 --- a/UVtools.Core/Operations/OperationLayerExportGif.cs +++ b/UVtools.Core/Operations/OperationLayerExportGif.cs @@ -6,18 +6,22 @@ * of this license document, but changing it is not allowed. */ -using AnimatedGif; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp; using System; using System.ComponentModel; -using System.Drawing; using System.IO; using System.Text; using System.Threading.Tasks; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; +using Point = System.Drawing.Point; +using Size = System.Drawing.Size; +using SixLabors.ImageSharp.Formats.Gif; +using System.Linq; namespace UVtools.Core.Operations; @@ -212,9 +216,11 @@ public override void InitWithSlicerFile() protected override bool ExecuteInternally(OperationProgress progress) { - using var gif = AnimatedGif.AnimatedGif.Create(_filePath, FPSToMilliseconds, _repeats); - var layerBuffer = new byte[TotalLayers][]; - progress.Reset("Optimized layers", TotalLayers); + + //using var gif = AnimatedGif.AnimatedGif.Create(_filePath, FPSToMilliseconds, _repeats); + // Set animation loop repeat count to 5. + + progress.Reset("Packed layers", TotalLayers); var fontFace = FontFace.HersheyDuplex; float fontScale = 1.5f; @@ -226,7 +232,94 @@ protected override bool ExecuteInternally(OperationProgress progress) ROI = SlicerFile.BoundingRectangle; } - Parallel.For(0, TotalLayers, CoreSettings.GetParallelOptions(progress), i => + var roiSize = GetRoiSizeOrDefault(); + var imgSize = new Size((int)(roiSize.Width * ScaleFactor), (int)(roiSize.Height * ScaleFactor)); + var finalSize = Size.Empty; + + Image? gif = null; + + var delay = FPSToMilliseconds / 10; + var layerBuffer = new byte[TotalLayers][]; + var batches = Enumerable.Range(0, (int)TotalLayers).Chunk(FileFormat.DefaultParallelBatchCount); + foreach (var batch in batches) + { + Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), i => + { + progress.PauseIfRequested(); + uint layerIndex = (uint)(LayerIndexStart + i * (_skip + 1)); + var layer = SlicerFile[layerIndex]; + using var mat = layer.LayerMat; + using var matRoi = GetRoiOrDefault(mat); + + if (_scale != 100) + { + CvInvoke.Resize(matRoi, matRoi, imgSize); + } + + if (_flipDirection != FlipDirection.None) + { + CvInvoke.Flip(matRoi, matRoi, (FlipType)_flipDirection); + } + + if (_rotateDirection != RotateDirection.None) + { + CvInvoke.Rotate(matRoi, matRoi, (RotateFlags)_rotateDirection); + } + + if (_renderLayerCount) + { + int baseLine = 0; + var text = string.Format($"{{0:D{SlicerFile.LayerDigits}}}/{{1}}", + layerIndex, + SlicerFile.LayerCount - 1); + var fontSize = CvInvoke.GetTextSize(text, fontFace, fontScale, fontThickness, ref baseLine); + + Point point = new( + matRoi.Width / 2 - fontSize.Width / 2, + 70); + CvInvoke.PutText(matRoi, text, point, fontFace, fontScale, textColor, fontThickness, LineType.AntiAlias); + } + + //ApplyMask(matOriginal, matRoi); + + if (finalSize == Size.Empty) + { + finalSize = matRoi.Size; + } + + layerBuffer[i] = matRoi.GetBytes(); + progress.LockAndIncrement(); + }); + + if (gif is null) + { + gif = new Image(imgSize.Width, imgSize.Height); + var gifMetaData = gif.Metadata.GetGifMetadata(); + gifMetaData.RepeatCount = _repeats; + + var metadata = gif.Frames.RootFrame.Metadata.GetGifMetadata(); + metadata.FrameDelay = delay; + } + + foreach (var i in batch) + { + // Create a color image, which will be added to the gif. + using var image = Image.LoadPixelData(layerBuffer[i], finalSize.Width, finalSize.Height); + layerBuffer[i] = null!; + + // Set the delay until the next image is displayed. + var metadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); + metadata.FrameDelay = delay; + metadata.DisposalMethod = GifDisposalMethod.RestoreToBackground; + + // Add the color image to the gif. + gif.Frames.AddFrame(image.Frames.RootFrame); + + progress.PauseOrCancelIfRequested(); + } + } + + /*Parallel.For(0, TotalLayers, CoreSettings.GetParallelOptions(progress), i => { progress.PauseIfRequested(); uint layerIndex = (uint) (LayerIndexStart + i * (_skip + 1)); @@ -237,7 +330,7 @@ protected override bool ExecuteInternally(OperationProgress progress) if (_scale != 100) { - CvInvoke.Resize(matRoi, matRoi, new Size((int) (matRoi.Width * ScaleFactor), (int)(matRoi.Height * ScaleFactor))); + CvInvoke.Resize(matRoi, matRoi, imgSize); } if (_flipDirection != FlipDirection.None) @@ -266,31 +359,59 @@ protected override bool ExecuteInternally(OperationProgress progress) //ApplyMask(matOriginal, matRoi); + if (finalSize == Size.Empty) + { + finalSize = matRoi.Size; + } + layerBuffer[i] = matRoi.GetPngByes(); progress.LockAndIncrement(); }); + + progress.ResetNameAndProcessed("Packed layers"); foreach (var buffer in layerBuffer) { progress.PauseOrCancelIfRequested(); - using var stream = new MemoryStream(buffer); - using var img = Image.FromStream(stream); - gif.AddFrame(img, -1, GifQuality.Bit8); + //using var stream = new MemoryStream(buffer); + //using var img = Image.FromStream(stream); + //gif.AddFrame(img, -1, GifQuality.Bit8); + + // Create a color image, which will be added to the gif. + using var image = Image.Load(buffer); + + // Set the delay until the next image is displayed. + metadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); + metadata.FrameDelay = delay; + metadata.DisposalMethod = GifDisposalMethod.RestoreToBackground; + + // Add the color image to the gif. + gif.Frames.AddFrame(image.Frames.RootFrame); + progress++; } + */ - if (progress.Token.IsCancellationRequested) + progress.Reset("Saving GIF to file"); + + if (!progress.Token.IsCancellationRequested && gif is not null) { try + { + gif.Frames.RemoveFrame(0); + gif.SaveAsGif(_filePath); + } + catch (Exception) { File.Delete(_filePath); } - catch + finally { - // ignored + gif.Dispose(); } + } return !progress.Token.IsCancellationRequested; diff --git a/UVtools.Core/Operations/OperationLithophane.cs b/UVtools.Core/Operations/OperationLithophane.cs index 16099dee..e6716316 100644 --- a/UVtools.Core/Operations/OperationLithophane.cs +++ b/UVtools.Core/Operations/OperationLithophane.cs @@ -54,6 +54,7 @@ public enum LithophaneBaseType : byte private byte _gapClosingIterations; private byte _removeNoiseIterations; private byte _gaussianBlur; + private bool _separateGrayscalePixels = true; private byte _startThresholdRange = 1; private byte _endThresholdRange = byte.MaxValue; private decimal _baseThickness = 2; @@ -115,13 +116,16 @@ public enum LithophaneBaseType : byte } } - if (_startThresholdRange == _endThresholdRange) + if (_separateGrayscalePixels) { - sb.AppendLine($"Start threshold can't be equal than end threshold ({_endThresholdRange})"); - } - else if (_startThresholdRange > _endThresholdRange) - { - sb.AppendLine("Start threshold can't be higher than end threshold"); + if (_startThresholdRange == _endThresholdRange) + { + sb.AppendLine($"Start threshold can't be equal than end threshold ({_endThresholdRange})"); + } + else if (_startThresholdRange > _endThresholdRange) + { + sb.AppendLine("Start threshold can't be higher than end threshold"); + } } return sb.ToString(); @@ -237,6 +241,16 @@ public byte GaussianBlur set => RaiseAndSetIfChanged(ref _gaussianBlur, value); } + public bool SeparateGrayscalePixels + { + get => _separateGrayscalePixels; + set + { + if (!RaiseAndSetIfChanged(ref _separateGrayscalePixels, value)) return; + OneLayerPerThreshold = false; + } + } + public byte StartThresholdRange { get => _startThresholdRange; @@ -363,92 +377,111 @@ protected override bool ExecuteInternally(OperationProgress progress) { using var mat = GetTargetMat(); if (mat is null) return false; - - var layersBag = new ConcurrentDictionary(); - progress.Reset("Threshold levels", byte.MaxValue); - Parallel.For(_startThresholdRange, _endThresholdRange, CoreSettings.GetParallelOptions(progress), threshold => + + Layer[] thresholdLayers; + + if (_separateGrayscalePixels) { - progress.PauseIfRequested(); - using var thresholdMat = new Mat(); - CvInvoke.Threshold(mat, thresholdMat, threshold, byte.MaxValue, ThresholdType.Binary); - if (!CvInvoke.HasNonZero(thresholdMat)) return; + var layersBag = new ConcurrentDictionary(); + progress.Reset("Threshold levels", byte.MaxValue); + Parallel.For(_startThresholdRange, _endThresholdRange, CoreSettings.GetParallelOptions(progress), + threshold => + { + progress.PauseIfRequested(); + using var thresholdMat = new Mat(); + CvInvoke.Threshold(mat, thresholdMat, threshold, byte.MaxValue, ThresholdType.Binary); + if (!CvInvoke.HasNonZero(thresholdMat)) return; - if (_enableAntiAliasing) - { - CvInvoke.GaussianBlur(thresholdMat, thresholdMat, new Size(3, 3), 0); - } + if (_enableAntiAliasing) + { + CvInvoke.GaussianBlur(thresholdMat, thresholdMat, new Size(3, 3), 0); + } - using var layerMat = EmguExtensions.InitMat(SlicerFile.Resolution); - thresholdMat.CopyToCenter(layerMat); - layersBag.TryAdd((byte)threshold, new Layer(layerMat, SlicerFile)); - progress.LockAndIncrement(); - }); + using var layerMat = SlicerFile.CreateMat(); + thresholdMat.CopyToCenter(layerMat); + layersBag.TryAdd((byte)threshold, new Layer(layerMat, SlicerFile)); + progress.LockAndIncrement(); + }); - if (layersBag.Count == 0) - { - throw new InvalidOperationException("Unable to continue due to no threshold layers was generated, either by lack of pixels or by using a short range."); - } + if (layersBag.Count == 0) + { + throw new InvalidOperationException( + "Unable to continue due to no threshold layers was generated, either by lack of pixels or by using a short range."); + } - var thresholdLayers = layersBag.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToArray(); + thresholdLayers = layersBag.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToArray(); - if (!_oneLayerPerThreshold) - { - var layerIncrementF = thresholdLayers.Length * _layerHeight / Math.Max(_layerHeight, _lithophaneHeight); - if (layerIncrementF >= 2) + if (!_oneLayerPerThreshold) { - var layerIncrement = (uint) layerIncrementF; - var indexes = new int[(int)Math.Ceiling(thresholdLayers.Length / (float)layerIncrement)]; - var newLayers = new Layer[indexes.Length]; - var count = 0; - for (int index = 0; index < thresholdLayers.Length; index++) + var layerIncrementF = thresholdLayers.Length * _layerHeight / Math.Max(_layerHeight, _lithophaneHeight); + if (layerIncrementF >= 2) { - if (index % layerIncrement != 0) continue; - newLayers[count] = thresholdLayers[index]; - indexes[count++] = index; - - } + var layerIncrement = (uint)layerIncrementF; + var indexes = new int[(int)Math.Ceiling(thresholdLayers.Length / (float)layerIncrement)]; + var newLayers = new Layer[indexes.Length]; + var count = 0; + for (int index = 0; index < thresholdLayers.Length; index++) + { + if (index % layerIncrement != 0) continue; + newLayers[count] = thresholdLayers[index]; + indexes[count++] = index; - progress.ResetNameAndProcessed("Packed layers"); - Parallel.ForEach(indexes, CoreSettings.GetParallelOptions(progress), i => - { - progress.PauseIfRequested(); - progress.LockAndIncrement(); - using var mat = thresholdLayers[i].LayerMat; - for (int index = i+1; index < i + layerIncrement && index < thresholdLayers.Length; index++) + } + + progress.ResetNameAndProcessed("Packed layers"); + Parallel.ForEach(indexes, CoreSettings.GetParallelOptions(progress), i => { - using var nextMat = thresholdLayers[index].LayerMat; - CvInvoke.Max(mat, nextMat, mat); + progress.PauseIfRequested(); progress.LockAndIncrement(); - } + using var mat = thresholdLayers[i].LayerMat; + for (int index = i + 1; index < i + layerIncrement && index < thresholdLayers.Length; index++) + { + using var nextMat = thresholdLayers[index].LayerMat; + CvInvoke.Max(mat, nextMat, mat); + progress.LockAndIncrement(); + } - thresholdLayers[i].LayerMat = mat; - }); + thresholdLayers[i].LayerMat = mat; + }); - thresholdLayers = newLayers; - } - else if (layerIncrementF < 1) - { - var layerIncrement = (uint)Math.Ceiling(1/layerIncrementF); - if (layerIncrement > 1) + thresholdLayers = newLayers; + } + else if (layerIncrementF < 1) { - progress.Reset("Packed layers"); - var newLayers = new Layer[thresholdLayers.Length * layerIncrement]; - for (int i = 0; i < thresholdLayers.Length; i++) + var layerIncrement = (uint)Math.Ceiling(1 / layerIncrementF); + if (layerIncrement > 1) { - var layer = thresholdLayers[i]; - var newIndex = i * layerIncrement; - newLayers[newIndex] = layer; - for (int x = 1; x < layerIncrement; x++) + progress.Reset("Packed layers"); + var newLayers = new Layer[thresholdLayers.Length * layerIncrement]; + for (int i = 0; i < thresholdLayers.Length; i++) { - newLayers[++newIndex] = layer.Clone(); + var layer = thresholdLayers[i]; + var newIndex = i * layerIncrement; + newLayers[newIndex] = layer; + for (int x = 1; x < layerIncrement; x++) + { + newLayers[++newIndex] = layer.Clone(); + } + } + thresholdLayers = newLayers; } - thresholdLayers = newLayers; } } } + else + { + using var layerMat = SlicerFile.CreateMat(); + mat.CopyToCenter(layerMat); + if (_enableAntiAliasing) + { + CvInvoke.GaussianBlur(layerMat, layerMat, new Size(3, 3), 0); + } + var layer = new Layer(layerMat, SlicerFile); + thresholdLayers = layer.Clone((uint)(_lithophaneHeight / _layerHeight)); + } if (_baseType != LithophaneBaseType.None && _baseThickness > 0) { diff --git a/UVtools.Core/Operations/OperationMask.cs b/UVtools.Core/Operations/OperationMask.cs index c7b49d9e..fc4cb27a 100644 --- a/UVtools.Core/Operations/OperationMask.cs +++ b/UVtools.Core/Operations/OperationMask.cs @@ -26,7 +26,7 @@ public class OperationMask : Operation public override string Description => "Mask the intensity of the LCD output using a greyscale input image.\n\n" + "Useful to correct LCD light uniformity for a specific printer.\n\n" + - "NOTE: This operation should be run only after repairs and other transformations. The provided" + + "NOTE: This operation should be run only after repairs and other transformations. The provided " + "input mask image must match the output resolution of the target printer."; public override string ConfirmationText => diff --git a/UVtools.Core/Operations/OperationRepairLayers.cs b/UVtools.Core/Operations/OperationRepairLayers.cs index a9ad1584..3ac60875 100644 --- a/UVtools.Core/Operations/OperationRepairLayers.cs +++ b/UVtools.Core/Operations/OperationRepairLayers.cs @@ -416,13 +416,13 @@ void InitImage() { if (_repairIslands && _removeIslandsBelowEqualPixelCount > 0 && _removeIslandsRecursiveIterations == 1) { - Span bytes = null; + var bytes = Span.Empty; foreach (IssueOfPoints issue in IssueManager.GetIssuesBy(issues, MainIssue.IssueType.Island, (uint)layerIndex)) { if (issue.PixelsCount > _removeIslandsBelowEqualPixelCount) continue; InitImage(); - if (bytes == null) bytes = image!.GetDataByteSpan(); + if (bytes.IsEmpty) bytes = image!.GetDataByteSpan(); foreach (var issuePixel in issue.Points) { diff --git a/UVtools.Core/Printer/Machine.cs b/UVtools.Core/Printer/Machine.cs index 133c0e62..ebdd26e2 100644 --- a/UVtools.Core/Printer/Machine.cs +++ b/UVtools.Core/Printer/Machine.cs @@ -235,8 +235,14 @@ public Machine Clone() new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono M5s Pro", "Photon Mono M5s Pro", 13312, 5120, 223.6416f, 126.976f, 200f, FlipDirection.Horizontally), new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono M7", "Photon Mono M7", 13312, 5120, 223.6416f, 126.976f, 230f, FlipDirection.Horizontally), new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono M7 Pro", "Photon Mono M7 Pro", 13312, 5120, 223.6416f, 126.976f, 230f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono M7 Max", "Photon Mono M7 Max", 6480, 3600, 298.08f, 165.6f, 300f, FlipDirection.Horizontally), new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono", "Photon Mono", 1620, 2560, 82.62f, 130.56f, 165f, FlipDirection.Horizontally), - new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 2", "Photon Mono 2", 4096, 2560, 143.36f, 89.60f, 165f, FlipDirection.Horizontally),new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4K", "Photon Mono 4K", 3840, 2400, 134.40f, 84f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 2", "Photon Mono 2", 4096, 2560, 143.36f, 89.60f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4K", "Photon Mono 4K", 3840, 2400, 134.40f, 84f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4", "Photon Mono 4", 9024, 5120, 153.408f, 87.040f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4K", "Photon Mono 4K", 3840, 2400, 134.40f, 84f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4 Ultra", "Photon Mono 4 Ultra", 9024, 5120, 153.408f, 87.040f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4K", "Photon Mono 4K", 3840, 2400, 134.40f, 84f, 165f, FlipDirection.Horizontally), new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono SE", "Photon Mono SE", 1620, 2560, 82.62f, 130.56f, 160f, FlipDirection.Horizontally), new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono SQ", "Photon Mono SQ", 2400, 2560, 120f, 128f, 200f, FlipDirection.Horizontally), new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono X 6K", "Photon Mono X 6K", 5760, 3600, 198.15f, 123.84f, 245f, FlipDirection.Horizontally), @@ -357,6 +363,7 @@ public Machine Clone() new(PrinterBrand.Phrozen, "Phrozen Sonic Mini 8K S", "Sonic Mini 8K S", 7536, 3240, 165.792f, 71.28f, 170f, FlipDirection.Horizontally), new(PrinterBrand.Phrozen, "Phrozen Sonic Mini", "Sonic Mini", 1080, 1920, 68.04f, 120.96f, 130f, FlipDirection.None), new(PrinterBrand.Phrozen, "Phrozen Sonic", "Sonic", 1080, 1920, 68.04f, 120.96f, 170f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Sonic Mighty Revo", "Sonic Mighty Revo", 13320, 5120, 223.776f, 126.976f, 235f, FlipDirection.Horizontally), new(PrinterBrand.Phrozen, "Phrozen Transform", "Transform", 3840, 2160, 291.84f, 164.16f, 400f, FlipDirection.None), new(PrinterBrand.QIDI, "QIDI I-Box Mono", "I-Box Mono", 3840, 2400, 192f, 120f, 200f, FlipDirection.Horizontally), diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 362bcd5e..6381ba9c 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -20,23 +20,23 @@ - - - + + - + - + + - + diff --git a/UVtools.Installer/Code/Product.wxs b/UVtools.Installer/Code/Product.wxs index f20627d1..b83764bb 100644 --- a/UVtools.Installer/Code/Product.wxs +++ b/UVtools.Installer/Code/Product.wxs @@ -145,7 +145,7 @@ - + diff --git a/UVtools.Installer/UVtools.Installer.wixproj b/UVtools.Installer/UVtools.Installer.wixproj index f352c2b8..6d5f45c8 100644 --- a/UVtools.Installer/UVtools.Installer.wixproj +++ b/UVtools.Installer/UVtools.Installer.wixproj @@ -1,4 +1,4 @@ - + x64 $(UVtoolsVersion) @@ -25,9 +25,9 @@ Debug;$(DefineConstants) - - - + + + diff --git a/UVtools.UI/Assets/Styles/Styles.axaml b/UVtools.UI/Assets/Styles/Styles.axaml index 5fe44da9..4fe5c756 100644 --- a/UVtools.UI/Assets/Styles/Styles.axaml +++ b/UVtools.UI/Assets/Styles/Styles.axaml @@ -143,7 +143,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/UVtools.UI/Controls/Calibrators/CalibrateExposureFinderControl.axaml b/UVtools.UI/Controls/Calibrators/CalibrateExposureFinderControl.axaml index 33094add..053116b1 100644 --- a/UVtools.UI/Controls/Calibrators/CalibrateExposureFinderControl.axaml +++ b/UVtools.UI/Controls/Calibrators/CalibrateExposureFinderControl.axaml @@ -15,7 +15,7 @@ IsExpanded="True"> - + + + + + + + + + + - - + + - - - - - - - - - - - - - diff --git a/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs b/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs index e76691ce..b085f07d 100644 --- a/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs +++ b/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs @@ -30,7 +30,7 @@ public override void Callback(ToolWindow.Callbacks callback) private void PhasedExposuresGrid_OnLoadingRow(object? sender, DataGridRowEventArgs e) { - e.Row.Header = e.Row.GetIndex() + 1; + e.Row.Header = e.Row.Index + 1; } public void AddExposure() diff --git a/UVtools.UI/MainWindow.PixelEditor.cs b/UVtools.UI/MainWindow.PixelEditor.cs index cb961b3d..d6baa2bd 100644 --- a/UVtools.UI/MainWindow.PixelEditor.cs +++ b/UVtools.UI/MainWindow.PixelEditor.cs @@ -68,7 +68,7 @@ public void InitPixelEditor() private void DrawingsGrid_OnLoadingRow(object? sender, DataGridRowEventArgs e) { - e.Row.Header = e.Row.GetIndex() + 1; + e.Row.Header = e.Row.Index + 1; } private void DrawingsGrid_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) diff --git a/UVtools.UI/MainWindow.Progress.cs b/UVtools.UI/MainWindow.Progress.cs index a92d4ce6..3e3241d2 100644 --- a/UVtools.UI/MainWindow.Progress.cs +++ b/UVtools.UI/MainWindow.Progress.cs @@ -8,6 +8,7 @@ using Avalonia.Threading; using System; +using System.ComponentModel.DataAnnotations; using System.Timers; using UVtools.Core.Operations; using UVtools.UI.Structures; @@ -30,7 +31,11 @@ public partial class MainWindow public bool IsProgressVisible { get => _isProgressVisible; - set => RaiseAndSetIfChanged(ref _isProgressVisible, value); + set + { + if (!RaiseAndSetIfChanged(ref _isProgressVisible, value)) return; + _ramUsageTimer.Enabled = value && Settings.General.AvailableRamLimit > 0; + } } #endregion @@ -53,9 +58,7 @@ public void InitProgress() public void ProgressOnClickPauseResume() { if (!Progress.CanCancel) return; - DialogResult = DialogResults.Cancel; - Progress.CanCancel = false; - Progress.TokenSource.Cancel(); + Progress.IsPaused = !Progress.IsPaused; } public void ProgressOnClickCancel() diff --git a/UVtools.UI/MainWindow.axaml b/UVtools.UI/MainWindow.axaml index 0f21f704..bf0c7ffe 100644 --- a/UVtools.UI/MainWindow.axaml +++ b/UVtools.UI/MainWindow.axaml @@ -2123,6 +2123,7 @@ PanWithArrows="False" ShowGrid="{Binding Settings.LayerPreview.ShowBackgroudGrid}" GridCellSize="15" + ConstrainZoomOutToFitLevel="True" Name="LayerImageBox"/> diff --git a/UVtools.UI/MainWindow.axaml.cs b/UVtools.UI/MainWindow.axaml.cs index 359f4949..4845d1e3 100644 --- a/UVtools.UI/MainWindow.axaml.cs +++ b/UVtools.UI/MainWindow.axaml.cs @@ -46,8 +46,8 @@ using Point = Avalonia.Point; using System.Runtime; using System.Runtime.InteropServices; +using System.Timers; using UVtools.Core.Scripting; -using static System.Net.Mime.MediaTypeNames; namespace UVtools.UI; @@ -132,7 +132,8 @@ public partial class MainWindow : WindowEx #region Members public Stopwatch LastStopWatch = new(); - + private readonly Timer _ramUsageTimer = new(2000) { AutoReset = true }; + private bool _isGUIEnabled = true; private uint _savesCount; private bool _canSave; @@ -166,7 +167,6 @@ public bool IsGUIEnabled //ProgressWindow = new ProgressWindow(); return; } - DragDrop.SetAllowDrop(this, true); LastStopWatch = Progress.StopWatch; @@ -456,11 +456,75 @@ public MainWindow() MainMenuFileSendTo.IsVisible = MainMenuFileSendTo.IsEnabled = menuItems.Count > 0; }; + _ramUsageTimer.Elapsed += RamUsageTimerOnElapsed; + #if DEBUG this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Control)); #endif } + private void RamUsageTimerOnElapsed(object? sender, ElapsedEventArgs e) + { + if (!IsProgressVisible) + { + _ramUsageTimer.Stop(); + return; + } + + var memoryStatus = SystemAware.GetMemoryStatus(); + if (memoryStatus.ullAvailPhys == 0) return; // Unable to check + + var availableMemory = (decimal)Math.Round(memoryStatus.ullAvailPhys / Math.Pow(1024, 3), 2, MidpointRounding.AwayFromZero); + if (availableMemory > Settings.General.AvailableRamLimit) return; + + var totalMemory = Math.Round(memoryStatus.ullTotalPhys / Math.Pow(1024, 3), 2, MidpointRounding.AwayFromZero); + var usedMemory = Math.Round((memoryStatus.ullTotalPhys - memoryStatus.ullAvailPhys) / Math.Pow(1024, 3), 2, MidpointRounding.AwayFromZero); + + var processMemory = Math.Round(Environment.WorkingSet / Math.Pow(1024, 3), 2, MidpointRounding.AwayFromZero); + var percentProcessMemory = Math.Round(processMemory * 100 / totalMemory, 2, MidpointRounding.AwayFromZero); + + _ramUsageTimer.Stop(); + Dispatcher.UIThread.InvokeAsync(() => + { + if (Progress.CanCancel) + { + + switch (Settings.General.AvailableRamOnHitLimitAction) + { + case RamLimitAction.Pause: + Progress.IsPaused = true; + break; + case RamLimitAction.Cancel: + Progress.TokenSource.Cancel(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(Settings.General.AvailableRamOnHitLimitAction), + Settings.General.AvailableRamOnHitLimitAction, null); + } + + this.MessageBoxWaring( + $"Your system memory RAM hit the limit of {usedMemory}GB from a total of {totalMemory}GB. Available: {availableMemory}GB. {About.Software}: {processMemory}GB ({percentProcessMemory}%).\n" + + $"The running operation will {Settings.General.AvailableRamOnHitLimitAction.ToString().ToLowerInvariant()} as soon as possible to relief pressure and maintain the system stability.\n" + + $"If you continue the operation this check will not be performed again for the self operation.\n\n" + + $"Monitor your memory RAM under the system tools and re-run the operation to check the usages and find the cause.\n" + + $"You may need to stop other processes or increase your memory RAM in order to deal with huge files and/or heavy operations.\n\n" + + $"Currently you have configured a lower limit of {Settings.General.AvailableRamLimit}GB of available memory RAM for the operations.", + $"Insufficient memory RAM! ({usedMemory}GB / {totalMemory}GB)"); + + } + else + { + // Kills + throw new InsufficientMemoryException( + $"Your system memory RAM hit the limit of {usedMemory}GB from a total of {totalMemory}GB. Available: {availableMemory}GB. {About.Software}: {processMemory}GB ({percentProcessMemory}%).\n" + + $"The program crashed on purpose due the impossibility to pause or cancel the running operation, this was to relief pressure and ensure the system stability.\n\n" + + $"Monitor your memory RAM under the system tools and re-run the operation to check the usages and find the cause.\n" + + $"You may need to stop other processes or increase your memory RAM in order to deal with huge files and/or heavy operations.\n\n" + + $"Currently you have configured a lower limit of {Settings.General.AvailableRamLimit}GB of available memory RAM for the operations."); + } + }); + } + private async void FileSendToItemClick(object? sender, RoutedEventArgs e) { if (sender is not MenuItem menuItem) return; @@ -727,7 +791,7 @@ protected override void OnOpened(EventArgs e) { ProcessFile(Path.Combine(App.ApplicationPath, About.DemoFile)); } - + DispatcherTimer.Run(() => { UpdateTitle(); diff --git a/UVtools.UI/UVtools.UI.csproj b/UVtools.UI/UVtools.UI.csproj index 657352fd..6c4f0594 100644 --- a/UVtools.UI/UVtools.UI.csproj +++ b/UVtools.UI/UVtools.UI.csproj @@ -33,9 +33,9 @@ - - - + + + diff --git a/UVtools.UI/UserSettings.cs b/UVtools.UI/UserSettings.cs index ec79ba5f..3ead11fc 100644 --- a/UVtools.UI/UserSettings.cs +++ b/UVtools.UI/UserSettings.cs @@ -75,7 +75,9 @@ public sealed class GeneralUserSettings : BindableBase private RangeObservableCollection _sendToProcess = new(); private ushort _lockedFilesOpenCounter; private bool _fileSaveUpdateNameWithNewInformation = true; - + private decimal _availableRamLimit = 1M; + private RamLimitAction _availableRamOnHitLimitAction; + public const byte LockedFilesMaxOpenCounter = 10; @@ -133,6 +135,21 @@ public bool LoadLastRecentFileOnStartup set => RaiseAndSetIfChanged(ref _loadLastRecentFileOnStartup, value); } + /// + /// Gets or sets the minimum amount of available RAM in GB to be able to run, otherwise will pause/cancel or exit. + /// + public decimal AvailableRamLimit + { + get => _availableRamLimit; + set => RaiseAndSetIfChanged(ref _availableRamLimit, Math.Max(0, value)); + } + + public RamLimitAction AvailableRamOnHitLimitAction + { + get => _availableRamOnHitLimitAction; + set => RaiseAndSetIfChanged(ref _availableRamOnHitLimitAction, value); + } + /// /// Gets or sets the maximum number of concurrent tasks enabled by a ParallelOptions instance. /// diff --git a/UVtools.UI/Windows/BenchmarkWindow.axaml.cs b/UVtools.UI/Windows/BenchmarkWindow.axaml.cs index 3f864a36..918ad781 100644 --- a/UVtools.UI/Windows/BenchmarkWindow.axaml.cs +++ b/UVtools.UI/Windows/BenchmarkWindow.axaml.cs @@ -78,8 +78,8 @@ public enum BenchmarkResolution /*Brotli 8K Compress*/ new BenchmarkTestResult(198.02f, 3968.25f), /*LZ4 4K Compress*/ new BenchmarkTestResult(1250f, 20833.33f), /*LZ4 8K Compress*/ new BenchmarkTestResult(344.83f, 6250f), - /*GC Memory Copy 4K*/ new BenchmarkTestResult(571.43f, 1219.51f), - /*GC Memory Copy 8K*/ new BenchmarkTestResult(186.92f, 327.65f), + /*GC Memory Copy 4K*/ new BenchmarkTestResult(952.38f, 2304.15f), + /*GC Memory Copy 8K*/ new BenchmarkTestResult(266.67f, 758.73f), /*Pooled Memory Copy 4K*/new BenchmarkTestResult(5000, 9433.96f), /*Pooled Memory Copy 8K*/new BenchmarkTestResult(769.23f, 2074.69f), /*Stress CPU test*/ new BenchmarkTestResult(0f, 0f), diff --git a/UVtools.UI/Windows/SettingsWindow.axaml b/UVtools.UI/Windows/SettingsWindow.axaml index a92dfc75..6ecc0d36 100644 --- a/UVtools.UI/Windows/SettingsWindow.axaml +++ b/UVtools.UI/Windows/SettingsWindow.axaml @@ -65,10 +65,46 @@ Header="Tasks"> - + + + - + + + + + - - - @@ -123,61 +159,61 @@ CommandParameter="-1"/> !--> - + + + Gets if the contour is closed + + + + + Gets if the contour is open + + Gets the centroid of the contour @@ -932,6 +942,11 @@ Default order of issues to show on the UI list + + + The action to take when the RAM hit the limit. + + The Excellon drill format is a subset of RS274D and is used by the drilling and routing machines made by the Excellon corporation. @@ -5425,12 +5440,19 @@ - From a pixel position get the equivalent position on the display + From a display position get the equivalent position on the pixel X position in millimeters Y position in millimeters Resolution position in pixels + + + From a display position get the equivalent position on the pixel + + + + Creates an empty mat of file size and create a dummy pixel to prevent an empty layer detection