From 57b8a9dc8411da7b008025f8ef20a945373ccee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Sat, 26 Mar 2022 03:38:39 +0000 Subject: [PATCH] v3.2.0 - **Core:** - (Add) Machine presets and able to load machine collection from PrusaSlicer - (Improvement) Core: Reference EmguCV runtimes into core instead of the UI project - **File formats:** - **CXDLP:** - (Add) Detection support for Halot One Pro - (Add) Detection support for Halot One Plus - (Add) Detection support for Halot Sky Plus - (Add) Detection support for Halot Lite - (Improvement) Better handling and detection of printer model when converting - (Improvement) Discovered more fields meanings on format - (Fix) Exposure time in format is `round(time * 10, 1)` - (Fix) Speeds in format are in mm/s, was using mm/min before - (Add) JXS format for Uniformation GKone [Zip+GCode] - (Improvement) Saving and converting files now handle the file backup on Core instead on the UI, which prevents scripts and other projects lose the original file in case of error while saving - (Fix) After load files they was flagged as requiring a full encode, preventing fast save a fresh file - **UVtoolsCmd:** - Bring back the commandline project - Consult README to see the available commands and syntax - Old terminal commands on UVtools still works for now, but consider switch to UVtoolsCmd or redirect the command using `UVtools --cmd "commands"` - **Tools:** - **Change print resolution:** - (Add) Allow to change the display size to match the new printer - (Add) Machine presets to help set both resolution and display size to a correct printer and auto set fix pixel ratio - (Improvement) Real pixel pitch fixer due new display size information, this allow full transfers between different printers "without" invalidating the model size - (Improvement) Better arrangement of the layout - (Add) Infill: Option "Reinforce infill if possible", it was always on before, now default is off and configurable - (Improvement) Always allow to export settings from tools - **GCode:** - (Improvement) After print the last layer, do one lift with the same layer settings before attempt a fast move to top - (Improvement) Use the highest defined speed to send the build plate to top after finish print - (Improvement) Append a wait sync command in the end of gcode if needed - (Fix) When lift without a retract it still output the motor sync delay for the retract time and the wait time after retract - **PrusaSlicer:** - (Add) Printer: Creality Halot One Pro CL-70 - (Add) Printer: Creality Halot One Plus CL-79 - (Add) Printer: Creality Halot Sky Plus CL-92 - (Add) Printer: Creality Halot Lite CL-89L - (Add) Printer: Creality Halot Lite CL-89L - (Add) Printer: Creality CT133 Pro - (Add) Printer: Creality CT-005 Pro - (Add) Printer: Uniformation GKone - (Add) Printer: FlashForge Foto 8.9S - (Add) Printer: Elegoo Mars 2 - (Improvement) Rename all Creality printers - (Fix) Creality model in print notes --- CHANGELOG.md | 50 ++ README.md | 42 +- Scripts/010 Editor/cxdlp.bt | 21 +- Scripts/ImportPrusaSlicerData.ps1 | 206 +++--- UVtools.Cmd/Program.cs | 442 +++++------ UVtools.Cmd/ProgressBar.cs | 126 ++++ UVtools.Cmd/Symbols/ConvertCommand.cs | 93 +++ UVtools.Cmd/Symbols/CopyParametersCommand.cs | 53 ++ UVtools.Cmd/Symbols/ExtractCommand.cs | 50 ++ UVtools.Cmd/Symbols/GlobalArguments.cs | 19 + UVtools.Cmd/Symbols/GlobalOptions.cs | 21 + UVtools.Cmd/Symbols/PrintGCodeCommand.cs | 42 ++ UVtools.Cmd/Symbols/PrintLayersCommand.cs | 106 +++ UVtools.Cmd/Symbols/PrintMachines.cs | 53 ++ UVtools.Cmd/Symbols/PrintPropertiesCommand.cs | 101 +++ UVtools.Cmd/Symbols/RunCommand.cs | 95 +++ UVtools.Cmd/UVtools.Cmd.csproj | 7 +- UVtools.Cmd/UVtools.ico | Bin 124548 -> 128964 bytes UVtools.Core/About.cs | 4 +- UVtools.Core/Converters/SpeedConverter.cs | 56 ++ .../TimeConverter.cs} | 4 +- UVtools.Core/Enumerations.cs | 144 ++-- UVtools.Core/Extensions/JsonExtensions.cs | 9 +- UVtools.Core/Extensions/TypeExtensions.cs | 9 + UVtools.Core/FileFormats/CTBEncryptedFile.cs | 18 +- UVtools.Core/FileFormats/CWSFile.cs | 69 +- UVtools.Core/FileFormats/CXDLPFile.cs | 112 ++- UVtools.Core/FileFormats/CXDLPv1File.cs | 6 +- UVtools.Core/FileFormats/ChituboxFile.cs | 16 +- UVtools.Core/FileFormats/ChituboxZipFile.cs | 20 +- UVtools.Core/FileFormats/FDGFile.cs | 12 +- UVtools.Core/FileFormats/FileExtension.cs | 10 +- UVtools.Core/FileFormats/FileFormat.cs | 221 ++++-- .../FileFormats/FlashForgeSVGXFile.cs | 10 +- UVtools.Core/FileFormats/GR1File.cs | 8 +- UVtools.Core/FileFormats/GenericZIPFile.cs | 12 +- UVtools.Core/FileFormats/ImageFile.cs | 4 +- UVtools.Core/FileFormats/JXSFile.cs | 698 ++++++++++++++++++ UVtools.Core/FileFormats/LGSFile.cs | 27 +- UVtools.Core/FileFormats/MDLPFile.cs | 8 +- UVtools.Core/FileFormats/OSLAFile.cs | 10 +- UVtools.Core/FileFormats/PHZFile.cs | 12 +- UVtools.Core/FileFormats/PhotonSFile.cs | 10 +- .../FileFormats/PhotonWorkshopFile.cs | 10 +- UVtools.Core/FileFormats/SL1File.cs | 26 +- UVtools.Core/FileFormats/UVJFile.cs | 8 +- UVtools.Core/FileFormats/VDAFile.cs | 14 +- UVtools.Core/FileFormats/VDTFile.cs | 18 +- UVtools.Core/FileFormats/ZCodeFile.cs | 33 +- UVtools.Core/FileFormats/ZCodexFile.cs | 29 +- UVtools.Core/GCode/GCodeBuilder.cs | 120 ++- UVtools.Core/GCode/GCodeLayer.cs | 2 +- UVtools.Core/Layers/Layer.cs | 14 +- UVtools.Core/Managers/MatCacheManager.cs | 8 +- UVtools.Core/Operations/Operation.cs | 40 +- .../Operations/OperationCalculator.cs | 2 +- .../OperationCalibrateElephantFoot.cs | 6 +- .../OperationCalibrateExposureFinder.cs | 6 +- .../OperationCalibrateExternalTests.cs | 2 +- .../Operations/OperationCalibrateGrayscale.cs | 6 +- .../OperationCalibrateLiftHeight.cs | 2 +- .../OperationCalibrateStressTower.cs | 6 +- .../Operations/OperationCalibrateTolerance.cs | 6 +- .../OperationCalibrateXYZAccuracy.cs | 6 +- .../Operations/OperationChangeResolution.cs | 97 ++- .../Operations/OperationDoubleExposure.cs | 2 +- .../Operations/OperationDynamicLifts.cs | 2 +- .../Operations/OperationEditParameters.cs | 2 +- .../Operations/OperationFadeExposureTime.cs | 2 +- .../Operations/OperationIPrintedThisFile.cs | 2 +- UVtools.Core/Operations/OperationInfill.cs | 67 +- .../Operations/OperationLayerArithmetic.cs | 2 +- .../Operations/OperationLayerClone.cs | 2 +- .../Operations/OperationLayerExportGif.cs | 12 +- .../Operations/OperationLayerExportHeatMap.cs | 12 +- .../Operations/OperationLayerExportImage.cs | 14 +- .../Operations/OperationLayerExportMesh.cs | 16 +- .../Operations/OperationLayerImport.cs | 4 +- .../Operations/OperationLayerReHeight.cs | 2 +- .../Operations/OperationLayerRemove.cs | 2 +- .../OperationLightBleedCompensation.cs | 2 +- UVtools.Core/Operations/OperationMove.cs | 34 +- UVtools.Core/Operations/OperationPattern.cs | 8 +- .../Operations/OperationRaftRelief.cs | 4 +- .../Operations/OperationRaiseOnPrintFinish.cs | 2 +- .../Operations/OperationRedrawModel.cs | 2 +- UVtools.Core/Operations/OperationTimelapse.cs | 2 +- UVtools.Core/Printer/Machine.cs | 409 ++++++++++ UVtools.Core/Printer/PrinterBrand.cs | 31 + UVtools.Core/SystemOS/SystemAware.cs | 5 + UVtools.Core/UVtools.Core.csproj | 4 +- UVtools.InstallerMM/UVtools.InstallerMM.wxs | 110 ++- UVtools.WPF/App.axaml.cs | 3 +- UVtools.WPF/ConsoleArguments.cs | 8 + .../Tools/ToolChangeResolutionControl.axaml | 100 ++- .../ToolChangeResolutionControl.axaml.cs | 37 +- .../Controls/Tools/ToolInfillControl.axaml | 25 +- UVtools.WPF/MainWindow.axaml.cs | 51 +- UVtools.WPF/Program.cs | 38 +- UVtools.WPF/UVtools.WPF.csproj | 4 +- UVtools.WPF/UserSettings.cs | 2 +- UVtools.WPF/Windows/ToolWindow.axaml | 2 +- UVtools.WPF/Windows/ToolWindow.axaml.cs | 46 +- UVtools.sln | 18 +- 104 files changed, 3556 insertions(+), 1031 deletions(-) create mode 100644 UVtools.Cmd/ProgressBar.cs create mode 100644 UVtools.Cmd/Symbols/ConvertCommand.cs create mode 100644 UVtools.Cmd/Symbols/CopyParametersCommand.cs create mode 100644 UVtools.Cmd/Symbols/ExtractCommand.cs create mode 100644 UVtools.Cmd/Symbols/GlobalArguments.cs create mode 100644 UVtools.Cmd/Symbols/GlobalOptions.cs create mode 100644 UVtools.Cmd/Symbols/PrintGCodeCommand.cs create mode 100644 UVtools.Cmd/Symbols/PrintLayersCommand.cs create mode 100644 UVtools.Cmd/Symbols/PrintMachines.cs create mode 100644 UVtools.Cmd/Symbols/PrintPropertiesCommand.cs create mode 100644 UVtools.Cmd/Symbols/RunCommand.cs create mode 100644 UVtools.Core/Converters/SpeedConverter.cs rename UVtools.Core/{Extensions/TimeExtensions.cs => Converters/TimeConverter.cs} (94%) create mode 100644 UVtools.Core/FileFormats/JXSFile.cs create mode 100644 UVtools.Core/Printer/Machine.cs create mode 100644 UVtools.Core/Printer/PrinterBrand.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 39fabf8d..e8601227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## 26/03/2022 - v3.2.0 + +- **Core:** + - (Add) Machine presets and able to load machine collection from PrusaSlicer + - (Improvement) Core: Reference EmguCV runtimes into core instead of the UI project +- **File formats:** + - **CXDLP:** + - (Add) Detection support for Halot One Pro + - (Add) Detection support for Halot One Plus + - (Add) Detection support for Halot Sky Plus + - (Add) Detection support for Halot Lite + - (Improvement) Better handling and detection of printer model when converting + - (Improvement) Discovered more fields meanings on format + - (Fix) Exposure time in format is `round(time * 10, 1)` + - (Fix) Speeds in format are in mm/s, was using mm/min before + - (Add) JXS format for Uniformation GKone [Zip+GCode] + - (Improvement) Saving and converting files now handle the file backup on Core instead on the UI, which prevents scripts and other projects lose the original file in case of error while saving + - (Improvement) When saving files the .tmp extension is no longer shown on `FileFullPath`, which now `TemporaryOutputFileFullPath` is who holds the file.tmp + - (Fix) After load files they was flagged as requiring a full encode, preventing fast save a fresh file +- **UVtoolsCmd:** + - Bring back the commandline project + - Consult README to see the available commands and syntax + - Old terminal commands on UVtools still works for now, but consider switch to UVtoolsCmd or redirect the command using `UVtools --cmd "commands"` +- **Tools:** + - **Change print resolution:** + - (Add) Allow to change the display size to match the new printer + - (Add) Machine presets to help set both resolution and display size to a correct printer and auto set fix pixel ratio + - (Improvement) Real pixel pitch fixer due new display size information, this allow full transfers between different printers "without" invalidating the model size + - (Improvement) Better arrangement of the layout + - (Add) Infill: Option "Reinforce infill if possible", it was always on before, now default is off and configurable + - (Improvement) Always allow to export settings from tools +- **GCode:** + - (Improvement) After print the last layer, do one lift with the same layer settings before attempt a fast move to top + - (Improvement) Use the highest defined speed to send the build plate to top after finish print + - (Improvement) Append a wait sync command in the end of gcode if needed + - (Fix) When lift without a retract it still output the motor sync delay for the retract time and the wait time after retract +- **PrusaSlicer:** + - (Add) Printer: Creality Halot One Pro CL-70 + - (Add) Printer: Creality Halot One Plus CL-79 + - (Add) Printer: Creality Halot Sky Plus CL-92 + - (Add) Printer: Creality Halot Lite CL-89L + - (Add) Printer: Creality Halot Lite CL-89L + - (Add) Printer: Creality CT133 Pro + - (Add) Printer: Creality CT-005 Pro + - (Add) Printer: Uniformation GKone + - (Add) Printer: FlashForge Foto 8.9S + - (Add) Printer: Elegoo Mars 2 + - (Improvement) Rename all Creality printers + - (Fix) Creality model in print notes + ## 21/03/2022 - v3.1.1 - (Add) Raft relief: Tabs type - Creates tabs around the raft to easily insert a tool under it and detach the raft from build plate diff --git a/README.md b/README.md index 304e31fc..004d66c4 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ But also, i need victims for test subject. Proceed at your own risk! - PWMX (Photon Workshop) - PWMB (Photon Workshop) - PWSQ (Photon Workshop) +- JXS (GKone Slicer) - ZCode (UnizMaker) - ZCodex (Z-Suite) - CWS (NovaMaker) @@ -146,15 +147,52 @@ Replace the "xxx" by your desired value in the correct units https://github.com/sn4k3/UVtools/wiki/Sliced-File-Conversion -# Command-line arguments +# Command-line -The UVtools executable allow to set some arguments to do special functions: +## UVtoolsCmd (Console) executable + +```bash +Usage: + UVtoolsCmd [command] [options] + +Options: + -q, --quiet Make output silent but exceptions error will still show + --no-progress Show no progress + --core-version Show core version information + --version Show version information + -?, -h, --help Show help and usage information + +Commands: + run Run operations and/or scripts + convert Convert input file into a output file format by a known type or + extension [] + extract Extract file contents to a folder [] + copy-parameters Copy print parameters from one file to another + print-properties Prints available properties + print-layers Prints layer(s) properties + print-gcode Prints the gcode of the file if available + print-machines Prints machine settings +``` + +Note: On each command you can use -? to see specific command help and extra options + +## UVtools (UI) executable - **Open file(s):** - **Syntax:** UVtools \ [file2] [file3] ... - **Example 1:** UVtools C:\model.osla - **Example 2:** UVtools C:\model.zip D:\other_model.osla - **Note:** When a invalid file is pass, the program will open as default. +- **Redirect a command to UVtoolsCmd:** + - **Syntax:** UVtools --cmd \ + - **Example 1:** UVtools --cmd convert C:\model.osla zip + - **Note:** This can be used when UVtoolsCmd is not directly exposed, for example if you are running via a .AppImage. + All commands will be redirected to `UVtoolsCmd` and the UI will not run. It still shows the terminal window. + +### Legacy + +The following commands are the old way and commands under the UI executable, they will be removed in near future, try to not use them, please prefer **UVtoolsCmd**. + - **Convert a file into another type(s)** - **Syntax:** UVtools -c/--convert \ \ [output_file2_or_ext] ... - **Example 1:** UVtools -c model.zip osla diff --git a/Scripts/010 Editor/cxdlp.bt b/Scripts/010 Editor/cxdlp.bt index 2512246b..6d82510d 100644 --- a/Scripts/010 Editor/cxdlp.bt +++ b/Scripts/010 Editor/cxdlp.bt @@ -100,15 +100,18 @@ if(header.Version >= 3){ char SoftwareName[SoftwareNameSize]; uint32 MaterialNameSize; char MaterialName[MaterialNameSize]; - //ubyte offset[67-SoftwareNameSize-MaterialNameSize]; - //ubyte offset[20]; - uint Unknown; - uint Unknown; - uint Unknown; - uint Unknown; - ubyte Unknown; - ubyte LightPWM; - ushort Unknown; + ubyte DistortionCompensationEnabled; + uint DistortionCompensationThickness; + uint DistortionCompensationFocalLength; + ubyte XYAxisProfileCompensationEnabled; + ushort XYAxisProfileCompensation; + ubyte ZPenetrationCompensationEnabled; + ushort ZPenetrationCompensationLevel; + ubyte AntiAliasEnabled; + ubyte AntiAliasGreyMinValue; + ubyte AntiAliasGreyMaxValue; + ubyte ImageBlurEnabled; + ubyte ImageBlurLevel; ubyte rn0[2] ; } slicerInfoV3; } diff --git a/Scripts/ImportPrusaSlicerData.ps1 b/Scripts/ImportPrusaSlicerData.ps1 index 2d492bb1..3ef558e5 100644 --- a/Scripts/ImportPrusaSlicerData.ps1 +++ b/Scripts/ImportPrusaSlicerData.ps1 @@ -5,111 +5,121 @@ $output_dir = 'PrusaSlicer' $print_dir = 'sla_print' $printer_dir = 'printer' -$printers = - 'UVtools Prusa SL1.ini', - 'UVtools Prusa SL1S Speed.ini', - 'EPAX E6 Mono.ini', - 'EPAX E10 Mono.ini', - 'EPAX E10 5K.ini', - 'EPAX E10 8K.ini', - 'EPAX X10 8K.ini', - 'EPAX X1.ini', - 'EPAX X1 4KS.ini', - 'EPAX X1K 2K Mono.ini', - 'EPAX X10.ini', - 'EPAX X10 4K Mono.ini', - 'EPAX X10 5K.ini', - 'EPAX X133 4K Mono.ini', - 'EPAX X133 6K.ini', - 'EPAX X156 4K Color.ini', - 'EPAX DX1 Pro.ini', - 'EPAX DX10 Pro 5K.ini', - 'EPAX DX10 Pro 8K.ini', - 'Zortrax Inkspire.ini', - 'Nova3D Elfin.ini', - 'Nova3D Elfin2.ini', - 'Nova3D Elfin2 Mono SE.ini', - 'Nova3D Elfin3 Mini.ini', - 'Nova3D Bene4.ini', - 'Nova3D Bene4 Mono.ini', - 'Nova3D Bene5.ini', - 'Nova3D Whale.ini', - 'Nova3D Whale2.ini', - 'AnyCubic Photon.ini', - 'AnyCubic Photon S.ini', - 'AnyCubic Photon Zero.ini', - 'AnyCubic Photon X.ini', - 'AnyCubic Photon Ultra.ini', - 'AnyCubic Photon Mono.ini', - 'AnyCubic Photon Mono 4K.ini', - 'AnyCubic Photon Mono SE.ini', - 'AnyCubic Photon Mono X.ini', - 'AnyCubic Photon Mono X 6K.ini', - 'AnyCubic Photon Mono SQ.ini', - 'Elegoo Mars.ini', - 'Elegoo Mars 2 Pro.ini', - 'Elegoo Mars 3.ini', - 'Elegoo Mars C.ini', - 'Elegoo Saturn.ini', - 'Elegoo Jupiter.ini', - 'Peopoly Phenom.ini', - 'Peopoly Phenom L.ini', - 'Peopoly Phenom Noir.ini', - 'Peopoly Phenom XXL.ini', - 'QIDI Shadow5.5.ini', - 'QIDI Shadow6.0 Pro.ini', - 'QIDI S-Box.ini', - 'QIDI I-Box Mono.ini', - 'Phrozen Shuffle.ini', - 'Phrozen Shuffle Lite.ini', - 'Phrozen Shuffle XL.ini', - 'Phrozen Shuffle XL Lite.ini', - 'Phrozen Shuffle 16.ini', - 'Phrozen Shuffle 4K.ini', - 'Phrozen Sonic.ini', - 'Phrozen Sonic 4K.ini', - 'Phrozen Sonic Mighty 4K.ini', - 'Phrozen Sonic Mini.ini', - 'Phrozen Sonic Mini 4K.ini', - 'Phrozen Sonic Mini 8K.ini', - 'Phrozen Transform.ini', - 'Phrozen Sonic Mega 8K.ini', - 'Kelant S400.ini', - 'Wanhao D7.ini', - 'Wanhao D8.ini', - 'Wanhao CGR Mini Mono.ini', - 'Wanhao CGR Mono.ini', - 'Creality LD-002R.ini', - 'Creality LD-002H.ini', - 'Creality LD-006.ini', - 'Creality HALOT-ONE CL-60.ini', - 'Creality HALOT-SKY CL-89.ini', - 'Creality HALOT-MAX CL-133.ini', - 'Voxelab Polaris 5.5.ini', - 'Voxelab Proxima 6.ini', - 'Voxelab Ceres 8.9.ini', - 'Longer Orange 10.ini', - 'Longer Orange 30.ini', - 'Longer Orange 120.ini', - 'Longer Orange 4K.ini', - 'Uniz IBEE.ini', - 'FlashForge Explorer MAX.ini', - 'FlashForge Focus 8.9.ini', - 'FlashForge Focus 13.3.ini', - 'FlashForge Foto 6.0.ini', - 'FlashForge Foto 8.9.ini', - 'FlashForge Foto 13.3.ini', - 'FlashForge Hunter.ini' -; +#$printers = +# 'UVtools Prusa SL1.ini', +# 'UVtools Prusa SL1S Speed.ini', +# 'EPAX E6 Mono.ini', +# 'EPAX E10 Mono.ini', +# 'EPAX E10 5K.ini', +# 'EPAX E10 8K.ini', +# 'EPAX X10 8K.ini', +# 'EPAX X1.ini', +# 'EPAX X1 4KS.ini', +# 'EPAX X1K 2K Mono.ini', +# 'EPAX X10.ini', +# 'EPAX X10 4K Mono.ini', +# 'EPAX X10 5K.ini', +# 'EPAX X133 4K Mono.ini', +# 'EPAX X133 6K.ini', +# 'EPAX X156 4K Color.ini', +# 'EPAX DX1 Pro.ini', +# 'EPAX DX10 Pro 5K.ini', +# 'EPAX DX10 Pro 8K.ini', +# 'Zortrax Inkspire.ini', +# 'Nova3D Elfin.ini', +# 'Nova3D Elfin2.ini', +# 'Nova3D Elfin2 Mono SE.ini', +# 'Nova3D Elfin3 Mini.ini', +# 'Nova3D Bene4.ini', +# 'Nova3D Bene4 Mono.ini', +# 'Nova3D Bene5.ini', +# 'Nova3D Whale.ini', +# 'Nova3D Whale2.ini', +# 'AnyCubic Photon.ini', +# 'AnyCubic Photon S.ini', +# 'AnyCubic Photon Zero.ini', +# 'AnyCubic Photon X.ini', +# 'AnyCubic Photon Ultra.ini', +# 'AnyCubic Photon Mono.ini', +# 'AnyCubic Photon Mono 4K.ini', +# 'AnyCubic Photon Mono SE.ini', +# 'AnyCubic Photon Mono X.ini', +# 'AnyCubic Photon Mono X 6K.ini', +# 'AnyCubic Photon Mono SQ.ini', +# 'Elegoo Mars.ini', +# 'Elegoo Mars 2 Pro.ini', +# 'Elegoo Mars 3.ini', +# 'Elegoo Mars C.ini', +# 'Elegoo Saturn.ini', +# 'Elegoo Jupiter.ini', +# 'Peopoly Phenom.ini', +# 'Peopoly Phenom L.ini', +# 'Peopoly Phenom Noir.ini', +# 'Peopoly Phenom XXL.ini', +# 'QIDI Shadow5.5.ini', +# 'QIDI Shadow6.0 Pro.ini', +# 'QIDI S-Box.ini', +# 'QIDI I-Box Mono.ini', +# 'Phrozen Shuffle.ini', +# 'Phrozen Shuffle Lite.ini', +# 'Phrozen Shuffle XL.ini', +# 'Phrozen Shuffle XL Lite.ini', +# 'Phrozen Shuffle 16.ini', +# 'Phrozen Shuffle 4K.ini', +# 'Phrozen Sonic.ini', +# 'Phrozen Sonic 4K.ini', +# 'Phrozen Sonic Mighty 4K.ini', +# 'Phrozen Sonic Mini.ini', +# 'Phrozen Sonic Mini 4K.ini', +# 'Phrozen Sonic Mini 8K.ini', +# 'Phrozen Transform.ini', +# 'Phrozen Sonic Mega 8K.ini', +# 'Kelant S400.ini', +# 'Wanhao D7.ini', +# 'Wanhao D8.ini', +# 'Wanhao CGR Mini Mono.ini', +# 'Wanhao CGR Mono.ini', +# 'Creality LD-002R.ini', +# 'Creality LD-002H.ini', +# 'Creality LD-006.ini', +# 'Creality HALOT-ONE CL-60.ini', +# 'Creality HALOT-SKY CL-89.ini', +# 'Creality HALOT-MAX CL-133.ini', +# 'Voxelab Polaris 5.5.ini', +# 'Voxelab Proxima 6.ini', +# 'Voxelab Ceres 8.9.ini', +# 'Longer Orange 10.ini', +# 'Longer Orange 30.ini', +# 'Longer Orange 120.ini', +# 'Longer Orange 4K.ini', +# 'Uniz IBEE.ini', +# 'FlashForge Explorer MAX.ini', +# 'FlashForge Focus 8.9.ini', +# 'FlashForge Focus 13.3.ini', +# 'FlashForge Foto 6.0.ini', +# 'FlashForge Foto 8.9.ini', +# 'FlashForge Foto 13.3.ini', +# 'FlashForge Hunter.ini' +#; Write-Output 'PrusaSlicer Printers Instalation' Write-Output 'This will replace printers, all changes will be discarded' Write-Output $input_dir Write-Output $output_dir -foreach ($printer in $printers) { - xcopy /d /y "$input_dir\$printer_dir\$printer" "$output_dir\$printer_dir" +# Need to ignore FDM printers since they are on the same folder +Get-ChildItem "$input_dir\$printer_dir" -Filter "*.ini" | +Foreach-Object { + $content = Get-Content $_.FullName + $regex = $content -match 'printer_technology.*=.*(SLA)' + if($regex){ + xcopy /d /y $_.FullName "$output_dir\$printer_dir" + } } +#foreach ($printer in $printers) { +# xcopy /d /y "$input_dir\$printer_dir\$printer" "$output_dir\$printer_dir" +#} + Write-Output 'Importing Profiles' xcopy /i /y /d "$input_dir\$print_dir" "$output_dir\$print_dir" \ No newline at end of file diff --git a/UVtools.Cmd/Program.cs b/UVtools.Cmd/Program.cs index 8f1a0043..b75ff020 100644 --- a/UVtools.Cmd/Program.cs +++ b/UVtools.Cmd/Program.cs @@ -1,263 +1,215 @@ -using System; -using System.Collections.Generic; +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; using System.CommandLine; -using System.CommandLine.Invocation; using System.Diagnostics; -using System.Drawing; using System.Globalization; using System.IO; -using System.Reflection; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using UVtools.Cmd.Symbols; using UVtools.Core; -using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Operations; -namespace UVtools.Cmd +namespace UVtools.Cmd; + +internal class Program { - class Program + internal const string EndOperationText = "Done in {0:F2}s"; + internal static OperationProgress Progress { get; } = new(); + internal static Stopwatch StopWatch { get; } = new(); + internal static bool Quiet { get; private set; } + internal static bool NoProgress { get; private set; } + + internal static string[] Args { get; private set; } = null!; + + public static async Task Main(params string[] args) + { + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); + Args = args; + + var rootCommand = new RootCommand("MSLA/DLP, file analysis, repair, conversion and manipulation") + { + RunCommand.CreateCommand(), + ConvertCommand.CreateCommand(), + ExtractCommand.CreateCommand(), + CopyParametersCommand.CreateCommand(), + + PrintPropertiesCommand.CreateCommand(), + PrintLayersCommand.CreateCommand(), + PrintGCodeCommand.CreateCommand(), + PrintMachinesCommand.CreateCommand(), + + GlobalOptions.QuietOption, + GlobalOptions.NoProgressOption, + new Option("--core-version", "Show core version information"), + }; + + //rootCommand.SetHandler(() => { }); + + HandleGlobals(); + await rootCommand.InvokeAsync(args); + + return 1; + + } + + internal static void HandleGlobals() { - public static async Task Main(params string[] args) + foreach (var arg in Args) { - Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); - OperationProgress progress = new OperationProgress(); - Stopwatch sw = new Stopwatch(); - uint count = 0; - var rootCommand = new RootCommand("MSLA/DLP, file analysis, repair, conversion and manipulation") + bool found = false; + if (GlobalOptions.QuietOption.Aliases.Any(alias => arg == alias)) { - new Option(new []{"-f", "--file"}, "Input file to read") - { - IsRequired = true, - Argument = new Argument("filepath").ExistingOnly() - }, - new Option(new []{"-o", "--output"}, "Output file to save the modifications, if aware, it saves to the same input file") - { - Argument = new Argument("filepath") - }, - new Option(new []{"-e", "--extract"}, "Extract file content to a folder") - { - Argument = new Argument("folder") - }, - new Option(new []{"-c", "--convert"}, "Converts input into a output file format by it extension") - { - Argument = new Argument("filepath"), - }, - - new Option(new []{"-p", "--properties"}, "Print a list of all properties/settings"), - new Option(new []{"-gcode"}, "Print the GCode if available"), - new Option(new []{"-i", "--issues"}, "Compute and print a list of all issues"), - new Option(new []{"-r", "--repair"}, "Attempt to repair all issues"){ - Argument = new Argument("[start layer index] [end layer index] [islands 0/1] [remove empty layers 0/1] [resin traps 0/1]"), - }, - - new Option(new []{"-mr", "--mut-resize"}, "Resizes layer images in a X and/or Y factor, starting from 100% value") - { - Argument = new Argument("[x%] [y%] [start layer index] [end layer index] [fade 0/1]") - }, - new Option(new []{"-ms", "--mut-solidify"}, "Closes all inner holes") - { - Argument = new Argument("[start layer index] [end layer index]") - }, - new Option(new []{"-me", "--mut-erode"}, "Erodes away the boundaries of foreground object") - { - Argument = new Argument("[start iterations] [end iterations] [start layer index] [end layer index] [fade 0/1]") - }, - new Option(new []{"-md", "--mut-dilate"}, "It is just opposite of erosion") - { - Argument = new Argument("[start iterations] [end iterations] [start layer index] [end layer index] [fade 0/1]") - }, - new Option(new []{"-mc", "--mut-close"}, "Dilation followed by Erosion") - { - Argument = new Argument("[start iterations] [end iterations] [start layer index] [end layer index] [fade 0/1]") - }, - new Option(new []{"-mo", "--mut-open"}, "Erosion followed by Dilation") - { - Argument = new Argument("[start iterations] [end iterations] [start layer index] [end layer index] [fade 0/1]") - }, - new Option(new []{"-mg", "--mut-gradient"}, "The difference between dilation and erosion of an image") - { - Argument = new Argument("[kernel size] [start layer index] [end layer index] [fade 0/1]") - }, - - new Option(new []{"-mpy", "--mut-py"}, "Performs down-sampling step of Gaussian pyramid decomposition") - { - Argument = new Argument("[start layer index] [end layer index]") - }, - new Option(new []{"-mgb", "--mut-gaussian-blur"}, "Each pixel is a sum of fractions of each pixel in its neighborhood") - { - Argument = new Argument("[aperture] [sigmaX] [sigmaY]") - }, - new Option(new []{"-mmb", "--mut-median-blur"}, "Each pixel becomes the median of its surrounding pixels") - { - Argument = new Argument("[aperture]") - }, - }; - - rootCommand.Handler = CommandHandler.Create( - ( - FileSystemInfo file, - FileSystemInfo convert, - DirectoryInfo extract, - bool properties, - bool gcode, - bool issues, - int[] repair - //decimal[] mutResize - ) => + Quiet = true; + found = true; + } + + if(found) continue; + + if (GlobalOptions.NoProgressOption.Aliases.Any(alias => arg == alias)) { - var fileFormat = FileFormat.FindByExtension(file.FullName, true, true); - if (ReferenceEquals(fileFormat, null)) - { - Console.WriteLine($"Error: {file.FullName} is not a known nor valid format."); - } - else - { - Console.Write($"Reading: {file}"); - sw.Restart(); - fileFormat.Decode(file.FullName, progress); - sw.Stop(); - Console.WriteLine($", in {sw.ElapsedMilliseconds}ms"); - Console.WriteLine("----------------------"); - Console.WriteLine($"Layers: {fileFormat.LayerCount} x {fileFormat.LayerHeight}mm = {fileFormat.PrintHeight}mm"); - Console.WriteLine($"Resolution: {new Size((int) fileFormat.ResolutionX, (int) fileFormat.ResolutionY)}"); - Console.WriteLine($"AntiAlias: {fileFormat.ValidateAntiAliasingLevel()}"); - - Console.WriteLine($"Bottom Layer Count: {fileFormat.BottomLayerCount}"); - Console.WriteLine($"Bottom Exposure Time: {fileFormat.BottomExposureTime}s"); - Console.WriteLine($"Layer Exposure Time: {fileFormat.ExposureTime}s"); - Console.WriteLine($"Print Time: {fileFormat.PrintTime}s"); - Console.WriteLine($"Cost: {fileFormat.MaterialCost}$"); - Console.WriteLine($"Resin Name: {fileFormat.MaterialName}"); - Console.WriteLine($"Machine Name: {fileFormat.MachineName}"); - - Console.WriteLine($"Thumbnails: {fileFormat.CreatedThumbnailsCount}"); - Console.WriteLine("----------------------"); - } - - if (!ReferenceEquals(extract, null)) - { - Console.Write($"Extracting to {extract.FullName}"); - sw.Restart(); - fileFormat.Extract(extract.FullName, true, true, progress); - sw.Stop(); - Console.WriteLine($", finished in {sw.ElapsedMilliseconds}ms"); - } - - if (properties) - { - count = 0; - Console.WriteLine("Listing all properties:"); - Console.WriteLine("----------------------"); - foreach (var config in fileFormat.Configs) - { - Console.WriteLine("******************************"); - Console.WriteLine($"\t{config.GetType().Name}"); - Console.WriteLine("******************************"); - foreach (PropertyInfo propertyInfo in config.GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - count++; - if (propertyInfo.Name.Equals("Item")) continue; - Console.WriteLine($"{propertyInfo.Name}: {propertyInfo.GetValue(config)}"); - } - } - - Console.WriteLine("----------------------"); - Console.WriteLine($"Total properties: {count}"); - } - - if (gcode) - { - if (ReferenceEquals(fileFormat.GCode, null)) - { - Console.WriteLine("No GCode available"); - } - else - { - Console.WriteLine("----------------------"); - Console.WriteLine(fileFormat.GCode); - Console.WriteLine("----------------------"); - Console.WriteLine($"Total lines: {fileFormat.GCode.Length}"); - } - - } - - if (issues) - { - Console.WriteLine("Computing Issues, please wait."); - sw.Restart(); - var issueList = fileFormat.LayerManager.GetAllIssues(null, null, null, null, true, null, progress); - sw.Stop(); - - Console.WriteLine("Issues:"); - Console.WriteLine("----------------------"); - count = 0; - foreach (var issue in issueList) - { - Console.WriteLine(issue); - count++; - } - /*for (uint layerIndex = 0; layerIndex < fileFormat.LayerCount; layerIndex++) - { - if(!issuesDict.TryGetValue(layerIndex, out var list)) continue; - foreach (var issue in list) - { - Console.WriteLine(issue); - count++; - } - }*/ - - Console.WriteLine("----------------------"); - Console.WriteLine($"Total Issues: {count} in {sw.ElapsedMilliseconds}ms"); - } - - if (!ReferenceEquals(convert, null)) - { - var fileConvert = FileFormat.FindByExtension(convert.FullName, true, true); - if (ReferenceEquals(fileFormat, null)) - { - Console.WriteLine($"Error: {convert.FullName} is not a known nor valid format."); - } - else - { - Console.WriteLine($"Converting {fileFormat.GetType().Name} to {fileConvert.GetType().Name}: {convert.Name}"); - - try - { - sw.Restart(); - fileFormat.Convert(fileConvert, convert.FullName, progress); - sw.Stop(); - Console.WriteLine($"Convertion done in {sw.ElapsedMilliseconds}ms"); - } - catch (Exception e) - { - Console.WriteLine(e); - } - - } - - } - - if (!ReferenceEquals(repair, null)) - { - uint layerStartIndex = (uint) (repair.Length >= 1 ? Math.Max(0, repair[0]) : 0); - uint layerEndIndex = repair.Length >= 2 ? (uint) repair[1].Clamp(0, (int) (fileFormat.LayerCount - 1)) : fileFormat.LayerCount-1; - bool repairIslands = repair.Length < 3 || repair[2] > 0 || repair[2] < 0; - bool removeEmptyLayers = repair.Length < 4 || repair[3] > 0 || repair[3] < 0; - bool repairResinTraps = repair.Length < 5 || repair[4] > 0 || repair[4] < 0; - - //fileFormat.LayerManager.RepairLayers(layerStartIndex, layerEndIndex, 2, 1, 4, repairIslands, removeEmptyLayers, repairResinTraps, null, progress); - } - - }); - - - //await rootCommand.InvokeAsync(args); - await rootCommand.InvokeAsync("-f body_Tough0.1mm_SL1_5h16m_HOLLOW_DRAIN.sl1 -r -1"); - - return 1; + NoProgress = true; + found = true; + } + + if (found) continue; + if (arg == "--core-version") + { + Console.WriteLine(About.VersionStr); + Environment.Exit(0); + } + } + } + + internal static FileFormat OpenInputFile(FileInfo inputFile, FileFormat.FileDecodeType decodeType = FileFormat.FileDecodeType.Full) + { + return ProgressBarWork($"Opening file {inputFile.Name}", () => + { + var slicerFile = FileFormat.Open(inputFile.FullName, decodeType, Progress); + if (slicerFile is null) + { + throw new IOException($"Invalid file: {inputFile.Name}"); + } + + return slicerFile; + }); + } + + internal static void SaveFile(FileFormat slicerFile, FileInfo? outputFile) => SaveFile(slicerFile, outputFile?.FullName); + + internal static void SaveFile(FileFormat slicerFile, string? outputFile = null) + { + var fileName = outputFile is null ? slicerFile.Filename : Path.GetFileName(outputFile); + ProgressBarWork($"Saving file {fileName}", () => + { + slicerFile.SaveAs(outputFile, Progress); + }); + } + + #region ProgressBar + + internal static T ProgressBarWork(string title, Func action) + { + T result; + Progress.Title = title; + + StopWatch.Restart(); + Write($"{title}: "); + if (Quiet || NoProgress) + { + result = action.Invoke(); } + else + { + using var progressBar = new ProgressBar(); + result = action.Invoke(); + } + StopWatch.Stop(); + + + WriteLine(string.Format(EndOperationText, StopWatch.ElapsedMilliseconds / 1000.0)); + return result; + } + + internal static void ProgressBarWork(string title, Action action) + { + ProgressBarWork(title, () => + { + action.Invoke(); + return true; + }); + } + + #endregion + + #region Write to console methods + internal static void Write(object? obj) + { + if (Quiet) return; + Console.Write(obj); + } + + internal static void WriteLine(object? obj) + { + if (Quiet) return; + Console.WriteLine(obj); + } + + internal static void WriteWarning(object? obj) + { + if (Quiet) return; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(obj); + Console.ResetColor(); + } + + internal static void WriteLineWarning(object? obj) + { + if (Quiet) return; + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(obj); + Console.ResetColor(); + } + + internal static void WriteError(object? obj, bool exit = false) + { + if (Quiet) + { + if (exit) Environment.Exit(-1); + return; + } + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(obj); + Console.ResetColor(); + if (exit) Environment.Exit(-1); + } + + internal static void WriteLineError(object? obj, bool exit = true) + { + if (Quiet) + { + if (exit) Environment.Exit(-1); + return; + } + + var str = obj?.ToString(); + if(str is not null && !str.StartsWith("Error:")) str = $"Error: {str}"; + + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(str); + Console.ResetColor(); + if (exit) Environment.Exit(-1); } -} + #endregion +} \ No newline at end of file diff --git a/UVtools.Cmd/ProgressBar.cs b/UVtools.Cmd/ProgressBar.cs new file mode 100644 index 00000000..278bcf8e --- /dev/null +++ b/UVtools.Cmd/ProgressBar.cs @@ -0,0 +1,126 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +namespace UVtools.Cmd; + +using System; +using System.Text; +using System.Threading; + +/// +/// An ASCII progress bar +/// +public class ProgressBar : IDisposable +{ + private const byte BlockCount = 10; + private readonly TimeSpan _animationInterval = TimeSpan.FromMilliseconds(125); + private const string Animation = @"|/-\"; + + private readonly Timer _timer; + + private string _currentText = string.Empty; + private bool _disposed; + private int _animationIndex; + + private double _lastProgressPercent = -1; + + private readonly StringBuilder _outputBuilder = new(); + + public ProgressBar() + { + _timer = new Timer(TimerHandler); + + // A progress bar is only for temporary display in a console window. + // If the console output is redirected to a file, draw nothing. + // Otherwise, we'll end up with a lot of garbage in the target file. + if (Program.Quiet || Program.NoProgress) return; + if(Console.IsOutputRedirected) Console.WriteLine(); + ResetTimer(); + } + + private void TimerHandler(object? state) + { + lock (_timer) + { + if (_disposed) return; + + + if (Math.Abs(_lastProgressPercent - Program.Progress.ProgressPercent) > 0.009) + { + _lastProgressPercent = Program.Progress.ProgressPercent; + var progressBlockCount = (int)(_lastProgressPercent * BlockCount / 100); + + if (Console.IsOutputRedirected) + { + var text = string.Format("[{0}{1}] {2:F2}% ({3:F2}s)", + new string('#', progressBlockCount), new string('-', BlockCount - progressBlockCount), + Program.Progress.ProgressPercent, + Program.StopWatch.ElapsedMilliseconds / 1000.0); + Console.WriteLine(text); + } + else + { + var text = string.Format("[{0}{1}] {2:F2}% {3} {4:F2}s", + new string('#', progressBlockCount), new string('-', BlockCount - progressBlockCount), + Program.Progress.ProgressPercent, + Animation[_animationIndex++ % Animation.Length], + Program.StopWatch.ElapsedMilliseconds / 1000.0); + UpdateText(text); + } + } + + ResetTimer(); + } + } + + private void UpdateText(string text) + { + if (Console.IsOutputRedirected) return; + // Get length of common portion + var commonPrefixLength = 0; + var commonLength = Math.Min(_currentText.Length, text.Length); + while (commonPrefixLength < commonLength && text[commonPrefixLength] == _currentText[commonPrefixLength]) + { + commonPrefixLength++; + } + + // Backtrack to the first differing character + _outputBuilder.Clear(); + _outputBuilder.Append('\b', _currentText.Length - commonPrefixLength); + + // Output new suffix + _outputBuilder.Append(text[commonPrefixLength..]); + + // If the new text is shorter than the old one: delete overlapping characters + var overlapCount = _currentText.Length - text.Length; + if (overlapCount > 0) + { + _outputBuilder.Append(' ', overlapCount); + _outputBuilder.Append('\b', overlapCount); + } + + Console.Write(_outputBuilder); + _currentText = text; + } + + private void ResetTimer() + { + _timer.Change(_animationInterval, TimeSpan.FromMilliseconds(-1)); + } + + public void Dispose() + { + TimerHandler(null); + lock (_timer) + { + _disposed = true; + UpdateText(string.Empty); + } + _timer.Dispose(); + } + +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/ConvertCommand.cs b/UVtools.Cmd/Symbols/ConvertCommand.cs new file mode 100644 index 00000000..3d782394 --- /dev/null +++ b/UVtools.Cmd/Symbols/ConvertCommand.cs @@ -0,0 +1,93 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +using System; +using System.CommandLine; +using System.IO; +using UVtools.Core.FileFormats; + +namespace UVtools.Cmd.Symbols; + +internal static class ConvertCommand +{ + internal static Command CreateCommand() + { + var command = new Command("convert", "Convert input file into a output file format by a known type or extension") + { + GlobalArguments.InputFileArgument, + new Argument("target-type/ext", "Target format type or extension"), + GlobalArguments.OutputFileArgument, + + new Option(new[] {"-v", "--version"}, "Sets the file format version"), + new Option("--no-overwrite", "If the output file exists do not overwrite"), + }; + + command.SetHandler((FileInfo inputFile, string targetTypeExt, FileInfo? outputFile, ushort version, bool noOverwrite) => + { + + var targetType = FileFormat.FindByAnyMeans(targetTypeExt); + if (targetType is null) + { + Program.WriteLineError($"Unable to find a valid convert type candidate from {targetTypeExt}."); + return; + } + + string? outputFilePath; + if (outputFile is not null) + { + outputFilePath = outputFile.FullName; + } + else + { + outputFilePath = FileFormat.GetFileNameStripExtensions(inputFile.Name)!; + if (targetType.FileExtensions.Length == 1) + { + outputFilePath = Path.Combine(inputFile.DirectoryName!, $"{outputFilePath}.{targetType.FileExtensions[0].Extension}"); + } + else + { + var ext = FileExtension.Find(targetTypeExt); + if (ext is null) + { + Program.WriteLineError($"Unable to construct the output filename from {targetTypeExt}, there are {targetType.FileExtensions.Length} extensions on this format, please specify an output file."); + return; + } + + outputFilePath = Path.Combine(inputFile.DirectoryName!, $"{outputFilePath}.{ext.Extension}"); + } + } + + var outputFileName = Path.GetFileName(outputFilePath); + + if (noOverwrite && File.Exists(outputFilePath)) + { + Program.WriteLineError($"{outputFileName} already exits! --no-overwrite is enabled."); + return; + } + + var slicerFile = Program.OpenInputFile(inputFile); + + Program.ProgressBarWork($"Converting to {outputFileName}", + () => + { + try + { + return slicerFile.Convert(targetType, outputFilePath, version, Program.Progress); + } + catch (Exception) + { + File.Delete(outputFilePath); + throw; + } + }); + + }, command.Arguments[0], command.Arguments[1], command.Arguments[2], + command.Options[0], command.Options[1]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/CopyParametersCommand.cs b/UVtools.Cmd/Symbols/CopyParametersCommand.cs new file mode 100644 index 00000000..942c5c86 --- /dev/null +++ b/UVtools.Cmd/Symbols/CopyParametersCommand.cs @@ -0,0 +1,53 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +using System.CommandLine; +using System.IO; +using System.Linq; +using UVtools.Core.FileFormats; + +namespace UVtools.Cmd.Symbols; + +internal static class CopyParametersCommand +{ + internal static Command CreateCommand() + { + var command = new Command("copy-parameters", "Copy print parameters from one file to another") + { + GlobalArguments.InputFileArgument, + new Argument("target-files", "Target file(s) to set the parameters").ExistingOnly() + }; + + command.SetHandler((FileInfo inputFile, FileInfo[] targetFiles) => + { + var slicerFile1 = Program.OpenInputFile(inputFile, FileFormat.FileDecodeType.Partial); + + var distinctFiles = targetFiles.DistinctBy(fi => fi.FullName); + + foreach (var file in distinctFiles) + { + if (inputFile.FullName == file.FullName) + { + Program.WriteWarning($"Skipping: {inputFile.Name}, same as input file"); + continue; + } + var slicerFileTarget = Program.OpenInputFile(file, FileFormat.FileDecodeType.Partial); + + var count = FileFormat.CopyParameters(slicerFile1, slicerFileTarget); + if (count > 0) + { + slicerFileTarget.Save(Program.Progress); + } + + Program.WriteLine($"{count} properties changed"); + } + + }, command.Arguments[0], command.Arguments[1]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/ExtractCommand.cs b/UVtools.Cmd/Symbols/ExtractCommand.cs new file mode 100644 index 00000000..02c19cf1 --- /dev/null +++ b/UVtools.Cmd/Symbols/ExtractCommand.cs @@ -0,0 +1,50 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.CommandLine; +using System.IO; + +namespace UVtools.Cmd.Symbols; + +internal static class ExtractCommand +{ + internal static Command CreateCommand() + { + var command = new Command("extract", "Extract file contents to a folder") + { + GlobalArguments.InputFileArgument, + GlobalArguments.OutputDirectoryArgument, + new Option("--no-overwrite", "If the output folder exists do not overwrite"), + }; + + command.SetHandler((FileInfo inputFile, DirectoryInfo? outputDirectory, bool noOverwrite) => + { + var path = outputDirectory is null + ? Path.Combine(inputFile.DirectoryName!, Path.GetFileNameWithoutExtension(inputFile.Name)) + : outputDirectory.FullName; + + if (noOverwrite && Directory.Exists(path)) + { + Program.WriteLineError($"{path} already exits! --no-overwrite is enabled."); + return; + } + + var slicerFile = Program.OpenInputFile(inputFile); + + Program.ProgressBarWork($"Extracting to {Path.GetFileName(path)}", + () => + { + slicerFile.Extract(path); + }); + + }, command.Arguments[0], command.Arguments[1], + command.Options[0]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/GlobalArguments.cs b/UVtools.Cmd/Symbols/GlobalArguments.cs new file mode 100644 index 00000000..f21d4c81 --- /dev/null +++ b/UVtools.Cmd/Symbols/GlobalArguments.cs @@ -0,0 +1,19 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.CommandLine; +using System.IO; + +namespace UVtools.Cmd.Symbols; + +internal static class GlobalArguments +{ + internal static Argument InputFileArgument { get; } = new Argument("input-file", "Input file to open and read").ExistingOnly(); + internal static Argument OutputFileArgument { get; } = new ("output-file", () => null, "Output file to save"); + internal static Argument OutputDirectoryArgument { get; } = new ("output-folder", () => null, "Output folder"); +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/GlobalOptions.cs b/UVtools.Cmd/Symbols/GlobalOptions.cs new file mode 100644 index 00000000..aca565a3 --- /dev/null +++ b/UVtools.Cmd/Symbols/GlobalOptions.cs @@ -0,0 +1,21 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.CommandLine; +using System.IO; + +namespace UVtools.Cmd.Symbols; + +internal static class GlobalOptions +{ + internal static Option QuietOption { get; } = new(new[] { "-q", "--quiet" }, "Make output silent but exceptions error will still show"); + internal static Option NoProgressOption { get; } = new(new[] { "--no-progress" }, "Show no progress"); + internal static Option OutputFile { get; } = new(new[] { "-o", "--output" }, "Output file to save"); + + internal static Option OpenInPartialMode { get; } = new(new []{ "--partial-mode"}, "Fast load the file in partial mode"); +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/PrintGCodeCommand.cs b/UVtools.Cmd/Symbols/PrintGCodeCommand.cs new file mode 100644 index 00000000..08a47532 --- /dev/null +++ b/UVtools.Cmd/Symbols/PrintGCodeCommand.cs @@ -0,0 +1,42 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.CommandLine; +using System.IO; +using UVtools.Core.FileFormats; + +namespace UVtools.Cmd.Symbols; + +internal static class PrintGCodeCommand +{ + internal static Command CreateCommand() + { + var command = new Command("print-gcode", "Prints the gcode of the file if available") + { + GlobalArguments.InputFileArgument, + }; + + command.SetHandler((FileInfo inputFile) => + { + var slicerFile = Program.OpenInputFile(inputFile, FileFormat.FileDecodeType.Partial); + if (slicerFile.SupportsGCode) + { + Console.WriteLine(slicerFile.GCodeStr); + } + else + { + Program.WriteLineWarning("File do not support gcode"); + } + + + }, command.Arguments[0]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/PrintLayersCommand.cs b/UVtools.Cmd/Symbols/PrintLayersCommand.cs new file mode 100644 index 00000000..143ced49 --- /dev/null +++ b/UVtools.Cmd/Symbols/PrintLayersCommand.cs @@ -0,0 +1,106 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Xml.Serialization; +using UVtools.Core.FileFormats; + +namespace UVtools.Cmd.Symbols; + +internal static class PrintLayersCommand +{ + internal static Command CreateCommand() + { + var command = new Command("print-layers", "Prints layer(s) properties") + { + GlobalArguments.InputFileArgument, + + new Option(new []{ "-r", "--range"}, "Prints only the matching layer index(es) in a range"), + new Option(new []{ "-i", "--indexes"}, "Prints only the matching layer index(es)"), + new Option(new []{ "-n", "--names"}, "Prints only the name matching properties"), + GlobalOptions.OpenInPartialMode + }; + + command.SetHandler((FileInfo inputFile, string layerRange, ushort[] layerIndexes, string[] matchNames, bool partialMode) => + { + var slicerFile = Program.OpenInputFile(inputFile, partialMode ? FileFormat.FileDecodeType.Partial : FileFormat.FileDecodeType.Full); + + var layerIndexesList = new List(); + + if (!string.IsNullOrWhiteSpace(layerRange)) + { + var match = Regex.Match(layerRange, @"(\d+)(:|\||-)(\d+)"); + if (match.Success && match.Groups.Count >= 4) + { + var startNumberStr = match.Groups[1].Value; + var endNumberStr = match.Groups[3].Value; + + if (uint.TryParse(startNumberStr, out var startLayerIndex) && + uint.TryParse(endNumberStr, out var endLayerIndex)) + { + if (startLayerIndex > endLayerIndex) + { + (startLayerIndex, endLayerIndex) = (endLayerIndex, startLayerIndex); + } + + for (var layerIndex = startLayerIndex; layerIndex <= endLayerIndex; layerIndex++) + { + layerIndexesList.Add(layerIndex); + } + } + } + } + + if (layerIndexes.Length == 0 && layerIndexesList.Count == 0) + { + for (uint i = 0; i < slicerFile.LayerCount; i++) + { + layerIndexesList.Add(i); + } + } + else + { + layerIndexesList.AddRange(layerIndexes.Select(layerIndex => (uint) layerIndex)); + } + + layerIndexesList = layerIndexesList.Distinct().OrderBy(layerIndex => layerIndex).ToList(); + + foreach (var layerIndex in layerIndexesList) + { + Console.WriteLine($"Layer: {layerIndex}"); + foreach (var propertyInfo in slicerFile[layerIndex].GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name.Equals("Item")) continue; + if (matchNames is not null && matchNames.Length > 0) + { + if (matchNames.All(s => s != propertyInfo.Name)) continue; + } + if (propertyInfo.GetCustomAttributes().Any(attribute => + { + var type = attribute.GetType(); + if (type == typeof(XmlIgnoreAttribute)) return true; + if (type == typeof(JsonIgnoreAttribute)) return true; + return false; + })) continue; + Console.WriteLine($"{propertyInfo.Name}: {propertyInfo.GetValue(slicerFile[layerIndex])}"); + } + } + + }, command.Arguments[0], command.Options[0], command.Options[1], command.Options[2], command.Options[3]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/PrintMachines.cs b/UVtools.Cmd/Symbols/PrintMachines.cs new file mode 100644 index 00000000..e9d6e234 --- /dev/null +++ b/UVtools.Cmd/Symbols/PrintMachines.cs @@ -0,0 +1,53 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.CommandLine; +using System.IO; +using System.Text.Json; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; +using UVtools.Core.Printer; + +namespace UVtools.Cmd.Symbols; + +internal static class PrintMachinesCommand +{ + internal static Command CreateCommand() + { + var command = new Command("print-machines", "Prints machine settings") + { + new Option("--json", "Print in json format"), + new Option("--xml", "Print in xml format") + }; + + command.SetHandler((bool jsonFormat, bool xmlFormat) => + { + if (jsonFormat) + { + Console.WriteLine(JsonSerializer.Serialize(Machine.Machines, JsonExtensions.SettingsIndent)); + return; + } + + if (xmlFormat) + { + Console.WriteLine(XmlExtensions.SerializeObject(Machine.Machines, XmlExtensions.SettingsIndent)); + return; + } + + foreach (var machine in Machine.Machines) + { + Console.WriteLine(machine); + } + + + }, command.Options[0], command.Options[1]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/PrintPropertiesCommand.cs b/UVtools.Cmd/Symbols/PrintPropertiesCommand.cs new file mode 100644 index 00000000..6ea937e2 --- /dev/null +++ b/UVtools.Cmd/Symbols/PrintPropertiesCommand.cs @@ -0,0 +1,101 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.CommandLine; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using UVtools.Core.FileFormats; + +namespace UVtools.Cmd.Symbols; + +internal static class PrintPropertiesCommand +{ + internal static Command CreateCommand() + { + var command = new Command("print-properties", "Prints available properties") + { + GlobalArguments.InputFileArgument, + + new Option(new []{ "-n", "--names"}, "Prints only the name matching properties"), + new Option(new []{ "-b", "--base"}, "Prints only the base properties of the file"), + GlobalOptions.OpenInPartialMode + }; + + command.SetHandler((FileInfo inputFile, string[] matchNames, bool baseOnly, bool partialMode) => + { + var slicerFile = Program.OpenInputFile(inputFile, partialMode ? FileFormat.FileDecodeType.Partial : FileFormat.FileDecodeType.Full); + uint count = 0; + + Console.WriteLine("Listing properties:"); + Console.WriteLine("----------------------"); + if (!baseOnly) + { + foreach (var config in slicerFile.Configs) + { + //Program.WriteLine("******************************"); + //Program.WriteLine($"\t{config.GetType().Name}"); + //Program.WriteLine("******************************"); + foreach (var propertyInfo in config.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name.Equals("Item")) continue; + if (matchNames.Length > 0) + { + if(matchNames.All(s => s != propertyInfo.Name)) continue; + } + if (propertyInfo.GetCustomAttributes().Any(attribute => + { + var type = attribute.GetType(); + if (type == typeof(XmlIgnoreAttribute)) return true; + if (type == typeof(JsonIgnoreAttribute)) return true; + return false; + })) continue; + count++; + Console.WriteLine($"{propertyInfo.Name}: {propertyInfo.GetValue(config)}"); + } + } + } + + //Program.WriteLine("******************************"); + //Program.WriteLine("\tBase"); + //Program.WriteLine("******************************"); + + var fileFormat = slicerFile as FileFormat; + + foreach (var propertyInfo in fileFormat.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name.Equals("Item")) continue; + if (matchNames is not null && matchNames.Length > 0) + { + if (matchNames.All(s => s != propertyInfo.Name)) continue; + } + if (propertyInfo.GetCustomAttributes().Any(attribute => + { + var type = attribute.GetType(); + if (type == typeof(XmlIgnoreAttribute)) return true; + if (type == typeof(JsonIgnoreAttribute)) return true; + return false; + })) continue; + count++; + Console.WriteLine($"{propertyInfo.Name}: {propertyInfo.GetValue(fileFormat)}"); + } + + + Console.WriteLine("----------------------"); + Console.WriteLine($"Total properties: {count}"); + + }, command.Arguments[0], command.Options[0], command.Options[1], command.Options[2]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/Symbols/RunCommand.cs b/UVtools.Cmd/Symbols/RunCommand.cs new file mode 100644 index 00000000..bb86dfc0 --- /dev/null +++ b/UVtools.Cmd/Symbols/RunCommand.cs @@ -0,0 +1,95 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.CommandLine; +using System.IO; +using System.Linq; +using UVtools.Core.FileFormats; +using UVtools.Core.Operations; + +namespace UVtools.Cmd.Symbols; + +internal static class RunCommand +{ + internal static Command CreateCommand() + { + var command = new Command("run", "Run operations and/or scripts") + { + GlobalArguments.InputFileArgument, + new Argument("files", "Operation and script files to run (.uvtop, .cs, .csx)").ExistingOnly(), + + GlobalOptions.OutputFile, + GlobalOptions.OpenInPartialMode + }; + + command.SetHandler((FileInfo inputFile, FileInfo[] files, FileInfo? outputFile, bool partialMode) => + { + if (files.Length == 0) + { + Program.WriteLineError("No files to run"); + return; + } + + var slicerFile = Program.OpenInputFile(inputFile, partialMode ? FileFormat.FileDecodeType.Partial : FileFormat.FileDecodeType.Full); + uint runs = 0; + uint sucessfullRuns = 0; + + + foreach (var file in files) + { + var operation = Operation.Deserialize(file.FullName); + + if (operation is not null) + { + operation.SlicerFile = slicerFile; + var result = operation.ValidateInternally(); + if (string.IsNullOrWhiteSpace(result)) + { + Program.ProgressBarWork($"Operation {++runs}: {operation.ProgressTitle}", + () => + { + if(operation.Execute(Program.Progress)) sucessfullRuns++; + }); + } + else + { + Program.WriteLineWarning($"Operation {file.Name} can not execute: {result}"); + } + + continue; + } + + if (file.Name.EndsWith(".cs") || file.Name.EndsWith(".csx")) + { + var operationScripting = new OperationScripting(slicerFile); + operationScripting.ReloadScriptFromFile(file.FullName); + var result = operationScripting.ValidateInternally(); + if (string.IsNullOrWhiteSpace(result)) + { + Program.ProgressBarWork($"Script {++runs}: {operationScripting.ScriptGlobals?.Script.Name ?? operationScripting.ProgressTitle}", + () => + { + if (operationScripting.Execute(Program.Progress)) sucessfullRuns++; + }); + } + else + { + Program.WriteLineWarning($"Script {file.Name} can not execute: {result}"); + } + continue; + } + + Program.WriteLineWarning($"Invalid file: {file.Name}"); + } + + if(sucessfullRuns > 0) Program.SaveFile(slicerFile, outputFile); + }, command.Arguments[0], command.Arguments[1], command.Options[0], command.Options[1]); + + return command; + } +} \ No newline at end of file diff --git a/UVtools.Cmd/UVtools.Cmd.csproj b/UVtools.Cmd/UVtools.Cmd.csproj index a6cc0cba..9240a901 100644 --- a/UVtools.Cmd/UVtools.Cmd.csproj +++ b/UVtools.Cmd/UVtools.Cmd.csproj @@ -2,10 +2,10 @@ Exe - net5.0 + net6.0 UVtoolsCmd UVtools.ico - 0.1 + 1.0.0 Tiago Conceição PTRTECH LICENSE @@ -17,10 +17,11 @@ Git true AnyCPU;x64 + enable - + diff --git a/UVtools.Cmd/UVtools.ico b/UVtools.Cmd/UVtools.ico index 2a642ecd2f19dc0fb8bcac979331a8d0ccb21af7..aa3e3f1f0f461aaccc5017a863c55974912acfc5 100644 GIT binary patch literal 128964 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 B(X0Rf literal 124548 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

XfA} z4g1RUNbj+3j$bbioigoxom(@E_4L}-9k{h6rhRT@@L<`|L}_pU&cGc$IQ&?t62`?b z7c4m?m8%bb$bsrF)9mdJ|ai$qFW)KZ&nd}nQo?`C-tT6)KCOt0E z`m@6f>#k8BidP?;zD7TD+r9R5Qg%g)-UIH-MK5M|o|2LxcC3t(gQzZ1J;oYx*d3GZ zA+(0*G`EHxG+-Se*4mr(y_bpq6Su-VN}PcEIflLG=5J2UWfWG&(?JjMHRTe7QL9=ImVeq@fMzAP7`Q=Z58hA zWA65tY$e$mk_nHixMNR$sn)FkcZmh|{{8!Gap&m}t4kyYarlZ}lXC7oaQ~GZzlHY> z9y}-!{-A@?CE9SwYeyDsTo&|9rRSwcz+GbeF~6PXu_Q-MmxzX({3W3bwq20Q{s4E0 z3C8?vN$?gvlc7t3_CbiR@IUIy%MVg@3V{1-N#!M+hbF`^r~g^mB_Zzk-wd-m+%V9lB} zndW|G=#m)UyE)APN4!GH1l;>d`i^k^E>B16)~#7^a4TOHFn0|06?MFC-@e>j zWvod6otU#Gl-+NC>9^mCI*YyfWJtXKCb9pFIWoezz~F7CPMtU$r%s*9gzGLbk31+S zC_~@2ZQGV<&KcwY-}@k~6O?hCH19u4)qmN%d2p8{`h52J19_HrdgqI}uA)~8TbDTPqW&BVe1H?!r?gT+^VUuA0CBakN zGyVJbXAuz*Le_};pJV)1KtngmKq{l~?%g|XZHc6GpuS^m4Qv5oF*zjQ0-QuuM(jDB zBamqPf}YEiFdr8F587e02dEeDk1*#Ib`)#KP~WjGMnc<<214TT3!Wf)N2c*3Au+I1 zsOPbl1^5o!r5ZmH&;U6=ZW7lAdN!{T%9L1Vc$`I#lm}=)okra+sD!dm zrYJfxog*KDf8_YJkl=JW&`z%;kSg9z&u5pA7xF~j!XoqKKguA0&j%k6eymismYzq# zJ9rm)Ag_GEHM>kvCIKJAMqg+-!8wB01n^USr33!s8t&nlz62HocsIKua=gcA9?=ZS zg3$yea~vZu3yhB5m{vqbwI^_Ka&#_-nf11xATBf*j;5MK3TE_PbvPmc-lRnTn^c;HfM;g91KFhMr`#y!OHn<%S15C9$on+UQ@;&8yYJN7`-vtLB}qVDAO#Wm`%kB*19u}I7v zy7?m8=6e$rh_Yd;a|4vq0p>_LjX*s%vC_NBx=oZvy)4C*_ZTYe!%8?{PL1WDcNzL}Ec^Xt`L-m>@)7G6_N_B;pfLr| zXhQl19l$&iJ^RJn{0!&=?g7@E%2u>|J0`Y#^vkfM-<8c{WuE$10O#24nBN|lxT?^Z-29G ze_(qN6Z7%$;cNlM8nGV-Vo-?5BL2kF&*KFgzJARShfPE~*9Oxu%l4P9t zrf@MZj1M5j3i*rf3kN)Mz#*gk6|})({STB6JIL?bi`YNLlQ5p=;NXxU3!Zl25zl0i zGWj3rIEeMX`1gSW#!-axlZ0i7{>Rt_VuoVUAO0671p1+8#rmJZJYKwb!HqG9<2m$X z0PWd>J)IEa6O*#^KLt2s>woikV!jh}^W@2s?AWnm-1_(E=xF}7xMPm~w*UvZ^*;nS zKp&tdV*OHa1d6$acacXXf158Orz5=wosdiaQ;1tS9=xvzo6Y+zXg>jbJI32{JS)s1 z%m2(IcuDe$KG<@od)q@9bVgDu0VV;#n>aIs#|p$t4f^UK{VH{hot+lbQFu%lv2_yNKz) zJ3PG^IA5o}O_lU-h@szuZ@w5ipWx>5uA^@U3KHJod6jV);|}Zg-{$f_p9%9%F`p3g zm}^cXzbP{*JfAb(_x1JV+MN|UuChwQUUA>&!2D-?p8<4|Xz)9abbgeheSSs0f5z(u_x{tTPr3IozXSFf zdx>D5EzkmE`Cf`2u*qXV0G8xcIs~*TnfE$Opb9e5`BN zt_fb{bX{B>K%N=22wzD5k9&9~)4P0ar0cvs^LoY0m*?{z^*4fIq#+bH^_IdK;aR+c GcmD@otJ=u` diff --git a/UVtools.Core/About.cs b/UVtools.Core/About.cs index afde01ed..db116bf9 100644 --- a/UVtools.Core/About.cs +++ b/UVtools.Core/About.cs @@ -25,7 +25,9 @@ public static class About public const string DemoFile = "UVtools_demo_file.sl1"; - public static Version Version => Assembly.GetExecutingAssembly().GetName().Version!; + public static Version Version => CoreAssembly.GetName().Version!; public static string VersionStr => Version.ToString(3); public static string Arch => Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; + + public static Assembly CoreAssembly => Assembly.GetExecutingAssembly(); } \ No newline at end of file diff --git a/UVtools.Core/Converters/SpeedConverter.cs b/UVtools.Core/Converters/SpeedConverter.cs new file mode 100644 index 00000000..1d93bb59 --- /dev/null +++ b/UVtools.Core/Converters/SpeedConverter.cs @@ -0,0 +1,56 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + + +using System; + +namespace UVtools.Core.Converters; + +public static class SpeedConverter +{ + ///

+ /// Converts a speed from one unit to another + /// + /// + /// + /// + /// + /// + /// + public static float Convert(float value, SpeedUnit from, SpeedUnit to, byte rounding = 2) + { + if (from == to) return value; + + return from switch + { + SpeedUnit.MillimetersPerSecond => to switch + { + SpeedUnit.MillimetersPerSecond => value, + SpeedUnit.MillimetersPerMinute => (float) Math.Round(value * 60, rounding), + SpeedUnit.CentimetersPerMinute => (float) Math.Round(value * 6, rounding), + _ => throw new ArgumentOutOfRangeException(nameof(to), to, null) + }, + SpeedUnit.MillimetersPerMinute => to switch + { + SpeedUnit.MillimetersPerSecond => (float) Math.Round(value / 60, rounding), + SpeedUnit.MillimetersPerMinute => value, + SpeedUnit.CentimetersPerMinute => (float) Math.Round(value / 10, rounding), + _ => throw new ArgumentOutOfRangeException(nameof(to), to, null) + }, + SpeedUnit.CentimetersPerMinute => to switch + { + SpeedUnit.MillimetersPerSecond => (float) Math.Round(value * (1.0/6.0), rounding), + SpeedUnit.MillimetersPerMinute => (float)Math.Round(value * 10, rounding), + SpeedUnit.CentimetersPerMinute => value, + _ => throw new ArgumentOutOfRangeException(nameof(to), to, null) + }, + _ => throw new ArgumentOutOfRangeException(nameof(from), from, null) + }; + } + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/TimeExtensions.cs b/UVtools.Core/Converters/TimeConverter.cs similarity index 94% rename from UVtools.Core/Extensions/TimeExtensions.cs rename to UVtools.Core/Converters/TimeConverter.cs index 2988613d..06d47ab9 100644 --- a/UVtools.Core/Extensions/TimeExtensions.cs +++ b/UVtools.Core/Converters/TimeConverter.cs @@ -9,9 +9,9 @@ using System; -namespace UVtools.Core.Extensions; +namespace UVtools.Core.Converters; -public static class TimeExtensions +public static class TimeConverter { /// /// Converts seconds to milliseconds diff --git a/UVtools.Core/Enumerations.cs b/UVtools.Core/Enumerations.cs index d82c9421..f389f02a 100644 --- a/UVtools.Core/Enumerations.cs +++ b/UVtools.Core/Enumerations.cs @@ -12,74 +12,102 @@ namespace UVtools.Core; -public class Enumerations +/// +/// Gets index start number, if starts on 0 or 1 +/// +public enum IndexStartNumber : byte { - /// - /// Gets index start number - /// - public enum IndexStartNumber : byte - { - Zero, - One - } + Zero, + One +} - public enum LayerRangeSelection : byte - { - None, - All, - Current, - Bottom, - Normal, - First, - Last - } +public enum LayerRangeSelection : byte +{ + None, + All, + Current, + Bottom, + Normal, + First, + Last +} - public enum FlipDirection : byte - { - None, - Horizontally, - Vertically, - Both, - } +public enum FlipDirection : byte +{ + None, + Horizontally, + Vertically, + Both, +} - public enum RotateDirection : sbyte - { - [Description("None")] - None = -1, - /// Rotate 90 degrees clockwise (0) - [Description("Rotate 90º CW")] - Rotate90Clockwise = 0, - /// Rotate 180 degrees clockwise (1) - [Description("Rotate 180º")] - Rotate180 = 1, - /// Rotate 270 degrees clockwise (2) - [Description("Rotate 90º CCW")] - Rotate90CounterClockwise = 2, - } +public enum RotateDirection : sbyte +{ + [Description("None")] + None = -1, + /// Rotate 90 degrees clockwise (0) + [Description("Rotate 90º CW")] + Rotate90Clockwise = 0, + /// Rotate 180 degrees clockwise (1) + [Description("Rotate 180º")] + Rotate180 = 1, + /// Rotate 270 degrees clockwise (2) + [Description("Rotate 90º CCW")] + Rotate90CounterClockwise = 2, +} - public enum Anchor : byte - { - TopLeft, TopCenter, TopRight, - MiddleLeft, MiddleCenter, MiddleRight, - BottomLeft, BottomCenter, BottomRight, - None - } +public enum Anchor : byte +{ + TopLeft, TopCenter, TopRight, + MiddleLeft, MiddleCenter, MiddleRight, + BottomLeft, BottomCenter, BottomRight, + None +} - public enum LightOffDelaySetMode : byte - { - [Description("Set the light-off with an extra delay")] - UpdateWithExtraDelay, +public enum LightOffDelaySetMode : byte +{ + [Description("Set the light-off with an extra delay")] + UpdateWithExtraDelay, - [Description("Set the light-off without an extra delay")] - UpdateWithoutExtraDelay, + [Description("Set the light-off without an extra delay")] + UpdateWithoutExtraDelay, - [Description("Set the light-off to zero")] - SetToZero, + [Description("Set the light-off to zero")] + SetToZero, - [Description("Disabled")] - NoAction - } + [Description("Disabled")] + NoAction +} +public enum SpeedUnit : byte +{ + /// + /// mm/s + /// + MillimetersPerSecond, + /// + /// mm/m + /// + MillimetersPerMinute, + /// + /// cm/m + /// + CentimetersPerMinute, +} + +public enum TimeUnits : byte +{ + /// + /// ms + /// + Milliseconds, + /// + /// s + /// + Seconds +} + +public static class Enumerations +{ public static FlipType ToOpenCVFlipType(FlipDirection flip) { return flip switch diff --git a/UVtools.Core/Extensions/JsonExtensions.cs b/UVtools.Core/Extensions/JsonExtensions.cs index e1d44cbc..90ce0d2f 100644 --- a/UVtools.Core/Extensions/JsonExtensions.cs +++ b/UVtools.Core/Extensions/JsonExtensions.cs @@ -5,7 +5,10 @@ * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ + +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; namespace UVtools.Core.Extensions; @@ -13,6 +16,10 @@ public static class JsonExtensions { public static readonly JsonSerializerOptions SettingsIndent = new() { - WriteIndented = true + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters ={ + new JsonStringEnumConverter() + }, }; } \ No newline at end of file diff --git a/UVtools.Core/Extensions/TypeExtensions.cs b/UVtools.Core/Extensions/TypeExtensions.cs index 99118f37..1c528395 100644 --- a/UVtools.Core/Extensions/TypeExtensions.cs +++ b/UVtools.Core/Extensions/TypeExtensions.cs @@ -7,6 +7,9 @@ */ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; namespace UVtools.Core.Extensions; @@ -36,4 +39,10 @@ public static class TypeExtensions } public static byte ToByte(this bool value) => (byte)(value ? 1 : 0); + + public static IEnumerable GetTypesInNamespace(Assembly assembly, string nameSpace) + { + return assembly.GetTypes() + .Where(t => string.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)); + } } \ No newline at end of file diff --git a/UVtools.Core/FileFormats/CTBEncryptedFile.cs b/UVtools.Core/FileFormats/CTBEncryptedFile.cs index b86a4068..14b0bd66 100644 --- a/UVtools.Core/FileFormats/CTBEncryptedFile.cs +++ b/UVtools.Core/FileFormats/CTBEncryptedFile.cs @@ -713,12 +713,12 @@ public override float MachineZ set => base.MachineZ = Settings.MachineZ = (float)Math.Round(value, 2); } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => Settings.ProjectorType == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; + get => Settings.ProjectorType == 0 ? FlipDirection.None : FlipDirection.Horizontally; set { - Settings.ProjectorType = value == Enumerations.FlipDirection.None ? 0u : 1; + Settings.ProjectorType = value == FlipDirection.None ? 0u : 1; RaisePropertyChanged(); } } @@ -1070,13 +1070,13 @@ public override void Clear() } } - public override bool CanProcess(string fileFullPath) + public override bool CanProcess(string? fileFullPath) { if (!base.CanProcess(fileFullPath)) return false; try { - using var fs = new BinaryReader(new FileStream(fileFullPath, FileMode.Open, FileAccess.Read)); + using var fs = new BinaryReader(new FileStream(fileFullPath!, FileMode.Open, FileAccess.Read)); var magic = fs.ReadUInt32(); return magic is MAGIC_CBT_ENCRYPTED; } @@ -1226,11 +1226,11 @@ protected override void DecodeInternally(OperationProgress progress) if (isBugged) { - this[layerIndex] = new Layer((uint)layerIndex, this); + _layers[layerIndex] = new Layer((uint)layerIndex, this); } else { - this[layerIndex] = new Layer((uint) layerIndex, layerDef.DecodeImage((uint) layerIndex), this); + _layers[layerIndex] = new Layer((uint) layerIndex, layerDef.DecodeImage((uint) layerIndex), this); } progress.LockAndIncrement(); @@ -1286,7 +1286,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); //uint currentOffset = 0; /* Create the file header and fill out what we can. SignatureOffset will have to be populated later * this will be the last thing written to file */ @@ -1413,7 +1413,7 @@ protected override void PartialSaveInternally(OperationProgress progress) { SanitizeProperties(); - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); var settingsBytes = Helpers.Serialize(Settings).ToArray(); diff --git a/UVtools.Core/FileFormats/CWSFile.cs b/UVtools.Core/FileFormats/CWSFile.cs index bd7622c5..8fb5f6af 100644 --- a/UVtools.Core/FileFormats/CWSFile.cs +++ b/UVtools.Core/FileFormats/CWSFile.cs @@ -20,6 +20,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Serialization; +using UVtools.Core.Converters; using UVtools.Core.Extensions; using UVtools.Core.GCode; using UVtools.Core.Layers; @@ -414,19 +415,19 @@ public override float MachineZ set => base.MachineZ = OutputSettings.PlatformZSize = (float)Math.Round(value, 2); } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { get { - if (OutputSettings.FlipX && OutputSettings.FlipY) return Enumerations.FlipDirection.Both; - if (OutputSettings.FlipX) return Enumerations.FlipDirection.Horizontally; - if (OutputSettings.FlipY) return Enumerations.FlipDirection.Vertically; - return Enumerations.FlipDirection.None; + if (OutputSettings.FlipX && OutputSettings.FlipY) return FlipDirection.Both; + if (OutputSettings.FlipX) return FlipDirection.Horizontally; + if (OutputSettings.FlipY) return FlipDirection.Vertically; + return FlipDirection.None; } set { - OutputSettings.FlipX = value is Enumerations.FlipDirection.Horizontally or Enumerations.FlipDirection.Both; - OutputSettings.FlipY = value is Enumerations.FlipDirection.Vertically or Enumerations.FlipDirection.Both; + OutputSettings.FlipX = value is FlipDirection.Horizontally or FlipDirection.Both; + OutputSettings.FlipY = value is FlipDirection.Vertically or FlipDirection.Both; RaisePropertyChanged(); } } @@ -483,32 +484,32 @@ public override float BottomWaitTimeBeforeCure public override float WaitTimeBeforeCure { - get => TimeExtensions.MillisecondsToSeconds(SliceSettings.WaitBeforeExpoMs); + get => TimeConverter.MillisecondsToSeconds(SliceSettings.WaitBeforeExpoMs); set { - SliceSettings.WaitBeforeExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); + SliceSettings.WaitBeforeExpoMs = TimeConverter.SecondsToMillisecondsUint(value); base.WaitTimeBeforeCure = base.LightOffDelay = value; } } public override float BottomExposureTime { - get => TimeExtensions.MillisecondsToSeconds(OutputSettings.BottomLayersTime); + get => TimeConverter.MillisecondsToSeconds(OutputSettings.BottomLayersTime); set { OutputSettings.BottomLayersTime = - SliceSettings.HeadLayersExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); + SliceSettings.HeadLayersExpoMs = TimeConverter.SecondsToMillisecondsUint(value); base.BottomExposureTime = value; } } public override float ExposureTime { - get => TimeExtensions.MillisecondsToSeconds(OutputSettings.LayerTime); + get => TimeConverter.MillisecondsToSeconds(OutputSettings.LayerTime); set { OutputSettings.LayerTime = - SliceSettings.LayersExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); + SliceSettings.LayersExpoMs = TimeConverter.SecondsToMillisecondsUint(value); base.ExposureTime = value; } } @@ -564,7 +565,7 @@ public CWSFile() { SyncMovementsWithDelay = true, UseComments = true, - GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Partial, + GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Relative, GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, @@ -613,7 +614,7 @@ protected override void EncodeInternally(OperationProgress progress) throw new InvalidOperationException($"Filename for this format should not end with a digit: {filename}"); } - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); if (Printer == PrinterType.Wanhao) { var manifest = new CWSManifest @@ -660,18 +661,18 @@ protected override void EncodeInternally(OperationProgress progress) if (Printer == PrinterType.BeneMono) { - EncodeLayersInZip(outputFile, filename, LayerDigits, Enumerations.IndexStartNumber.Zero, progress, matGenFunc: + EncodeLayersInZip(outputFile, filename, LayerDigits, IndexStartNumber.Zero, progress, matGenFunc: (_, mat) => { - var matEncode = new Mat(mat.Height, mat.GetRealStep() / 3, DepthType.Cv8U, 3); - var span = mat.GetDataByteSpan(); - var spanEncode = matEncode.GetDataByteSpan(); - for (int i = 0; i < span.Length; i++) + var bgrMat = new Mat(mat.Height, mat.GetRealStep() / 3, DepthType.Cv8U, 3); + var bgrMatSpan = bgrMat.GetDataByteSpan(); + var greySpan = mat.GetDataByteSpan(); + for (int i = 0; i < greySpan.Length; i++) { - spanEncode[i] = span[i]; + bgrMatSpan[i] = greySpan[i]; } - return matEncode; + return bgrMat; }); /*Parallel.For(0, LayerCount, CoreSettings.GetParallelOptions(progress), //new ParallelOptions { MaxDegreeOfParallelism = Printer == PrinterType.BeneMono ? 1 : 1 }, @@ -699,7 +700,7 @@ protected override void EncodeInternally(OperationProgress progress) } else { - EncodeLayersInZip(outputFile, filename, LayerDigits, Enumerations.IndexStartNumber.Zero, progress); + EncodeLayersInZip(outputFile, filename, LayerDigits, IndexStartNumber.Zero, progress); } RebuildGCode(); @@ -863,25 +864,25 @@ protected override void DecodeInternally(OperationProgress progress) if (Printer == PrinterType.BeneMono) { - DecodeLayersFromZipRegex(inputFile, @"(\d+).png", Enumerations.IndexStartNumber.Zero, progress, + DecodeLayersFromZipRegex(inputFile, @"(\d+).png", IndexStartNumber.Zero, progress, (layerIndex, pngBytes) => { - using Mat mat = new(); - CvInvoke.Imdecode(pngBytes, ImreadModes.AnyColor, mat); - var matDecode = new Mat(mat.Height, mat.GetRealStep(), DepthType.Cv8U, 1); - var span = mat.GetDataByteSpan(); - var spanDecode = matDecode.GetDataByteSpan(); - for (int i = 0; i < span.Length; i++) + using Mat bgrMat = new(); + CvInvoke.Imdecode(pngBytes, ImreadModes.AnyColor, bgrMat); + var greyMat = new Mat(bgrMat.Height, bgrMat.GetRealStep(), DepthType.Cv8U, 1); + var bgrSpan = bgrMat.GetDataByteSpan(); + var greySpan = greyMat.GetDataByteSpan(); + for (int i = 0; i < bgrSpan.Length; i++) { - spanDecode[i] = span[i]; + greySpan[i] = bgrSpan[i]; } - return matDecode; + return greyMat; }); } else { - DecodeLayersFromZipRegex(inputFile, @"(\d+).png", Enumerations.IndexStartNumber.Zero, progress); + DecodeLayersFromZipRegex(inputFile, @"(\d+).png", IndexStartNumber.Zero, progress); } GCode.ParseLayersFromGCode(this); @@ -997,7 +998,7 @@ public override void RebuildGCode() protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); var arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; var entry = outputFile.GetPutFile("slice.conf"); var stream = entry.Open(); diff --git a/UVtools.Core/FileFormats/CXDLPFile.cs b/UVtools.Core/FileFormats/CXDLPFile.cs index e92316a8..a90faf26 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -17,11 +17,14 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using UVtools.Core.Converters; using UVtools.Core.Extensions; using UVtools.Core.Layers; using UVtools.Core.Objects; using UVtools.Core.Operations; +using UVtools.Core.Printer; namespace UVtools.Core.FileFormats; @@ -33,6 +36,7 @@ public class CXDLPFile : FileFormat #endregion #region Sub Classes + #region Header public sealed class Header { @@ -247,18 +251,24 @@ public sealed class SlicerInfoV3 [FieldOrder(1)] public NullTerminatedUintStringBigEndian MaterialName { get; set; } = new(); - [FieldOrder(2)] public uint Unknown1 { get; set; } - [FieldOrder(3)] public uint Unknown2 { get; set; } - [FieldOrder(4)] public uint Unknown3 { get; set; } - [FieldOrder(5)] public uint Unknown4 { get; set; } - [FieldOrder(6)] public byte Unknown5 { get; set; } = 1; - [FieldOrder(7)] public byte LightPWM { get; set; } = byte.MaxValue; - [FieldOrder(8)] public ushort Unknown6 { get; set; } = 2; - [FieldOrder(9)] public PageBreak PageBreak { get; set; } = new(); + [FieldOrder(2)] public byte DistortionCompensationEnabled { get; set; } + [FieldOrder(3)] public uint DistortionCompensationThickness { get; set; } = 600; + [FieldOrder(4)] public uint DistortionCompensationFocalLength { get; set; } = 300000; + [FieldOrder(5)] public byte XYAxisProfileCompensationEnabled { get; set; } = 1; + [FieldOrder(6)] public ushort XYAxisProfileCompensationValue { get; set; } + [FieldOrder(7)] public byte ZPenetrationCompensationEnabled { get; set; } + [FieldOrder(8)] public ushort ZPenetrationCompensationLevel { get; set; } = 1000; + [FieldOrder(9)] public byte AntiAliasEnabled { get; set; } = 1; + [FieldOrder(10)] public byte AntiAliasGreyMinValue { get; set; } = 1; + [FieldOrder(11)] public byte AntiAliasGreyMaxValue { get; set; } = byte.MaxValue; + [FieldOrder(12)] public byte ImageBlurEnabled { get; set; } = 0; + [FieldOrder(13)] public byte ImageBlurLevel { get; set; } = 2; + [FieldOrder(14)] public PageBreak PageBreak { get; set; } = new(); + public override string ToString() { - return $"{nameof(SoftwareName)}: {SoftwareName}, {nameof(MaterialName)}: {MaterialName}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(Unknown3)}: {Unknown3}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(LightPWM)}: {LightPWM}, {nameof(Unknown6)}: {Unknown6}, {nameof(PageBreak)}: {PageBreak}"; + return $"{nameof(SoftwareName)}: {SoftwareName}, {nameof(MaterialName)}: {MaterialName}, {nameof(DistortionCompensationEnabled)}: {DistortionCompensationEnabled}, {nameof(DistortionCompensationThickness)}: {DistortionCompensationThickness}, {nameof(DistortionCompensationFocalLength)}: {DistortionCompensationFocalLength}, {nameof(XYAxisProfileCompensationEnabled)}: {XYAxisProfileCompensationEnabled}, {nameof(XYAxisProfileCompensationValue)}: {XYAxisProfileCompensationValue}, {nameof(ZPenetrationCompensationEnabled)}: {ZPenetrationCompensationEnabled}, {nameof(ZPenetrationCompensationLevel)}: {ZPenetrationCompensationLevel}, {nameof(AntiAliasEnabled)}: {AntiAliasEnabled}, {nameof(AntiAliasGreyMinValue)}: {AntiAliasGreyMinValue}, {nameof(AntiAliasGreyMaxValue)}: {AntiAliasGreyMaxValue}, {nameof(ImageBlurEnabled)}: {ImageBlurEnabled}, {nameof(ImageBlurLevel)}: {ImageBlurLevel}, {nameof(PageBreak)}: {PageBreak}"; } } @@ -550,8 +560,13 @@ public override float BottomExposureTime public override float ExposureTime { - get => SlicerInfoSettings.ExposureTime; - set => base.ExposureTime = SlicerInfoSettings.ExposureTime = (ushort) value; + get => (float)Math.Round(SlicerInfoSettings.ExposureTime / 10.0f, 1); + set + { + value = (float)Math.Round(value, 1); + SlicerInfoSettings.ExposureTime = (ushort) (value * 10); + base.ExposureTime = value; + } } public override float BottomLiftHeight @@ -560,30 +575,30 @@ public override float BottomLiftHeight set => base.BottomLiftHeight = SlicerInfoSettings.BottomLiftHeight = (ushort) value; } - public override float LiftHeight + public override float BottomLiftSpeed { - get => SlicerInfoSettings.LiftHeight; - set => base.LiftHeight = SlicerInfoSettings.LiftHeight = (ushort)value; + get => SpeedConverter.Convert(SlicerInfoSettings.BottomLiftSpeed, SpeedUnit.MillimetersPerSecond, DefaultSpeedUnit); + set => base.BottomLiftSpeed = SlicerInfoSettings.BottomLiftSpeed = SlicerInfoSettings.BottomLiftSpeed = (ushort)SpeedConverter.Convert(value, DefaultSpeedUnit, SpeedUnit.MillimetersPerSecond); } - public override float BottomLiftSpeed + public override float LiftHeight { - get => SlicerInfoSettings.BottomLiftSpeed; - set => base.BottomLiftSpeed = SlicerInfoSettings.BottomLiftSpeed = (ushort)value; + get => SlicerInfoSettings.LiftHeight; + set => base.LiftHeight = SlicerInfoSettings.LiftHeight = (ushort)value; } public override float LiftSpeed { - get => SlicerInfoSettings.LiftSpeed; - set => base.LiftSpeed = SlicerInfoSettings.LiftSpeed = (ushort)value; + get => SpeedConverter.Convert(SlicerInfoSettings.LiftSpeed, SpeedUnit.MillimetersPerSecond, DefaultSpeedUnit); + set => base.LiftSpeed = SlicerInfoSettings.LiftSpeed = (ushort)SpeedConverter.Convert(value, DefaultSpeedUnit, SpeedUnit.MillimetersPerSecond); } public override float BottomRetractSpeed => RetractSpeed; public override float RetractSpeed { - get => SlicerInfoSettings.RetractSpeed; - set => base.RetractSpeed = SlicerInfoSettings.RetractSpeed = (ushort)value; + get => SpeedConverter.Convert(SlicerInfoSettings.RetractSpeed, SpeedUnit.MillimetersPerSecond, DefaultSpeedUnit); + set => base.RetractSpeed = SlicerInfoSettings.RetractSpeed = (ushort)SpeedConverter.Convert(value, DefaultSpeedUnit, SpeedUnit.MillimetersPerSecond); } public override byte BottomLightPWM @@ -601,7 +616,19 @@ public override byte LightPWM public override string MachineName { get => HeaderSettings.PrinterModel; - set => base.MachineName = HeaderSettings.PrinterModel = value; + set + { + if (!string.IsNullOrWhiteSpace(value) && !value.StartsWith("CL-") && !value.StartsWith("CT-")) + { + // Parse from machine name, if coming from PrusaSlicer this will help + var match = Regex.Match(value, @"(CL|CT)-\d+"); + if (match.Success && match.Groups.Count > 1) + { + value = match.Value; + } + } + base.MachineName = HeaderSettings.PrinterModel = value; + } } public override string? MaterialName @@ -626,26 +653,31 @@ private void SanitizeProperties() protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.ReadWrite); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.ReadWrite); - if (!MachineName.StartsWith("CL-")) + if (string.IsNullOrWhiteSpace(MachineName)) { - if (ResolutionX == 1620 && ResolutionY == 2560) - { - MachineName = "CL-60"; // One - } - else if (ResolutionX == 3840 && ResolutionY == 2400) // Sky or lite - { - MachineName = "CL-89"; // Sky - } - else if (ResolutionX == 3840 && ResolutionY == 2160) - { - MachineName = "CL-133"; // Max - } - else + throw new InvalidDataException("Unable to detect the printer model from resolution, check if resolution is well defined on slicer for your printer model."); + } + + + if (!MachineName.StartsWith("CL-") && !MachineName.StartsWith("CT")) + { + bool found = false; + foreach (var machine in Printer.Machine.Machines + .Where(machine => machine.Brand == PrinterBrand.Creality + && (machine.Model.StartsWith("CL-") || machine.Model.StartsWith("CT")) + )) { - throw new InvalidDataException("Unable to detect the printer model from resolution, check if resolution is well defined on slicer for your printer model."); + if (ResolutionX == machine.ResolutionX && ResolutionY == machine.ResolutionY) + { + found = true; + MachineName = machine.Model; + break; + } } + + if(!found) throw new InvalidDataException("Unable to detect the printer model from resolution, check if resolution is well defined on slicer for your printer model."); } SanitizeProperties(); @@ -952,7 +984,7 @@ protected override void DecodeInternally(OperationProgress progress) linesBytes[layerIndex] = null!; - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); } progress.LockAndIncrement(); @@ -978,7 +1010,7 @@ protected override void PartialSaveInternally(OperationProgress progress) SanitizeProperties(); - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.ReadWrite); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.ReadWrite); outputFile.Seek(offset, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); diff --git a/UVtools.Core/FileFormats/CXDLPv1File.cs b/UVtools.Core/FileFormats/CXDLPv1File.cs index 682c050a..6e1bb1c3 100644 --- a/UVtools.Core/FileFormats/CXDLPv1File.cs +++ b/UVtools.Core/FileFormats/CXDLPv1File.cs @@ -501,7 +501,7 @@ public override byte LightPWM protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); if (ResolutionX == 2560 && ResolutionY == 1620) { @@ -697,7 +697,7 @@ protected override void DecodeInternally(OperationProgress progress) linesBytes[layerIndex] = null!; - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); } progress.LockAndIncrement(); @@ -721,7 +721,7 @@ protected override void PartialSaveInternally(OperationProgress progress) offset += size.Area() * 2 + 2; // + page break } - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(offset, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); } diff --git a/UVtools.Core/FileFormats/ChituboxFile.cs b/UVtools.Core/FileFormats/ChituboxFile.cs index 49cf0477..aa144306 100644 --- a/UVtools.Core/FileFormats/ChituboxFile.cs +++ b/UVtools.Core/FileFormats/ChituboxFile.cs @@ -1290,12 +1290,12 @@ public override float MachineZ set => base.MachineZ = HeaderSettings.BedSizeZ = (float)Math.Round(value, 2); } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => HeaderSettings.ProjectorType == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; + get => HeaderSettings.ProjectorType == 0 ? FlipDirection.None : FlipDirection.Horizontally; set { - HeaderSettings.ProjectorType = value == Enumerations.FlipDirection.None ? 0u : 1; + HeaderSettings.ProjectorType = value == FlipDirection.None ? 0u : 1; RaisePropertyChanged(); } } @@ -1729,13 +1729,13 @@ public override void Clear() LayerDefinitions = null; } - public override bool CanProcess(string fileFullPath) + public override bool CanProcess(string? fileFullPath) { if (!base.CanProcess(fileFullPath)) return false; try { - using var fs = new BinaryReader(new FileStream(fileFullPath, FileMode.Open, FileAccess.Read)); + using var fs = new BinaryReader(new FileStream(fileFullPath!, FileMode.Open, FileAccess.Read)); var magic = fs.ReadUInt32(); return magic is MAGIC_CBDDLP or MAGIC_CTB or MAGIC_CTBv4; } @@ -1838,7 +1838,7 @@ protected override void EncodeInternally(OperationProgress progress) //uint currentOffset = (uint)Helpers.Serializer.SizeOf(HeaderSettings); LayerDefinitions = new LayerDef[HeaderSettings.AntiAliasLevel, LayerCount]; - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); outputFile.Seek(Helpers.Serializer.SizeOf(HeaderSettings), SeekOrigin.Begin); Mat?[] thumbnails = {GetThumbnail(true), GetThumbnail(false)}; @@ -2145,7 +2145,7 @@ protected override void DecodeInternally(OperationProgress progress) Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => { using var mat = LayerDefinitions[0, layerIndex].Decode((uint)layerIndex); - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); @@ -2169,7 +2169,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { SanitizeProperties(); - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(0, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); diff --git a/UVtools.Core/FileFormats/ChituboxZipFile.cs b/UVtools.Core/FileFormats/ChituboxZipFile.cs index 43a6aff2..ec53230f 100644 --- a/UVtools.Core/FileFormats/ChituboxZipFile.cs +++ b/UVtools.Core/FileFormats/ChituboxZipFile.cs @@ -186,13 +186,13 @@ public override float MachineZ set => base.MachineZ = HeaderSettings.MachineZ = (float)Math.Round(value, 2); } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => HeaderSettings.Mirror == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; + get => HeaderSettings.Mirror == 0 ? FlipDirection.None : FlipDirection.Horizontally; set { - HeaderSettings.ProjectType = value == Enumerations.FlipDirection.None ? "Normal" : "LCD_mirror"; - HeaderSettings.Mirror = (byte)(value == Enumerations.FlipDirection.None ? 0 : 1); + HeaderSettings.ProjectType = value == FlipDirection.None ? "Normal" : "LCD_mirror"; + HeaderSettings.Mirror = (byte)(value == FlipDirection.None ? 0 : 1); RaisePropertyChanged(); } } @@ -384,13 +384,13 @@ public ChituboxZipFile() #region Methods - public override bool CanProcess(string fileFullPath) + public override bool CanProcess(string? fileFullPath) { if (!base.CanProcess(fileFullPath)) return false; try { - var zip = ZipFile.Open(fileFullPath, ZipArchiveMode.Read); + var zip = ZipFile.Open(fileFullPath!, ZipArchiveMode.Read); if (zip.Entries.Any(entry => entry.Name.EndsWith(".gcode"))) return true; } catch (Exception e) @@ -404,7 +404,7 @@ public override bool CanProcess(string fileFullPath) protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); if (Thumbnails is not null && Thumbnails.Length > 0 && Thumbnails[0] is not null) { using var stream = outputFile.CreateEntry("preview.png").Open(); @@ -426,7 +426,7 @@ protected override void EncodeInternally(OperationProgress progress) outputFile.PutFileContent(GCodeFilename, GCodeStr, ZipArchiveMode.Create); } - EncodeLayersInZip(outputFile, Enumerations.IndexStartNumber.One, progress); + EncodeLayersInZip(outputFile, IndexStartNumber.One, progress); } protected override void DecodeInternally(OperationProgress progress) @@ -482,7 +482,7 @@ protected override void DecodeInternally(OperationProgress progress) Init(HeaderSettings.LayerCount, DecodeType == FileDecodeType.Partial); - DecodeLayersFromZip(inputFile, Enumerations.IndexStartNumber.One, progress); + DecodeLayersFromZip(inputFile, IndexStartNumber.One, progress); if (IsPHZZip) // PHZ file { @@ -518,7 +518,7 @@ public override void RebuildGCode() protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); var entriesToRemove = outputFile.Entries.Where(zipEntry => zipEntry.Name.EndsWith(".gcode")).ToArray(); foreach (var zipEntry in entriesToRemove) { diff --git a/UVtools.Core/FileFormats/FDGFile.cs b/UVtools.Core/FileFormats/FDGFile.cs index 29686ecb..51b31ff6 100644 --- a/UVtools.Core/FileFormats/FDGFile.cs +++ b/UVtools.Core/FileFormats/FDGFile.cs @@ -723,12 +723,12 @@ public override float MachineZ set => base.MachineZ = HeaderSettings.BedSizeZ = (float)Math.Round(value, 2); } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => HeaderSettings.ProjectorType == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; + get => HeaderSettings.ProjectorType == 0 ? FlipDirection.None : FlipDirection.Horizontally; set { - HeaderSettings.ProjectorType = value == Enumerations.FlipDirection.None ? 0u : 1; + HeaderSettings.ProjectorType = value == FlipDirection.None ? 0u : 1; RaisePropertyChanged(); } } @@ -925,7 +925,7 @@ protected override void EncodeInternally(OperationProgress progress) HeaderSettings.EncryptionKey = (uint)rnd.Next(short.MaxValue, int.MaxValue); }*/ - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); outputFile.Seek(Helpers.Serializer.SizeOf(HeaderSettings), SeekOrigin.Begin); for (byte i = 0; i < ThumbnailsCount; i++) @@ -1111,7 +1111,7 @@ protected override void DecodeInternally(OperationProgress progress) if (DecodeType == FileDecodeType.Full) { using var mat = LayersDefinitions[layerIndex].Decode((uint)layerIndex); - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); } progress.LockAndIncrement(); @@ -1128,7 +1128,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { HeaderSettings.ModifiedTimestampMinutes = (uint)DateTimeExtensions.TimestampMinutes; - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(0, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); diff --git a/UVtools.Core/FileFormats/FileExtension.cs b/UVtools.Core/FileFormats/FileExtension.cs index 173cc4c1..20c63581 100644 --- a/UVtools.Core/FileFormats/FileExtension.cs +++ b/UVtools.Core/FileFormats/FileExtension.cs @@ -129,7 +129,13 @@ FileFormatType is null ? FileFormat.FindByExtensionOrFilePath(Extension, createNewInstance) : FileFormat.FindByType(FileFormatType, createNewInstance); - public static FileExtension? Find(string extension) => - FileFormat.FindExtension(extension); + public static FileExtension? Find(string extension) + { + if (string.IsNullOrWhiteSpace(extension)) return null; + if (extension.StartsWith('.')) extension = extension.Remove(1); + return FileFormat.FindExtension(extension); + } + + #endregion } \ No newline at end of file diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 442b1118..d651bdcd 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -43,6 +43,8 @@ namespace UVtools.Core.FileFormats; public abstract class FileFormat : BindableBase, IDisposable, IEquatable, IList { #region Constants + + public const SpeedUnit DefaultSpeedUnit = SpeedUnit.MillimetersPerMinute; public const string TemporaryFileAppend = ".tmp"; public const ushort ExtraPrintTime = 300; @@ -351,6 +353,7 @@ public override string ToString() new PhotonWorkshopFile(), // PSW new CWSFile(), // CWS new OSLAFile(), // OSLA + new JXSFile(), // jxs new ZCodeFile(), // zcode new ZCodexFile(), // zcodex new MDLPFile(), // MKS v1 @@ -476,9 +479,37 @@ public static List AllFileExtensions : fileFormats[0]; } - public static FileExtension? FindExtension(string extension) + /// + /// Find by an type name + /// + /// Type name to find + /// True to create a new instance of found file format, otherwise will return a pre created one which should be used for read-only purpose + /// object or null if not found + public static FileFormat? FindByType(string type, bool createNewInstance = false) { - return AvailableFormats.SelectMany(format => format.FileExtensions).FirstOrDefault(ext => ext.Equals(extension)); + if (!type.EndsWith("File")) + { + type += "File"; + } + + var fileFormat = AvailableFormats.FirstOrDefault(format => string.Equals(format.GetType().Name, type, StringComparison.OrdinalIgnoreCase)); + if (fileFormat is null) return null; + return createNewInstance + ? Activator.CreateInstance(fileFormat.GetType()) as FileFormat + : fileFormat; + //return (from t in AvailableFormats where type == t.GetType() select createNewInstance ? (FileFormat) Activator.CreateInstance(type) : t).FirstOrDefault(); + } + + /// + /// Find by any means (type name, extension, filepath) + /// + /// Name to find + /// True to create a new instance of found file format, otherwise will return a pre created one which should be used for read-only purpose + /// object or null if not found + public static FileFormat? FindByAnyMeans(string name, bool createNewInstance = false) + { + return FindByType(name, true) + ?? FindByExtensionOrFilePath(name, true); } /// @@ -497,6 +528,11 @@ public static List AllFileExtensions //return (from t in AvailableFormats where type == t.GetType() select createNewInstance ? (FileFormat) Activator.CreateInstance(type) : t).FirstOrDefault(); } + public static FileExtension? FindExtension(string extension) + { + return AvailableFormats.SelectMany(format => format.FileExtensions).FirstOrDefault(ext => ext.Equals(extension)); + } + public static string? GetFileNameStripExtensions(string? filepath) { if (filepath is null) return null; @@ -521,6 +557,11 @@ public static string GetFileNameStripExtensions(string filepath, out string stri public static FileFormat? Open(string fileFullPath, OperationProgress? progress = null) => Open(fileFullPath, FileDecodeType.Full, progress); + public static Task OpenAsync(string fileFullPath, FileDecodeType decodeType, OperationProgress? progress = null) + => Task.Run(() => Open(fileFullPath, decodeType, progress), progress?.Token ?? default); + + public static Task OpenAsync(string fileFullPath, OperationProgress? progress = null) => OpenAsync(fileFullPath, FileDecodeType.Full, progress); + /// /// Copy parameters from one file to another /// @@ -889,7 +930,7 @@ public static int MutateGetIterationChamfer(uint layerIndex, uint startLayerInde #region Members public object Mutex = new(); - private Layer[] _layers = Array.Empty(); + protected Layer[] _layers = Array.Empty(); private bool _haveModifiedLayers; private uint _version; @@ -1045,6 +1086,11 @@ public string FileFilterExtensionsOnly /// public bool SuppressRebuildProperties { get; set; } + /// + /// Gets the temporary output file path to use on save and encode + /// + public string TemporaryOutputFileFullPath => $"{FileFullPath}{TemporaryFileAppend}"; + /// /// Gets the input file path loaded into this /// @@ -1476,7 +1522,7 @@ public string DisplayAspectRatioStr /// /// Gets or sets if images need to be mirrored on lcd to print on the correct orientation /// - public virtual Enumerations.FlipDirection DisplayMirror { get; set; } = Enumerations.FlipDirection.None; + public virtual FlipDirection DisplayMirror { get; set; } = FlipDirection.None; /// /// Gets if the display is in portrait mode @@ -2984,7 +3030,7 @@ public virtual void Clear() /// /// /// - public virtual bool CanProcess(string fileFullPath) + public virtual bool CanProcess(string? fileFullPath) { if (fileFullPath is null) return false; if (!File.Exists(fileFullPath)) return false; @@ -2997,17 +3043,12 @@ public virtual bool CanProcess(string fileFullPath) /// Validate if a file is a valid /// /// Full file path - public void FileValidation(string fileFullPath) + public void FileValidation(string? fileFullPath) { if (string.IsNullOrWhiteSpace(fileFullPath)) throw new ArgumentNullException(nameof(FileFullPath), "FileFullPath can't be null nor empty."); if (!File.Exists(fileFullPath)) throw new FileNotFoundException("The specified file does not exists.", fileFullPath); - if (IsExtensionValid(fileFullPath, true)) - { - return; - } - - throw new FileLoadException($"The specified file is not valid.", fileFullPath); + if (!IsExtensionValid(fileFullPath, true)) throw new FileLoadException("The specified file is not valid.", fileFullPath); } /// @@ -3169,25 +3210,16 @@ public void SetThumbnail(int index, string filePath) /// public void Encode(string? fileFullPath, OperationProgress? progress = null) { - if (fileFullPath is null) - { - throw new ArgumentNullException(nameof(fileFullPath)); - } + fileFullPath ??= FileFullPath ?? throw new ArgumentNullException(nameof(fileFullPath)); if (DecodeType == FileDecodeType.Partial) - { throw new InvalidOperationException("File was partial decoded, a full encode is not possible."); - } progress ??= new OperationProgress(); progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); Sanitize(); - if (File.Exists(fileFullPath)) File.Delete(fileFullPath); - - FileFullPath = fileFullPath; - for (var i = 0; i < Thumbnails.Length; i++) { if (Thumbnails[i] is null || Thumbnails[i]!.IsEmpty) continue; @@ -3195,12 +3227,38 @@ public void Encode(string? fileFullPath, OperationProgress? progress = null) CvInvoke.Resize(Thumbnails[i], Thumbnails[i], new Size(ThumbnailsOriginalSize[i].Width, ThumbnailsOriginalSize[i].Height)); } - EncodeInternally(progress); + // Backup old file name and prepare the temporary file to be written next + var oldFilePath = FileFullPath; + FileFullPath = fileFullPath; + var tempFile = TemporaryOutputFileFullPath; + if (File.Exists(tempFile)) File.Delete(tempFile); - IsModified = false; - RequireFullEncode = false; + try + { + EncodeInternally(progress); + + IsModified = false; + RequireFullEncode = false; + + // Move temporary output file in place + File.Move(tempFile, fileFullPath, true); + } + catch (Exception) + { + // Restore backup file path and delete the temporary + FileFullPath = oldFilePath; + if (File.Exists(tempFile)) File.Delete(tempFile); + throw; + } } + public void Encode(OperationProgress progress) => Encode(null, progress); + + public Task EncodeAsync(string? fileFullPath, OperationProgress? progress = null) => + Task.Run(() => Encode(fileFullPath, progress), progress?.Token ?? default); + + public Task EncodeAsync(OperationProgress progress) => EncodeAsync(null, progress); + /// /// Decode a slicer file /// @@ -3210,26 +3268,28 @@ public void Encode(string? fileFullPath, OperationProgress? progress = null) /// /// Decode a slicer file /// - /// + /// file path to load, use null to reload file /// - public void Decode(string fileFullPath, OperationProgress? progress = null) => Decode(fileFullPath, FileDecodeType.Full, progress); + public void Decode(string? fileFullPath = null, OperationProgress? progress = null) => Decode(fileFullPath, FileDecodeType.Full, progress); /// /// Decode a slicer file /// - /// + /// file path to load, use null to reload file /// /// - public void Decode(string fileFullPath, FileDecodeType fileDecodeType, OperationProgress? progress = null) + public void Decode(string? fileFullPath, FileDecodeType fileDecodeType, OperationProgress? progress = null) { Clear(); - FileValidation(fileFullPath); - FileFullPath = fileFullPath; + if(!string.IsNullOrWhiteSpace(fileFullPath)) FileFullPath = fileFullPath; + FileValidation(FileFullPath); + DecodeType = fileDecodeType; progress ??= new OperationProgress(); progress.Reset(OperationProgress.StatusGatherLayers, LayerCount); DecodeInternally(progress); + IsModified = false; progress.ThrowIfCancellationRequested(); @@ -3249,7 +3309,40 @@ public void Decode(string fileFullPath, FileDecodeType fileDecodeType, Operation GetBoundingRectangle(progress); } - public void EncodeLayersInZip(ZipArchive zipArchive, string prepend, byte padDigits, Enumerations.IndexStartNumber layerIndexStartNumber = default, + public Task DecodeAsync(string? fileFullPath, FileDecodeType fileDecodeType, OperationProgress? progress = null) => + Task.Run(() => Decode(fileFullPath, fileDecodeType, progress), progress?.Token ?? default); + + public Task DecodeAsync(string? fileFullPath = null, OperationProgress? progress = null) + => DecodeAsync(fileFullPath, FileDecodeType.Full, progress); + + + /// + /// Reloads the file + /// + /// + /// + public void Reload(FileDecodeType fileDecodeType, OperationProgress? progress = null) => Decode(null, fileDecodeType, progress); + + /// + /// Reloads the file + /// + /// + public void Reload(OperationProgress? progress = null) => Reload(FileDecodeType.Full, progress); + + /// + /// Reloads the file + /// + /// + /// + public Task ReloadAsync(FileDecodeType fileDecodeType, OperationProgress? progress = null) => DecodeAsync(null, fileDecodeType, progress); + + /// + /// Reloads the file + /// + /// + public Task ReloadAsync(OperationProgress? progress = null) => ReloadAsync(FileDecodeType.Full, progress); + + public void EncodeLayersInZip(ZipArchive zipArchive, string prepend, byte padDigits, IndexStartNumber layerIndexStartNumber = default, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) { if (DecodeType != FileDecodeType.Full || LayerCount == 0) return; @@ -3283,17 +3376,17 @@ public void EncodeLayersInZip(ZipArchive zipArchive, string prepend, byte padDig } } - public void EncodeLayersInZip(ZipArchive zipArchive, byte padDigits, Enumerations.IndexStartNumber layerIndexStartNumber = default, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) + public void EncodeLayersInZip(ZipArchive zipArchive, byte padDigits, IndexStartNumber layerIndexStartNumber = default, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) => EncodeLayersInZip(zipArchive, string.Empty, padDigits, layerIndexStartNumber, progress, path, matGenFunc); - public void EncodeLayersInZip(ZipArchive zipArchive, string prepend, Enumerations.IndexStartNumber layerIndexStartNumber = default, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) + public void EncodeLayersInZip(ZipArchive zipArchive, string prepend, IndexStartNumber layerIndexStartNumber = default, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) => EncodeLayersInZip(zipArchive, prepend, 0, layerIndexStartNumber, progress, path, matGenFunc); - public void EncodeLayersInZip(ZipArchive zipArchive, Enumerations.IndexStartNumber layerIndexStartNumber, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) + public void EncodeLayersInZip(ZipArchive zipArchive, IndexStartNumber layerIndexStartNumber, OperationProgress? progress = null, string path = "", Func? matGenFunc = null) => EncodeLayersInZip(zipArchive, string.Empty, 0, layerIndexStartNumber, progress, path, matGenFunc); public void EncodeLayersInZip(ZipArchive zipArchive, OperationProgress progress, string path = "", Func? matGenFunc = null) - => EncodeLayersInZip(zipArchive, string.Empty, 0, Enumerations.IndexStartNumber.Zero, progress, path, matGenFunc); + => EncodeLayersInZip(zipArchive, string.Empty, 0, IndexStartNumber.Zero, progress, path, matGenFunc); public void DecodeLayersFromZip(ZipArchiveEntry[] layerEntries, OperationProgress? progress = null, Func? matGenFunc = null) @@ -3313,19 +3406,19 @@ public void DecodeLayersFromZip(ZipArchiveEntry[] layerEntries, OperationProgres if (matGenFunc is null) { - this[layerIndex] = new Layer((uint)layerIndex, pngBytes, this); + _layers[layerIndex] = new Layer((uint)layerIndex, pngBytes, this); } - if (matGenFunc is not null) + else { using var mat = matGenFunc.Invoke((uint) layerIndex, pngBytes); - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); } progress.LockAndIncrement(); }); } - public void DecodeLayersFromZipRegex(ZipArchive zipArchive, string regex, Enumerations.IndexStartNumber layerIndexStartNumber = Enumerations.IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) + public void DecodeLayersFromZipRegex(ZipArchive zipArchive, string regex, IndexStartNumber layerIndexStartNumber = IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) { var layerEntries = new ZipArchiveEntry?[LayerCount]; @@ -3335,7 +3428,7 @@ public void DecodeLayersFromZipRegex(ZipArchive zipArchive, string regex, Enumer if (!match.Success || match.Groups.Count < 2) continue; if (!uint.TryParse(match.Groups[1].Value, out var layerIndex)) continue; - if (layerIndexStartNumber == Enumerations.IndexStartNumber.One && layerIndex > 0) layerIndex--; + if (layerIndexStartNumber == IndexStartNumber.One && layerIndex > 0) layerIndex--; if (layerIndex >= LayerCount) { continue; @@ -3355,17 +3448,17 @@ public void DecodeLayersFromZipRegex(ZipArchive zipArchive, string regex, Enumer DecodeLayersFromZip(layerEntries!, progress, matGenFunc); } - public void DecodeLayersFromZip(ZipArchive zipArchive, byte padDigits, Enumerations.IndexStartNumber layerIndexStartNumber = Enumerations.IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) + public void DecodeLayersFromZip(ZipArchive zipArchive, byte padDigits, IndexStartNumber layerIndexStartNumber = IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) => DecodeLayersFromZipRegex(zipArchive, $@"(\d{{{padDigits}}}).png", layerIndexStartNumber, progress, matGenFunc); - public void DecodeLayersFromZip(ZipArchive zipArchive, Enumerations.IndexStartNumber layerIndexStartNumber = Enumerations.IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) + public void DecodeLayersFromZip(ZipArchive zipArchive, IndexStartNumber layerIndexStartNumber = IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) => DecodeLayersFromZipRegex(zipArchive, @"^(\d+).png", layerIndexStartNumber, progress, matGenFunc); - public void DecodeLayersFromZip(ZipArchive zipArchive, string prepend, Enumerations.IndexStartNumber layerIndexStartNumber = Enumerations.IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) + public void DecodeLayersFromZip(ZipArchive zipArchive, string prepend, IndexStartNumber layerIndexStartNumber = IndexStartNumber.Zero, OperationProgress? progress = null, Func? matGenFunc = null) => DecodeLayersFromZipRegex(zipArchive, $@"^{Regex.Escape(prepend)}(\d+).png", layerIndexStartNumber, progress, matGenFunc); public void DecodeLayersFromZip(ZipArchive zipArchive, OperationProgress progress, Func? matGenFunc = null) - => DecodeLayersFromZipRegex(zipArchive, @"^(\d+).png", Enumerations.IndexStartNumber.Zero, progress, matGenFunc); + => DecodeLayersFromZipRegex(zipArchive, @"^(\d+).png", IndexStartNumber.Zero, progress, matGenFunc); /// /// Extract contents to a folder @@ -4281,11 +4374,12 @@ public void Save(OperationProgress? progress = null) /// /// File path to save copy as, use null to overwrite active file (Same as ) /// + /// public void SaveAs(string? filePath = null, OperationProgress? progress = null) { if (RequireFullEncode) { - if (!string.IsNullOrEmpty(filePath)) + if (!string.IsNullOrWhiteSpace(filePath)) { FileFullPath = filePath; } @@ -4293,20 +4387,39 @@ public void SaveAs(string? filePath = null, OperationProgress? progress = null) return; } - if (FileFullPath is null) + if (string.IsNullOrWhiteSpace(FileFullPath)) { - throw new ArgumentNullException(nameof(FileFullPath)); + if (!string.IsNullOrWhiteSpace(filePath)) + { + Encode(filePath, progress); + return; + } + throw new ArgumentNullException(nameof(FileFullPath), "Not encoded yet and both source and output files are null"); } - if (!string.IsNullOrEmpty(filePath)) + + // Backup old file name and prepare the temporary file to be written next + var oldFilePath = FileFullPath!; + if(!string.IsNullOrWhiteSpace(filePath)) FileFullPath = filePath; + var tempFile = TemporaryOutputFileFullPath; + File.Copy(oldFilePath, tempFile, true); + + try { - File.Copy(FileFullPath, filePath, true); - FileFullPath = filePath; + progress ??= new OperationProgress(); + PartialSaveInternally(progress); + // Move temporary output file in place + File.Move(tempFile, FileFullPath, true); } - - progress ??= new OperationProgress(); - PartialSaveInternally(progress); + catch (Exception) + { + // Restore backup file path and delete the temporary + FileFullPath = oldFilePath; + if (File.Exists(tempFile)) File.Delete(tempFile); + throw; + } + } /// diff --git a/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs b/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs index c9571b85..a1d21c07 100644 --- a/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs +++ b/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs @@ -316,9 +316,9 @@ public override float DisplayHeight } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => Enumerations.FlipDirection.Vertically; + get => FlipDirection.Vertically; set {} } @@ -456,7 +456,7 @@ protected override void EncodeInternally(OperationProgress progress) "Please use other compatible slicer capable of output the correct information to load the file in here.", FileFullPath); } - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); outputFile.Seek(Helpers.Serializer.SizeOf(HeaderSettings), SeekOrigin.Begin); HeaderSettings.Preview1Address = 0; @@ -723,14 +723,14 @@ protected override void DecodeInternally(OperationProgress progress) } - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); } protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(HeaderSettings.SVGDocumentAddress, SeekOrigin.Begin); outputFile.SetLength(outputFile.Position); outputFile.WriteString(SVGDocument.SerializeToString()); diff --git a/UVtools.Core/FileFormats/GR1File.cs b/UVtools.Core/FileFormats/GR1File.cs index 46cf63f1..b55f678a 100644 --- a/UVtools.Core/FileFormats/GR1File.cs +++ b/UVtools.Core/FileFormats/GR1File.cs @@ -229,7 +229,7 @@ public override float DisplayHeight } } - public override Enumerations.FlipDirection DisplayMirror { get; set; } + public override FlipDirection DisplayMirror { get; set; } public override float LayerHeight { @@ -356,7 +356,7 @@ public override byte LightPWM #region Methods protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); var pageBreak = PageBreak.Bytes; Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); @@ -520,7 +520,7 @@ protected override void DecodeInternally(OperationProgress progress) linesBytes[layerIndex] = null!; - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); @@ -540,7 +540,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(SlicerInfoAddress, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); } diff --git a/UVtools.Core/FileFormats/GenericZIPFile.cs b/UVtools.Core/FileFormats/GenericZIPFile.cs index 87a949c0..0c9a9567 100644 --- a/UVtools.Core/FileFormats/GenericZIPFile.cs +++ b/UVtools.Core/FileFormats/GenericZIPFile.cs @@ -156,13 +156,13 @@ public GenericZIPFile() #region Methods - public override bool CanProcess(string fileFullPath) + public override bool CanProcess(string? fileFullPath) { if(!base.CanProcess(fileFullPath)) return false; try { - using var zip = ZipFile.Open(fileFullPath, ZipArchiveMode.Read); + using var zip = ZipFile.Open(fileFullPath!, ZipArchiveMode.Read); foreach (var entry in zip.Entries) { if (entry.Name == ManifestFileName) return true; @@ -182,7 +182,7 @@ public override bool CanProcess(string fileFullPath) protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); if (Thumbnails is not null) { @@ -202,7 +202,7 @@ protected override void EncodeInternally(OperationProgress progress) } } - EncodeLayersInZip(outputFile, Enumerations.IndexStartNumber.One, progress); + EncodeLayersInZip(outputFile, IndexStartNumber.One, progress); ManifestFile.Update(); @@ -247,7 +247,7 @@ protected override void DecodeInternally(OperationProgress progress) } Init(layerCount, DecodeType == FileDecodeType.Partial); - DecodeLayersFromZip(inputFile, Enumerations.IndexStartNumber.One, progress); + DecodeLayersFromZip(inputFile, IndexStartNumber.One, progress); entry = inputFile.GetEntry("preview.png"); if (entry is not null) @@ -267,7 +267,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); bool deleted; do diff --git a/UVtools.Core/FileFormats/ImageFile.cs b/UVtools.Core/FileFormats/ImageFile.cs index fc1be99f..dc26d711 100644 --- a/UVtools.Core/FileFormats/ImageFile.cs +++ b/UVtools.Core/FileFormats/ImageFile.cs @@ -78,7 +78,7 @@ public override float DisplayHeight protected override void EncodeInternally(OperationProgress progress) { - this[0].LayerMat.Save(FileFullPath); + this[0].LayerMat.Save(TemporaryOutputFileFullPath); } protected override void DecodeInternally(OperationProgress progress) @@ -103,7 +103,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - this[0].LayerMat.Save(FileFullPath); + this[0].LayerMat.Save(TemporaryOutputFileFullPath); } public override FileFormat Convert(Type to, string fileFullPath, uint version = 0, OperationProgress? progress = null) diff --git a/UVtools.Core/FileFormats/JXSFile.cs b/UVtools.Core/FileFormats/JXSFile.cs new file mode 100644 index 00000000..bc8758a0 --- /dev/null +++ b/UVtools.Core/FileFormats/JXSFile.cs @@ -0,0 +1,698 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using Emgu.CV; +using Emgu.CV.CvEnum; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Xml.Serialization; +using UVtools.Core.Converters; +using UVtools.Core.Extensions; +using UVtools.Core.GCode; +using UVtools.Core.Layers; +using UVtools.Core.Operations; + +namespace UVtools.Core.FileFormats; + +public class JXSFile : FileFormat +{ + #region Constants + + public const string ConfigFileName = "config.ini"; + public const string ControlFilename = "control.json"; + + #endregion + + #region Sub Classes + + public sealed class JXSConfig + { + public string Action { get; set; } = "print"; + + [DisplayName("FirstLayerTime")] public uint BottomExposureTimeMs { get; set; } = TimeConverter.SecondsToMillisecondsUint(DefaultBottomExposureTime); + [DisplayName("LayerTime")] public uint ExposureTimeMs { get; set; } = TimeConverter.SecondsToMillisecondsUint(DefaultExposureTime); + public uint NumFade { get; set; } + [DisplayName("NumLayers")] public uint LayerCount { get; set; } + [DisplayName("UsedMaterial")] public float MaterialMl { get; set; } + + [DisplayName("layerHeight")] public float LayerHeight { get; set; } + + [DisplayName("nJobName")] public string JobName { get; set; } = string.Empty; + [DisplayName("NumBottomLayers")] public ushort BottomLayerCount { get; set; } = DefaultBottomLayerCount; + [DisplayName("LiftDistance1")] public float LiftHeight1 { get; set; } = DefaultBottomLiftHeight; + [DisplayName("LiftFeedrate1")] public float LiftSpeed1 { get; set; } = SpeedConverter.Convert(DefaultLiftSpeed, SpeedUnit.MillimetersPerMinute, SpeedUnit.MillimetersPerSecond); + [DisplayName("LiftDistance2")] public float LiftHeight2 { get; set; } = DefaultLiftHeight2; + [DisplayName("LiftFeedrate2")] public float LiftSpeed2 { get; set; } = SpeedConverter.Convert(DefaultLiftSpeed2, SpeedUnit.MillimetersPerMinute, SpeedUnit.MillimetersPerSecond); + [DisplayName("BottomLiftFeedrate")] public float BottomLiftSpeed { get; set; } = SpeedConverter.Convert(DefaultBottomLiftSpeed, SpeedUnit.MillimetersPerMinute, SpeedUnit.MillimetersPerSecond); + [DisplayName("RetractFeedrate")] public float RetractSpeed { get; set; } = SpeedConverter.Convert(DefaultRetractSpeed, SpeedUnit.MillimetersPerMinute, SpeedUnit.MillimetersPerSecond); + [DisplayName("ZMoveTimeCompensation")] public uint WaitTimeBeforeCure { get; set; } = 1500; + [DisplayName("ZMoveTimeCompensationBottom")] public uint BottomWaitTimeBeforeCure { get; set; } = 8000; + public string OnLightCode { get; set; } = "M106 S255"; + public string OffLightCode { get; set; } = "M106 S0"; + public string GcodeHeader { get; set; } = "G21|G91|M17|M106 S0"; + public string GcodeFooter { get; set; } = "G0 Z100 F150"; + public string ApplicationName { get; set; } = "GKone-Slicer"; + } + + public sealed class JXSControl + { + [JsonPropertyName("total_time")] public float PrintTime { get; set; } + + [JsonPropertyName("total_slices")] public uint LayerCount { get; set; } + + [JsonPropertyName("used_material")] public float MaterialMl { get; set; } + + [JsonPropertyName("action_list")] public List Actions { get; set; } = new(); + } + #endregion + + #region Properties + public JXSConfig ConfigFile { get; set; } = new (); + public JXSControl ControlFile { get; set; } = new (); + + public override FileFormatType FileType => FileFormatType.Archive; + + public override FileExtension[] FileExtensions { get; } = { + new(typeof(JXSFile), "jxs", "Uniformation GKone (JXS)") + }; + + public override PrintParameterModifier[]? PrintParameterModifiers { get; } = { + PrintParameterModifier.BottomLayerCount, + + PrintParameterModifier.BottomWaitTimeBeforeCure, + PrintParameterModifier.WaitTimeBeforeCure, + + PrintParameterModifier.BottomExposureTime, + PrintParameterModifier.ExposureTime, + + PrintParameterModifier.BottomWaitTimeAfterCure, + PrintParameterModifier.WaitTimeAfterCure, + + PrintParameterModifier.BottomLiftHeight, + PrintParameterModifier.BottomLiftSpeed, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + + PrintParameterModifier.BottomLiftHeight2, + PrintParameterModifier.BottomLiftSpeed2, + PrintParameterModifier.LiftHeight2, + PrintParameterModifier.LiftSpeed2, + + PrintParameterModifier.BottomWaitTimeAfterLift, + PrintParameterModifier.WaitTimeAfterLift, + + PrintParameterModifier.BottomRetractSpeed, + PrintParameterModifier.RetractSpeed, + + PrintParameterModifier.BottomRetractHeight2, + PrintParameterModifier.BottomRetractSpeed2, + PrintParameterModifier.RetractHeight2, + PrintParameterModifier.RetractSpeed2, + + PrintParameterModifier.BottomLightPWM, + PrintParameterModifier.LightPWM, + }; + + public override PrintParameterModifier[]? PrintParameterPerLayerModifiers { get; } = { + PrintParameterModifier.PositionZ, + PrintParameterModifier.WaitTimeBeforeCure, + PrintParameterModifier.ExposureTime, + PrintParameterModifier.WaitTimeAfterCure, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.LiftHeight2, + PrintParameterModifier.LiftSpeed2, + PrintParameterModifier.WaitTimeAfterLift, + PrintParameterModifier.RetractSpeed, + PrintParameterModifier.RetractHeight2, + PrintParameterModifier.RetractSpeed2, + PrintParameterModifier.LightPWM, + }; + + //public override Size[]? ThumbnailsOriginalSize { get; } = {new(640, 480)}; + + public override uint ResolutionX + { + get => 4920; + set + { + if (value != ResolutionX) throw new ArgumentOutOfRangeException(nameof(ResolutionX), $"{nameof(ResolutionX)} can not be different from {ResolutionX}"); + RaisePropertyChanged(); + } + } + + public override uint ResolutionY + { + get => 2880; + set + { + if(value != ResolutionY) throw new ArgumentOutOfRangeException(nameof(ResolutionY), $"{nameof(ResolutionY)} can not be different from {ResolutionY}"); + RaisePropertyChanged(); + } + } + + public override float DisplayWidth + { + get => 221.40f; + set => RaisePropertyChanged(); + } + + public override float DisplayHeight + { + get => 129.60f; + set => RaisePropertyChanged(); + } + + public override float MachineZ + { + get => 245; + set => RaisePropertyChanged(); + } + + public override FlipDirection DisplayMirror => FlipDirection.Vertically; + + public override float LayerHeight + { + get => ConfigFile.LayerHeight; + set + { + ConfigFile.LayerHeight = Layer.RoundHeight(value); + RaisePropertyChanged(); + } + } + + public override uint LayerCount + { + get => base.LayerCount; + set => base.LayerCount = ConfigFile.LayerCount = ControlFile.LayerCount = base.LayerCount; + } + + public override ushort BottomLayerCount + { + get => ConfigFile.BottomLayerCount; + set => base.BottomLayerCount = ConfigFile.BottomLayerCount = value; + } + + public override float BottomLightOffDelay + { + get => BottomWaitTimeBeforeCure; + set => BottomWaitTimeBeforeCure = value; + } + + public override float LightOffDelay + { + get => WaitTimeBeforeCure; + set => WaitTimeBeforeCure = value; + } + + public override float BottomWaitTimeBeforeCure + { + get => TimeConverter.MillisecondsToSeconds(ConfigFile.BottomWaitTimeBeforeCure); + set + { + ConfigFile.BottomWaitTimeBeforeCure = TimeConverter.SecondsToMillisecondsUint(value); + base.BottomWaitTimeBeforeCure = base.BottomLightOffDelay = value; + } + } + + public override float WaitTimeBeforeCure + { + get => TimeConverter.MillisecondsToSeconds(ConfigFile.WaitTimeBeforeCure); + set + { + ConfigFile.WaitTimeBeforeCure = TimeConverter.SecondsToMillisecondsUint(value); + base.WaitTimeBeforeCure = base.LightOffDelay = value; + } + } + + public override float BottomExposureTime + { + get => TimeConverter.MillisecondsToSeconds(ConfigFile.BottomExposureTimeMs); + set + { + ConfigFile.BottomExposureTimeMs = TimeConverter.SecondsToMillisecondsUint(value); + base.BottomExposureTime = value; + } + } + + public override float ExposureTime + { + get => TimeConverter.MillisecondsToSeconds(ConfigFile.ExposureTimeMs); + set + { + ConfigFile.ExposureTimeMs = TimeConverter.SecondsToMillisecondsUint(value); + base.ExposureTime = value; + } + } + + public override float BottomLiftHeight + { + get => ConfigFile.LiftHeight1; + set => base.BottomLiftHeight = (float)Math.Round(value, 2); + } + + public override float LiftHeight + { + get => ConfigFile.LiftHeight1; + set => base.LiftHeight = ConfigFile.LiftHeight1 = (float)Math.Round(value, 2); + } + + public override float BottomLiftSpeed + { + get => ConfigFile.BottomLiftSpeed; + set => base.BottomLiftSpeed = ConfigFile.BottomLiftSpeed = (float)Math.Round(value, 2); + } + + public override float LiftSpeed + { + get => ConfigFile.LiftSpeed1; + set => base.LiftSpeed = ConfigFile.LiftSpeed1 = (float)Math.Round(value, 2); + } + + public override float PrintTime + { + get => base.PrintTime; + set + { + base.PrintTime = value; + ControlFile.PrintTime = base.PrintTime; + } + } + + public override float MaterialMilliliters + { + get => base.MaterialMilliliters; + set + { + base.MaterialMilliliters = value; + ConfigFile.MaterialMl = ControlFile.MaterialMl = base.MaterialMilliliters; + } + } + + public override string MachineName + { + get => "Uniformation GKone"; + set {} + } + + public override object[] Configs => new object[] { ConfigFile }; + + #endregion + + #region Constructor + public JXSFile() + { + GCode = new GCodeBuilder + { + UseTailComma = true, + UseComments = true, + GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Relative, + GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, + GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.FilenamePng0Started, + LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G0, + EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G0, + SyncMovementsWithDelay = true + }; + } + #endregion + + #region Methods + + private void RebuildFileProperties() + { + ConfigFile.JobName = FilenameNoExt!; + ConfigFile.ApplicationName = About.SoftwareWithVersion; + + RebuildGCode(); + ControlFile.Actions.Clear(); + + using var tw = new StringReader(GCodeStr!); + string? line; + var lastG0 = "G1 Z100 F150"; + while ((line = tw.ReadLine()) is not null) + { + line = line.Trim(); + if (line == string.Empty) continue; + + if (line.StartsWith(GCode!.CommandClearImage.Command)) + { + ControlFile.Actions.Add(new object[] + { + "slice", "" + }); + continue; + } + + if(line[0] == ';') continue; + + var index = line.IndexOf(';'); + if (index >= 0) + { + line = line[..index].TrimEnd(); + } + + if (line == string.Empty) continue; + + if (line.StartsWith(GCode.CommandShowImageM6054.Command)) + { + var match = Regex.Match(line, GCode.GetShowImageString(@"(\d+)")); + + if (!match.Success || match.Groups.Count <= 1) + { + throw new InvalidDataException($"Unable to parse layer index from: {line}"); + } + + var layerIndex = match.Groups[1].Value.PadLeft(5, '0'); + if(!uint.TryParse(layerIndex, out var layerNumber)) + { + throw new InvalidDataException($"Unable to parse layer number from: {line}"); + } + layerNumber++; + + ControlFile.Actions.Add(new object[] + { + "layerno", layerNumber, + }); + ControlFile.Actions.Add(new object[] + { + "slice", $"{{PWD}}/{layerIndex}.png" + }); + continue; + } + + if (line.StartsWith(GCode.CommandWaitG4.Command)) + { + var match = Regex.Match(line, GCode.CommandWaitG4.ToStringWithoutComments(@"(\d+)")); + + if (!match.Success || match.Groups.Count <= 1) + { + throw new InvalidDataException($"Unable to delay from: {line}"); + } + + var delayStr = match.Groups[1].Value; + if (!uint.TryParse(delayStr, out var delay)) + { + throw new InvalidDataException($"Unable to parse delay number from: {line}"); + } + + if(delay <= 0) continue; + + ControlFile.Actions.Add(new object[] + { + "delay", delay + }); + continue; + } + + if (line.StartsWith("G0") || line.StartsWith("G1")) + { + lastG0 = line; + } + + ControlFile.Actions.Add(new object[] + { + "gcode", line + }); + } + + ConfigFile.GcodeFooter = lastG0; + } + + protected override void DecodeInternally(OperationProgress progress) + { + using var inputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Read); + var entry = inputFile.GetEntry(ConfigFileName); + if (entry is null) + { + Clear(); + throw new FileLoadException($"{ConfigFileName} not found", FileFullPath); + } + + try + { + using var stream = entry.Open(); + using TextReader reader = new StreamReader(stream); + string? line; + while((line = reader.ReadLine()) is not null) + { + var keyValue = line.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if(keyValue.Length < 2) continue; + + foreach (var propertyInfo in ConfigFile.GetType().GetProperties()) + { + if (!propertyInfo.CanWrite) continue; + var customAttributes = propertyInfo.GetCustomAttributes(); + if (customAttributes.Any(attribute => + { + var type = attribute.GetType(); + if (type == typeof(XmlIgnoreAttribute)) return true; + if (type == typeof(JsonIgnoreAttribute)) return true; + return false; + })) continue; + + var property = propertyInfo.Name; + foreach (var attribute in customAttributes) + { + if (attribute.GetType() != typeof(DisplayNameAttribute)) continue; + property = ((DisplayNameAttribute)attribute).DisplayName; + break; + } + + if(property != keyValue[0]) continue; + + //Debug.WriteLine(attribute.Name); + Helpers.SetPropertyValue(propertyInfo, ConfigFile, keyValue[1]); + } + } + + } + catch (Exception e) + { + Clear(); + throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", FileFullPath); + } + + entry = inputFile.GetEntry(ControlFilename); + if (entry is null) + { + Clear(); + throw new FileLoadException($"{ControlFilename} not found", FileFullPath); + } + + try + { + using var stream = entry.Open(); + ControlFile = JsonSerializer.Deserialize(stream)!; + if (ControlFile is null) + { + Clear(); + throw new FileLoadException($"Unable to deserialize '{entry.Name}'", FileFullPath); + } + } + catch (Exception e) + { + Clear(); + throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", FileFullPath); + } + + Init(ConfigFile.LayerCount, DecodeType == FileDecodeType.Partial); + sbyte[] bgrToRgbTable = { + 2, + 0, + -2 + }; + DecodeLayersFromZip(inputFile, IndexStartNumber.Zero, progress, + (layerIndex, pngBytes) => + { + using Mat rgbMat = new(); + CvInvoke.Imdecode(pngBytes, ImreadModes.AnyColor, rgbMat); + var greyMat = new Mat(rgbMat.Height, rgbMat.GetRealStep(), DepthType.Cv8U, 1); + var rgbSpan = rgbMat.GetDataByteSpan(); + var greySpan = greyMat.GetDataByteSpan(); + + for (var i = 0; i < rgbSpan.Length; i++) + { + greySpan[i] = rgbSpan[i + bgrToRgbTable[i%3]]; + } + + return greyMat; + }); + + GCode!.Clear(); + + string lastCommand = string.Empty; + // Rebuild gcode from json + foreach (var action in ControlFile.Actions) + { + var key = action[0].ToString()!; + var value = action[1].ToString()!; + + switch (key) + { + case "gcode": + lastCommand = value; + if (value.StartsWith("M106 S") && value.StartsWith("M106 S0")) GCode.AppendWaitG4(0); + GCode.AppendLine(value); + break; + case "slice": + if (value.StartsWith("{PWD}/")) + { + GCode.AppendShowImageM6054(value.Remove(0, "{PWD}/".Length)); + } + else if (value == "") + { + GCode.AppendClearImage(); + } + break; + case "delay": + if (lastCommand.StartsWith("G0") || lastCommand.StartsWith("G1")) + { + lastCommand = $"G4 0{value}"; + GCode.AppendWaitG4($"0{value}"); + break; + } + GCode.AppendWaitG4(value); + break; + case "layerno": + GCode.AppendLine(); + GCode.AppendLine($";LAYER_NUMBER:{value}"); + break; + } + } + + GCode!.ParseLayersFromGCode(this); + + if (!ConfigFile.ApplicationName.StartsWith(About.Software)) + { + BottomWaitTimeBeforeCure = TimeConverter.MillisecondsToSeconds(ConfigFile.BottomWaitTimeBeforeCure); + WaitTimeBeforeCure = TimeConverter.MillisecondsToSeconds(ConfigFile.WaitTimeBeforeCure); + } + } + + protected override void EncodeInternally(OperationProgress progress) + { + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); + + sbyte[] bgrToRgbTable = { + 2, + 0, + -2 + }; + EncodeLayersInZip(outputFile, 5, IndexStartNumber.Zero, progress, matGenFunc: + (_, mat) => + { + var rgbMat = new Mat(mat.Height, mat.GetRealStep() / 3, DepthType.Cv8U, 3); + var rgbMatSpan = rgbMat.GetDataByteSpan(); + var greySpan = mat.GetDataByteSpan(); + for (int i = 0; i < greySpan.Length; i++) + { + rgbMatSpan[i + bgrToRgbTable[i % 3]] = greySpan[i]; + } + + return rgbMat; + }); + + + RebuildFileProperties(); + + var entry = outputFile.CreateEntry(ConfigFileName); + using (var stream = entry.Open()) + using (var tw = new StreamWriter(stream)) + { + foreach (var propertyInfo in ConfigFile.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name.Equals("Item")) continue; + + var customAttributes = propertyInfo.GetCustomAttributes(); + if (customAttributes.Any(attribute => + { + var type = attribute.GetType(); + if (type == typeof(XmlIgnoreAttribute)) return true; + if (type == typeof(JsonIgnoreAttribute)) return true; + return false; + })) continue; + + var property = propertyInfo.Name; + foreach (var attribute in customAttributes) + { + if (attribute.GetType() != typeof(DisplayNameAttribute)) continue; + property = ((DisplayNameAttribute)attribute).DisplayName; + break; + } + + tw.WriteLine($"{property}={propertyInfo.GetValue(ConfigFile)}"); + } + } + + entry = outputFile.CreateEntry(ControlFilename); + using (var stream = entry.Open()) + { + JsonSerializer.Serialize(stream, ControlFile, JsonExtensions.SettingsIndent); + } + } + + protected override void PartialSaveInternally(OperationProgress progress) + { + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); + var entriesToRemove = outputFile.Entries.Where(zipEntry => zipEntry.Name.EndsWith(".ini") || zipEntry.Name.EndsWith(".json")).ToArray(); + foreach (var zipEntry in entriesToRemove) + { + zipEntry.Delete(); + } + + + RebuildFileProperties(); + + var entry = outputFile.CreateEntry(ConfigFileName); + using (var stream = entry.Open()) + using (var tw = new StreamWriter(stream)) + { + foreach (var propertyInfo in ConfigFile.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name.Equals("Item")) continue; + + var customAttributes = propertyInfo.GetCustomAttributes(); + if (customAttributes.Any(attribute => + { + var type = attribute.GetType(); + if (type == typeof(XmlIgnoreAttribute)) return true; + if (type == typeof(JsonIgnoreAttribute)) return true; + return false; + })) continue; + + var property = propertyInfo.Name; + foreach (var attribute in customAttributes) + { + if (attribute.GetType() != typeof(DisplayNameAttribute)) continue; + property = ((DisplayNameAttribute)attribute).DisplayName; + break; + } + + tw.WriteLine($"{property}={propertyInfo.GetValue(ConfigFile)}"); + } + } + + entry = outputFile.CreateEntry(ControlFilename); + using (var stream = entry.Open()) + { + JsonSerializer.Serialize(stream, ControlFile, JsonExtensions.SettingsIndent); + } + } + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/FileFormats/LGSFile.cs b/UVtools.Core/FileFormats/LGSFile.cs index 62109063..c4da12a0 100644 --- a/UVtools.Core/FileFormats/LGSFile.cs +++ b/UVtools.Core/FileFormats/LGSFile.cs @@ -15,6 +15,7 @@ using System.Drawing; using System.IO; using System.Threading.Tasks; +using UVtools.Core.Converters; using UVtools.Core.Extensions; using UVtools.Core.Layers; using UVtools.Core.Operations; @@ -345,9 +346,9 @@ public override float MachineZ set => base.MachineZ = HeaderSettings.MachineZ = (float)Math.Round(value, 2); } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => Enumerations.FlipDirection.Horizontally; + get => FlipDirection.Horizontally; set { } } @@ -379,20 +380,20 @@ public override ushort BottomLayerCount public override float BottomLightOffDelay { - get => TimeExtensions.MillisecondsToSeconds(HeaderSettings.BottomLightOffDelayMs); + get => TimeConverter.MillisecondsToSeconds(HeaderSettings.BottomLightOffDelayMs); set { - HeaderSettings.BottomLightOffDelayMs = TimeExtensions.SecondsToMilliseconds(value); + HeaderSettings.BottomLightOffDelayMs = TimeConverter.SecondsToMilliseconds(value); base.BottomLightOffDelay = value; } } public override float LightOffDelay { - get => TimeExtensions.MillisecondsToSeconds(HeaderSettings.LightOffDelayMs); + get => TimeConverter.MillisecondsToSeconds(HeaderSettings.LightOffDelayMs); set { - HeaderSettings.LightOffDelayMs = TimeExtensions.SecondsToMilliseconds(value); + HeaderSettings.LightOffDelayMs = TimeConverter.SecondsToMilliseconds(value); base.LightOffDelay = value; } } @@ -419,20 +420,20 @@ public override float WaitTimeBeforeCure public override float BottomExposureTime { - get => TimeExtensions.MillisecondsToSeconds(HeaderSettings.BottomExposureTimeMs); + get => TimeConverter.MillisecondsToSeconds(HeaderSettings.BottomExposureTimeMs); set { - HeaderSettings.BottomExposureTimeMs = TimeExtensions.SecondsToMilliseconds(value); + HeaderSettings.BottomExposureTimeMs = TimeConverter.SecondsToMilliseconds(value); base.BottomExposureTime = value; } } public override float ExposureTime { - get => TimeExtensions.MillisecondsToSeconds(HeaderSettings.ExposureTimeMs); + get => TimeConverter.MillisecondsToSeconds(HeaderSettings.ExposureTimeMs); set { - HeaderSettings.ExposureTimeMs = TimeExtensions.SecondsToMilliseconds(value); + HeaderSettings.ExposureTimeMs = TimeConverter.SecondsToMilliseconds(value); base.ExposureTime = value; } } @@ -511,7 +512,7 @@ protected override void EncodeInternally(OperationProgress progress) } //uint currentOffset = (uint)Helpers.Serializer.SizeOf(HeaderSettings); - using (var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write)) + using (var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write)) { outputFile.WriteSerialize(HeaderSettings); outputFile.WriteBytes(EncodeImage(DATATYPE_RGB565_BE, Thumbnails[0]!)); @@ -612,7 +613,7 @@ protected override void DecodeInternally(OperationProgress progress) Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => { using var mat = layerData[layerIndex].Decode(); - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); @@ -624,7 +625,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(0, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); } diff --git a/UVtools.Core/FileFormats/MDLPFile.cs b/UVtools.Core/FileFormats/MDLPFile.cs index 942ef0e7..0680483b 100644 --- a/UVtools.Core/FileFormats/MDLPFile.cs +++ b/UVtools.Core/FileFormats/MDLPFile.cs @@ -233,7 +233,7 @@ public override float DisplayHeight } } - public override Enumerations.FlipDirection DisplayMirror { get; set; } + public override FlipDirection DisplayMirror { get; set; } public override float LayerHeight { get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.LayerHeightBytes.Where(b => b != 0).ToArray())); @@ -315,7 +315,7 @@ public override float ExposureTime #region Methods protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); var pageBreak = PageBreak.Bytes; Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); @@ -477,7 +477,7 @@ protected override void DecodeInternally(OperationProgress progress) linesBytes[layerIndex] = null!; - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); } progress.LockAndIncrement(); @@ -495,7 +495,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(SlicerInfoAddress, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); } diff --git a/UVtools.Core/FileFormats/OSLAFile.cs b/UVtools.Core/FileFormats/OSLAFile.cs index 4baf114d..a195bb88 100644 --- a/UVtools.Core/FileFormats/OSLAFile.cs +++ b/UVtools.Core/FileFormats/OSLAFile.cs @@ -363,9 +363,9 @@ public override float MachineZ } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => (Enumerations.FlipDirection)HeaderSettings.DisplayMirror; + get => (FlipDirection)HeaderSettings.DisplayMirror; set { HeaderSettings.DisplayMirror = (byte)value; @@ -458,7 +458,7 @@ public OSLAFile() #region Methods protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); FileSettings.Update(); var fileDefSize = Helpers.SerializeWriteFileStream(outputFile, FileSettings); HeaderSettings.TableSize = (uint)Helpers.Serializer.SizeOf(HeaderSettings); @@ -680,7 +680,7 @@ protected override void DecodeInternally(OperationProgress progress) using var mat = DecodeImage(HeaderSettings.LayerDataType, layerBytes[layerIndex], Resolution); layerBytes[layerIndex] = null!; // Clean - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); @@ -703,7 +703,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(0, SeekOrigin.Begin); FileSettings.Update(); Helpers.SerializeWriteFileStream(outputFile, FileSettings); diff --git a/UVtools.Core/FileFormats/PHZFile.cs b/UVtools.Core/FileFormats/PHZFile.cs index ed663bc6..96f1054a 100644 --- a/UVtools.Core/FileFormats/PHZFile.cs +++ b/UVtools.Core/FileFormats/PHZFile.cs @@ -743,12 +743,12 @@ public override float MachineZ } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => HeaderSettings.ProjectorType == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; + get => HeaderSettings.ProjectorType == 0 ? FlipDirection.None : FlipDirection.Horizontally; set { - HeaderSettings.ProjectorType = value == Enumerations.FlipDirection.None ? 0u : 1; + HeaderSettings.ProjectorType = value == FlipDirection.None ? 0u : 1; RaisePropertyChanged(); } } @@ -950,7 +950,7 @@ protected override void EncodeInternally(OperationProgress progress) }*/ LayersDefinitions = new LayerDef[HeaderSettings.LayerCount]; - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); outputFile.Seek(Helpers.Serializer.SizeOf(HeaderSettings), SeekOrigin.Begin); for (byte i = 0; i < ThumbnailsCount; i++) @@ -1136,7 +1136,7 @@ protected override void DecodeInternally(OperationProgress progress) Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => { using var mat = LayersDefinitions[layerIndex].Decode((uint)layerIndex); - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); } @@ -1151,7 +1151,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { HeaderSettings.ModifiedTimestampMinutes = (uint) DateTimeExtensions.TimestampMinutes; - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(0, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); diff --git a/UVtools.Core/FileFormats/PhotonSFile.cs b/UVtools.Core/FileFormats/PhotonSFile.cs index 01087dca..fdf4683f 100644 --- a/UVtools.Core/FileFormats/PhotonSFile.cs +++ b/UVtools.Core/FileFormats/PhotonSFile.cs @@ -311,9 +311,9 @@ public override float DisplayHeight set { } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => Enumerations.FlipDirection.Horizontally; + get => FlipDirection.Horizontally; set { } } @@ -437,7 +437,7 @@ public PhotonSFile() protected override void EncodeInternally(OperationProgress progress) { //throw new NotSupportedException("PhotonS is read-only format, please use pws instead!"); - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); outputFile.WriteSerialize(HeaderSettings); outputFile.WriteBytes(EncodeImage(DATATYPE_BGR565, Thumbnails[0]!)); outputFile.WriteSerialize(LayerSettings); @@ -526,7 +526,7 @@ protected override void DecodeInternally(OperationProgress progress) Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => { using var mat = layersDefinitions[layerIndex].Decode(); - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); }); } @@ -537,7 +537,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(0, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); } diff --git a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs index d2db0fb1..0e656ef0 100644 --- a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs +++ b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs @@ -1223,9 +1223,9 @@ public override float MachineZ } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => Enumerations.FlipDirection.Horizontally; + get => FlipDirection.Horizontally; set {} } @@ -1632,7 +1632,7 @@ protected override void EncodeInternally(OperationProgress progress) FileMarkSettings.HeaderAddress = (uint) Helpers.Serializer.SizeOf(FileMarkSettings); - using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); if (FileMarkSettings.Version >= VERSION_516) { HeaderSettings.Section.Length = 84; @@ -1842,7 +1842,7 @@ protected override void DecodeInternally(OperationProgress progress) Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => { using var mat = LayersDefinition[layerIndex].Decode(); - this[layerIndex] = new Layer((uint)layerIndex, mat, this) + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this) { PositionZ = LayersDefinition.Layers .Where((_, i) => i <= layerIndex) @@ -1874,7 +1874,7 @@ protected override void PartialSaveInternally(OperationProgress progress) { HeaderSettings.PerLayerOverride = AllLayersAreUsingGlobalParameters ? 0 : 1u; - using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); outputFile.Seek(FileMarkSettings.HeaderAddress, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); diff --git a/UVtools.Core/FileFormats/SL1File.cs b/UVtools.Core/FileFormats/SL1File.cs index 1ad605e6..f9ae3803 100644 --- a/UVtools.Core/FileFormats/SL1File.cs +++ b/UVtools.Core/FileFormats/SL1File.cs @@ -405,19 +405,19 @@ public override float MachineZ } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { get { - if (PrinterSettings.DisplayMirrorX > 0 && PrinterSettings.DisplayMirrorY > 0) return Enumerations.FlipDirection.Both; - if (PrinterSettings.DisplayMirrorX > 0) return Enumerations.FlipDirection.Horizontally; - if (PrinterSettings.DisplayMirrorY > 0) return Enumerations.FlipDirection.Vertically; - return Enumerations.FlipDirection.None; + if (PrinterSettings.DisplayMirrorX > 0 && PrinterSettings.DisplayMirrorY > 0) return FlipDirection.Both; + if (PrinterSettings.DisplayMirrorX > 0) return FlipDirection.Horizontally; + if (PrinterSettings.DisplayMirrorY > 0) return FlipDirection.Vertically; + return FlipDirection.None; } set { - PrinterSettings.DisplayMirrorX = (byte)(value is Enumerations.FlipDirection.Horizontally or Enumerations.FlipDirection.Both ? 1 : 0); - PrinterSettings.DisplayMirrorY = (byte)(value is Enumerations.FlipDirection.Vertically or Enumerations.FlipDirection.Both ? 1 : 0); + PrinterSettings.DisplayMirrorX = (byte)(value is FlipDirection.Horizontally or FlipDirection.Both ? 1 : 0); + PrinterSettings.DisplayMirrorY = (byte)(value is FlipDirection.Vertically or FlipDirection.Both ? 1 : 0); RaisePropertyChanged(); } } @@ -569,7 +569,7 @@ protected override void EncodeInternally(OperationProgress progress) if (filename!.EndsWith(TemporaryFileAppend)) filename = Path.GetFileNameWithoutExtension(filename); // tmp filename = Path.GetFileNameWithoutExtension(filename); // sl1 OutputConfigSettings.JobDir = filename; - using ZipArchive outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); var entry = outputFile.CreateEntry("config.ini"); using (TextWriter tw = new StreamWriter(entry.Open())) { @@ -614,7 +614,7 @@ protected override void EncodeInternally(OperationProgress progress) stream.Close(); } - EncodeLayersInZip(outputFile, filename, 5, Enumerations.IndexStartNumber.Zero, progress); + EncodeLayersInZip(outputFile, filename, 5, IndexStartNumber.Zero, progress); } @@ -637,10 +637,8 @@ protected override void DecodeInternally(OperationProgress progress) string? line; while ((line = streamReader.ReadLine()) != null) { - string[] keyValue = line.Split(new[] {'='}, 2); + var keyValue = line.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (keyValue.Length < 2) continue; - keyValue[0] = keyValue[0].Trim(); - keyValue[1] = keyValue[1].Trim(); var fieldName = IniKeyToMemberName(keyValue[0]); bool foundMember = false; @@ -732,7 +730,7 @@ protected override void DecodeInternally(OperationProgress progress) //thumbnailIndex++; } - DecodeLayersFromZip(inputFile, 5, Enumerations.IndexStartNumber.Zero, progress); + DecodeLayersFromZip(inputFile, 5, IndexStartNumber.Zero, progress); if (TransitionLayerCount > 0) { @@ -746,7 +744,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); //InputFile.CreateEntry("Modified"); using (TextWriter tw = new StreamWriter(outputFile.PutFileContent("config.ini", string.Empty, ZipArchiveMode.Update).Open())) { diff --git a/UVtools.Core/FileFormats/UVJFile.cs b/UVtools.Core/FileFormats/UVJFile.cs index 1b9e0eee..ebd7dc49 100644 --- a/UVtools.Core/FileFormats/UVJFile.cs +++ b/UVtools.Core/FileFormats/UVJFile.cs @@ -278,7 +278,7 @@ public override float DisplayHeight } } - public override Enumerations.FlipDirection DisplayMirror { get; set; } + public override FlipDirection DisplayMirror { get; set; } public override byte AntiAliasing { @@ -485,7 +485,7 @@ protected override void EncodeInternally(OperationProgress progress) JsonSettings.Layers.Add(new LayerDef(this[layerIndex])); } - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); outputFile.PutFileContent(FileConfigName, JsonSerializer.SerializeToUtf8Bytes(JsonSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Create); if (CreatedThumbnailsCount > 0) @@ -502,7 +502,7 @@ protected override void EncodeInternally(OperationProgress progress) stream.Close(); } - EncodeLayersInZip(outputFile, 8, Enumerations.IndexStartNumber.Zero, progress, FolderImageName); + EncodeLayersInZip(outputFile, 8, IndexStartNumber.Zero, progress, FolderImageName); } protected override void DecodeInternally(OperationProgress progress) @@ -566,7 +566,7 @@ protected override void PartialSaveInternally(OperationProgress progress) JsonSettings.Layers.Add(new LayerDef(this[layerIndex])); } - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); outputFile.PutFileContent(FileConfigName, JsonSerializer.SerializeToUtf8Bytes(JsonSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Update); //Decode(FileFullPath, progress); diff --git a/UVtools.Core/FileFormats/VDAFile.cs b/UVtools.Core/FileFormats/VDAFile.cs index 4ced4d23..3f8e81a4 100644 --- a/UVtools.Core/FileFormats/VDAFile.cs +++ b/UVtools.Core/FileFormats/VDAFile.cs @@ -284,13 +284,13 @@ public VDAFile() #region Methods - public override bool CanProcess(string fileFullPath) + public override bool CanProcess(string? fileFullPath) { if(!base.CanProcess(fileFullPath)) return false; try { - using var zip = ZipFile.Open(fileFullPath, ZipArchiveMode.Read); + using var zip = ZipFile.Open(fileFullPath!, ZipArchiveMode.Read); if (zip.Entries.Any(entry => entry.Name.EndsWith(".xml"))) return true; } catch (Exception e) @@ -304,12 +304,12 @@ public override bool CanProcess(string fileFullPath) protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); var manifestFilename = Filename!. Replace($".{FileExtensions[0].Extension}{TemporaryFileAppend}", ".xml"). Replace($".{FileExtensions[0].Extension}", ".xml"); - EncodeLayersInZip(outputFile, 4, Enumerations.IndexStartNumber.One, progress); + EncodeLayersInZip(outputFile, 4, IndexStartNumber.One, progress); UpdateManifest(); @@ -341,12 +341,12 @@ protected override void DecodeInternally(OperationProgress progress) Init(ManifestFile.Slices.LayerCount, DecodeType == FileDecodeType.Partial); - DecodeLayersFromZip(inputFile, Enumerations.IndexStartNumber.One, progress); + DecodeLayersFromZip(inputFile, IndexStartNumber.One, progress); } protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); bool deleted; do @@ -381,7 +381,7 @@ public void UpdateManifest() for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { var layer = this[layerIndex]; - ManifestFile.Layers.Add(new VDARoot.VDALayer(layerIndex, layer.PositionZ, layer.FormatFileName(4, Enumerations.IndexStartNumber.One))); + ManifestFile.Layers.Add(new VDARoot.VDALayer(layerIndex, layer.PositionZ, layer.FormatFileName(4, IndexStartNumber.One))); } } #endregion diff --git a/UVtools.Core/FileFormats/VDTFile.cs b/UVtools.Core/FileFormats/VDTFile.cs index ac5abe2c..bf82ad7d 100644 --- a/UVtools.Core/FileFormats/VDTFile.cs +++ b/UVtools.Core/FileFormats/VDTFile.cs @@ -337,19 +337,19 @@ public override float MachineZ } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { get { - if (ManifestFile.Machine.XMirror && ManifestFile.Machine.YMirror) return Enumerations.FlipDirection.Both; - if (ManifestFile.Machine.XMirror) return Enumerations.FlipDirection.Horizontally; - if (ManifestFile.Machine.YMirror) return Enumerations.FlipDirection.Vertically; - return Enumerations.FlipDirection.None; + if (ManifestFile.Machine.XMirror && ManifestFile.Machine.YMirror) return FlipDirection.Both; + if (ManifestFile.Machine.XMirror) return FlipDirection.Horizontally; + if (ManifestFile.Machine.YMirror) return FlipDirection.Vertically; + return FlipDirection.None; } set { - ManifestFile.Machine.XMirror = value is Enumerations.FlipDirection.Horizontally or Enumerations.FlipDirection.Both; - ManifestFile.Machine.YMirror = value is Enumerations.FlipDirection.Vertically or Enumerations.FlipDirection.Both; + ManifestFile.Machine.XMirror = value is FlipDirection.Horizontally or FlipDirection.Both; + ManifestFile.Machine.YMirror = value is FlipDirection.Vertically or FlipDirection.Both; RaisePropertyChanged(); } } @@ -635,7 +635,7 @@ protected override void EncodeInternally(OperationProgress progress) // Redo layer data RebuildVDTLayers(); - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); outputFile.PutFileContent(FileManifestName, JsonSerializer.SerializeToUtf8Bytes(ManifestFile, JsonExtensions.SettingsIndent), ZipArchiveMode.Create); if (CreatedThumbnailsCount > 0) @@ -695,7 +695,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { RebuildVDTLayers(); - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); outputFile.PutFileContent(FileManifestName, JsonSerializer.SerializeToUtf8Bytes(ManifestFile, JsonExtensions.SettingsIndent), ZipArchiveMode.Update); //Decode(FileFullPath, progress); diff --git a/UVtools.Core/FileFormats/ZCodeFile.cs b/UVtools.Core/FileFormats/ZCodeFile.cs index 6e8613d7..4c6ee4d8 100644 --- a/UVtools.Core/FileFormats/ZCodeFile.cs +++ b/UVtools.Core/FileFormats/ZCodeFile.cs @@ -13,14 +13,13 @@ using Org.BouncyCastle.OpenSsl; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Drawing; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Xml.Serialization; +using UVtools.Core.Converters; using UVtools.Core.Extensions; using UVtools.Core.GCode; using UVtools.Core.Layers; @@ -86,10 +85,10 @@ public class ZcodePrintProfileSlice public float LiftSpeed { get; set; } = FileFormat.DefaultLiftSpeed; [XmlAttribute("cooldown_bottom")] - public uint BottomLightOffDelay { get; set; } + public uint BottomWaitTimeBeforeCure { get; set; } [XmlAttribute("cooldown")] - public uint LightOffDelay { get; set; } + public uint WaitTimeBeforeCure { get; set; } [XmlAttribute("thickness")] public float LayerHeight { get; set; } = FileFormat.DefaultLayerHeight; @@ -279,7 +278,7 @@ public override float MachineZ } } - public override Enumerations.FlipDirection DisplayMirror => Enumerations.FlipDirection.Vertically; + public override FlipDirection DisplayMirror => FlipDirection.Vertically; public override byte AntiAliasing { @@ -323,40 +322,40 @@ public override float LightOffDelay public override float BottomWaitTimeBeforeCure { - get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.BottomLightOffDelay); + get => TimeConverter.MillisecondsToSeconds(ManifestFile.Profile.Slice.BottomWaitTimeBeforeCure); set { - ManifestFile.Profile.Slice.BottomLightOffDelay = TimeExtensions.SecondsToMillisecondsUint(value); + ManifestFile.Profile.Slice.BottomWaitTimeBeforeCure = TimeConverter.SecondsToMillisecondsUint(value); base.BottomWaitTimeBeforeCure = base.BottomLightOffDelay = value; } } public override float WaitTimeBeforeCure { - get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.LightOffDelay); + get => TimeConverter.MillisecondsToSeconds(ManifestFile.Profile.Slice.WaitTimeBeforeCure); set { - ManifestFile.Profile.Slice.LightOffDelay = TimeExtensions.SecondsToMillisecondsUint(value); + ManifestFile.Profile.Slice.WaitTimeBeforeCure = TimeConverter.SecondsToMillisecondsUint(value); base.WaitTimeBeforeCure = base.LightOffDelay = value; } } public override float BottomExposureTime { - get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.BottomExposureTime); + get => TimeConverter.MillisecondsToSeconds(ManifestFile.Profile.Slice.BottomExposureTime); set { - ManifestFile.Profile.Slice.BottomExposureTime = TimeExtensions.SecondsToMillisecondsUint(value); + ManifestFile.Profile.Slice.BottomExposureTime = TimeConverter.SecondsToMillisecondsUint(value); base.BottomExposureTime = value; } } public override float ExposureTime { - get => TimeExtensions.MillisecondsToSeconds(ManifestFile.Profile.Slice.ExposureTime); + get => TimeConverter.MillisecondsToSeconds(ManifestFile.Profile.Slice.ExposureTime); set { - ManifestFile.Profile.Slice.ExposureTime = TimeExtensions.SecondsToMillisecondsUint(value); + ManifestFile.Profile.Slice.ExposureTime = TimeConverter.SecondsToMillisecondsUint(value); base.ExposureTime = value; } } @@ -471,7 +470,7 @@ public ZCodeFile() protected override void EncodeInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); if (Thumbnails.Length > 0 && Thumbnails[0] is not null) { using var thumbnailsStream = outputFile.CreateEntry(PreviewFilename).Open(); @@ -479,7 +478,7 @@ protected override void EncodeInternally(OperationProgress progress) thumbnailsStream.Close(); } - EncodeLayersInZip(outputFile, Enumerations.IndexStartNumber.One, progress); + EncodeLayersInZip(outputFile, IndexStartNumber.One, progress); var entry = outputFile.CreateEntry(ManifestFilename); using (var stream = entry.Open()) @@ -546,7 +545,7 @@ protected override void DecodeInternally(OperationProgress progress) Init(ManifestFile.Job.LayerCount, DecodeType == FileDecodeType.Partial); - DecodeLayersFromZip(inputFile, Enumerations.IndexStartNumber.One, progress); + DecodeLayersFromZip(inputFile, IndexStartNumber.One, progress); GCode!.ParseLayersFromGCode(this); @@ -560,7 +559,7 @@ protected override void DecodeInternally(OperationProgress progress) protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); var entriesToRemove = outputFile.Entries.Where(zipEntry => zipEntry.Name.EndsWith(".gcode") || zipEntry.Name.EndsWith(".xml")).ToArray(); foreach (var zipEntry in entriesToRemove) diff --git a/UVtools.Core/FileFormats/ZCodexFile.cs b/UVtools.Core/FileFormats/ZCodexFile.cs index fc02a169..6ecbe4bb 100644 --- a/UVtools.Core/FileFormats/ZCodexFile.cs +++ b/UVtools.Core/FileFormats/ZCodexFile.cs @@ -16,6 +16,7 @@ using System.IO.Compression; using System.Text.Json; using System.Text.RegularExpressions; +using UVtools.Core.Converters; using UVtools.Core.Extensions; using UVtools.Core.GCode; using UVtools.Core.Layers; @@ -210,9 +211,9 @@ public override float DisplayHeight set { } } - public override Enumerations.FlipDirection DisplayMirror + public override FlipDirection DisplayMirror { - get => Enumerations.FlipDirection.Horizontally; + get => FlipDirection.Horizontally; set { } } @@ -256,30 +257,30 @@ public override ushort BottomLayerCount public override float BottomWaitTimeBeforeCure => WaitTimeBeforeCure; public override float WaitTimeBeforeCure { - get => TimeExtensions.MillisecondsToSeconds(ResinMetadataSettings.BlankingLayerTime); + get => TimeConverter.MillisecondsToSeconds(ResinMetadataSettings.BlankingLayerTime); set { - UserSettings.ExposureOffTime = ResinMetadataSettings.BlankingLayerTime = TimeExtensions.SecondsToMillisecondsUint(value); + UserSettings.ExposureOffTime = ResinMetadataSettings.BlankingLayerTime = TimeConverter.SecondsToMillisecondsUint(value); base.WaitTimeBeforeCure = base.LightOffDelay = value; } } public override float BottomExposureTime { - get => TimeExtensions.MillisecondsToSeconds(UserSettings.BottomLayerExposureTime); + get => TimeConverter.MillisecondsToSeconds(UserSettings.BottomLayerExposureTime); set { - ResinMetadataSettings.BottomLayersTime = UserSettings.BottomLayerExposureTime = TimeExtensions.SecondsToMillisecondsUint(value); + ResinMetadataSettings.BottomLayersTime = UserSettings.BottomLayerExposureTime = TimeConverter.SecondsToMillisecondsUint(value); base.BottomExposureTime = value; } } public override float ExposureTime { - get => TimeExtensions.MillisecondsToSeconds(UserSettings.LayerExposureTime); + get => TimeConverter.MillisecondsToSeconds(UserSettings.LayerExposureTime); set { - ResinMetadataSettings.LayerTime = UserSettings.LayerExposureTime = TimeExtensions.SecondsToMillisecondsUint(value); + ResinMetadataSettings.LayerTime = UserSettings.LayerExposureTime = TimeConverter.SecondsToMillisecondsUint(value); base.ExposureTime = value; } } @@ -357,7 +358,7 @@ public ZCodexFile() GCode = new() { UseComments = true, - GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Partial, + GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Relative, GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, @@ -389,7 +390,7 @@ protected override void EncodeInternally(OperationProgress progress) }); } - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Create); outputFile.PutFileContent("ResinMetadata", JsonSerializer.SerializeToUtf8Bytes(ResinMetadataSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Create); outputFile.PutFileContent("UserSettingsData", JsonSerializer.SerializeToUtf8Bytes(UserSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Create); outputFile.PutFileContent("ZCodeMetadata", JsonSerializer.SerializeToUtf8Bytes(ZCodeMetadataSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Create); @@ -401,7 +402,7 @@ protected override void EncodeInternally(OperationProgress progress) stream.Close(); } - EncodeLayersInZip(outputFile, FolderImageName, 5, Enumerations.IndexStartNumber.Zero, progress, FolderImages); + EncodeLayersInZip(outputFile, FolderImageName, 5, IndexStartNumber.Zero, progress, FolderImages); GCode!.Clear(); @@ -486,7 +487,7 @@ protected override void DecodeInternally(OperationProgress progress) } Init(ResinMetadataSettings.TotalLayersCount, DecodeType == FileDecodeType.Partial); - DecodeLayersFromZip(inputFile, FolderImageName, Enumerations.IndexStartNumber.Zero, progress); + DecodeLayersFromZip(inputFile, FolderImageName, IndexStartNumber.Zero, progress); GCode!.Clear(); using (TextReader tr = new StreamReader(entry.Open())) @@ -571,7 +572,7 @@ M106 S0 /*if (DecodeType == FileDecodeType.Full) { using var stream = LayersSettings[layerIndex].LayerEntry!.Open(); - this[layerIndex] = new Layer((uint)layerIndex, stream, this); + _layers[layerIndex] = new Layer((uint)layerIndex, stream, this); }*/ this[layerIndex].PositionZ = currentHeight; @@ -615,7 +616,7 @@ public override void RebuildGCode() protected override void PartialSaveInternally(OperationProgress progress) { - using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + using var outputFile = ZipFile.Open(TemporaryOutputFileFullPath, ZipArchiveMode.Update); outputFile.PutFileContent("ResinMetadata", JsonSerializer.SerializeToUtf8Bytes(ResinMetadataSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Update); outputFile.PutFileContent("UserSettingsData", JsonSerializer.SerializeToUtf8Bytes(UserSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Update); outputFile.PutFileContent("ZCodeMetadata", JsonSerializer.SerializeToUtf8Bytes(ZCodeMetadataSettings, JsonExtensions.SettingsIndent), ZipArchiveMode.Update); diff --git a/UVtools.Core/GCode/GCodeBuilder.cs b/UVtools.Core/GCode/GCodeBuilder.cs index d56d9cd9..98b224fa 100644 --- a/UVtools.Core/GCode/GCodeBuilder.cs +++ b/UVtools.Core/GCode/GCodeBuilder.cs @@ -16,7 +16,7 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using UVtools.Core.Extensions; +using UVtools.Core.Converters; using UVtools.Core.FileFormats; using UVtools.Core.Layers; using UVtools.Core.Objects; @@ -51,7 +51,7 @@ public class GCodeBuilder : BindableBase public enum GCodePositioningTypes : byte { Absolute, - Partial + Relative } public enum GCodeTimeUnits : byte @@ -317,15 +317,74 @@ public void AppendStartGCode() AppendLineIfCanComment(BeginStartGCodeComments); AppendUnitsMmG21(); AppendPositioningType(); - AppendLightOffM106(); AppendMotorsOn(); + AppendLightOffM106(); AppendClearImage(); AppendHomeZG28(); AppendLineIfCanComment(EndStartGCodeComments); AppendLine(); } - public void AppendEndGCode(float raiseZ = 0, float feedRate = 0) + public void AppendEndGCode(FileFormat slicerFile) + { + AppendLineIfCanComment(BeginEndGCodeComments); + AppendLightOffM106(); + + var lastLayer = slicerFile.LastLayer; + + if (lastLayer is not null && lastLayer.PositionZ < slicerFile.MachineZ) + { + float lastLiftHeight = Math.Max(4, lastLayer.LiftHeight); + float lastPosition = Math.Min(Layer.RoundHeight(lastLayer.PositionZ + lastLiftHeight), slicerFile.MachineZ); + + var lift = new List<(float z, float feedrate)>(); + switch (_gCodePositioningType) + { + case GCodePositioningTypes.Absolute: + lift.Add((lastPosition, ConvertFromMillimetersPerMinute(slicerFile.LiftSpeed))); + break; + case GCodePositioningTypes.Relative: + lift.Add((lastLiftHeight, ConvertFromMillimetersPerMinute(slicerFile.LiftSpeed))); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + AppendLiftMoveGx(lift, new List<(float z, float feedrate)>(), 0, 0, lastLayer); + + if (lastPosition < slicerFile.MachineZ) + { + float finalRaiseZPositionRelative = Layer.RoundHeight(slicerFile.MachineZ - lastPosition); + var finalRaiseZPosition = _gCodePositioningType switch + { + GCodePositioningTypes.Relative => finalRaiseZPositionRelative, + _ => slicerFile.MachineZ + }; + + + if (finalRaiseZPositionRelative > 0) + { + if (_endGCodeMoveCommand == GCodeMoveCommands.G0) + AppendMoveG0(finalRaiseZPosition, ConvertFromMillimetersPerMinute(slicerFile.MaximumSpeed)); + else + AppendMoveG1(finalRaiseZPosition, ConvertFromMillimetersPerMinute(slicerFile.MaximumSpeed)); + + if (_syncMovementsWithDelay) + { + var seconds = OperationCalculator.LightOffDelayC.CalculateSecondsLiftOnly( + finalRaiseZPositionRelative + lastLiftHeight, slicerFile.MaximumSpeed, 0.75f); + var time = ConvertFromSeconds(seconds); + if (seconds > 0) AppendWaitG4($"0{time}", "Sync movement"); + } + } + } + } + + AppendMotorsOff(); + AppendLineIfCanComment(EndEndGCodeComments); + } + + public void AppendEndGCode(float raiseZ = 0, float feedRate = 0, float absRaiseZ = 0, float rawSpeed = 0) { AppendLineIfCanComment(BeginEndGCodeComments); AppendLightOffM106(); @@ -335,6 +394,13 @@ public void AppendEndGCode(float raiseZ = 0, float feedRate = 0) AppendMoveG0(raiseZ, feedRate); else AppendMoveG1(raiseZ, feedRate); + + if (_syncMovementsWithDelay) + { + var seconds = OperationCalculator.LightOffDelayC.CalculateSecondsLiftOnly(absRaiseZ, rawSpeed, 0.75f); + var time = ConvertFromSeconds(seconds); + AppendWaitG4($"0{time}", "Sync movement"); + } } AppendMotorsOff(); @@ -353,7 +419,7 @@ public void AppendPositioningType() case GCodePositioningTypes.Absolute: AppendLine(CommandPositioningAbsoluteG90); break; - case GCodePositioningTypes.Partial: + case GCodePositioningTypes.Relative: AppendLine(CommandPositioningPartialG91); break; default: @@ -429,7 +495,7 @@ public void AppendLiftMoveG0(List<(float z, float feedrate)> lifts, List<(float AppendWaitG4(waitAfterLift, "Wait after lift"); } - if (retracts.Count > 0 || retracts.All(tuple => tuple.z != 0)) + if (retracts.Count > 0 && retracts.All(tuple => tuple.z != 0)) { for (var i = 0; i < retracts.Count; i++) { @@ -443,11 +509,11 @@ public void AppendLiftMoveG0(List<(float z, float feedrate)> lifts, List<(float var time = ConvertFromSeconds(seconds); AppendWaitG4($"0{time}", "Sync movement"); } - } - if (waitAfterRetract > 0) - { - AppendWaitG4(waitAfterRetract, "Wait after retract"); + if (waitAfterRetract > 0) + { + AppendWaitG4(waitAfterRetract, "Wait after retract"); + } } } @@ -486,7 +552,7 @@ public void AppendLiftMoveG1(List<(float z, float feedrate)> lifts, List<(float AppendWaitG4(waitAfterLift, "Wait after lift"); } - if (retracts.Count > 0 || retracts.All(tuple => tuple.z != 0)) + if (retracts.Count > 0 && retracts.All(tuple => tuple.z != 0)) { for (var i = 0; i < retracts.Count; i++) { @@ -500,11 +566,11 @@ public void AppendLiftMoveG1(List<(float z, float feedrate)> lifts, List<(float var time = ConvertFromSeconds(seconds); AppendWaitG4($"0{time}", "Sync movement"); } - } - if (waitAfterRetract > 0) - { - AppendWaitG4(waitAfterRetract, "Wait after retract"); + if (waitAfterRetract > 0) + { + AppendWaitG4(waitAfterRetract, "Wait after retract"); + } } } @@ -637,7 +703,7 @@ public void RebuildGCode(FileFormat slicerFile, string? header = null) if (retractHeight > 0 && absRetractPos >= layer.PositionZ) retracts.Add((absRetractPos, retractSpeed)); if (retractHeight2 > 0 && absRetractPos > layer.PositionZ) retracts.Add((layer.PositionZ, retractSpeed2)); break; - case GCodePositioningTypes.Partial: + case GCodePositioningTypes.Relative: var partialLiftPos = Layer.RoundHeight(layer.PositionZ - lastZPosition + liftHeight); if (liftHeight > 0) { @@ -680,7 +746,7 @@ public void RebuildGCode(FileFormat slicerFile, string? header = null) case GCodePositioningTypes.Absolute: AppendMoveGx(layer.PositionZ, lastZPosition < layer.PositionZ ? liftSpeed : retractSpeed); break; - case GCodePositioningTypes.Partial: + case GCodePositioningTypes.Relative: AppendMoveGx(Layer.RoundHeight(layer.PositionZ - lastZPosition), lastZPosition < layer.PositionZ ? liftSpeed : retractSpeed); break; } @@ -697,17 +763,7 @@ public void RebuildGCode(FileFormat slicerFile, string? header = null) lastZPosition = layer.PositionZ; } - float finalRaiseZPosition = Math.Max(lastZPosition, slicerFile.MachineZ); - switch (GCodePositioningType) - { - - case GCodePositioningTypes.Partial: - finalRaiseZPosition = Layer.RoundHeight(finalRaiseZPosition - lastZPosition); - break; - } - - - AppendEndGCode(finalRaiseZPosition, ConvertFromMillimetersPerMinute(slicerFile.RetractSpeed)); + AppendEndGCode(slicerFile); } public void RebuildGCode(FileFormat slicerFile, object[]? configs, string separator = ":") @@ -746,7 +802,7 @@ public GCodePositioningTypes ParsePositioningTypeFromGCode(string gcode) while ((line = reader.ReadLine()) != null) { if (line.StartsWith(CommandPositioningAbsoluteG90.Command)) return GCodePositioningTypes.Absolute; - if (line.StartsWith(CommandPositioningPartialG91.Command)) return GCodePositioningTypes.Partial; + if (line.StartsWith(CommandPositioningPartialG91.Command)) return GCodePositioningTypes.Relative; } return _gCodePositioningType; @@ -797,7 +853,7 @@ public void ParseLayersFromGCode(FileFormat slicerFile, string? gcode, bool rebu if (line.StartsWith(CommandPositioningPartialG91.Command)) { - positionType = GCodePositioningTypes.Partial; + positionType = GCodePositioningTypes.Relative; continue; } @@ -1098,7 +1154,7 @@ public float ConvertFromSeconds(float seconds) return _gCodeTimeUnit switch { GCodeTimeUnits.Seconds => seconds, - GCodeTimeUnits.Milliseconds => TimeExtensions.SecondsToMilliseconds(seconds), + GCodeTimeUnits.Milliseconds => TimeConverter.SecondsToMilliseconds(seconds), _ => throw new InvalidExpressionException($"Unhandled time unit for {_gCodeTimeUnit}") }; } @@ -1129,7 +1185,7 @@ public float ConvertToSeconds(float time) return _gCodeTimeUnit switch { GCodeTimeUnits.Seconds => time, - GCodeTimeUnits.Milliseconds => TimeExtensions.MillisecondsToSeconds(time), + GCodeTimeUnits.Milliseconds => TimeConverter.MillisecondsToSeconds(time), _ => throw new InvalidExpressionException($"Unhandled time unit for {_gCodeTimeUnit}") }; } diff --git a/UVtools.Core/GCode/GCodeLayer.cs b/UVtools.Core/GCode/GCodeLayer.cs index baf2c275..3f9501e7 100644 --- a/UVtools.Core/GCode/GCodeLayer.cs +++ b/UVtools.Core/GCode/GCodeLayer.cs @@ -177,7 +177,7 @@ public void AssignMovements(GCodeBuilder.GCodePositioningTypes positionType) partialPositionZ = Layer.RoundHeight(pos - currentZ); currentZ = pos; break; - case GCodeBuilder.GCodePositioningTypes.Partial: + case GCodeBuilder.GCodePositioningTypes.Relative: partialPositionZ = pos; currentZ = Layer.RoundHeight(currentZ + pos); break; diff --git a/UVtools.Core/Layers/Layer.cs b/UVtools.Core/Layers/Layer.cs index f5b49aa2..2015ac72 100644 --- a/UVtools.Core/Layers/Layer.cs +++ b/UVtools.Core/Layers/Layer.cs @@ -15,6 +15,8 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Text.Json.Serialization; +using System.Xml.Serialization; using K4os.Compression.LZ4; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; @@ -649,6 +651,8 @@ public byte[]? CompressedPngBytes /// /// Gets or sets a new image instance /// + [XmlIgnore] + [JsonInclude] public Mat LayerMat { get @@ -728,6 +732,8 @@ public Mat LayerMat /// /// Gets a new Brg image instance /// + [XmlIgnore] + [JsonInclude] public Mat BrgMat { get @@ -1087,20 +1093,20 @@ public void SetNoDelays() WaitTimeAfterLift = 0; } - public string FormatFileName(string prepend = "", byte padDigits = 0, Enumerations.IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") + public string FormatFileName(string prepend = "", byte padDigits = 0, IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") { var index = Index; - if (layerIndexStartNumber == Enumerations.IndexStartNumber.One) + if (layerIndexStartNumber == IndexStartNumber.One) { index++; } return $"{prepend}{index.ToString().PadLeft(padDigits, '0')}{appendExt}"; } - public string FormatFileName(byte padDigits, Enumerations.IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") + public string FormatFileName(byte padDigits, IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") => FormatFileName(string.Empty, padDigits, layerIndexStartNumber, appendExt); - public string FormatFileNameWithLayerDigits(string prepend = "", Enumerations.IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") + public string FormatFileNameWithLayerDigits(string prepend = "", IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") => FormatFileName(prepend, SlicerFile.LayerDigits, layerIndexStartNumber, appendExt); diff --git a/UVtools.Core/Managers/MatCacheManager.cs b/UVtools.Core/Managers/MatCacheManager.cs index 82c19d2f..2934c3fd 100644 --- a/UVtools.Core/Managers/MatCacheManager.cs +++ b/UVtools.Core/Managers/MatCacheManager.cs @@ -51,12 +51,12 @@ public class MatCacheManager : IDisposable /// /// Gets the image rotation to cache /// - public Enumerations.RotateDirection Rotate { get; init; } = Enumerations.RotateDirection.None; + public RotateDirection Rotate { get; init; } = RotateDirection.None; /// /// Gets the image flip to cache /// - public Enumerations.FlipDirection Flip { get; init; } = Enumerations.FlipDirection.None; + public FlipDirection Flip { get; init; } = FlipDirection.None; /// /// Gets if striping anti-aliasing is enabled, cache will be threshold'ed @@ -168,12 +168,12 @@ public Mat[] Get(uint layerIndex) MatCache[currentCacheIndex] = new Mat[ElementsPerCache]; MatCache[currentCacheIndex][0] = SlicerFile[currentLayerIndex].LayerMat; - if (Flip != Enumerations.FlipDirection.None) + if (Flip != FlipDirection.None) { CvInvoke.Flip(MatCache[currentCacheIndex][0], MatCache[currentCacheIndex][0], Enumerations.ToOpenCVFlipType(Flip)); } - if (Rotate != Enumerations.RotateDirection.None) + if (Rotate != RotateDirection.None) { CvInvoke.Rotate(MatCache[currentCacheIndex][0], MatCache[currentCacheIndex][0], Enumerations.ToOpenCVRotateFlags(Rotate)); } diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index 4b9f7880..c22cee7d 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -50,7 +50,7 @@ public enum OperationImportFrom : byte private uint _layerIndexStart; private string? _profileName; private bool _profileIsDefault; - private Enumerations.LayerRangeSelection _layerRangeSelection = Enumerations.LayerRangeSelection.All; + private LayerRangeSelection _layerRangeSelection = LayerRangeSelection.All; public const byte ClassNameLength = 9; #endregion @@ -93,12 +93,12 @@ public Rectangle OriginalBoundingRectangle /// public string Id => GetType().Name.Remove(0, ClassNameLength); - public virtual Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.All; + public virtual LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.All; /// /// Gets the last used layer range selection, returns none if custom /// - public Enumerations.LayerRangeSelection LayerRangeSelection + public LayerRangeSelection LayerRangeSelection { get => _layerRangeSelection; set => RaiseAndSetIfChanged(ref _layerRangeSelection, value); @@ -108,7 +108,7 @@ public virtual string LayerRangeString { get { - if (LayerRangeSelection == Enumerations.LayerRangeSelection.None) + if (LayerRangeSelection == LayerRangeSelection.None) { return $" [Layers: {LayerIndexStart}-{LayerIndexEnd}]"; } @@ -354,39 +354,39 @@ public void SelectAllLayers() { LayerIndexStart = 0; LayerIndexEnd = SlicerFile.LastLayerIndex; - LayerRangeSelection = Enumerations.LayerRangeSelection.All; + LayerRangeSelection = LayerRangeSelection.All; } public void SelectCurrentLayer(uint layerIndex) { LayerIndexStart = LayerIndexEnd = layerIndex; - LayerRangeSelection = Enumerations.LayerRangeSelection.Current; + LayerRangeSelection = LayerRangeSelection.Current; } public void SelectBottomLayers() { LayerIndexStart = 0; LayerIndexEnd = Math.Max(1, SlicerFile.FirstNormalLayer?.Index ?? 1) - 1u; - LayerRangeSelection = Enumerations.LayerRangeSelection.Bottom; + LayerRangeSelection = LayerRangeSelection.Bottom; } public void SelectNormalLayers() { LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; LayerIndexEnd = SlicerFile.LastLayerIndex; - LayerRangeSelection = Enumerations.LayerRangeSelection.Normal; + LayerRangeSelection = LayerRangeSelection.Normal; } public void SelectFirstLayer() { LayerIndexStart = LayerIndexEnd = 0; - LayerRangeSelection = Enumerations.LayerRangeSelection.First; + LayerRangeSelection = LayerRangeSelection.First; } public void SelectLastLayer() { LayerIndexStart = LayerIndexEnd = SlicerFile.LastLayerIndex; - LayerRangeSelection = Enumerations.LayerRangeSelection.Last; + LayerRangeSelection = LayerRangeSelection.Last; } public void SelectFirstToCurrentLayer(uint currentLayerIndex) @@ -401,28 +401,28 @@ public void SelectCurrentToLastLayer(uint currentLayerIndex) LayerIndexEnd = SlicerFile.LastLayerIndex; } - public void SelectLayers(Enumerations.LayerRangeSelection range) + public void SelectLayers(LayerRangeSelection range) { switch (range) { - case Enumerations.LayerRangeSelection.None: + case LayerRangeSelection.None: break; - case Enumerations.LayerRangeSelection.All: + case LayerRangeSelection.All: SelectAllLayers(); break; - case Enumerations.LayerRangeSelection.Current: + case LayerRangeSelection.Current: //SelectCurrentLayer(); break; - case Enumerations.LayerRangeSelection.Bottom: + case LayerRangeSelection.Bottom: SelectBottomLayers(); break; - case Enumerations.LayerRangeSelection.Normal: + case LayerRangeSelection.Normal: SelectNormalLayers(); break; - case Enumerations.LayerRangeSelection.First: + case LayerRangeSelection.First: SelectFirstLayer(); break; - case Enumerations.LayerRangeSelection.Last: + case LayerRangeSelection.Last: SelectLastLayer(); break; default: @@ -557,14 +557,14 @@ public bool Execute(OperationProgress? progress = null) return result; } - public async Task ExecuteAsync(OperationProgress? progress = null) => await new Task(() => Execute(progress)); + public Task ExecuteAsync(OperationProgress? progress = null) => Task.Run(() => Execute(progress), progress?.Token ?? default); public virtual bool Execute(Mat mat, params object[]? arguments) { throw new NotImplementedException(); } - public async Task ExecuteAsync(Mat mat, params object[]? arguments) => await new Task(() => Execute(mat, arguments)); + public Task ExecuteAsync(Mat mat, params object[]? arguments) => Task.Run(() => Execute(mat, arguments)); /// /// Get the selected layer range in a new array, array index will not match layer index when a range is selected diff --git a/UVtools.Core/Operations/OperationCalculator.cs b/UVtools.Core/Operations/OperationCalculator.cs index b6395e39..fe407afb 100644 --- a/UVtools.Core/Operations/OperationCalculator.cs +++ b/UVtools.Core/Operations/OperationCalculator.cs @@ -32,7 +32,7 @@ public class OperationCalculator : Operation public override string ProgressAction => null!; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override bool CanHaveProfiles => false; diff --git a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs index b8e17aa3..bce1855d 100644 --- a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs +++ b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs @@ -57,7 +57,7 @@ public sealed class OperationCalibrateElephantFoot : Operation public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "mdi-elephant"; public override string Title => "Elephant foot"; public override string Description => @@ -380,7 +380,7 @@ public override void InitWithSlicerFile() if (_bottomLayers <= 0) _bottomLayers = (ushort) Slicer.Slicer.MillimetersToLayers(1M, _layerHeight); if (_normalLayers <= 0) _normalLayers = (ushort) Slicer.Slicer.MillimetersToLayers(3.5M, _layerHeight); - _mirrorOutput = SlicerFile.DisplayMirror != Enumerations.FlipDirection.None; + _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; } #endregion @@ -670,7 +670,7 @@ void addText(Mat mat, ushort number, params string[]? text) if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; - if (flip == Enumerations.FlipDirection.None) flip = Enumerations.FlipDirection.Horizontally; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); } diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index 3a281d6f..5adc1752 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -202,7 +202,7 @@ public BullsEyeCircle(ushort diameter, ushort thickness) public override bool CanROI => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "mdi-timer-cog"; public override string Title => "Exposure time finder"; public override string Description => @@ -1162,7 +1162,7 @@ public override void InitWithSlicerFile() { base.InitWithSlicerFile(); - _mirrorOutput = SlicerFile.DisplayMirror != Enumerations.FlipDirection.None; + _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; @@ -2237,7 +2237,7 @@ lastExposureItem is not null && if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; - if (flip == Enumerations.FlipDirection.None) flip = Enumerations.FlipDirection.Horizontally; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; new OperationFlip(SlicerFile) { FlipDirection = Enumerations.ToOpenCVFlipType(flip) }.Execute(progress); } } diff --git a/UVtools.Core/Operations/OperationCalibrateExternalTests.cs b/UVtools.Core/Operations/OperationCalibrateExternalTests.cs index bebef99d..8cc5f3c1 100644 --- a/UVtools.Core/Operations/OperationCalibrateExternalTests.cs +++ b/UVtools.Core/Operations/OperationCalibrateExternalTests.cs @@ -18,7 +18,7 @@ public class OperationCalibrateExternalTests : Operation #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override bool CanHaveProfiles => false; public override string ButtonOkText => null!; diff --git a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs index 49bc53e0..4b64dd46 100644 --- a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs +++ b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs @@ -54,7 +54,7 @@ public sealed class OperationCalibrateGrayscale : Operation public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fas fa-chart-pie"; public override string Title => "Grayscale"; public override string Description => @@ -114,7 +114,7 @@ public override void InitWithSlicerFile() if(_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if(_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if(_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.DisplayMirror != Enumerations.FlipDirection.None; + _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; } #endregion @@ -466,7 +466,7 @@ public Mat[] GetLayers() if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; - if (flip == Enumerations.FlipDirection.None) flip = Enumerations.FlipDirection.Horizontally; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); } diff --git a/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs b/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs index db74a365..45d81ac3 100644 --- a/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs +++ b/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs @@ -46,7 +46,7 @@ public sealed class OperationCalibrateLiftHeight : Operation public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "mdi-arrow-expand-up"; public override string Title => "Lift height"; public override string Description => diff --git a/UVtools.Core/Operations/OperationCalibrateStressTower.cs b/UVtools.Core/Operations/OperationCalibrateStressTower.cs index ff220304..c651fe4e 100644 --- a/UVtools.Core/Operations/OperationCalibrateStressTower.cs +++ b/UVtools.Core/Operations/OperationCalibrateStressTower.cs @@ -49,7 +49,7 @@ public sealed class OperationCalibrateStressTower : Operation public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fas fa-chess-rook"; public override string Title => "Stress tower"; public override string Description => @@ -111,7 +111,7 @@ public override void InitWithSlicerFile() if(_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if(_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if(_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.DisplayMirror != Enumerations.FlipDirection.None; + _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; @@ -386,7 +386,7 @@ public Mat[] GetLayers() if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; - if (flip == Enumerations.FlipDirection.None) flip = Enumerations.FlipDirection.Horizontally; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); } diff --git a/UVtools.Core/Operations/OperationCalibrateTolerance.cs b/UVtools.Core/Operations/OperationCalibrateTolerance.cs index 57615bcc..d675c827 100644 --- a/UVtools.Core/Operations/OperationCalibrateTolerance.cs +++ b/UVtools.Core/Operations/OperationCalibrateTolerance.cs @@ -59,7 +59,7 @@ public sealed class OperationCalibrateTolerance : Operation public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fas fa-dot-circle"; public override string Title => "Tolerance"; public override string Description => @@ -430,7 +430,7 @@ public override void InitWithSlicerFile() if (_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if (_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if (_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.DisplayMirror != Enumerations.FlipDirection.None; + _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; @@ -686,7 +686,7 @@ bool addPart(decimal step) if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; - if (flip == Enumerations.FlipDirection.None) flip = Enumerations.FlipDirection.Horizontally; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); } diff --git a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs index 77b6a1dd..8e64255c 100644 --- a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs +++ b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs @@ -60,7 +60,7 @@ public sealed class OperationCalibrateXYZAccuracy : Operation public override bool CanCancel => false; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fas fa-cubes"; public override string Title => "XYZ Accuracy"; public override string Description => @@ -464,7 +464,7 @@ public override void InitWithSlicerFile() if (_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if (_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if (_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.DisplayMirror != Enumerations.FlipDirection.None; + _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; @@ -721,7 +721,7 @@ public Mat[] GetLayers() if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; - if (flip == Enumerations.FlipDirection.None) flip = Enumerations.FlipDirection.Horizontally; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, Enumerations.ToOpenCVFlipType(flip))); } diff --git a/UVtools.Core/Operations/OperationChangeResolution.cs b/UVtools.Core/Operations/OperationChangeResolution.cs index 57623662..b9bd0aa3 100644 --- a/UVtools.Core/Operations/OperationChangeResolution.cs +++ b/UVtools.Core/Operations/OperationChangeResolution.cs @@ -23,6 +23,8 @@ public sealed class OperationChangeResolution : Operation private uint _newResolutionX; private uint _newResolutionY; private bool _fixRatio; + private decimal _newDisplayWidth; + private decimal _newDisplayHeight; #endregion @@ -56,7 +58,7 @@ public override string ToString() #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override string IconClass => "mdi-resize"; public override string Title => "Change print resolution"; @@ -64,13 +66,11 @@ public override string ToString() "Crops or resizes all layer images to fit an alternate print resolution.\n" + "Useful to make files printable on a different printer than they were originally sliced for without the need to re-slice.\n\n" + "NOTE: Please ensure that the actual model will fit within the new print resolution. The operation will be aborted if it will result in any of the actual model being clipped.\n" + - "Only use this tool if both source and target printer have the same pixel pitch spec, otherwise the model size will be invalidated and result in a different size than the originally sliced for. " + - "As alternative is possible to resize the model to match the new pixel pitch."; + "Only use this tool if both source and target printer have the same pixel pitch spec, otherwise the model size will be invalidated and result in a different size than the originally sliced for.\n" + + "As alternative, set the correct display size of the target printer or select an machine preset to match the new pixel pitch and let it auto fix the pixel ratio. It's always good practice to set the correct display size, even if you don't want to fix the pixel ratio, that is a critical information for both software and printers."; public override string ConfirmationText => - "change print resolution " + - $"from {SlicerFile.ResolutionX}x{SlicerFile.ResolutionY} " + - $"to {NewResolutionX}x{NewResolutionY}?"; + $"change print resolution from {SlicerFile.ResolutionX}x{SlicerFile.ResolutionY} to {NewResolutionX}x{NewResolutionY}?"; public override string ProgressTitle => $"Changing print resolution from ({SlicerFile.ResolutionX}x{SlicerFile.ResolutionY}) to ({NewResolutionX}x{NewResolutionY})"; @@ -98,7 +98,7 @@ public override string ToString() public override string ToString() { - var result = $"{_newResolutionX} x {_newResolutionY} [Fix ratio: {_fixRatio}]"; + var result = $"{_newResolutionX}x{_newResolutionY} [Display: {_newDisplayWidth}x{_newDisplayHeight}] [Fix ratio: {_fixRatio}]"; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; } @@ -111,8 +111,8 @@ public uint NewResolutionX get => _newResolutionX; set { - if(!RaiseAndSetIfChanged(ref _newResolutionX, value)) return; - RaisePropertyChanged(nameof(NewRatioX)); + if(!RaiseAndSetIfChanged(ref _newResolutionX, Math.Max(1, value))) return; + RaisePropertyChanged(nameof(NewPixelSizeMicrons)); RaisePropertyChanged(nameof(NewFixedRatioX)); RaisePropertyChanged(nameof(FinalBoundsWidth)); } @@ -123,18 +123,63 @@ public uint NewResolutionY get => _newResolutionY; set { - RaiseAndSetIfChanged(ref _newResolutionY, value); - RaisePropertyChanged(nameof(NewRatioY)); + if(!RaiseAndSetIfChanged(ref _newResolutionY, Math.Max(1, value))) return; + RaisePropertyChanged(nameof(NewPixelSizeMicrons)); RaisePropertyChanged(nameof(NewFixedRatioY)); RaisePropertyChanged(nameof(FinalBoundsHeight)); } } - public double NewRatioX => Math.Round((double)SlicerFile.ResolutionX / _newResolutionX, 2); - public double NewRatioY => Math.Round((double)SlicerFile.ResolutionY / _newResolutionY, 2); + public decimal NewDisplayWidth + { + get => _newDisplayWidth; + set + { + if(!RaiseAndSetIfChanged(ref _newDisplayWidth, Math.Max(0, value))) return; + RaisePropertyChanged(nameof(NewPixelSizeMicrons)); + RaisePropertyChanged(nameof(NewFixedRatioX)); + } + } + + public decimal NewDisplayHeight + { + get => _newDisplayHeight; + set + { + if (!RaiseAndSetIfChanged(ref _newDisplayHeight, Math.Max(0, value))) return; + RaisePropertyChanged(nameof(NewPixelSizeMicrons)); + RaisePropertyChanged(nameof(NewFixedRatioY)); + } + } + + public SizeF NewPixelSizeMicrons => + new (_newResolutionX <= 0 || SlicerFile.Display.Width <= 0 || _newDisplayWidth <= 0 + ? SlicerFile.PixelWidthMicrons + : (float) Math.Round((float) _newDisplayWidth / _newResolutionX * 1000, 3), + _newResolutionY <= 0 || SlicerFile.Display.Height <= 0 || _newDisplayHeight <= 0 + ? SlicerFile.PixelHeightMicrons + : (float) Math.Round((float) _newDisplayHeight / _newResolutionY * 1000, 3)); + + public double NewFixedRatioX + { - public double NewFixedRatioX => Math.Round((double)_newResolutionX / SlicerFile.ResolutionX, 2); - public double NewFixedRatioY => Math.Round((double)_newResolutionY / SlicerFile.ResolutionY, 2); + get + { + if (_newResolutionX <= 0 || SlicerFile.Display.Width <= 0 || _newDisplayWidth <= 0) return 1; + var ratio = SlicerFile.PixelWidth / ((double)_newDisplayWidth / _newResolutionX); + return Math.Round(ratio, 3); + } + } + + public double NewFixedRatioY + { + get + { + if (_newResolutionY <= 0 || SlicerFile.Display.Height <= 0 || _newDisplayHeight <= 0) return 1; + var ratio = SlicerFile.PixelHeight / ((double)_newDisplayHeight / _newResolutionY); + return Math.Round(ratio, 3); + } + } public bool FixRatio { @@ -164,6 +209,8 @@ public override void InitWithSlicerFile() base.InitWithSlicerFile(); if(_newResolutionX <= 0) _newResolutionX = SlicerFile.ResolutionX; if(_newResolutionY <= 0) _newResolutionY = SlicerFile.ResolutionY; + if(_newDisplayWidth <= 0) _newDisplayWidth = (decimal) SlicerFile.DisplayWidth; + if(_newDisplayHeight <= 0) _newDisplayHeight = (decimal) SlicerFile.DisplayHeight; } #endregion @@ -217,6 +264,7 @@ protected override bool ExecuteInternally(OperationProgress progress) var newFixedRatioY = NewFixedRatioY; if (_fixRatio && (newFixedRatioX != 1.0 || newFixedRatioY != 1.0)) { + //mat.TransformFromCenter(); CvInvoke.Resize(mat, mat, SlicerFile.Resolution.Multiply(newFixedRatioX, newFixedRatioY)); } @@ -228,8 +276,19 @@ protected override bool ExecuteInternally(OperationProgress progress) progress.LockAndIncrement(); }); - SlicerFile.ResolutionX = NewResolutionX; - SlicerFile.ResolutionY = NewResolutionY; + SlicerFile.ResolutionX = _newResolutionX; + SlicerFile.ResolutionY = _newResolutionY; + if (_newDisplayWidth > 0) + { + SlicerFile.DisplayWidth = (float) _newDisplayWidth; + } + + if (_newDisplayHeight > 0) + { + SlicerFile.DisplayHeight = (float)_newDisplayHeight; + } + + SlicerFile.BoundingRectangle = Rectangle.Empty; return !progress.Token.IsCancellationRequested; } @@ -254,7 +313,7 @@ protected override bool ExecuteInternally(OperationProgress progress) private bool Equals(OperationChangeResolution other) { - return _newResolutionX == other._newResolutionX && _newResolutionY == other._newResolutionY && _fixRatio == other._fixRatio; + return _newResolutionX == other._newResolutionX && _newResolutionY == other._newResolutionY && _fixRatio == other._fixRatio && _newDisplayWidth == other._newDisplayWidth && _newDisplayHeight == other._newDisplayHeight; } public override bool Equals(object? obj) @@ -264,7 +323,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(_newResolutionX, _newResolutionY, _fixRatio); + return HashCode.Combine(_newResolutionX, _newResolutionY, _fixRatio, _newDisplayWidth, _newDisplayHeight); } #endregion diff --git a/UVtools.Core/Operations/OperationDoubleExposure.cs b/UVtools.Core/Operations/OperationDoubleExposure.cs index 5b6d42da..9cfd608d 100644 --- a/UVtools.Core/Operations/OperationDoubleExposure.cs +++ b/UVtools.Core/Operations/OperationDoubleExposure.cs @@ -41,7 +41,7 @@ public class OperationDoubleExposure : Operation #endregion #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Bottom; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Bottom; public override string IconClass => "fas fa-grip-lines"; public override string Title => "Double exposure"; public override string Description => diff --git a/UVtools.Core/Operations/OperationDynamicLifts.cs b/UVtools.Core/Operations/OperationDynamicLifts.cs index 90d2c6e0..10e9e1f8 100644 --- a/UVtools.Core/Operations/OperationDynamicLifts.cs +++ b/UVtools.Core/Operations/OperationDynamicLifts.cs @@ -66,7 +66,7 @@ public enum DynamicLiftsLightOffDelaySetMode : byte public override bool CanRunInPartialMode => true; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Normal; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Normal; public override string IconClass => "fas fa-chart-line"; public override string Title => "Dynamic lifts"; diff --git a/UVtools.Core/Operations/OperationEditParameters.cs b/UVtools.Core/Operations/OperationEditParameters.cs index b3a9cdd8..515989bd 100644 --- a/UVtools.Core/Operations/OperationEditParameters.cs +++ b/UVtools.Core/Operations/OperationEditParameters.cs @@ -29,7 +29,7 @@ public class OperationEditParameters : Operation #region Overrides public override bool CanRunInPartialMode => true; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override string IconClass => "fas fa-edit"; diff --git a/UVtools.Core/Operations/OperationFadeExposureTime.cs b/UVtools.Core/Operations/OperationFadeExposureTime.cs index 16cb2f47..a2fa304a 100644 --- a/UVtools.Core/Operations/OperationFadeExposureTime.cs +++ b/UVtools.Core/Operations/OperationFadeExposureTime.cs @@ -27,7 +27,7 @@ public class OperationFadeExposureTime : Operation #region Overrides public override bool CanRunInPartialMode => true; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Normal; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Normal; public override bool LayerIndexEndEnabled => false; public override string IconClass => "fas fa-history"; public override string Title => "Fade exposure time"; diff --git a/UVtools.Core/Operations/OperationIPrintedThisFile.cs b/UVtools.Core/Operations/OperationIPrintedThisFile.cs index 742db65a..466a3780 100644 --- a/UVtools.Core/Operations/OperationIPrintedThisFile.cs +++ b/UVtools.Core/Operations/OperationIPrintedThisFile.cs @@ -29,7 +29,7 @@ public class OperationIPrintedThisFile : Operation public override bool CanRunInPartialMode => true; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override bool CanHaveProfiles => false; diff --git a/UVtools.Core/Operations/OperationInfill.cs b/UVtools.Core/Operations/OperationInfill.cs index 5384e5d0..5e2e5d6e 100644 --- a/UVtools.Core/Operations/OperationInfill.cs +++ b/UVtools.Core/Operations/OperationInfill.cs @@ -10,6 +10,7 @@ using Emgu.CV.CvEnum; using Emgu.CV.Structure; using System; +using System.ComponentModel; using System.Drawing; using System.Threading.Tasks; using UVtools.Core.Extensions; @@ -21,11 +22,13 @@ namespace UVtools.Core.Operations; public sealed class OperationInfill : Operation { #region Members - private InfillAlgorithm _infillType = InfillAlgorithm.CubicDynamicLink; + private InfillAlgorithm _infillType = InfillAlgorithm.CubicCrossAlternating; private ushort _wallThickness = 64; private ushort _infillThickness = 45; - private ushort _infillSpacing = 160; + private ushort _infillSpacing = 200; private ushort _infillBrightness = 255; + private bool _reinforceInfill; + #endregion #region Overrides @@ -49,16 +52,24 @@ public sealed class OperationInfill : Operation public enum InfillAlgorithm { //Rhombus, - Cubic, - CubicCenterLink, - CubicDynamicLink, - CubicInterlinked, + [Description("Straight pillars (Weak)")] + Pillars, + + [Description("Cubic cross: Fixed pilars with crossing sections (Optimal)")] + CubicCross, + + [Description("Cubic alternating cross: Fixed pilars with crossing sections alternating in directions (Optimal)")] + CubicCrossAlternating, + + [Description("Cubic star: Fixed pilars with crossing sections in star pattern (Strong)")] + CubicStar, + + [Description("Honeycomb (Strong)")] Honeycomb } #endregion #region Properties - public static Array InfillAlgorithmTypes => Enum.GetValues(typeof(InfillAlgorithm)); public InfillAlgorithm InfillType { get => _infillType; @@ -89,9 +100,15 @@ public ushort InfillSpacing set => RaiseAndSetIfChanged(ref _infillSpacing, value); } + public bool ReinforceInfill + { + get => _reinforceInfill; + set => RaiseAndSetIfChanged(ref _reinforceInfill, value); + } + public override string ToString() { - var result = $"[{_infillType}] [Wall: {_wallThickness}px] [B: {_infillBrightness}px] [T: {_infillThickness}px] [S: {_infillSpacing}px]" + LayerRangeString; + var result = $"[{_infillType}] [Wall: {_wallThickness}px] [B: {_infillBrightness}px] [T: {_infillThickness}px] [S: {_infillSpacing}px] [R: {_reinforceInfill}]" + LayerRangeString; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; } @@ -110,7 +127,7 @@ public OperationInfill(FileFormat slicerFile) : base(slicerFile) { } private bool Equals(OperationInfill other) { - return _infillType == other._infillType && _wallThickness == other._wallThickness && _infillThickness == other._infillThickness && _infillSpacing == other._infillSpacing && _infillBrightness == other._infillBrightness; + return _infillType == other._infillType && _wallThickness == other._wallThickness && _infillThickness == other._infillThickness && _infillSpacing == other._infillSpacing && _infillBrightness == other._infillBrightness && _reinforceInfill == other._reinforceInfill; } public override bool Equals(object? obj) @@ -120,7 +137,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine((int) _infillType, _wallThickness, _infillThickness, _infillSpacing, _infillBrightness); + return HashCode.Combine((int) _infillType, _wallThickness, _infillThickness, _infillSpacing, _infillBrightness, _reinforceInfill); } #endregion @@ -163,10 +180,10 @@ public override bool Execute(Mat mat, params object[]? arguments) using var mask = GetMask(mat); bool disposeTargetMask = true; - if (_infillType is InfillAlgorithm.Cubic - or InfillAlgorithm.CubicCenterLink - or InfillAlgorithm.CubicDynamicLink - or InfillAlgorithm.CubicInterlinked) + if (_infillType is InfillAlgorithm.Pillars + or InfillAlgorithm.CubicCross + or InfillAlgorithm.CubicCrossAlternating + or InfillAlgorithm.CubicStar) { using var infillPattern = EmguExtensions.InitMat(new Size(_infillSpacing, _infillSpacing)); using var matPattern = mat.NewBlank(); @@ -180,8 +197,16 @@ or InfillAlgorithm.CubicDynamicLink accumulator += _infillSpacing; if (accumulator >= layerIndex) break; - firstPattern = false; - accumulator += _infillThickness; + + if (_reinforceInfill) + { + firstPattern = false; + accumulator += _infillThickness; + } + else + { + //accumulator += _infillSpacing; + } } if (firstPattern) @@ -212,10 +237,10 @@ or InfillAlgorithm.CubicDynamicLink int margin = (int) (InfillSpacing - accumulator + layerIndex) - thickness; int marginInv = (int) (accumulator - layerIndex) - thickness; - if (_infillType == InfillAlgorithm.CubicCenterLink || - (_infillType == InfillAlgorithm.CubicDynamicLink && + if (_infillType == InfillAlgorithm.CubicCross || + (_infillType == InfillAlgorithm.CubicCrossAlternating && dynamicCenter) || - _infillType == InfillAlgorithm.CubicInterlinked) + _infillType == InfillAlgorithm.CubicStar) { CvInvoke.Rectangle(infillPattern, @@ -239,8 +264,8 @@ or InfillAlgorithm.CubicDynamicLink } - if (_infillType == InfillAlgorithm.CubicInterlinked || - (_infillType == InfillAlgorithm.CubicDynamicLink && + if (_infillType == InfillAlgorithm.CubicStar || + (_infillType == InfillAlgorithm.CubicCrossAlternating && !dynamicCenter)) { CvInvoke.Rectangle(infillPattern, diff --git a/UVtools.Core/Operations/OperationLayerArithmetic.cs b/UVtools.Core/Operations/OperationLayerArithmetic.cs index 0a11d107..026bcded 100644 --- a/UVtools.Core/Operations/OperationLayerArithmetic.cs +++ b/UVtools.Core/Operations/OperationLayerArithmetic.cs @@ -55,7 +55,7 @@ public ArithmeticOperation(uint layerIndex, LayerArithmeticOperators layerArithm #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fas fa-square-root-alt"; public override string Title => "Layer arithmetic"; public override string Description => diff --git a/UVtools.Core/Operations/OperationLayerClone.cs b/UVtools.Core/Operations/OperationLayerClone.cs index 3dd62297..cce810fb 100644 --- a/UVtools.Core/Operations/OperationLayerClone.cs +++ b/UVtools.Core/Operations/OperationLayerClone.cs @@ -25,7 +25,7 @@ public sealed class OperationLayerClone : Operation #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Current; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Current; public override bool CanROI => false; public override bool PassActualLayerIndex => true; public override string IconClass => "fas fa-clone"; diff --git a/UVtools.Core/Operations/OperationLayerExportGif.cs b/UVtools.Core/Operations/OperationLayerExportGif.cs index dcea6f0b..cce026a2 100644 --- a/UVtools.Core/Operations/OperationLayerExportGif.cs +++ b/UVtools.Core/Operations/OperationLayerExportGif.cs @@ -32,8 +32,8 @@ public sealed class OperationLayerExportGif : Operation private ushort _repeats; private ushort _skip; private decimal _scale = 50; - private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; - private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; + private RotateDirection _rotateDirection = RotateDirection.None; + private FlipDirection _flipDirection = FlipDirection.None; #endregion @@ -165,13 +165,13 @@ public decimal Scale public float ScaleFactor => (float)_scale / 100f; - public Enumerations.RotateDirection RotateDirection + public RotateDirection RotateDirection { get => _rotateDirection; set => RaiseAndSetIfChanged(ref _rotateDirection, value); } - public Enumerations.FlipDirection FlipDirection + public FlipDirection FlipDirection { get => _flipDirection; set => RaiseAndSetIfChanged(ref _flipDirection, value); @@ -237,12 +237,12 @@ protected override bool ExecuteInternally(OperationProgress progress) CvInvoke.Resize(matRoi, matRoi, new Size((int) (matRoi.Width * ScaleFactor), (int)(matRoi.Height * ScaleFactor))); } - if (_flipDirection != Enumerations.FlipDirection.None) + if (_flipDirection != FlipDirection.None) { CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); } - if (_rotateDirection != Enumerations.RotateDirection.None) + if (_rotateDirection != RotateDirection.None) { CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); } diff --git a/UVtools.Core/Operations/OperationLayerExportHeatMap.cs b/UVtools.Core/Operations/OperationLayerExportHeatMap.cs index c65ed6b8..47ca83c2 100644 --- a/UVtools.Core/Operations/OperationLayerExportHeatMap.cs +++ b/UVtools.Core/Operations/OperationLayerExportHeatMap.cs @@ -22,8 +22,8 @@ public sealed class OperationLayerExportHeatMap : Operation { #region Members private string _filePath = null!; - private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; - private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; + private RotateDirection _rotateDirection = RotateDirection.None; + private FlipDirection _flipDirection = FlipDirection.None; private bool _mergeSamePositionedLayers = true; private bool _cropByRoi = true; @@ -78,13 +78,13 @@ public string FilePath set => RaiseAndSetIfChanged(ref _filePath, value); } - public Enumerations.RotateDirection RotateDirection + public RotateDirection RotateDirection { get => _rotateDirection; set => RaiseAndSetIfChanged(ref _rotateDirection, value); } - public Enumerations.FlipDirection FlipDirection + public FlipDirection FlipDirection { get => _flipDirection; set => RaiseAndSetIfChanged(ref _flipDirection, value); @@ -173,12 +173,12 @@ protected override bool ExecuteInternally(OperationProgress progress) using var sumMat = EmguExtensions.InitMat(sumMat32.Size); sumMat32.ConvertTo(sumMat, DepthType.Cv8U, 1.0 / layerRange.Length); - if (_flipDirection != Enumerations.FlipDirection.None) + if (_flipDirection != FlipDirection.None) { CvInvoke.Flip(sumMat, sumMat, Enumerations.ToOpenCVFlipType(_flipDirection)); } - if (_rotateDirection != Enumerations.RotateDirection.None) + if (_rotateDirection != RotateDirection.None) { CvInvoke.Rotate(sumMat, sumMat, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); } diff --git a/UVtools.Core/Operations/OperationLayerExportImage.cs b/UVtools.Core/Operations/OperationLayerExportImage.cs index 8839e527..918c66b3 100644 --- a/UVtools.Core/Operations/OperationLayerExportImage.cs +++ b/UVtools.Core/Operations/OperationLayerExportImage.cs @@ -59,8 +59,8 @@ public enum LayerExportImageTypes : byte private string _outputFolder = null!; private string _filename = "layer"; private LayerExportImageTypes _imageType = LayerExportImageTypes.PNG; - private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; - private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; + private RotateDirection _rotateDirection = RotateDirection.None; + private FlipDirection _flipDirection = FlipDirection.None; private bool _padLayerIndex = true; private bool _cropByRoi = true; @@ -126,13 +126,13 @@ public LayerExportImageTypes ImageType set => RaiseAndSetIfChanged(ref _imageType, value); } - public Enumerations.RotateDirection RotateDirection + public RotateDirection RotateDirection { get => _rotateDirection; set => RaiseAndSetIfChanged(ref _rotateDirection, value); } - public Enumerations.FlipDirection FlipDirection + public FlipDirection FlipDirection { get => _flipDirection; set => RaiseAndSetIfChanged(ref _flipDirection, value); @@ -189,17 +189,17 @@ protected override bool ExecuteInternally(OperationProgress progress) matRoi = GetRoiOrDefault(mat); } - if (_flipDirection != Enumerations.FlipDirection.None) + if (_flipDirection != FlipDirection.None) { CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); } - if (_rotateDirection != Enumerations.RotateDirection.None) + if (_rotateDirection != RotateDirection.None) { CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); } - var filename = SlicerFile[layerIndex].FormatFileName(_filename, _padLayerIndex ? SlicerFile.LayerDigits : byte.MinValue, Enumerations.IndexStartNumber.Zero, string.Empty); + var filename = SlicerFile[layerIndex].FormatFileName(_filename, _padLayerIndex ? SlicerFile.LayerDigits : byte.MinValue, IndexStartNumber.Zero, string.Empty); var fileFullPath = Path.Combine(_outputFolder, $"{filename}.{_imageType.ToString().ToLower()}"); if (_imageType != LayerExportImageTypes.SVG) diff --git a/UVtools.Core/Operations/OperationLayerExportMesh.cs b/UVtools.Core/Operations/OperationLayerExportMesh.cs index 8240ad27..fda3d5e8 100644 --- a/UVtools.Core/Operations/OperationLayerExportMesh.cs +++ b/UVtools.Core/Operations/OperationLayerExportMesh.cs @@ -46,8 +46,8 @@ public enum ExportMeshQuality : byte private string _filePath = null!; private MeshFile.MeshFileFormat _meshFileFormat = MeshFile.MeshFileFormat.BINARY; private ExportMeshQuality _quality = ExportMeshQuality.Accurate; - private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; - private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; + private RotateDirection _rotateDirection = RotateDirection.None; + private FlipDirection _flipDirection = FlipDirection.None; private bool _stripAntiAliasing = true; #endregion @@ -113,13 +113,13 @@ public ExportMeshQuality Quality set => RaiseAndSetIfChanged(ref _quality, value); } - public Enumerations.RotateDirection RotateDirection + public RotateDirection RotateDirection { get => _rotateDirection; set => RaiseAndSetIfChanged(ref _rotateDirection, value); } - public Enumerations.FlipDirection FlipDirection + public FlipDirection FlipDirection { get => _flipDirection; set => RaiseAndSetIfChanged(ref _flipDirection, value); @@ -181,10 +181,10 @@ protected override unsafe bool ExecuteInternally(OperationProgress progress) * ideally we would fix the algorithm itself but that's more invovled. for the time being we'll just flip it verticaly. */ var workAroundFlip = _flipDirection switch { - Enumerations.FlipDirection.None => Enumerations.FlipDirection.Vertically, - Enumerations.FlipDirection.Horizontally => Enumerations.FlipDirection.Both, - Enumerations.FlipDirection.Vertically => Enumerations.FlipDirection.None, - Enumerations.FlipDirection.Both => Enumerations.FlipDirection.Horizontally, + FlipDirection.None => FlipDirection.Vertically, + FlipDirection.Horizontally => FlipDirection.Both, + FlipDirection.Vertically => FlipDirection.None, + FlipDirection.Both => FlipDirection.Horizontally, _ => throw new NotImplementedException($"Flip type: {_flipDirection} not handled!") }; diff --git a/UVtools.Core/Operations/OperationLayerImport.cs b/UVtools.Core/Operations/OperationLayerImport.cs index e1b0d394..d22c7329 100644 --- a/UVtools.Core/Operations/OperationLayerImport.cs +++ b/UVtools.Core/Operations/OperationLayerImport.cs @@ -63,7 +63,7 @@ public enum ImportTypes : byte #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override string IconClass => "mdi-database-import"; @@ -288,7 +288,7 @@ protected override bool ExecuteInternally(OperationProgress progress) if (_importType == ImportTypes.Stack) { - new OperationMove(SlicerFile, Enumerations.Anchor.TopLeft).Execute(progress); + new OperationMove(SlicerFile, Anchor.TopLeft).Execute(progress); } foreach (var fileFormat in fileFormats) diff --git a/UVtools.Core/Operations/OperationLayerReHeight.cs b/UVtools.Core/Operations/OperationLayerReHeight.cs index e4aec8f9..682ddaa6 100644 --- a/UVtools.Core/Operations/OperationLayerReHeight.cs +++ b/UVtools.Core/Operations/OperationLayerReHeight.cs @@ -56,7 +56,7 @@ public enum OperationLayerReHeightAntiAliasingType : byte #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override bool CanROI => false; public override string IconClass => "mdi-arrow-split-horizontal"; public override string Title => "Adjust layer height"; diff --git a/UVtools.Core/Operations/OperationLayerRemove.cs b/UVtools.Core/Operations/OperationLayerRemove.cs index c2563e2d..20e53ba8 100644 --- a/UVtools.Core/Operations/OperationLayerRemove.cs +++ b/UVtools.Core/Operations/OperationLayerRemove.cs @@ -25,7 +25,7 @@ public sealed class OperationLayerRemove : Operation #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Current; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Current; public override bool CanROI => false; public override bool PassActualLayerIndex => true; public override string IconClass => "mdi-layers-remove"; diff --git a/UVtools.Core/Operations/OperationLightBleedCompensation.cs b/UVtools.Core/Operations/OperationLightBleedCompensation.cs index 4cd50309..d307a71c 100644 --- a/UVtools.Core/Operations/OperationLightBleedCompensation.cs +++ b/UVtools.Core/Operations/OperationLightBleedCompensation.cs @@ -47,7 +47,7 @@ public enum LightBleedCompensationLookupMode : byte #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Normal; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Normal; public override string IconClass => "mdi-lightbulb-on"; public override string Title => "Light bleed compensation"; public override string Description => diff --git a/UVtools.Core/Operations/OperationMove.cs b/UVtools.Core/Operations/OperationMove.cs index d3df8b95..b83a1161 100644 --- a/UVtools.Core/Operations/OperationMove.cs +++ b/UVtools.Core/Operations/OperationMove.cs @@ -63,7 +63,7 @@ public override string ToString() private Rectangle _dstRoi = Rectangle.Empty; private uint _imageWidth; private uint _imageHeight; - private Enumerations.Anchor _anchor = Enumerations.Anchor.MiddleCenter; + private Anchor _anchor = Anchor.MiddleCenter; private int _marginLeft; private int _marginTop; private int _marginRight; @@ -96,7 +96,7 @@ public uint ImageHeight set => RaiseAndSetIfChanged(ref _imageHeight, value); } - public Enumerations.Anchor Anchor + public Anchor Anchor { get => _anchor; set @@ -176,15 +176,15 @@ public bool IsWithinBoundary public OperationMove() { } - public OperationMove(FileFormat slicerFile) : this(slicerFile, Enumerations.Anchor.MiddleCenter) + public OperationMove(FileFormat slicerFile) : this(slicerFile, Anchor.MiddleCenter) { } - public OperationMove(FileFormat slicerFile, Enumerations.Anchor anchor) : base(slicerFile) + public OperationMove(FileFormat slicerFile, Anchor anchor) : base(slicerFile) { _anchor = anchor; } - public OperationMove(FileFormat slicerFile, Rectangle srcRoi, Enumerations.Anchor anchor = Enumerations.Anchor.MiddleCenter) : this(slicerFile, anchor) + public OperationMove(FileFormat slicerFile, Rectangle srcRoi, Anchor anchor = Anchor.MiddleCenter) : this(slicerFile, anchor) { if(!srcRoi.IsEmpty) ROI = srcRoi; } @@ -197,7 +197,7 @@ public override void InitWithSlicerFile() _imageHeight = SlicerFile.ResolutionY; } - /*public OperationMove(FileFormat slicerFile, Rectangle srcRoi, Mat mat, Enumerations.Anchor anchor = Enumerations.Anchor.MiddleCenter) : this(slicerFile, srcRoi, anchor) + /*public OperationMove(FileFormat slicerFile, Rectangle srcRoi, Mat mat, Anchor anchor = Anchor.MiddleCenter) : this(slicerFile, srcRoi, anchor) { ImageWidth = (uint) mat.Width; ImageHeight = (uint) mat.Height; @@ -212,15 +212,15 @@ public void CalculateDstRoi() _dstRoi.Location = Anchor switch { - Enumerations.Anchor.TopLeft => new Point(0, 0), - Enumerations.Anchor.TopCenter => new Point((int) (ImageWidth / 2 - ROI.Width / 2), 0), - Enumerations.Anchor.TopRight => new Point((int) (ImageWidth - ROI.Width), 0), - Enumerations.Anchor.MiddleLeft => new Point(0, (int) (ImageHeight / 2 - ROI.Height / 2)), - Enumerations.Anchor.MiddleCenter => new Point((int) (ImageWidth / 2 - ROI.Width / 2), (int) (ImageHeight / 2 - ROI.Height / 2)), - Enumerations.Anchor.MiddleRight => new Point((int) (ImageWidth - ROI.Width), (int) (ImageHeight / 2 - ROI.Height / 2)), - Enumerations.Anchor.BottomLeft => new Point(0, (int) (ImageHeight - ROI.Height)), - Enumerations.Anchor.BottomCenter => new Point((int) (ImageWidth / 2 - ROI.Width / 2), (int) (ImageHeight - ROI.Height)), - Enumerations.Anchor.BottomRight => new Point((int) (ImageWidth - ROI.Width), (int) (ImageHeight - ROI.Height)), + Anchor.TopLeft => new Point(0, 0), + Anchor.TopCenter => new Point((int) (ImageWidth / 2 - ROI.Width / 2), 0), + Anchor.TopRight => new Point((int) (ImageWidth - ROI.Width), 0), + Anchor.MiddleLeft => new Point(0, (int) (ImageHeight / 2 - ROI.Height / 2)), + Anchor.MiddleCenter => new Point((int) (ImageWidth / 2 - ROI.Width / 2), (int) (ImageHeight / 2 - ROI.Height / 2)), + Anchor.MiddleRight => new Point((int) (ImageWidth - ROI.Width), (int) (ImageHeight / 2 - ROI.Height / 2)), + Anchor.BottomLeft => new Point(0, (int) (ImageHeight - ROI.Height)), + Anchor.BottomCenter => new Point((int) (ImageWidth / 2 - ROI.Width / 2), (int) (ImageHeight - ROI.Height)), + Anchor.BottomRight => new Point((int) (ImageWidth - ROI.Width), (int) (ImageHeight - ROI.Height)), _ => throw new ArgumentOutOfRangeException() }; @@ -242,14 +242,14 @@ public void CalculateDstRoi() public void Reset() { MarginLeft = MarginTop = MarginRight = MarginBottom = 0; - Anchor = Enumerations.Anchor.MiddleCenter; + Anchor = Anchor.MiddleCenter; IsCutMove = true; } public void SetAnchor(byte value) { - Anchor = (Enumerations.Anchor)value; + Anchor = (Anchor)value; } diff --git a/UVtools.Core/Operations/OperationPattern.cs b/UVtools.Core/Operations/OperationPattern.cs index 24023a31..b4a2127b 100644 --- a/UVtools.Core/Operations/OperationPattern.cs +++ b/UVtools.Core/Operations/OperationPattern.cs @@ -20,7 +20,7 @@ namespace UVtools.Core.Operations; public class OperationPattern : Operation { #region Members - private Enumerations.Anchor _anchor = Enumerations.Anchor.None; + private Anchor _anchor = Anchor.None; private ushort _colSpacing; private ushort _rowSpacing; private ushort _maxColSpacing; @@ -80,7 +80,7 @@ public class OperationPattern : Operation #endregion #region Properties - public Enumerations.Anchor Anchor + public Anchor Anchor { get => _anchor; set => RaiseAndSetIfChanged(ref _anchor, value); @@ -227,7 +227,7 @@ public override void InitWithSlicerFile() #region Methods public void SetAnchor(byte value) { - Anchor = (Enumerations.Anchor)value; + Anchor = (Anchor)value; } public void SetRoi(Rectangle srcRoi) @@ -319,7 +319,7 @@ protected override bool ExecuteInternally(OperationProgress progress) SlicerFile.BoundingRectangle = Rectangle.Empty; - if (Anchor == Enumerations.Anchor.None) return true; + if (Anchor == Anchor.None) return true; var operationMove = new OperationMove(SlicerFile, Anchor) { LayerIndexStart = LayerIndexStart, diff --git a/UVtools.Core/Operations/OperationRaftRelief.cs b/UVtools.Core/Operations/OperationRaftRelief.cs index 8314d757..97253175 100644 --- a/UVtools.Core/Operations/OperationRaftRelief.cs +++ b/UVtools.Core/Operations/OperationRaftRelief.cs @@ -70,8 +70,8 @@ public enum RaftReliefTypes : byte public override string ProgressAction => "Relieved layers"; - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => - Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => + LayerRangeSelection.None; public override string? ValidateInternally() { diff --git a/UVtools.Core/Operations/OperationRaiseOnPrintFinish.cs b/UVtools.Core/Operations/OperationRaiseOnPrintFinish.cs index d8473d68..c958e348 100644 --- a/UVtools.Core/Operations/OperationRaiseOnPrintFinish.cs +++ b/UVtools.Core/Operations/OperationRaiseOnPrintFinish.cs @@ -28,7 +28,7 @@ public class OperationRaiseOnPrintFinish : Operation #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fas fa-level-up-alt"; public override string Title => "Raise platform on print finish"; public override string Description => diff --git a/UVtools.Core/Operations/OperationRedrawModel.cs b/UVtools.Core/Operations/OperationRedrawModel.cs index 9407d498..479d51d7 100644 --- a/UVtools.Core/Operations/OperationRedrawModel.cs +++ b/UVtools.Core/Operations/OperationRedrawModel.cs @@ -33,7 +33,7 @@ public class OperationRedrawModel : Operation #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection { get; } = Enumerations.LayerRangeSelection.None; + public override LayerRangeSelection StartLayerRangeSelection { get; } = LayerRangeSelection.None; public override string IconClass => "mdi-puzzle-edit"; public override string Title => "Redraw model/supports"; diff --git a/UVtools.Core/Operations/OperationTimelapse.cs b/UVtools.Core/Operations/OperationTimelapse.cs index 256d797f..41b5a1e6 100644 --- a/UVtools.Core/Operations/OperationTimelapse.cs +++ b/UVtools.Core/Operations/OperationTimelapse.cs @@ -50,7 +50,7 @@ public enum TimelapseRaiseMode #region Overrides - public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Normal; + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Normal; public override string IconClass => "fas fa-camera"; public override string Title => "Timelapse"; public override string Description => diff --git a/UVtools.Core/Printer/Machine.cs b/UVtools.Core/Printer/Machine.cs new file mode 100644 index 00000000..2c93cfc4 --- /dev/null +++ b/UVtools.Core/Printer/Machine.cs @@ -0,0 +1,409 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Text; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Printer +{ + public class Machine + { + #region Properties + public PrinterBrand Brand { get; set; } + public string Name { get; set; } = FileFormats.FileFormat.DefaultMachineName; + public string Model { get; set; } = FileFormats.FileFormat.DefaultMachineName; + + public ushort ResolutionX { get; set; } + + public ushort ResolutionY { get; set; } + + public float DisplayWidth { get; set; } + + public float DisplayHeight { get; set; } + + public float MachineZ { get; set; } + + public FlipDirection DisplayMirror { get; set; } + + public object? Tag { get; set; } + #endregion + + #region Constructor + + public Machine() { } + + public Machine(PrinterBrand brand, string name, string? model, ushort resolutionX, ushort resolutionY, float displayWidth, float displayHeight, float machineZ, FlipDirection displayMirror = default) + { + Brand = brand; + Name = name; + Model = model ?? name; + ResolutionX = resolutionX; + ResolutionY = resolutionY; + DisplayWidth = displayWidth; + DisplayHeight = displayHeight; + MachineZ = machineZ; + DisplayMirror = displayMirror; + } + + public Machine(PrinterBrand brand, string name, string? model, Size resolution, SizeF display, float machineZ, FlipDirection displayMirror = default) + : this(brand, name, model, (ushort)resolution.Width, (ushort)resolution.Height, display.Width, display.Height, machineZ, displayMirror) { } + + public Machine(ushort resolutionX, ushort resolutionY, float displayWidth, float displayHeight, float machineZ, FlipDirection displayMirror = default, PrinterBrand brand = default, string name = FileFormats.FileFormat.DefaultMachineName, string? model = null) + : this(brand, name, model, resolutionX, resolutionY, displayWidth, displayHeight, machineZ, displayMirror) {} + + #endregion + + #region Overrides + + protected bool Equals(Machine other) + { + return ResolutionX == other.ResolutionX && ResolutionY == other.ResolutionY && DisplayWidth.Equals(other.DisplayWidth) && DisplayHeight.Equals(other.DisplayHeight) && MachineZ.Equals(other.MachineZ) && Brand == other.Brand && Name == other.Name && Model == other.Model; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Machine)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(ResolutionX, ResolutionY, DisplayWidth, DisplayHeight, MachineZ, (int)Brand, Name, Model); + } + + public override string ToString() + { + return $"{Name} | Resolution={ResolutionX}x{ResolutionY}(px) Display={DisplayWidth}x{DisplayHeight}(mm) Z={MachineZ}mm"; + } + #endregion + + #region Methods + /// + /// Gets or sets the pixels per mm on X direction + /// + public virtual float Xppmm => DisplayWidth > 0 ? ResolutionX / DisplayWidth : 0; + + /// + /// Gets or sets the pixels per mm on Y direction + /// + public virtual float Yppmm => DisplayHeight > 0 ? ResolutionY / DisplayHeight : 0; + + /// + /// Gets or sets the pixels per mm + /// + public SizeF Ppmm => new(Xppmm, Yppmm); + + /// + /// Gets the maximum (Width or Height) pixels per mm + /// + public float PpmmMax => Ppmm.Max(); + + /// + /// Gets the pixel width in millimeters + /// + public float PixelWidth => DisplayWidth > 0 && ResolutionX > 0 ? (float)Math.Round(DisplayWidth / ResolutionX, 3) : 0; + + /// + /// Gets the pixel height in millimeters + /// + public float PixelHeight => DisplayHeight > 0 && ResolutionY > 0 ? (float)Math.Round(DisplayHeight / ResolutionY, 3) : 0; + + /// + /// Gets the pixel size in millimeters + /// + public SizeF PixelSize => new(PixelWidth, PixelHeight); + + /// + /// Gets the maximum pixel between width and height in millimeters + /// + public float PixelSizeMax => PixelSize.Max(); + + /// + /// Gets the pixel area in millimeters + /// + public float PixelArea => PixelSize.Area(); + + /// + /// Gets the pixel width in microns + /// + public float PixelWidthMicrons => DisplayWidth > 0 ? (float)Math.Round(DisplayWidth / ResolutionX * 1000, 3) : 0; + + /// + /// Gets the pixel height in microns + /// + public float PixelHeightMicrons => DisplayHeight > 0 ? (float)Math.Round(DisplayHeight / ResolutionY * 1000, 3) : 0; + + /// + /// Gets the pixel size in microns + /// + public SizeF PixelSizeMicrons => new(PixelWidthMicrons, PixelHeightMicrons); + + /// + /// Gets the maximum pixel between width and height in microns + /// + public float PixelSizeMicronsMax => PixelSizeMicrons.Max(); + + /// + /// Gets the pixel area in millimeters + /// + public float PixelAreaMicrons => PixelSizeMicrons.Area(); + + public Machine Clone() + { + return (Machine)MemberwiseClone(); + } + #endregion + + #region Static methods + + /// + /// Preset list of machines + /// + public static Machine[] Machines => + new Machine[]{ + // Creality + /*new(PrinterBrand.Creality, "Halot One", "CL-60", 1620, 2560, 81, 128, 160), + new(PrinterBrand.Creality, "Halot One Pro", "CL-70", 2560, 2400, 130.56f, 122.4f, 160), + new(PrinterBrand.Creality, "Halot One Plus", "CL-79", 4320, 2560, 172.8f, 102.4f, 160), + new(PrinterBrand.Creality, "Halot Sky", "CL-89", 3840, 2400, 192, 120, 200), + new(PrinterBrand.Creality, "Halot Sky Plus", "CL-92", 5760, 3600, 198.14f, 123.84f, 210), + new(PrinterBrand.Creality, "Halot Lite", "CL-89L", 3840, 2400, 192, 120, 200), + new(PrinterBrand.Creality, "Halot Max", "CL-133", 3840, 2160, 293.76f, 165.28f, 300), + new(PrinterBrand.Creality, "CT133 Pro", "CT133PRO", 3840, 2160, 293.76f, 165.24f, 300), + new(PrinterBrand.Creality, "CT-005 Pro", "CT-005", 3840, 2400, 192, 120, 250),*/ + + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono 4K", "Photon Mono 4K", 3840, 2400, 134.4f, 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.14f, 123.84f, 245f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono X", "Photon Mono X", 3840, 2400, 192f, 120f, 245f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Mono", "Photon Mono", 1620, 2560, 82.62f, 130.56f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon S", "Photon S", 1440, 2560, 68.04f, 120.96f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Ultra", "Photon Ultra", 1280, 720, 102.4f, 57.6f, 165f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon X", "Photon X", 2560, 1600, 192f, 120f, 245f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon Zero", "Photon Zero", 480, 854, 55.4f, 98.63f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.AnyCubic, "AnyCubic Photon", "Photon", 1440, 2560, 68.04f, 120.96f, 155f, FlipDirection.Horizontally), + + new(PrinterBrand.Creality, "Creality CT-005 Pro", "CT-005", 3840, 2400, 192f, 120f, 250f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality CT-133 Pro", "CT133PRO", 3840, 2160, 293.76f, 165.24f, 300f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot Lite CL-89L", "CL-89L", 3840, 2400, 192f, 120f, 200f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot Max CL-133", "CL-133", 3840, 2160, 293.76f, 165.24f, 300f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot One CL-60", "CL-60", 1620, 2560, 81f, 128f, 160f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot One Plus CL-79", "CL-79", 4320, 2560, 172.8f, 102.4f, 160f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot One Pro CL-70", "CL-70", 2560, 2400, 130.56f, 122.4f, 160f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot Sky CL-89", "", 3840, 2400, 192f, 120f, 200f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality Halot Sky Plus CL-92", "CL-92", 5760, 3600, 198.14f, 123.84f, 210f, FlipDirection.None), + new(PrinterBrand.Creality, "Creality LD-002H", "LD-002H", 1620, 2560, 82.62f, 130.56f, 160f, FlipDirection.Horizontally), + new(PrinterBrand.Creality, "Creality LD-002R", "LD-002R", 1440, 2560, 68.04f, 120.96f, 160f, FlipDirection.Horizontally), + new(PrinterBrand.Creality, "Creality LD-006", "LD-006", 3840, 2400, 192f, 120f, 245f, FlipDirection.Horizontally), + + new(PrinterBrand.Elegoo, "Elegoo Jupiter", "Jupiter", 5448, 3064, 277.848f, 156.264f, 300f, FlipDirection.Horizontally), + new(PrinterBrand.Elegoo, "Elegoo Mars 2 Pro", "Mars 2 Pro", 1620, 2560, 82.62f, 130.56f, 160f, FlipDirection.Horizontally), + new(PrinterBrand.Elegoo, "Elegoo Mars 2", "Mars 2", 1620, 2560, 82.62f, 130.56f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.Elegoo, "Elegoo Mars 3", "Mars 3", 4098, 2560, 143.43f, 89.6f, 175f, FlipDirection.Horizontally), + new(PrinterBrand.Elegoo, "Elegoo Mars C", "Mars C", 1440, 2560, 68.04f, 120.96f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.Elegoo, "Elegoo Mars", "Mars", 1440, 2560, 68.04f, 120.96f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.Elegoo, "Elegoo Saturn", "Saturn", 3840, 2400, 192f, 120f, 200f, FlipDirection.Horizontally), + + new(PrinterBrand.EPAX, "EPAX DX1 PRO", "DX1 PRO", 4098, 2560, 143.43f, 89.6f, 155f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX DX10 Pro 5K", "DX10 Pro 5K", 4920, 2880, 221.4f, 129.6f, 120f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX DX10 Pro 8K", "DX10 Pro 8K", 7680, 4320, 218.88f, 123.12f, 120f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX E10 5K", "E10 5K", 4920, 2880, 221.4f, 129.6f, 250f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX E10 8K", "E10 8K", 7680, 4320, 218.88f, 123.12f, 250f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX E10 Mono", "E10 Mono", 3840, 2400, 192f, 120f, 250f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX E6 Mono", "E6 Mono", 1620, 2560, 81f, 128f, 155f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X1 4KS", "X1 4KS", 4098, 2560, 143.43f, 89.6f, 155f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X1", "X1", 1440, 2560, 68.04f, 120.96f, 155f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X10 4K Mono", "X10 4K Mono", 3840, 2400, 192f, 120f, 250f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X10 5K", "X10 5K", 4920, 2880, 221.4f, 129.6f, 250f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X10", "X10", 1600, 2560, 135.36f, 216.57f, 250f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X133 4K Mono", "X133 4K Mono", 3840, 2160, 293.76f, 165.24f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X133 6K", "X133 6K", 5760, 3240, 288f, 162f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X156 4K Color", "X156 4K Color", 3840, 2160, 345.6f, 194.4f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.EPAX, "EPAX X1K 2K Mono", "X1K 2K Mono", 1620, 2560, 82.62f, 130.56f, 155f, FlipDirection.Horizontally), + + new(PrinterBrand.FlashForge, "FlashForge Explorer MAX", "Explorer MAX", 2560, 1600, 192f, 120f, 200f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Focus 13.3", "Focus 13.3", 3842, 2171, 292f, 165f, 400f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Focus 8.9", "Focus 8.9", 3840, 2400, 192f, 120f, 200f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Foto 13.3", "Foto 13.3", 3842, 2171, 292f, 165f, 400f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Foto 6.0", "Foto 6.0", 2600, 1560, 130f, 78f, 155f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Foto 8.9", "Foto 8.9", 3840, 2400, 192f, 120f, 200f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Foto 8.9S", "Foto 8.9S", 3840, 2400, 192f, 120f, 200f, FlipDirection.Vertically), + new(PrinterBrand.FlashForge, "FlashForge Hunter", "Hunter", 1920, 1080, 120f, 67.5f, 150f, FlipDirection.Vertically), + + new(PrinterBrand.Kelant, "Kelant S400", "S400", 2560, 1600, 192f, 120f, 200f, FlipDirection.Horizontally), + + new(PrinterBrand.Longer, "Longer Orange 10", "Orange 10", 480, 854, 55.44f, 98.64f, 140f, FlipDirection.Horizontally), + new(PrinterBrand.Longer, "Longer Orange 120", "Orange 120", 1440, 2560, 68.04f, 120.96f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.Longer, "Longer Orange 30", "Orange 30", 1440, 2560, 68.04f, 120.96f, 170f, FlipDirection.Horizontally), + new(PrinterBrand.Longer, "Longer Orange 4K", "Orange 4K", 3840, 6480, 120.96f, 68.04f, 190f, FlipDirection.Horizontally), + + new(PrinterBrand.Nova3D, "Nova3D Bene4 Mono", "Bene4 Mono", 1566, 2549, 79.865f, 129.998f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Bene4", "Bene4", 1352, 2512, 70f, 130f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Bene5", "Bene5", 1566, 2549, 79.865f, 129.998f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Elfin", "Elfin", 1410, 2531, 73f, 131f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Elfin2 Mono SE", "Elfin2 Mono SE", 1470, 2549, 75f, 130f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Elfin2", "Elfin2", 1352, 2512, 70f, 130f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Elfin3 Mini", "Elfin3 Mini", 1079, 1904, 68f, 120f, 150f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Whale", "Whale", 3840, 2400, 192f, 120f, 250f, FlipDirection.Vertically), + new(PrinterBrand.Nova3D, "Nova3D Whale2", "Whale2", 3840, 2400, 192f, 120f, 250f, FlipDirection.Vertically), + + new(PrinterBrand.Peopoly, "Peopoly Phenom L", "Phenom L", 3840, 2160, 345.6f, 194.4f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.Peopoly, "Peopoly Phenom Noir", "Phenom Noir", 3840, 2160, 293.76f, 165.24f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.Peopoly, "Peopoly Phenom XXL", "Phenom XXL", 3840, 2160, 527.04f, 296.46f, 550f, FlipDirection.Horizontally), + new(PrinterBrand.Peopoly, "Peopoly Phenom", "Phenom", 3840, 2160, 276.48f, 155.52f, 400f, FlipDirection.Horizontally), + + new(PrinterBrand.Phrozen, "Phrozen Shuffle 16", "Shuffle 16", 3840, 2160, 337.92f, 190.08f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Shuffle 4K", "Shuffle 4K", 2160, 3840, 68.04f, 120.96f, 170f, FlipDirection.None), + new(PrinterBrand.Phrozen, "Phrozen Shuffle Lite", "Shuffle Lite", 1440, 2560, 68.04f, 120.96f, 170f, FlipDirection.None), + new(PrinterBrand.Phrozen, "Phrozen Shuffle XL Lite", "Shuffle XL Lite", 2560, 1600, 192f, 120f, 200f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Shuffle XL", "Shuffle XL", 2560, 1600, 192f, 120f, 200f, FlipDirection.None), + new(PrinterBrand.Phrozen, "Phrozen Shuffle", "Shuffle", 1440, 2560, 67.68f, 120.32f, 200f, FlipDirection.None), + new(PrinterBrand.Phrozen, "Phrozen Sonic 4K", "Sonic 4K", 3840, 2160, 134.4f, 75.6f, 200f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Sonic Mega 8K", "Sonic Mega 8K", 7680, 4320, 330.24f, 185.76f, 400f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Sonic Mighty 4K", "Sonic Mighty 4K", 3840, 2400, 199.68f, 124.8f, 220f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Sonic Mini 4K", "Sonic Mini 4K", 3840, 2160, 134.4f, 75.6f, 130f, FlipDirection.Horizontally), + new(PrinterBrand.Phrozen, "Phrozen Sonic Mini 8K", "Sonic Mini 8K", 7500, 3240, 165f, 71.28f, 180f, 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 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), + new(PrinterBrand.QIDI, "QIDI S-Box", "S-Box", 1600, 2560, 135.36f, 216.576f, 200f, FlipDirection.Horizontally), + new(PrinterBrand.QIDI, "QIDI Shadow5.5", "Shadow5.5", 1440, 2560, 68.04f, 120.96f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.QIDI, "QIDI Shadow6.0 Pro", "Shadow6.0 Pro", 1440, 2560, 74.52f, 132.48f, 150f, FlipDirection.Horizontally), + + new(PrinterBrand.Uniformation, "Uniformation GKone", "GKone", 4920, 2880, 221.4f, 129.6f, 245f, FlipDirection.Vertically), + + new(PrinterBrand.Uniz, "Uniz IBEE", "IBEE", 3840, 2400, 192f, 120f, 220f, FlipDirection.Vertically), + + new(PrinterBrand.Prusa, "Prusa SL1", "SL1", 1440, 2560, 68.04f, 120.96f, 150f, FlipDirection.Horizontally), + new(PrinterBrand.Prusa, "Prusa SL1S SPEED", "SL1S SPEED", 1620, 2560, 128f, 81f, 150f, FlipDirection.Horizontally), + + new(PrinterBrand.Voxelab, "Voxelab Ceres 8.9", "Ceres 8.9", 3840, 2400, 192f, 120f, 200f, FlipDirection.Horizontally), + new(PrinterBrand.Voxelab, "Voxelab Polaris 5.5", "Polaris 5.5", 1440, 2560, 68.04f, 120.96f, 155f, FlipDirection.Horizontally), + new(PrinterBrand.Voxelab, "Voxelab Proxima 6", "Proxima 6", 1620, 2560, 82.62f, 130.56f, 155f, FlipDirection.Horizontally), + + new(PrinterBrand.Wanhao, "Wanhao CGR Mini Mono", "CGR Mini Mono", 1620, 2560, 82.62f, 130.56f, 200f, FlipDirection.Horizontally), + new(PrinterBrand.Wanhao, "Wanhao CGR Mono", "CGR Mono", 1620, 2560, 192f, 120f, 200f, FlipDirection.Horizontally), + new(PrinterBrand.Wanhao, "Wanhao D7", "D7", 2560, 1440, 120.96f, 68.5f, 180f, FlipDirection.Horizontally), + new(PrinterBrand.Wanhao, "Wanhao D8", "D8", 2560, 1600, 192f, 120f, 180f, FlipDirection.Horizontally), + + new(PrinterBrand.Zortrax, "Zortrax Inkspire", "Inkspire", 1440, 2560, 74.67f, 132.88f, 175f, FlipDirection.Horizontally), + }; + + /// + /// Gets all machines from PrusaSlicer profiles + /// + /// + public static IEnumerable GetMachinesFromPrusaSlicer() + { + var psPrinterFiles = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "Assets", "PrusaSlicer", "printer")); + foreach (var file in psPrinterFiles) + { + var displayMirror = FlipDirection.None; + var filenameNoExt = Path.GetFileNameWithoutExtension(file); + var split = filenameNoExt.Split(' ', 2); + if (!Enum.TryParse(split[0], true, out PrinterBrand brand)) + { + brand = filenameNoExt.StartsWith("UVtools Prusa") ? PrinterBrand.Prusa : PrinterBrand.Generic; + } + + using var reader = new StreamReader(file); + string? line; + + var machine = new Machine + { + Name = filenameNoExt, + Model = split[1] + }; + + while ((line = reader.ReadLine()) is not null) + { + var keyValue = line.Split('=', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if(keyValue.Length < 2) continue; + + var key = keyValue[0]; + var value = keyValue[1]; + if (key.StartsWith("display_pixels_x")) + { + if (!ushort.TryParse(value, out var resolutionX)) continue; + machine.ResolutionX = resolutionX; + } + + if (key.StartsWith("display_pixels_y")) + { + if (!ushort.TryParse(value, out var resolutionY)) continue; + machine.ResolutionY = resolutionY; + } + + if (key.StartsWith("display_width")) + { + if (!float.TryParse(value, out var displayWidth)) continue; + machine.DisplayWidth = displayWidth; + } + + if (key.StartsWith("display_height")) + { + if (!float.TryParse(value, out var displayHeight)) continue; + machine.DisplayHeight = displayHeight; + } + + if (key.StartsWith("max_print_height")) + { + if (!float.TryParse(value, out var machineZ)) continue; + machine.MachineZ = machineZ; + } + + if (key.StartsWith("display_mirror_x")) + { + if(value.StartsWith("1")) displayMirror = displayMirror == FlipDirection.None ? FlipDirection.Horizontally : FlipDirection.Both; + } + + if (key.StartsWith("display_mirror_y")) + { + if (value.StartsWith("1")) displayMirror = displayMirror == FlipDirection.None ? FlipDirection.Vertically : FlipDirection.Both; + } + } + + if(machine.ResolutionX == 0 || machine.ResolutionY == 0) continue; + machine.Brand = brand; + machine.DisplayMirror = displayMirror; + yield return machine; + } + } + + + public static string GenerateMachinePresetsFromPrusaSlicer() + { + var machines = GetMachinesFromPrusaSlicer(); + var sb = new StringBuilder(); + + PrinterBrand lastBrand = default; + foreach (var machine in machines) + { + if (lastBrand != machine.Brand) + { + if(sb.Length > 0) sb.AppendLine(); + lastBrand = machine.Brand; + } + sb.AppendLine($"new(PrinterBrand.{machine.Brand}, \"{machine.Name}\", \"{machine.Model}\", {machine.ResolutionX}, {machine.ResolutionY}, {machine.DisplayWidth}f, {machine.DisplayHeight}f, {machine.MachineZ}f, FlipDirection.{machine.DisplayMirror}),"); + } + + return sb.ToString(); + } + + #endregion + } +} diff --git a/UVtools.Core/Printer/PrinterBrand.cs b/UVtools.Core/Printer/PrinterBrand.cs new file mode 100644 index 00000000..3bfd459d --- /dev/null +++ b/UVtools.Core/Printer/PrinterBrand.cs @@ -0,0 +1,31 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +namespace UVtools.Core.Printer +{ + public enum PrinterBrand : byte + { + Generic, + AnyCubic, + Creality, + Elegoo, + EPAX, + FlashForge, + Kelant, + Longer, + Nova3D, + Peopoly, + Phrozen, + Prusa, + QIDI, + Uniformation, + Uniz, + Voxelab, + Wanhao, + Zortrax, + } +} diff --git a/UVtools.Core/SystemOS/SystemAware.cs b/UVtools.Core/SystemOS/SystemAware.cs index ea19e803..33a8d434 100644 --- a/UVtools.Core/SystemOS/SystemAware.cs +++ b/UVtools.Core/SystemOS/SystemAware.cs @@ -255,6 +255,11 @@ public static void StartProcess(string name, string? arguments = null, bool wait } } + public static string GetExecutableName(string executable) + { + return OperatingSystem.IsWindows() ? $"{executable}.exe" : executable; + } + public static string? GetProcessOutput(string filename, string? arguments = null) { using var proc = Process.Start(new ProcessStartInfo diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 6a9120b8..f54b303c 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ https://github.com/sn4k3/UVtools https://github.com/sn4k3/UVtools MSLA/DLP, file analysis, calibration, repair, conversion and manipulation - 3.1.1 + 3.2.0 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 @@ -62,6 +62,8 @@ + + diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index 5ba08d3e..bd403a9e 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -2,7 +2,7 @@ - + @@ -903,15 +903,6 @@ - - - - - - - - - @@ -1143,12 +1134,48 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1250,6 +1277,9 @@ + + + @@ -1264,6 +1294,9 @@ + + + @@ -1278,6 +1311,9 @@ + + + @@ -1292,6 +1328,9 @@ + + + @@ -1306,6 +1345,9 @@ + + + @@ -1320,6 +1362,9 @@ + + + @@ -1334,6 +1379,9 @@ + + + @@ -1348,6 +1396,9 @@ + + + @@ -1362,6 +1413,9 @@ + + + @@ -1376,6 +1430,9 @@ + + + @@ -1390,6 +1447,9 @@ + + + @@ -1404,6 +1464,9 @@ + + + @@ -1418,6 +1481,9 @@ + + + @@ -1464,6 +1530,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs index 45e55c67..7003486d 100644 --- a/UVtools.WPF/App.axaml.cs +++ b/UVtools.WPF/App.axaml.cs @@ -9,7 +9,6 @@ using System; using System.ComponentModel; using System.Diagnostics; -using System.Drawing; using System.IO; using System.Linq; using System.Reflection; @@ -182,7 +181,7 @@ public override async void OnFrameworkInitializationCompleted() LayerHeight = 0.05f, Resolution = new (1440, 2560), Display = new (68.04f, 120.96f), - DisplayMirror = Enumerations.FlipDirection.Horizontally, + DisplayMirror = FlipDirection.Horizontally, MachineZ = 155, BottomLayerCount = 3, MachineName = "Epax X1" diff --git a/UVtools.WPF/ConsoleArguments.cs b/UVtools.WPF/ConsoleArguments.cs index d8e99cc7..788b104f 100644 --- a/UVtools.WPF/ConsoleArguments.cs +++ b/UVtools.WPF/ConsoleArguments.cs @@ -12,6 +12,7 @@ using UVtools.Core.FileFormats; using UVtools.Core.MeshFormats; using UVtools.Core.Operations; +using UVtools.Core.SystemOS; namespace UVtools.WPF; @@ -26,6 +27,13 @@ public static bool ParseArgs(string[] args) { if(args is null || args.Length == 0) return false; + if (args[0] is "--cmd" && args.Length > 1) + { + var newArgs = string.Join(' ', args[1..]); + SystemAware.StartProcess(Path.Combine(App.ApplicationPath, SystemAware.GetExecutableName("UVtoolsCmd")), newArgs); + return true; + } + // Convert to other file if (args[0] is "-c" or "--convert") { diff --git a/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml b/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml index 5cc041e1..be89a95f 100644 --- a/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml +++ b/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml @@ -2,77 +2,103 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="300" x:Class="UVtools.WPF.Controls.Tools.ToolChangeResolutionControl"> - - + + - - - - - - - - - - - - - - + + + + + + + + + + - + - - - + + + + + + + + - - + - - + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml.cs index 43e50448..92a7814b 100644 --- a/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolChangeResolutionControl.axaml.cs @@ -1,15 +1,17 @@ -using System.Diagnostics; -using System.Timers; +using System.Collections.Generic; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using UVtools.Core.Operations; +using UVtools.Core.Printer; namespace UVtools.WPF.Controls.Tools; public class ToolChangeResolutionControl : ToolControl { - private OperationChangeResolution.Resolution _selectedPresetItem; - public OperationChangeResolution Operation => BaseOperation as OperationChangeResolution; + public static IEnumerable MachinePresets => Machine.Machines; + public OperationChangeResolution Operation => BaseOperation as OperationChangeResolution; + private OperationChangeResolution.Resolution _selectedPresetItem; public OperationChangeResolution.Resolution SelectedPresetItem { get => _selectedPresetItem; @@ -19,15 +21,24 @@ public OperationChangeResolution.Resolution SelectedPresetItem if (_selectedPresetItem is null || _selectedPresetItem.IsEmpty) return; Operation.NewResolutionX = _selectedPresetItem.ResolutionX; Operation.NewResolutionY = _selectedPresetItem.ResolutionY; - - //SelectedPresetItem = null; - Timer timer = new(1); - timer.Elapsed += (sender, args) => - { - SelectedPresetItem = null; - timer.Dispose(); - }; - timer.Start(); + Dispatcher.UIThread.Post(() => SelectedPresetItem = null, DispatcherPriority.Loaded); + } + } + + private Machine _selectedMachinePresetItem; + public Machine SelectedMachinePresetItem + { + get => _selectedMachinePresetItem; + set + { + RaiseAndSetIfChanged(ref _selectedMachinePresetItem, value); + if (_selectedMachinePresetItem is null) return; + Operation.NewResolutionX = _selectedMachinePresetItem.ResolutionX; + Operation.NewResolutionY = _selectedMachinePresetItem.ResolutionY; + Operation.NewDisplayWidth = (decimal)_selectedMachinePresetItem.DisplayWidth; + Operation.NewDisplayHeight = (decimal)_selectedMachinePresetItem.DisplayHeight; + Operation.FixRatio = true; + Dispatcher.UIThread.Post(() => SelectedMachinePresetItem = null, DispatcherPriority.Loaded); } } diff --git a/UVtools.WPF/Controls/Tools/ToolInfillControl.axaml b/UVtools.WPF/Controls/Tools/ToolInfillControl.axaml index 21f319c6..71c97ed7 100644 --- a/UVtools.WPF/Controls/Tools/ToolInfillControl.axaml +++ b/UVtools.WPF/Controls/Tools/ToolInfillControl.axaml @@ -2,32 +2,29 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300" + mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="300" x:Class="UVtools.WPF.Controls.Tools.ToolInfillControl"> + Text="Pattern:"/> + Items="{Binding Operation.InfillType, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}" + SelectedItem="{Binding Operation.InfillType, Converter={StaticResource FromValueDescriptionToEnumConverter}}" + HorizontalAlignment="Stretch"/> - + + Text="Wall thickness:"/> + + diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 46524a62..c5a16bba 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -1401,6 +1401,18 @@ async void ProcessFile(string fileName, FileFormat.FileDecodeType fileDecodeType IsGUIEnabled = false; ShowProgressWindow($"Opening: {fileNameOnly}"); + /*var success = false; + try + { + await SlicerFile.DecodeAsync(fileName, fileDecodeType, Progress); + success = true; + } + catch (OperationCanceledException) { } + catch (Exception exception) + { + await this.MessageBoxError(exception.ToString(), "Error opening the file"); + }*/ + var task = await Task.Factory.StartNew( () => { try @@ -1408,10 +1420,8 @@ async void ProcessFile(string fileName, FileFormat.FileDecodeType fileDecodeType SlicerFile.Decode(fileName, fileDecodeType, Progress); return true; } - catch (OperationCanceledException) - { - } - catch (Exception exception) + catch (OperationCanceledException) {} + catch (Exception exception) { Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(exception.ToString(), "Error opening the file")); @@ -1626,15 +1636,15 @@ await this.MessageBoxError(exception.ToString(), if (Settings.LayerPreview.AutoFlipLayerIfMirrored) { - if (SlicerFile.DisplayMirror == Enumerations.FlipDirection.None) + if (SlicerFile.DisplayMirror == FlipDirection.None) { _showLayerImageFlipped = false; } else { _showLayerImageFlipped = true; - _showLayerImageFlippedHorizontally = SlicerFile.DisplayMirror is Enumerations.FlipDirection.Horizontally or Enumerations.FlipDirection.Both; - _showLayerImageFlippedVertically = SlicerFile.DisplayMirror is Enumerations.FlipDirection.Vertically or Enumerations.FlipDirection.Both; + _showLayerImageFlippedHorizontally = SlicerFile.DisplayMirror is FlipDirection.Horizontally or FlipDirection.Both; + _showLayerImageFlippedVertically = SlicerFile.DisplayMirror is FlipDirection.Vertically or FlipDirection.Both; } } } @@ -1910,44 +1920,25 @@ public async Task SaveFile(string filepath = null, bool ignoreOverwriteWar if(result != ButtonResult.Yes) return false; } - - filepath = SlicerFile.FileFullPath; } - var oldFile = SlicerFile.FileFullPath; - var tempFile = filepath + FileFormat.TemporaryFileAppend; - IsGUIEnabled = false; ShowProgressWindow($"Saving {Path.GetFileName(filepath)}"); - + + var oldFile = SlicerFile.FileFullPath; + var task = await Task.Factory.StartNew( () => { try { - SlicerFile.SaveAs(tempFile, Progress); - if (File.Exists(filepath)) - { - File.Delete(filepath); - } - File.Move(tempFile, filepath); - SlicerFile.FileFullPath = filepath; + SlicerFile.SaveAs(filepath, Progress); return true; } catch (OperationCanceledException) { - SlicerFile.FileFullPath = oldFile; - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } } catch (Exception ex) { - SlicerFile.FileFullPath = oldFile; - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(ex.ToString(), "Error while saving the file")); } diff --git a/UVtools.WPF/Program.cs b/UVtools.WPF/Program.cs index 92ae882a..27672a83 100644 --- a/UVtools.WPF/Program.cs +++ b/UVtools.WPF/Program.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Runtime.ExceptionServices; using Avalonia; using Projektanker.Icons.Avalonia; @@ -39,6 +38,39 @@ public static void Main(string[] args) return; } + //var mat = EmguExtensions.InitMat(new System.Drawing.Size(2000, 1080)); + /*const byte z = 1; + int pixel = 1000; + + for (int y = 0; y < mat.Height; y+=200) + for (int x = 0; x < mat.Width; x+=1) + { + float x1 = x / (float)mat.Width; + float y1 = y / (float)mat.Height; + + //var result = Math.Sin(x1) * Math.Cos(y1) + Math.Sin(y1) * Math.Cos(1) + Math.Sin(1) * Math.Cos(x1); + //mat.SetByte((int) result* mat.Width, 255); + + //CvInvoke.Circle(mat, new Point(x, (int)pixelY + y), 1, EmguExtensions.WhiteColor, -1, LineType.AntiAlias); + }*/ + + + /*var sineHeight = 100; + var sineWidth = 100; + byte radius = 10; + + for (int y1 = 0; y1 < mat.Height; y1 += sineHeight) + for (int x = 0; x < mat.Width; x++) + { + int y2 = (int)(Math.Sin((double)x / sineWidth) * sineHeight / 2.0 + sineHeight / 2.0 + radius); + + CvInvoke.Circle(mat, new Point(x, y1+y2), radius, EmguExtensions.WhiteColor, -1, LineType.AntiAlias); + } + + CvInvoke.Imshow("gyroid", mat); + CvInvoke.WaitKey(); + return;*/ + /*Slicer slicer = new(Size.Empty, SizeF.Empty, "D:\\Cube100x100x100.stl"); var slices = slicer.SliceModel(0.05f); @@ -51,6 +83,10 @@ public static void Main(string[] args) mat.Save(@$"D:\SLICE\{slice.Key}.png"); }*/ + // PrusaSlicer to Machine.cs + //var machines = Machine.GetMachinesFromPrusaSlicer(); + //var machinesText = Machine.GenerateMachinePresetsFromPrusaSlicer(); + // Add the event handler for handling non-UI thread exceptions to the event. AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException; //AppDomain.CurrentDomain.FirstChanceException += CurrentDomainOnFirstChanceException; diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index d240df3b..40f70d1c 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ LICENSE https://github.com/sn4k3/UVtools Git - 3.1.1 + 3.2.0 AnyCPU;x64 UVtools.png README.md @@ -43,8 +43,6 @@ - - diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs index d764ed9d..73f3d37a 100644 --- a/UVtools.WPF/UserSettings.cs +++ b/UVtools.WPF/UserSettings.cs @@ -1789,7 +1789,7 @@ public static void Load() { var application = new MappedProcess(executable, "Open archive: WinRAR") { - CompatibleExtensions = "zip;sl1;sl1s;cws;zcode;zcodex;vdt;uvj" + CompatibleExtensions = "zip;sl1;sl1s;cws;zcode;zcodex;jxs;vdt;uvj" }; _instance.General.SendToProcess.Add(application); } diff --git a/UVtools.WPF/Windows/ToolWindow.axaml b/UVtools.WPF/Windows/ToolWindow.axaml index 504f6c44..3ad837b0 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml +++ b/UVtools.WPF/Windows/ToolWindow.axaml @@ -370,7 +370,7 @@ i:MenuItem.Icon="fas fa-file-import"/> - diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs index 4dd03568..a7772fa9 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml.cs +++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs @@ -123,7 +123,7 @@ public uint LayerIndexStart if (ToolControl?.BaseOperation is not null) { - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.None; ToolControl.BaseOperation.LayerIndexStart = value; } @@ -151,7 +151,7 @@ public uint LayerIndexEnd if (ToolControl?.BaseOperation is not null) { - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.None; ToolControl.BaseOperation.LayerIndexEnd = value; } @@ -189,14 +189,14 @@ public void SelectAllLayers() LayerIndexStart = 0; LayerIndexEnd = MaximumLayerIndex; if(ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.All; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.All; } public void SelectCurrentLayer() { LayerIndexStart = LayerIndexEnd = App.MainWindow.ActualLayer; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Current; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.Current; } public void SelectFirstToCurrentLayer() @@ -204,7 +204,7 @@ public void SelectFirstToCurrentLayer() LayerIndexEnd = App.MainWindow.ActualLayer; LayerIndexStart = 0; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.None; } public void SelectCurrentToLastLayer() @@ -212,7 +212,7 @@ public void SelectCurrentToLastLayer() LayerIndexStart = App.MainWindow.ActualLayer; LayerIndexEnd = SlicerFile.LastLayerIndex; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.None; } public void SelectBottomLayers() @@ -220,7 +220,7 @@ public void SelectBottomLayers() LayerIndexStart = 0; LayerIndexEnd = Math.Max(1, SlicerFile.FirstNormalLayer?.Index ?? 1) - 1u; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Bottom; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.Bottom; } public void SelectNormalLayers() @@ -228,45 +228,45 @@ public void SelectNormalLayers() LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; LayerIndexEnd = MaximumLayerIndex; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Normal; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.Normal; } public void SelectFirstLayer() { LayerIndexStart = LayerIndexEnd = 0; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.First; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.First; } public void SelectLastLayer() { LayerIndexStart = LayerIndexEnd = MaximumLayerIndex; if (ToolControl is not null) - ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Last; + ToolControl.BaseOperation.LayerRangeSelection = LayerRangeSelection.Last; } - public void SelectLayers(Enumerations.LayerRangeSelection range) + public void SelectLayers(LayerRangeSelection range) { switch (range) { - case Enumerations.LayerRangeSelection.None: + case LayerRangeSelection.None: break; - case Enumerations.LayerRangeSelection.All: + case LayerRangeSelection.All: SelectAllLayers(); break; - case Enumerations.LayerRangeSelection.Current: + case LayerRangeSelection.Current: SelectCurrentLayer(); break; - case Enumerations.LayerRangeSelection.Bottom: + case LayerRangeSelection.Bottom: SelectBottomLayers(); break; - case Enumerations.LayerRangeSelection.Normal: + case LayerRangeSelection.Normal: SelectNormalLayers(); break; - case Enumerations.LayerRangeSelection.First: + case LayerRangeSelection.First: SelectFirstLayer(); break; - case Enumerations.LayerRangeSelection.Last: + case LayerRangeSelection.Last: SelectLastLayer(); break; default: @@ -387,7 +387,7 @@ public Operation SelectedProfileItem ToolControl.BaseOperation = operation; switch (operation.LayerRangeSelection) { - case Enumerations.LayerRangeSelection.None: + case LayerRangeSelection.None: LayerIndexStart = operation.LayerIndexStart; LayerIndexEnd = operation.LayerIndexEnd; break; @@ -598,7 +598,7 @@ public ToolWindow(string description = null, bool layerRangeVisible = true, bool _layerIndexEndEnabled = layerEndIndexEnabled; } - public ToolWindow(ToolControl toolControl) : this(toolControl.BaseOperation.Description, toolControl.BaseOperation.StartLayerRangeSelection != Enumerations.LayerRangeSelection.None, toolControl.BaseOperation.LayerIndexEndEnabled) + public ToolWindow(ToolControl toolControl) : this(toolControl.BaseOperation.Description, toolControl.BaseOperation.StartLayerRangeSelection != LayerRangeSelection.None, toolControl.BaseOperation.LayerIndexEndEnabled) { ToolControl = toolControl; toolControl.ParentWindow = this; @@ -607,7 +607,7 @@ public ToolWindow(ToolControl toolControl) : this(toolControl.BaseOperation.Desc ToolControl.BaseOperation.MaskPoints = Masks; Title = toolControl.BaseOperation.Title; - //LayerRangeVisible = toolControl.BaseOperation.StartLayerRangeSelection != Enumerations.LayerRangeSelection.None; + //LayerRangeVisible = toolControl.BaseOperation.StartLayerRangeSelection != LayerRangeSelection.None; //IsROIVisible = toolControl.BaseOperation.CanROI; _contentControl = toolControl; _buttonOkText = toolControl.BaseOperation.ButtonOkText; @@ -639,7 +639,7 @@ public ToolWindow(ToolControl toolControl) : this(toolControl.BaseOperation.Desc App.MainWindow.AddMaskPoints(toolControl.BaseOperation.MaskPoints); } - if (toolControl.BaseOperation.LayerRangeSelection == Enumerations.LayerRangeSelection.None) + if (toolControl.BaseOperation.LayerRangeSelection == LayerRangeSelection.None) { LayerIndexStart = toolControl.BaseOperation.LayerIndexStart; LayerIndexEnd = toolControl.BaseOperation.LayerIndexEnd; @@ -884,7 +884,7 @@ public async void ImportSettings() ToolControl.BaseOperation = operation; switch (operation.LayerRangeSelection) { - case Enumerations.LayerRangeSelection.None: + case LayerRangeSelection.None: LayerIndexStart = operation.LayerIndexStart; LayerIndexEnd = operation.LayerIndexEnd; break; diff --git a/UVtools.sln b/UVtools.sln index 329a646e..9253b9e7 100644 --- a/UVtools.sln +++ b/UVtools.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29926.136 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32228.430 MinimumVisualStudioVersion = 10.0.40219.1 Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "UVtools.InstallerMM", "UVtools.InstallerMM\UVtools.InstallerMM.wixproj", "{E53BAA5D-29A8-4287-B3AA-1AFF5A4BDC6C}" ProjectSection(ProjectDependencies) = postProject @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UVtools.ScriptSample", "UVt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UVtools.AvaloniaControls", "UVtools.AvaloniaControls\UVtools.AvaloniaControls.csproj", "{0858E09F-023D-4F9B-A0E9-802C26EF9C0A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UVtools.Cmd", "UVtools.Cmd\UVtools.Cmd.csproj", "{D0733F51-0CB1-483B-8F93-36B815FBFFF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,18 @@ Global {0858E09F-023D-4F9B-A0E9-802C26EF9C0A}.Release|x64.Build.0 = Release|Any CPU {0858E09F-023D-4F9B-A0E9-802C26EF9C0A}.Release|x86.ActiveCfg = Release|Any CPU {0858E09F-023D-4F9B-A0E9-802C26EF9C0A}.Release|x86.Build.0 = Release|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Debug|x64.ActiveCfg = Debug|x64 + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Debug|x64.Build.0 = Debug|x64 + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Debug|x86.Build.0 = Debug|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Release|Any CPU.Build.0 = Release|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Release|x64.ActiveCfg = Release|x64 + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Release|x64.Build.0 = Release|x64 + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Release|x86.ActiveCfg = Release|Any CPU + {D0733F51-0CB1-483B-8F93-36B815FBFFF3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE