diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-08-09 01:57:00 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-08-09 01:57:00 +0300 |
commit | c94b5fb7f13ffa15bf714aff4623978402ec11db (patch) | |
tree | b83b33830ca066f40c0d62b2dd07a67a96e2abcf /UVtools.Core | |
parent | ff354559a5c91d18cfec69118d1333db610d026a (diff) |
v2.17.0v2.17.0
- **Windows MSI:**
- (Fix) Use the folder programs x64 instead of x86 for new installation path (#254)
- (Improvement) Mark program and all files as x64
- (Improvement) Add UVtools logo to side panel and top banner
- (Improvement) Add open-source logo to side panel
- (Improvement) License text aligment and bold title
- (Add) File format: OSLA / ODLP / OMSLA - Universal binary file format
- (Add) Calibration - Lift height: Generates test models with various strategies and increments to measure the optimal lift height or peel forces for layers given the printed area
- (Add) Layer Actions - Export layers to images (PNG, JPG/JPEG, JP2, TIF/TIFF, BMP, PBM, PGM, SR/RAS and SVG)
- (Add) About box: License with link
- (Add) Include a copy of the LICENSE on the packages
- (Improvement) File formats: Implement `Wait time before cure` properties on file formats with light-off delay, when used it will calculate the right light-off delay with that extra time and set to `LightOffDelay` property
- (Improvement) Change all date times to Utc instead of local
- (Fix) Tool - Flip: 'Both' were not working correctly
- (Fix) Linux: File 'UVtools.sh' with incorrect line break type, changed to \n (#258)
Diffstat (limited to 'UVtools.Core')
40 files changed, 2019 insertions, 227 deletions
diff --git a/UVtools.Core/About.cs b/UVtools.Core/About.cs index ba904d5..1bc41d5 100644 --- a/UVtools.Core/About.cs +++ b/UVtools.Core/About.cs @@ -13,15 +13,20 @@ namespace UVtools.Core { public static class About { - public static string Software = "UVtools"; - public static string Author = "Tiago Conceição"; - public static string Company = "PTRTECH"; - public static string Website = "https://github.com/sn4k3/UVtools"; - public static string Donate = "https://paypal.me/SkillTournament"; + public const string Software = "UVtools"; + public static string SoftwareWithVersion => $"{Software} v{VersionStr}"; + public const string Author = "Tiago Conceição"; + public const string Company = "PTRTECH"; + public const string License = "GNU Affero General Public License v3.0 (AGPL)"; + public const string LicenseUrl = "https://github.com/sn4k3/UVtools/blob/master/LICENSE"; + public const string Website = "https://github.com/sn4k3/UVtools"; + public const string Donate = "https://paypal.me/SkillTournament"; + public const string Sponsor = "https://github.com/sponsors/sn4k3"; - public static string DemoFile = "UVtools_demo_file.sl1"; + public const string DemoFile = "UVtools_demo_file.sl1"; public static Version Version => Assembly.GetExecutingAssembly().GetName().Version; public static string VersionStr => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); + public static string Arch => Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; } } diff --git a/UVtools.Core/Enumerations.cs b/UVtools.Core/Enumerations.cs index 468b389..98be0a5 100644 --- a/UVtools.Core/Enumerations.cs +++ b/UVtools.Core/Enumerations.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; using System.ComponentModel; +using Emgu.CV.CvEnum; namespace UVtools.Core { @@ -24,11 +27,27 @@ namespace UVtools.Core public enum FlipDirection : byte { + None, Horizontally, Vertically, Both, } + public enum RotateDirection : sbyte + { + [Description("None")] + None = -1, + /// <summary>Rotate 90 degrees clockwise (0)</summary> + [Description("Rotate 90º CW")] + Rotate90Clockwise = 0, + /// <summary>Rotate 180 degrees clockwise (1)</summary> + [Description("Rotate 180º")] + Rotate180 = 1, + /// <summary>Rotate 270 degrees clockwise (2)</summary> + [Description("Rotate 90º CCW")] + Rotate90CounterClockwise = 2, + } + public enum Anchor : byte { TopLeft, TopCenter, TopRight, @@ -51,5 +70,29 @@ namespace UVtools.Core [Description("Disabled")] NoAction } + + public static FlipType ToOpenCVFlipType(FlipDirection flip) + { + return flip switch + { + FlipDirection.None => throw new NotSupportedException($"Flip type: {flip} is not supported by OpenCV."), + FlipDirection.Horizontally => FlipType.Horizontal, + FlipDirection.Vertically => FlipType.Vertical, + FlipDirection.Both => FlipType.Both, + _ => throw new ArgumentOutOfRangeException(nameof(flip), flip, null) + }; + } + + public static RotateFlags ToOpenCVRotateFlags(RotateDirection rotate) + { + return rotate switch + { + RotateDirection.None => throw new NotSupportedException($"Rotate direction: {rotate} is not supported by OpenCV."), + RotateDirection.Rotate90Clockwise => RotateFlags.Rotate90Clockwise, + RotateDirection.Rotate90CounterClockwise => RotateFlags.Rotate90CounterClockwise, + RotateDirection.Rotate180 => RotateFlags.Rotate180, + _ => throw new ArgumentOutOfRangeException(nameof(rotate), rotate, null) + }; + } } } diff --git a/UVtools.Core/Extensions/EnumExtensions.cs b/UVtools.Core/Extensions/EnumExtensions.cs index 493418c..6847164 100644 --- a/UVtools.Core/Extensions/EnumExtensions.cs +++ b/UVtools.Core/Extensions/EnumExtensions.cs @@ -33,7 +33,7 @@ namespace UVtools.Core.Extensions if (!t.IsEnum) throw new ArgumentException($"{nameof(t)} must be an enum type"); - return Enum.GetValues(t).Cast<Enum>().Select(e => new ValueDescription(e, e.GetDescription())).ToList(); + return Enum.GetValues(t).Cast<Enum>().OrderBy(e => e).Select(e => new ValueDescription(e, e.GetDescription())).ToList(); } } } diff --git a/UVtools.Core/Extensions/FileStreamExtensions.cs b/UVtools.Core/Extensions/FileStreamExtensions.cs index 02fba02..c9b530c 100644 --- a/UVtools.Core/Extensions/FileStreamExtensions.cs +++ b/UVtools.Core/Extensions/FileStreamExtensions.cs @@ -24,6 +24,9 @@ namespace UVtools.Core.Extensions return buffer; } + public static byte[] ReadBytes(this FileStream fs, uint length, int offset = 0) + => fs.ReadBytes((int)length, offset); + public static uint ReadUShortLittleEndian(this FileStream fs, int offset = 0) { return BitExtensions.ToUShortLittleEndian(fs.ReadBytes(2, offset)); @@ -44,6 +47,26 @@ namespace UVtools.Core.Extensions return BitExtensions.ToUIntBigEndian(fs.ReadBytes(4, offset)); } + public static void WriteUShortLittleEndian(this FileStream fs, ushort value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesLittleEndian(value), offset); + } + + public static void WriteUShortBigEndian(this FileStream fs, ushort value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesBigEndian(value), offset); + } + + public static void WriteUIntLittleEndian(this FileStream fs, uint value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesLittleEndian(value), offset); + } + + public static void WriteUIntBigEndian(this FileStream fs, uint value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesBigEndian(value), offset); + } + public static uint WriteStream(this FileStream fs, MemoryStream stream, int offset = 0) { return fs.WriteBytes(stream.ToArray(), offset); diff --git a/UVtools.Core/FileFormats/CWSFile.cs b/UVtools.Core/FileFormats/CWSFile.cs index ca8388b..40f4ca1 100644 --- a/UVtools.Core/FileFormats/CWSFile.cs +++ b/UVtools.Core/FileFormats/CWSFile.cs @@ -398,7 +398,7 @@ namespace UVtools.Core.FileFormats set => base.MachineZ = OutputSettings.PlatformZSize = (float)Math.Round(value, 2); } - public override bool MirrorDisplay + public override bool DisplayMirror { get => OutputSettings.FlipX; set @@ -546,7 +546,7 @@ namespace UVtools.Core.FileFormats GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Partial, GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, - GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndexZero, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G1, EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1 }; @@ -629,7 +629,7 @@ namespace UVtools.Core.FileFormats var entry = outputFile.CreateEntry("slice.conf"); using TextWriter tw = new StreamWriter(entry.Open()); - tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.Now}"); + tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); tw.WriteLine("# conf version 1.0"); tw.WriteLine(""); @@ -875,7 +875,7 @@ namespace UVtools.Core.FileFormats if (!SupportsGCode || SuppressRebuildGCode) return; //string arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; //GCode.Clear(); - //GCode.AppendLine($"; {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.Now}"); + //GCode.AppendLine($"; {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); StringBuilder sb = new(); sb.AppendLine(";(**** Build and Slicing Parameters ****)"); @@ -1005,7 +1005,7 @@ namespace UVtools.Core.FileFormats using (TextWriter tw = new StreamWriter(stream)) { - tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.Now}"); + tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); tw.WriteLine("# conf version 1.0"); tw.WriteLine(""); diff --git a/UVtools.Core/FileFormats/CXDLPFile.cs b/UVtools.Core/FileFormats/CXDLPFile.cs index 1e93bda..f6f9d6d 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -492,6 +492,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = SlicerInfoSettings.LightOffDelay = (ushort)value; } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => SlicerInfoSettings.BottomExposureTime; diff --git a/UVtools.Core/FileFormats/CXDLPv1File.cs b/UVtools.Core/FileFormats/CXDLPv1File.cs index 0e96f5a..752cebc 100644 --- a/UVtools.Core/FileFormats/CXDLPv1File.cs +++ b/UVtools.Core/FileFormats/CXDLPv1File.cs @@ -441,6 +441,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = SlicerInfoSettings.LightOffDelay = (ushort)value; } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => SlicerInfoSettings.BottomExposureTime; diff --git a/UVtools.Core/FileFormats/ChituboxFile.cs b/UVtools.Core/FileFormats/ChituboxFile.cs index 512ae46..d2fdc78 100644 --- a/UVtools.Core/FileFormats/ChituboxFile.cs +++ b/UVtools.Core/FileFormats/ChituboxFile.cs @@ -27,21 +27,31 @@ namespace UVtools.Core.FileFormats { #region Constants - private const uint MAGIC_CBDDLP = 0x12FD0019; // 318570521 - private const uint MAGIC_CBT = 0x12FD0086; // 318570630 - private const uint MAGIC_CBTv4 = 0x12FD0106; // 318570758 - private const ushort REPEATRGB15MASK = 0x20; + public const uint MAGIC_CBDDLP = 0x12FD0019; // 318570521 + public const uint MAGIC_CBT = 0x12FD0086; // 318570630 + public const uint MAGIC_CBTv4 = 0x12FD0106; // 318570758 + public const uint MAGIC_CBT_ENCRYPTED = 0x12FD0107; // 318570759 + public const ushort REPEATRGB15MASK = 0x20; - private const byte RLE8EncodingLimit = 0x7d; // 125; - private const ushort RLE16EncodingLimit = 0xFFF; + public const byte RLE8EncodingLimit = 0x7d; // 125; + public const ushort RLE16EncodingLimit = 0xFFF; - private const uint ENCRYPTYION_MODE_CBDDLP = 0x8; // 0 or 8 - private const uint ENCRYPTYION_MODE_CTBv2 = 0xF; // 15 for ctb v2 files - private const uint ENCRYPTYION_MODE_CTBv3 = 536870927; // 536870927 for ctb v3 files (This allow per layer settings, while 15 don't) - private const uint ENCRYPTYION_MODE_CTBv4 = 1073741839; // 1073741839 for ctb v3 files (This allow per layer settings, while 15 don't) + public const uint ENCRYPTYION_MODE_CBDDLP = 0x8; // 0 or 8 + public const uint ENCRYPTYION_MODE_CTBv2 = 0xF; // 15 for ctb v2 files + public const uint ENCRYPTYION_MODE_CTBv3 = 536870927; // 536870927 for ctb v3 files (This allow per layer settings, while 15 don't) + public const uint ENCRYPTYION_MODE_CTBv4 = 1073741839; // 1073741839 for ctb v3 files (This allow per layer settings, while 15 don't) private const string CTBv4_DISCLAIMER = "Layout and record format for the ctb and cbddlp file types are the copyrighted programs or codes of CBD Technology (China) Inc..The Customer or User shall not in any manner reproduce, distribute, modify, decompile, disassemble, decrypt, extract, reverse engineer, lease, assign, or sublicense the said programs or codes."; private const ushort CTBv4_DISCLAIMER_SIZE = 320; + + public static readonly byte[] SomethingNew1 = { + 0xD0, 0x5B, 0x8E, 0x33, 0x71, 0xDE, 0x3D, 0x1A, 0xE5, 0x4F, 0x22, 0xDD, 0xDF, 0x5B, 0xFD, 0x94, + 0xAB, 0x5D, 0x64, 0x3A, 0x9D, 0x7E, 0xBF, 0xAF, 0x42, 0x03, 0xF3, 0x10, 0xD8, 0x52, 0x2A, 0xEA + }; + + public static readonly byte[] SomethingNew2 = { + 0x0F, 0x01, 0x0A, 0x05, 0x05, 0x0B, 0x06, 0x07, 0x08, 0x06, 0x0A, 0x0C, 0x0C, 0x0D, 0x09, 0x0F + }; #endregion #region Sub Classes @@ -1060,7 +1070,7 @@ namespace UVtools.Core.FileFormats public LayerData[,] LayerDefinitions { get; private set; } - public Dictionary<string, LayerData> LayersHash { get; } = new Dictionary<string, LayerData>(); + public Dictionary<string, LayerData> LayersHash { get; } = new(); public override FileFormatType FileType => FileFormatType.Binary; @@ -1233,7 +1243,7 @@ namespace UVtools.Core.FileFormats set => base.MachineZ = HeaderSettings.BedSizeZ = (float)Math.Round(value, 2); } - public override bool MirrorDisplay + public override bool DisplayMirror { get => HeaderSettings.ProjectorType > 0; set @@ -1321,7 +1331,21 @@ namespace UVtools.Core.FileFormats } } - public override float BottomWaitTimeBeforeCure => WaitTimeBeforeCure; + public override float BottomWaitTimeBeforeCure + { + get => WaitTimeBeforeCure; + set + { + if (HeaderSettings.Version < 4) + { + if (value > 0) + { + SetBottomLightOffDelay(value); + } + } + } + } + public override float WaitTimeBeforeCure { get => HeaderSettings.Version >= 4 ? PrintParametersV4Settings.RestTimeAfterRetract : 0; @@ -1331,7 +1355,6 @@ namespace UVtools.Core.FileFormats { if (value > 0) { - SetBottomLightOffDelay(value); SetNormalLightOffDelay(value); } @@ -1729,7 +1752,7 @@ namespace UVtools.Core.FileFormats FileFullPath = fileFullPath; - progress.Reset(OperationProgress.StatusDecodeThumbnails, ThumbnailsCount); + progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount); Debug.Write("Header -> "); Debug.WriteLine(HeaderSettings); diff --git a/UVtools.Core/FileFormats/ChituboxZipFile.cs b/UVtools.Core/FileFormats/ChituboxZipFile.cs index 6a29dff..7d663e8 100644 --- a/UVtools.Core/FileFormats/ChituboxZipFile.cs +++ b/UVtools.Core/FileFormats/ChituboxZipFile.cs @@ -168,7 +168,7 @@ namespace UVtools.Core.FileFormats set => base.MachineZ = HeaderSettings.MachineZ = (float)Math.Round(value, 2); } - public override bool MirrorDisplay + public override bool DisplayMirror { get => HeaderSettings.Mirror > 0; set @@ -360,7 +360,7 @@ namespace UVtools.Core.FileFormats GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Absolute, GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, - GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.FilenameNonZeroPNG, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.FilenamePng1Started, LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G0, EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1 }; @@ -468,9 +468,6 @@ namespace UVtools.Core.FileFormats progress.ItemCount = LayerCount; - var gcode = GCodeStr; - float lastPostZ = LayerHeight; - for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { if (progress.Token.IsCancellationRequested) break; diff --git a/UVtools.Core/FileFormats/FDGFile.cs b/UVtools.Core/FileFormats/FDGFile.cs index cab1b55..a80e0c1 100644 --- a/UVtools.Core/FileFormats/FDGFile.cs +++ b/UVtools.Core/FileFormats/FDGFile.cs @@ -751,7 +751,7 @@ namespace UVtools.Core.FileFormats set => base.MachineZ = HeaderSettings.BedSizeZ = (float)Math.Round(value, 2); } - public override bool MirrorDisplay + public override bool DisplayMirror { get => HeaderSettings.ProjectorType > 0; set @@ -811,6 +811,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = HeaderSettings.LightOffDelay = (float)Math.Round(value, 2); } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => HeaderSettings.BottomExposureSeconds; @@ -1070,7 +1090,7 @@ namespace UVtools.Core.FileFormats FileFullPath = fileFullPath; - progress.Reset(OperationProgress.StatusDecodeThumbnails, ThumbnailsCount); + progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount); Debug.Write("Header -> "); Debug.WriteLine(HeaderSettings); diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 5f627c4..465e68a 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -235,6 +235,7 @@ namespace UVtools.Core.FileFormats new FDGFile(), // fdg new PhotonWorkshopFile(), // PSW new CWSFile(), // CWS + new OSLAFile(), // OSLA new ZCodeFile(), // zcode new ZCodexFile(), // zcodex new MDLPFile(), // MKS v1 @@ -487,6 +488,9 @@ namespace UVtools.Core.FileFormats /// </summary> public string FileFullPath { get; set; } + public string Filename => Path.GetFileName(FileFullPath); + public string FilenameNoExt => GetFileNameStripExtensions(FileFullPath); + /// <summary> /// Gets the thumbnails count present in this file format /// </summary> @@ -640,7 +644,7 @@ namespace UVtools.Core.FileFormats /// <summary> /// Gets or sets if images need to be mirrored on lcd to print on the correct orientation /// </summary> - public virtual bool MirrorDisplay { get; set; } + public virtual bool DisplayMirror { get; set; } /// <summary> /// Gets if the display is in portrait mode @@ -827,22 +831,49 @@ namespace UVtools.Core.FileFormats /// </summary> public uint NormalLayerCount => LayerCount - BottomLayerCount; + /// <summary> + /// Gets or sets the bottom layer off time in seconds + /// </summary> + public virtual float BottomLightOffDelay + { + get => _bottomLightOffDelay; + set + { + RaiseAndSet(ref _bottomLightOffDelay, (float)Math.Round(value, 2)); + RaisePropertyChanged(nameof(LightOffDelayRepresentation)); + } + } + + /// <summary> + /// Gets or sets the layer off time in seconds + /// </summary> + public virtual float LightOffDelay + { + get => _lightOffDelay; + set + { + RaiseAndSet(ref _lightOffDelay, (float)Math.Round(value, 2)); + RaisePropertyChanged(nameof(LightOffDelayRepresentation)); + } + } + public virtual float BottomWaitTimeBeforeCure { get => _bottomWaitTimeBeforeCure; set { - RaiseAndSet(ref _bottomWaitTimeBeforeCure, value); + RaiseAndSet(ref _bottomWaitTimeBeforeCure, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(WaitTimeRepresentation)); } } + public virtual float WaitTimeBeforeCure { get => _waitTimeBeforeCure; set { - RaiseAndSet(ref _waitTimeBeforeCure, value); + RaiseAndSet(ref _waitTimeBeforeCure, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(WaitTimeRepresentation)); } } @@ -855,7 +886,7 @@ namespace UVtools.Core.FileFormats get => _bottomExposureTime; set { - RaiseAndSet(ref _bottomExposureTime, value); + RaiseAndSet(ref _bottomExposureTime, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(ExposureRepresentation)); } } @@ -868,7 +899,7 @@ namespace UVtools.Core.FileFormats get => _exposureTime; set { - RaiseAndSet(ref _exposureTime, value); + RaiseAndSet(ref _exposureTime, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(ExposureRepresentation)); } } @@ -878,7 +909,7 @@ namespace UVtools.Core.FileFormats get => _bottomWaitTimeAfterCure; set { - RaiseAndSet(ref _bottomWaitTimeAfterCure, value); + RaiseAndSet(ref _bottomWaitTimeAfterCure, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(WaitTimeRepresentation)); } } @@ -888,7 +919,7 @@ namespace UVtools.Core.FileFormats get => _waitTimeAfterCure; set { - RaiseAndSet(ref _waitTimeAfterCure, value); + RaiseAndSet(ref _waitTimeAfterCure, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(WaitTimeRepresentation)); } } @@ -901,7 +932,7 @@ namespace UVtools.Core.FileFormats get => _bottomLiftHeight; set { - RaiseAndSet(ref _bottomLiftHeight, value); + RaiseAndSet(ref _bottomLiftHeight, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(LiftRepresentation)); } } @@ -914,7 +945,7 @@ namespace UVtools.Core.FileFormats get => _liftHeight; set { - RaiseAndSet(ref _liftHeight, value); + RaiseAndSet(ref _liftHeight, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(LiftRepresentation)); } } @@ -927,7 +958,7 @@ namespace UVtools.Core.FileFormats get => _bottomLiftSpeed; set { - RaiseAndSet(ref _bottomLiftSpeed, value); + RaiseAndSet(ref _bottomLiftSpeed, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(LiftRepresentation)); } } @@ -940,7 +971,7 @@ namespace UVtools.Core.FileFormats get => _liftSpeed; set { - RaiseAndSet(ref _liftSpeed, value); + RaiseAndSet(ref _liftSpeed, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(LiftRepresentation)); } } @@ -950,7 +981,7 @@ namespace UVtools.Core.FileFormats get => _bottomWaitTimeAfterLift; set { - RaiseAndSet(ref _bottomWaitTimeAfterLift, value); + RaiseAndSet(ref _bottomWaitTimeAfterLift, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(WaitTimeRepresentation)); } } @@ -960,7 +991,7 @@ namespace UVtools.Core.FileFormats get => _waitTimeAfterLift; set { - RaiseAndSet(ref _waitTimeAfterLift, value); + RaiseAndSet(ref _waitTimeAfterLift, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(WaitTimeRepresentation)); } } @@ -973,38 +1004,12 @@ namespace UVtools.Core.FileFormats get => _retractSpeed; set { - RaiseAndSet(ref _retractSpeed, value); + RaiseAndSet(ref _retractSpeed, (float)Math.Round(value, 2)); RaisePropertyChanged(nameof(RetractRepresentation)); } } /// <summary> - /// Gets or sets the bottom layer off time in seconds - /// </summary> - public virtual float BottomLightOffDelay - { - get => _bottomLightOffDelay; - set - { - RaiseAndSet(ref _bottomLightOffDelay, value); - RaisePropertyChanged(nameof(LightOffDelayRepresentation)); - } - } - - /// <summary> - /// Gets or sets the layer off time in seconds - /// </summary> - public virtual float LightOffDelay - { - get => _lightOffDelay; - set - { - RaiseAndSet(ref _lightOffDelay, value); - RaisePropertyChanged(nameof(LightOffDelayRepresentation)); - } - } - - /// <summary> /// Gets or sets the bottom pwm value from 0 to 255 /// </summary> public virtual byte BottomLightPWM @@ -1547,7 +1552,7 @@ namespace UVtools.Core.FileFormats if (Thumbnails is not null) { - for (int i = 0; i < ThumbnailsCount; i++) + for (int i = 0; i < Thumbnails.Length; i++) { Thumbnails[i]?.Dispose(); } @@ -2316,6 +2321,7 @@ namespace UVtools.Core.FileFormats public float CalculateLightOffDelay(bool isBottomLayer, float extraTime = 0) { + extraTime = (float)Math.Round(extraTime, 2); if (SupportsGCode) return extraTime; return isBottomLayer ? OperationCalculator.LightOffDelayC.CalculateSeconds(BottomLiftHeight, BottomLiftSpeed, RetractSpeed, extraTime) @@ -2401,34 +2407,38 @@ namespace UVtools.Core.FileFormats slicerFile.DisplayWidth = DisplayWidth; slicerFile.DisplayHeight = DisplayHeight; slicerFile.MachineZ = MachineZ; - slicerFile.MirrorDisplay = MirrorDisplay; - - slicerFile.BottomLightOffDelay = BottomLightOffDelay; - slicerFile.LightOffDelay = LightOffDelay; - - slicerFile.BottomWaitTimeBeforeCure = BottomWaitTimeBeforeCure; - slicerFile.WaitTimeBeforeCure = WaitTimeBeforeCure; + slicerFile.DisplayMirror = DisplayMirror; + // Exposure slicerFile.BottomExposureTime = BottomExposureTime; slicerFile.ExposureTime = ExposureTime; - slicerFile.BottomWaitTimeAfterCure = BottomWaitTimeAfterCure; - slicerFile.WaitTimeAfterCure = WaitTimeAfterCure; - + // Lift slicerFile.BottomLiftHeight = BottomLiftHeight; slicerFile.LiftHeight = LiftHeight; slicerFile.BottomLiftSpeed = BottomLiftSpeed; slicerFile.LiftSpeed = LiftSpeed; + + slicerFile.RetractSpeed = RetractSpeed; + + // Wait times + slicerFile.BottomLightOffDelay = BottomLightOffDelay; + slicerFile.LightOffDelay = LightOffDelay; + + slicerFile.BottomWaitTimeBeforeCure = BottomWaitTimeBeforeCure; + slicerFile.WaitTimeBeforeCure = WaitTimeBeforeCure; + + slicerFile.BottomWaitTimeAfterCure = BottomWaitTimeAfterCure; + slicerFile.WaitTimeAfterCure = WaitTimeAfterCure; slicerFile.BottomWaitTimeAfterLift = BottomWaitTimeAfterLift; slicerFile.WaitTimeAfterLift = WaitTimeAfterLift; - slicerFile.RetractSpeed = RetractSpeed; - slicerFile.BottomLightPWM = BottomLightPWM; slicerFile.LightPWM = LightPWM; + slicerFile.MachineName = MachineName; slicerFile.MaterialName = MaterialName; slicerFile.MaterialMilliliters = MaterialMilliliters; diff --git a/UVtools.Core/FileFormats/GR1File.cs b/UVtools.Core/FileFormats/GR1File.cs index 34993bf..fdf8ef2 100644 --- a/UVtools.Core/FileFormats/GR1File.cs +++ b/UVtools.Core/FileFormats/GR1File.cs @@ -230,7 +230,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay { get; set; } + public override bool DisplayMirror { get; set; } public override byte AntiAliasing { @@ -276,6 +276,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = SlicerInfoSettings.LightOffDelay = (ushort)value; } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => SlicerInfoSettings.BottomExposureTime; diff --git a/UVtools.Core/FileFormats/ImageFile.cs b/UVtools.Core/FileFormats/ImageFile.cs index 42c4323..07f5139 100644 --- a/UVtools.Core/FileFormats/ImageFile.cs +++ b/UVtools.Core/FileFormats/ImageFile.cs @@ -59,7 +59,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay + public override bool DisplayMirror { get => false; set { } diff --git a/UVtools.Core/FileFormats/LGSFile.cs b/UVtools.Core/FileFormats/LGSFile.cs index b5dc348..5ecbe22 100644 --- a/UVtools.Core/FileFormats/LGSFile.cs +++ b/UVtools.Core/FileFormats/LGSFile.cs @@ -326,7 +326,7 @@ namespace UVtools.Core.FileFormats set => base.MachineZ = HeaderSettings.MachineZ = (float)Math.Round(value, 2); } - public override bool MirrorDisplay + public override bool DisplayMirror { get => true; set { } @@ -384,6 +384,26 @@ namespace UVtools.Core.FileFormats } } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => TimeExtensions.MillisecondsToSeconds(HeaderSettings.BottomExposureTimeMs); diff --git a/UVtools.Core/FileFormats/MDLPFile.cs b/UVtools.Core/FileFormats/MDLPFile.cs index 2899843..babe25b 100644 --- a/UVtools.Core/FileFormats/MDLPFile.cs +++ b/UVtools.Core/FileFormats/MDLPFile.cs @@ -234,7 +234,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay { get; set; } + public override bool DisplayMirror { get; set; } public override byte AntiAliasing { @@ -280,6 +280,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = SlicerInfoSettings.LightOffDelay = (ushort)value; } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => SlicerInfoSettings.BottomExposureTime; diff --git a/UVtools.Core/FileFormats/OSLAFile.cs b/UVtools.Core/FileFormats/OSLAFile.cs new file mode 100644 index 0000000..94d956d --- /dev/null +++ b/UVtools.Core/FileFormats/OSLAFile.cs @@ -0,0 +1,800 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +// https://github.com/sn4k3/UVtools/blob/master/Documentation/osla.md + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using BinarySerialization; +using Emgu.CV; +using Emgu.CV.CvEnum; +using MoreLinq; +using UVtools.Core.Extensions; +using UVtools.Core.GCode; +using UVtools.Core.Operations; + +namespace UVtools.Core.FileFormats +{ + public class OSLAFile : FileFormat + { + #region Constants + + public const string MARKER = "OSLATiCo"; + #endregion + + #region Sub Classes + #region Header + public class FileDef + { + [FieldOrder(0)] + [FieldLength(8)] + public string Marker { get; set; } = MARKER; + + [FieldOrder(1)] + public ushort Version { get; set; } = 0; + + [FieldOrder(2)] + [FieldLength(20)] + public string CreatedDateTime { get; set; } = DateTime.UtcNow.ToString("u"); + + [FieldOrder(3)] + [FieldLength(50)] + [SerializeAs(SerializedType.TerminatedString)] + public string CreatedBy { get; set; } = About.SoftwareWithVersion; + + [FieldOrder(4)] + [FieldLength(20)] + public string ModifiedDateTime { get; set; } = DateTime.UtcNow.ToString("u"); + + [FieldOrder(5)] + [FieldLength(50)] + [SerializeAs(SerializedType.TerminatedString)] + public string ModifiedBy { get; set; } = About.SoftwareWithVersion; + + public override string ToString() + { + return $"{nameof(Marker)}: {Marker}, {nameof(Version)}: {Version}, {nameof(CreatedDateTime)}: {CreatedDateTime}, {nameof(CreatedBy)}: {CreatedBy}, {nameof(ModifiedDateTime)}: {ModifiedDateTime}, {nameof(ModifiedBy)}: {ModifiedBy}"; + } + + public void Update() + { + ModifiedDateTime= DateTime.UtcNow.ToString("u"); + ModifiedBy = About.SoftwareWithVersion;; + } + + public void Validate() + { + if (Marker != MARKER) + { + throw new FileLoadException($"Invalid marker: {Marker}, not a valid OSLA file."); + } + } + } + + + public class Header + { + [FieldOrder(0)] public uint TableSize { get; set; } + [FieldOrder(1)] public uint ResolutionX { get; set; } + [FieldOrder(2)] public uint ResolutionY { get; set; } + [FieldOrder(3)] public float MachineZ { get; set; } + [FieldOrder(4)] public float DisplayWidth { get; set; } + [FieldOrder(5)] public float DisplayHeight { get; set; } + [FieldOrder(6)] public byte DisplayMirror { get; set; } // 0 = No mirror | 1 = Horizontally | 2 = Vertically | 3 = Horizontally+Vertically | >3 = No mirror + [FieldOrder(7)] [FieldLength(16)] [SerializeAs(SerializedType.TerminatedString)] public string PreviewDataType { get; set; } = "RGB565"; + [FieldOrder(8)] [FieldLength(16)] [SerializeAs(SerializedType.TerminatedString)] public string LayerDataType { get; set; } = "PNG"; + [FieldOrder(9)] public uint PreviewTableSize { get; set; } = 8; + [FieldOrder(10)] public uint PreviewCount { get; set; } + [FieldOrder(11)] public float LayerHeight { get; set; } = 0.05f; + [FieldOrder(12)] public ushort BottomLayersCount { get; set; } = 4; + [FieldOrder(13)] public uint LayerCount { get; set; } + [FieldOrder(14)] public uint LayerTableSize { get; set; } = 4; + [FieldOrder(15)] public uint LayerDefinitionsAddress { get; set; } + [FieldOrder(16)] public uint GCodeAddress { get; set; } + [FieldOrder(17)] public uint PrintTime { get; set; } + [FieldOrder(18)] public float MaterialMilliliters { get; set; } + [FieldOrder(19)] public float MaterialCost { get; set; } + [FieldOrder(20)] [FieldLength(50)] [SerializeAs(SerializedType.TerminatedString)] public string MaterialName { get; set; } + [FieldOrder(21)] [FieldLength(50)] [SerializeAs(SerializedType.TerminatedString)] public string MachineName { get; set; } = "Unknown"; + + public override string ToString() + { + return $"{nameof(TableSize)}: {TableSize}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(MachineZ)}: {MachineZ}, {nameof(DisplayWidth)}: {DisplayWidth}, {nameof(DisplayHeight)}: {DisplayHeight}, {nameof(DisplayMirror)}: {DisplayMirror}, {nameof(PreviewDataType)}: {PreviewDataType}, {nameof(LayerDataType)}: {LayerDataType}, {nameof(PreviewTableSize)}: {PreviewTableSize}, {nameof(PreviewCount)}: {PreviewCount}, {nameof(LayerTableSize)}: {LayerTableSize}, {nameof(BottomLayersCount)}: {BottomLayersCount}, {nameof(LayerCount)}: {LayerCount}, {nameof(LayerDefinitionsAddress)}: {LayerDefinitionsAddress}, {nameof(GCodeAddress)}: {GCodeAddress}, {nameof(PrintTime)}: {PrintTime}, {nameof(MaterialMilliliters)}: {MaterialMilliliters}, {nameof(MaterialCost)}: {MaterialCost}, {nameof(MaterialName)}: {MaterialName}, {nameof(MachineName)}: {MachineName}"; + } + } + #endregion + + #region Custom Table + + public class CustomTable + { + [FieldOrder(0)] public uint TableSize { get; set; } + + [FieldOrder(1)] [FieldCount(nameof(TableSize))] public byte[] Bytes { get; set; } = Array.Empty<byte>(); + + public override string ToString() + { + return $"{nameof(TableSize)}: {TableSize}, {nameof(Bytes)}: {Bytes.Length}"; + } + } + + #endregion + + #region Preview + public class Preview + { + /// <summary> + /// Gets the X dimension of the preview image, in pixels. + /// </summary> + [FieldOrder(0)] public ushort ResolutionX { get; set; } + + /// <summary> + /// Gets the Y dimension of the preview image, in pixels. + /// </summary> + [FieldOrder(1)] public ushort ResolutionY { get; set; } + + /// <summary> + /// Gets the image length in bytes. + /// </summary> + [FieldOrder(2)] public uint ImageLength { get; set; } + //[FieldOrder(3)] [FieldCount(nameof(ImageLength))] public byte[] ImageData { get; set; } + + + public override string ToString() + { + return $"{nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(ImageLength)}: {ImageLength}"; + } + } + + #endregion + + #region Layer + public class LayerDef + { + [FieldOrder(0)] public uint DataAddress { get; set; } + + [Ignore] public byte[] ImageData { get; set; } + + public LayerDef() + { + } + + public override string ToString() + { + return $"{nameof(DataAddress)}: {DataAddress}, {nameof(ImageData)}: {ImageData.Length}"; + } + } + #endregion + + #region GCode + + public class GCodeDef + { + [FieldOrder(0)] + public uint GCodeSize { get; set; } + + [FieldOrder(1)] [FieldLength(nameof(GCodeSize))] + public string GCodeText { get; set; } + } + #endregion + + #endregion + + #region Properties + + public FileDef FileSettings { get; protected internal set; } = new(); + public Header HeaderSettings { get; protected internal set; } = new(); + public CustomTable CustomTableSettings { get; protected internal set; } = new(); + public Preview[] Previews { get; protected internal set; } + + public override FileFormatType FileType => FileFormatType.Binary; + + public override FileExtension[] FileExtensions { get; } = { + new ("osla", "Open SLA universal binary file"), + //new ("omsla", "Open mSLA universal binary file"), + //new ("odlp", "Open DLP universal binary file"), + }; + + 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.BottomWaitTimeAfterLift, + PrintParameterModifier.WaitTimeAfterLift, + + PrintParameterModifier.RetractSpeed, + + + PrintParameterModifier.BottomLightPWM, + PrintParameterModifier.LightPWM, + }; + + public override PrintParameterModifier[] PrintParameterPerLayerModifiers { get; } = { + PrintParameterModifier.WaitTimeBeforeCure, + PrintParameterModifier.ExposureTime, + PrintParameterModifier.WaitTimeAfterCure, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.WaitTimeAfterLift, + PrintParameterModifier.RetractSpeed, + PrintParameterModifier.LightPWM, + }; + + public override Size[] ThumbnailsOriginalSize { get; } = + { + new(400, 400), + new(200, 200) + }; + + public override uint ResolutionX + { + get => HeaderSettings.ResolutionX; + set + { + HeaderSettings.ResolutionX = value; + RaisePropertyChanged(); + } + } + + public override uint ResolutionY + { + get => HeaderSettings.ResolutionY; + set + { + HeaderSettings.ResolutionY = value; + RaisePropertyChanged(); + } + } + + public override float DisplayWidth + { + get => HeaderSettings.DisplayWidth; + set + { + HeaderSettings.DisplayWidth = (float)Math.Round(value, 2); + RaisePropertyChanged(); + } + } + + + public override float DisplayHeight + { + get => HeaderSettings.DisplayHeight; + set + { + HeaderSettings.DisplayHeight = (float)Math.Round(value, 2); + RaisePropertyChanged(); + } + } + + public override float MachineZ + { + get => HeaderSettings.MachineZ > 0 ? HeaderSettings.MachineZ : base.MachineZ; + set + { + HeaderSettings.MachineZ = (float)Math.Round(value, 2); + RaisePropertyChanged(); + } + } + + public override bool DisplayMirror + { + get => HeaderSettings.DisplayMirror is >= 1 and <= 3; + set + { + HeaderSettings.DisplayMirror = value ? (byte)1 : (byte)0; + RaisePropertyChanged(); + } + } + + public override byte AntiAliasing + { + get => 8; + set => RaisePropertyChanged(); + } + + public override float LayerHeight + { + get => HeaderSettings.LayerHeight; + set + { + HeaderSettings.LayerHeight = Layer.RoundHeight(value); + RaisePropertyChanged(); + } + } + + public override uint LayerCount + { + get => base.LayerCount; + set => HeaderSettings.LayerCount = base.LayerCount; + } + + public override ushort BottomLayerCount + { + get => HeaderSettings.BottomLayersCount; + set => base.BottomLayerCount = HeaderSettings.BottomLayersCount = value; + } + + public override float PrintTime + { + get => base.PrintTime; + set + { + base.PrintTime = value; + HeaderSettings.PrintTime = (uint)base.PrintTime; + } + } + + public override float MaterialMilliliters + { + get => base.MaterialMilliliters; + set + { + base.MaterialMilliliters = value; + HeaderSettings.MaterialMilliliters = base.MaterialMilliliters; + } + } + + public override float MaterialCost + { + get => (float) Math.Round(HeaderSettings.MaterialCost, 3); + set => base.MaterialCost = HeaderSettings.MaterialCost = (float)Math.Round(value, 3); + } + + public override string MaterialName + { + get => HeaderSettings.MaterialName; + set => base.MaterialName = HeaderSettings.MaterialName = value; + } + + public override string MachineName + { + get => HeaderSettings.MachineName; + set => base.MachineName = HeaderSettings.MachineName = value; + } + + public override object[] Configs => new object[] { FileSettings, HeaderSettings }; + + #endregion + + #region Constructors + public OSLAFile() + { + //Previews = new Preview[ThumbnailsCount]; + GCode = new GCodeBuilder + { + UseTailComma = true, + UseComments = true, + GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Absolute, + GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, + GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, + LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G0, + EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G0, + CommandShowImageM6054 = {Arguments = "{0}"}, + }; + } + #endregion + + #region Methods + public override void Clear() + { + base.Clear(); + + Previews = null; + } + + protected override void EncodeInternally(string fileFullPath, OperationProgress progress) + { + using var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write); + FileSettings.Update(); + var fileDefSize = Helpers.SerializeWriteFileStream(outputFile, FileSettings); + HeaderSettings.TableSize = (uint)Helpers.Serializer.SizeOf(HeaderSettings); + + outputFile.Seek((int)HeaderSettings.TableSize, SeekOrigin.Current); + + Helpers.SerializeWriteFileStream(outputFile, CustomTableSettings); // Custom table + + // Previews + progress.Reset(OperationProgress.StatusEncodePreviews, ThumbnailsCount); + HeaderSettings.PreviewCount = 0; + uint sizeofPreview = 0; + for (byte i = 0; i < ThumbnailsCount; i++) + { + var image = Thumbnails[i]; + if(image is null) continue; + + progress.Token.ThrowIfCancellationRequested(); + + var bytes = EncodeImage(image, HeaderSettings.PreviewDataType); + if (bytes.Length == 0) continue; + var preview = new Preview + { + ResolutionX = (ushort) image.Width, + ResolutionY = (ushort) image.Height, + ImageLength = (uint) bytes.Length, + }; + + if (sizeofPreview == 0) + { + sizeofPreview = (uint) Helpers.Serializer.SizeOf(preview); + } + + HeaderSettings.PreviewCount++; + + Helpers.SerializeWriteFileStream(outputFile, preview); + // Need to fill what we don't know + if (HeaderSettings.PreviewTableSize > sizeofPreview) + { + outputFile.Seek(HeaderSettings.LayerTableSize - sizeofPreview, SeekOrigin.Current); + } + outputFile.WriteBytes(bytes); + + progress++; + } + + uint[] layerDataAddresses = new uint[LayerCount]; + progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); + HeaderSettings.LayerDefinitionsAddress = (uint) outputFile.Position; + + outputFile.Seek(HeaderSettings.LayerTableSize * LayerCount, SeekOrigin.Current); // Start of layer data + + var layerHash = new Dictionary<string, uint>(); + + var range = Enumerable.Range(0, (int)LayerCount); + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) + { + var layerBytes = new byte[LayerCount][]; + + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using var mat = this[layerIndex].LayerMat; + layerBytes[layerIndex] = EncodeImage(mat, HeaderSettings.LayerDataType); + progress.LockAndIncrement(); + }); + + foreach (var layerIndex in batch) + { + progress.Token.ThrowIfCancellationRequested(); + + // Try to reuse layers + var hash = Helpers.ComputeSHA1Hash(layerBytes[layerIndex]); + if (layerHash.TryGetValue(hash, out var address)) + { + layerDataAddresses[layerIndex] = address; + } + else + { + layerDataAddresses[layerIndex] = (uint)outputFile.Position; + outputFile.WriteUIntLittleEndian((uint)layerBytes[layerIndex].Length); + outputFile.WriteBytes(layerBytes[layerIndex]); + layerHash.Add(hash, layerDataAddresses[layerIndex]); + } + + + + layerBytes[layerIndex] = null; // Clean + } + } + + HeaderSettings.GCodeAddress = (uint)outputFile.Position; + + outputFile.Seek(HeaderSettings.LayerDefinitionsAddress, SeekOrigin.Begin); + for (int i = 0; i < layerDataAddresses.Length; i++) + { + progress.Token.ThrowIfCancellationRequested(); + outputFile.WriteUIntLittleEndian(layerDataAddresses[i]); + // Need to fill what we don't know + if (HeaderSettings.LayerTableSize > 4) + { + outputFile.Seek(HeaderSettings.LayerTableSize - 4, SeekOrigin.Current); + } + } + + progress.Reset(OperationProgress.StatusEncodeGcode); + outputFile.Seek(HeaderSettings.GCodeAddress, SeekOrigin.Begin); + RebuildGCode(); + var gcodeSettings = new GCodeDef { GCodeText = GCodeStr }; + gcodeSettings.GCodeSize = (uint)gcodeSettings.GCodeText.Length; + Helpers.SerializeWriteFileStream(outputFile, gcodeSettings); + + outputFile.Seek(fileDefSize, SeekOrigin.Begin); + Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); + + Debug.WriteLine("Encode Results:"); + Debug.WriteLine(FileSettings); + Debug.WriteLine(HeaderSettings); + Debug.WriteLine("-End-"); + } + + protected override void DecodeInternally(string fileFullPath, OperationProgress progress) + { + using var inputFile = new FileStream(fileFullPath, FileMode.Open, FileAccess.Read); + FileSettings = Helpers.Deserialize<FileDef>(inputFile); + Debug.Write("File -> "); + Debug.WriteLine(FileSettings); + FileSettings.Validate(); + + HeaderSettings = Helpers.Deserialize<Header>(inputFile); + Debug.Write("Header -> "); + Debug.WriteLine(HeaderSettings); + + var headerSize = Helpers.Serializer.SizeOf(HeaderSettings); + if (HeaderSettings.TableSize > headerSize) // By pass what we dont know + { + inputFile.Seek(HeaderSettings.TableSize - headerSize, SeekOrigin.Current); + } + + CustomTableSettings = Helpers.Deserialize<CustomTable>(inputFile); + Debug.Write("Custom table -> "); + Debug.WriteLine(CustomTableSettings); + + progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount); + + Previews = new Preview[HeaderSettings.PreviewCount]; + for (byte i = 0; i < HeaderSettings.PreviewCount; i++) + { + progress.Token.ThrowIfCancellationRequested(); + Previews[i] = Helpers.Deserialize<Preview>(inputFile); + + Debug.Write($"Preview {i} -> "); + Debug.WriteLine(Previews[i]); + + // Need to fill what we don't know + if (HeaderSettings.PreviewTableSize > 8) + { + inputFile.Seek(HeaderSettings.LayerTableSize - 8, SeekOrigin.Current); + } + + + var bytes = inputFile.ReadBytes((int)Previews[i].ImageLength); + + Thumbnails[i] = DecodeImage(bytes, HeaderSettings.PreviewDataType, Previews[i].ResolutionX, Previews[i].ResolutionY); + progress++; + } + + inputFile.Seek(HeaderSettings.LayerDefinitionsAddress, SeekOrigin.Begin); + + LayerManager.Init(HeaderSettings.LayerCount); + progress.Reset(OperationProgress.StatusGatherLayers, HeaderSettings.LayerCount); + uint[] layerDataAddresses = new uint[LayerCount]; + for (uint layerIndex = 0; layerIndex < HeaderSettings.LayerCount; layerIndex++) + { + progress.Token.ThrowIfCancellationRequested(); + layerDataAddresses[layerIndex] = inputFile.ReadUIntLittleEndian(); + if (HeaderSettings.LayerTableSize > 4) + { + inputFile.Seek(HeaderSettings.LayerTableSize - 4, SeekOrigin.Current); + } + + progress++; + } + + + + progress.Reset(OperationProgress.StatusDecodeLayers, HeaderSettings.LayerCount); + var range = Enumerable.Range(0, (int)LayerCount); + foreach (var batch in MoreEnumerable.Batch(range, Environment.ProcessorCount * 10)) + { + var layerBytes = new byte[LayerCount][]; + + foreach (var layerIndex in batch) + { + progress.Token.ThrowIfCancellationRequested(); + + inputFile.Seek(layerDataAddresses[layerIndex], SeekOrigin.Begin); + layerBytes[layerIndex] = inputFile.ReadBytes(inputFile.ReadUIntLittleEndian()); + } + + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using var mat = DecodeImage(layerBytes[layerIndex], HeaderSettings.LayerDataType, Resolution); + this[layerIndex] = new Layer((uint)layerIndex, mat, this); + layerBytes[layerIndex] = null; // Clean + + progress.LockAndIncrement(); + }); + } + + progress.Reset(OperationProgress.StatusDecodeGcode); + inputFile.Seek(HeaderSettings.GCodeAddress, SeekOrigin.Begin); + var gcodeDef = Helpers.Deserialize<GCodeDef>(inputFile); + GCodeStr = gcodeDef.GCodeText; + GCode.ParseLayersFromGCode(this); + } + + public override void SaveAs(string filePath = null, OperationProgress progress = null) + { + if (RequireFullEncode) + { + if (!string.IsNullOrEmpty(filePath)) + { + FileFullPath = filePath; + } + Encode(FileFullPath, progress); + return; + } + + if (!string.IsNullOrEmpty(filePath)) + { + File.Copy(FileFullPath, filePath, true); + FileFullPath = filePath; + } + + using var outputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Write); + outputFile.Seek(0, SeekOrigin.Begin); + FileSettings.Update(); + Helpers.SerializeWriteFileStream(outputFile, FileSettings); + Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); + + outputFile.Seek(HeaderSettings.GCodeAddress, SeekOrigin.Begin); + outputFile.SetLength(HeaderSettings.GCodeAddress); + + RebuildGCode(); + var gcodeSettings = new GCodeDef {GCodeText = GCodeStr}; + gcodeSettings.GCodeSize = (uint) gcodeSettings.GCodeText.Length; + Helpers.SerializeWriteFileStream(outputFile, gcodeSettings); + } + #endregion + + #region Static Methods + + public static byte[] EncodeImage(Mat mat, string encodeTo) + { + encodeTo = encodeTo.ToUpperInvariant(); + if (encodeTo is "PNG" or "JPG" or "JPEG" or "JP2" or "BMP" or "TIF" or "TIFF" or "PPM" or "PMG" or "SR" or "RAS") + { + return CvInvoke.Imencode($".{encodeTo.ToLowerInvariant()}", mat); + } + + if (encodeTo is "RGB555" or "RGB565" or "RGB888" + or "BGR555" or "BGR565" or "BGR888") + { + var bytesPerPixel = encodeTo is "RGB888" or "BGR888" ? 3 : 2; + var bytes = new byte[mat.Width * mat.Height * bytesPerPixel]; + uint index = 0; + var span = mat.GetDataByteSpan(); + for (int i = 0; i < span.Length;) + { + byte b = span[i++]; + byte g; + byte r; + + if (mat.NumberOfChannels == 1) // 8 bit safe-guard + { + r = g = b; + } + else + { + g = span[i++]; + r = span[i++]; + } + + if (mat.NumberOfChannels == 4) i++; // skip alpha + + switch (encodeTo) + { + case "RGB555": + var rgb555 = (ushort) (((r & 0b11111000) << 7) | ((g & 0b11111000) << 2) | (b >> 3)); + BitExtensions.ToBytesLittleEndian(rgb555, bytes, index); + index += 2; + break; + case "RGB565": + var rgb565 = (ushort) (((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3)); + BitExtensions.ToBytesLittleEndian(rgb565, bytes, index); + index += 2; + break; + case "RGB888": + bytes[index++] = r; + bytes[index++] = g; + bytes[index++] = b; + break; + } + } + + return bytes; + } + + throw new NotSupportedException($"The encode type: {encodeTo} is not supported."); + } + + public static Mat DecodeImage(byte[] bytes, string decodeFrom, Size resolution) + { + if (decodeFrom is "PNG" or "JPG" or "JPEG" or "JP2" or "BMP" or "TIF" or "TIFF" or "PPM" or "PMG" or "SR" or "RAS") + { + var mat = new Mat(); + CvInvoke.Imdecode(bytes, ImreadModes.AnyColor, mat); + return mat; + } + + if (decodeFrom is "RGB555" or "RGB565" or "RGB888" + or "BGR555" or "BGR565" or "BGR888") + { + var mat = new Mat(resolution, DepthType.Cv8U, 3); + var span = mat.GetDataByteSpan(); + var pixel = 0; + for (int i = 0; i < bytes.Length;) + { + switch (decodeFrom) + { + case "RGB555": + ushort rgb555 = BitExtensions.ToUShortLittleEndian(bytes, i); + span[pixel++] = (byte)((rgb555 & 0b00000000_00011111) << 3); // b + span[pixel++] = (byte)((rgb555 & 0b00000011_11100000) >> 2); // g + span[pixel++] = (byte)((rgb555 & 0b01111100_00000000) >> 7); // r + i += 2; + break; + case "RGB565": + ushort rgb565 = BitExtensions.ToUShortLittleEndian(bytes, i); + span[pixel++] = (byte)((rgb565 & 0b00000000_00011111) << 3); // b + span[pixel++] = (byte)((rgb565 & 0b00000111_11100000) >> 3); // g + span[pixel++] = (byte)((rgb565 & 0b11111000_00000000) >> 8); // r + i += 2; + break; + case "RGB888": + span[pixel++] = bytes[i + 2]; // b + span[pixel++] = bytes[i + 1]; // g + span[pixel++] = bytes[i]; // r + i += 3; + break; + case "BGR555": + ushort bgr555 = BitExtensions.ToUShortLittleEndian(bytes, i); + span[pixel++] = (byte)((bgr555 & 0b01111100_00000000) >> 7); // b + span[pixel++] = (byte)((bgr555 & 0b00000011_11100000) >> 2); // g + span[pixel++] = (byte)((bgr555 & 0b00000000_00011111) << 3); // r + i += 2; + break; + case "BGR565": + ushort bgr565 = BitExtensions.ToUShortLittleEndian(bytes, i); + span[pixel++] = (byte)((bgr565 & 0b11111000_00000000) >> 8); // b + span[pixel++] = (byte)((bgr565 & 0b00000111_11100000) >> 3); // g + span[pixel++] = (byte)((bgr565 & 0b00000000_00011111) << 3); // r + i += 2; + break; + case "BGR888": + span[pixel++] = bytes[i]; // b + span[pixel++] = bytes[i+1]; // g + span[pixel++] = bytes[i+2]; // r + i += 3; + break; + } + } + return mat; + } + + throw new NotSupportedException($"The decode type: {decodeFrom} is not supported."); + } + + public static Mat DecodeImage(byte[] bytes, string decodeFrom, uint resolutionX = 0, uint resolutionY = 0) + => DecodeImage(bytes, decodeFrom, new Size((int) resolutionX, (int) resolutionY)); + + + #endregion + } +} diff --git a/UVtools.Core/FileFormats/PHZFile.cs b/UVtools.Core/FileFormats/PHZFile.cs index c9a34ab..65e7819 100644 --- a/UVtools.Core/FileFormats/PHZFile.cs +++ b/UVtools.Core/FileFormats/PHZFile.cs @@ -775,7 +775,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay + public override bool DisplayMirror { get => HeaderSettings.ProjectorType > 0; set @@ -841,6 +841,26 @@ namespace UVtools.Core.FileFormats set => base.BottomExposureTime = HeaderSettings.BottomExposureSeconds = (float)Math.Round(value, 2); } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float ExposureTime { get => HeaderSettings.LayerExposureSeconds; @@ -1094,7 +1114,7 @@ namespace UVtools.Core.FileFormats HeaderSettings.AntiAliasLevel = 1; - progress.Reset(OperationProgress.StatusDecodeThumbnails, ThumbnailsCount); + progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount); Debug.Write("Header -> "); Debug.WriteLine(HeaderSettings); diff --git a/UVtools.Core/FileFormats/PhotonSFile.cs b/UVtools.Core/FileFormats/PhotonSFile.cs index 2195e9d..21436be 100644 --- a/UVtools.Core/FileFormats/PhotonSFile.cs +++ b/UVtools.Core/FileFormats/PhotonSFile.cs @@ -273,7 +273,7 @@ namespace UVtools.Core.FileFormats set { } } - public override bool MirrorDisplay + public override bool DisplayMirror { get => true; set { } @@ -321,10 +321,30 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = (float)(HeaderSettings.LightOffDelay = Math.Round(value, 2)); } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => (float) HeaderSettings.BottomExposureSeconds; - set => base.BottomExposureTime = (float) (HeaderSettings.BottomExposureSeconds = value); + set => base.BottomExposureTime = (float) (HeaderSettings.BottomExposureSeconds = Math.Round(value, 2)); } public override float ExposureTime diff --git a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs index 0c733b9..b442a57 100644 --- a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs +++ b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs @@ -1041,7 +1041,7 @@ namespace UVtools.Core.FileFormats set { } } - public override bool MirrorDisplay + public override bool DisplayMirror { get => true; set {} @@ -1090,6 +1090,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = HeaderSettings.LightOffDelay = (float)Math.Round(value, 2); } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomExposureTime { get => HeaderSettings.BottomExposureSeconds; diff --git a/UVtools.Core/FileFormats/SL1File.cs b/UVtools.Core/FileFormats/SL1File.cs index c4580cd..763d021 100644 --- a/UVtools.Core/FileFormats/SL1File.cs +++ b/UVtools.Core/FileFormats/SL1File.cs @@ -267,7 +267,7 @@ namespace UVtools.Core.FileFormats get { //2021-01-23 at 04:07:36 UTC - var now = DateTime.Now; + var now = DateTime.UtcNow; return $"{now.Year}-{now.Month:D2}-{now.Day:D2} at {now.Hour:D2}:{now.Minute:D2}:{now.Second:D2} {now.Kind}"; } set{} @@ -375,7 +375,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay + public override bool DisplayMirror { get => PrinterSettings.DisplayMirrorX || PrinterSettings.DisplayMirrorY; set diff --git a/UVtools.Core/FileFormats/UVJFile.cs b/UVtools.Core/FileFormats/UVJFile.cs index f6e96ab..24e160f 100644 --- a/UVtools.Core/FileFormats/UVJFile.cs +++ b/UVtools.Core/FileFormats/UVJFile.cs @@ -210,7 +210,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay { get; set; } + public override bool DisplayMirror { get; set; } public override byte AntiAliasing { @@ -251,6 +251,26 @@ namespace UVtools.Core.FileFormats set => base.LightOffDelay = JsonSettings.Properties.Exposure.LightOffTime = (float)Math.Round(value, 2); } + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set + { + SetBottomLightOffDelay(value); + base.BottomWaitTimeBeforeCure = value; + } + } + + public override float WaitTimeBeforeCure + { + get => base.WaitTimeBeforeCure; + set + { + SetNormalLightOffDelay(value); + base.WaitTimeBeforeCure = value; + } + } + public override float BottomLiftHeight { get => JsonSettings.Properties.Bottom.LiftHeight; diff --git a/UVtools.Core/FileFormats/VDAFile.cs b/UVtools.Core/FileFormats/VDAFile.cs index 126c325..ab4a0fd 100644 --- a/UVtools.Core/FileFormats/VDAFile.cs +++ b/UVtools.Core/FileFormats/VDAFile.cs @@ -60,11 +60,11 @@ namespace UVtools.Core.FileFormats public VDABy By { get; set; } = new(); - public string When { get; set; } = DateTime.Now.ToString("u"); + public string When { get; set; } = DateTime.UtcNow.ToString("u"); public void Reset() { - When = DateTime.Now.ToString("u"); + When = DateTime.UtcNow.ToString("u"); By.Reset(); } } diff --git a/UVtools.Core/FileFormats/VDTFile.cs b/UVtools.Core/FileFormats/VDTFile.cs index 7657917..9dfc917 100644 --- a/UVtools.Core/FileFormats/VDTFile.cs +++ b/UVtools.Core/FileFormats/VDTFile.cs @@ -44,7 +44,7 @@ namespace UVtools.Core.FileFormats [JsonProperty("application_version")] public string ApplicationVersion { get; set; } = "2.1.15.14"; //2021-04-09 17:48:46 - [JsonProperty("create_datetime")] public string CreateDateTime { get; set; } = DateTime.Now.ToString("u"); + [JsonProperty("create_datetime")] public string CreateDateTime { get; set; } = DateTime.UtcNow.ToString("u"); [JsonProperty("file_version")] public byte FileVersion { get; set; } = 1; @@ -257,7 +257,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay => ManifestFile.Machine.XMirror || ManifestFile.Machine.YMirror; + public override bool DisplayMirror => ManifestFile.Machine.XMirror || ManifestFile.Machine.YMirror; public override byte AntiAliasing { @@ -442,7 +442,7 @@ namespace UVtools.Core.FileFormats public void RebuildVDTLayers() { - ManifestFile.CreateDateTime = DateTime.Now.ToString("u"); + ManifestFile.CreateDateTime = DateTime.UtcNow.ToString("u"); var layers = new VDTLayer[LayerCount]; for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { diff --git a/UVtools.Core/FileFormats/ZCodeFile.cs b/UVtools.Core/FileFormats/ZCodeFile.cs index 4b62ea3..643f14d 100644 --- a/UVtools.Core/FileFormats/ZCodeFile.cs +++ b/UVtools.Core/FileFormats/ZCodeFile.cs @@ -273,7 +273,7 @@ namespace UVtools.Core.FileFormats } } - public override bool MirrorDisplay => true; + public override bool DisplayMirror => true; public override byte AntiAliasing { @@ -456,7 +456,7 @@ namespace UVtools.Core.FileFormats GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Absolute, GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.CentimetersPerMinute, GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, - GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.FilenameNonZeroPNG, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.FilenamePng1Started, LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G0, EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1, MaxLEDPower = MaxLEDPower, diff --git a/UVtools.Core/FileFormats/ZCodexFile.cs b/UVtools.Core/FileFormats/ZCodexFile.cs index 0239e4d..a268d32 100644 --- a/UVtools.Core/FileFormats/ZCodexFile.cs +++ b/UVtools.Core/FileFormats/ZCodexFile.cs @@ -209,7 +209,7 @@ namespace UVtools.Core.FileFormats set { } } - public override bool MirrorDisplay + public override bool DisplayMirror { get => true; set { } @@ -364,7 +364,7 @@ namespace UVtools.Core.FileFormats GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Partial, GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, - GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndexZero, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G1, EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1 }; diff --git a/UVtools.Core/GCode/GCodeBuilder.cs b/UVtools.Core/GCode/GCodeBuilder.cs index 282c624..e53faa2 100644 --- a/UVtools.Core/GCode/GCodeBuilder.cs +++ b/UVtools.Core/GCode/GCodeBuilder.cs @@ -15,7 +15,6 @@ using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using Org.BouncyCastle.Asn1.Cms; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Objects; @@ -89,10 +88,10 @@ namespace UVtools.Core.GCode public enum GCodeShowImageTypes : byte { - FilenameZeroPNG, - FilenameNonZeroPNG, - LayerIndexZero, - LayerIndexNonZero, + FilenamePng0Started, + FilenamePng1Started, + LayerIndex0Started, + LayerIndex1Started, } #endregion @@ -103,7 +102,7 @@ namespace UVtools.Core.GCode private GCodePositioningTypes _gCodePositioningType = GCodePositioningTypes.Absolute; private GCodeTimeUnits _gCodeTimeUnit = GCodeTimeUnits.Milliseconds; private GCodeSpeedUnits _gCodeSpeedUnit = GCodeSpeedUnits.MillimetersPerMinute; - private GCodeShowImageTypes _gCodeShowImageType = GCodeShowImageTypes.FilenameNonZeroPNG; + private GCodeShowImageTypes _gCodeShowImageType = GCodeShowImageTypes.FilenamePng1Started; private bool _syncMovementsWithDelay; private bool _useTailComma = true; private bool _useComments = true; @@ -308,9 +307,7 @@ namespace UVtools.Core.GCode public void AppendUVtools() { - string arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; - var version = Assembly.GetExecutingAssembly().GetName().Version; - AppendComment($"Generated by {About.Software} v{version.ToString(3)} {arch} @ {DateTime.Now}"); + AppendComment($"Generated by {About.Software} v{About.VersionStr} {About.Arch} @ {DateTime.UtcNow}"); } public void AppendStartGCode() @@ -538,19 +535,19 @@ namespace UVtools.Core.GCode public string GetShowImageString(uint layerIndex) => _gCodeShowImageType switch { - GCodeShowImageTypes.FilenameZeroPNG => $"{layerIndex}.png", - GCodeShowImageTypes.FilenameNonZeroPNG => $"{layerIndex + 1}.png", - GCodeShowImageTypes.LayerIndexZero => $"{layerIndex}", - GCodeShowImageTypes.LayerIndexNonZero => $"{layerIndex + 1}", + GCodeShowImageTypes.FilenamePng0Started => $"{layerIndex}.png", + GCodeShowImageTypes.FilenamePng1Started => $"{layerIndex + 1}.png", + GCodeShowImageTypes.LayerIndex0Started => $"{layerIndex}", + GCodeShowImageTypes.LayerIndex1Started => $"{layerIndex + 1}", _ => throw new InvalidExpressionException($"Unhandled image type for {_gCodeShowImageType}") }; public string GetShowImageString(string value) => _gCodeShowImageType switch { - GCodeShowImageTypes.FilenameZeroPNG => $"{value}.png", - GCodeShowImageTypes.FilenameNonZeroPNG => $"{value}.png", - GCodeShowImageTypes.LayerIndexZero => $"{value}", - GCodeShowImageTypes.LayerIndexNonZero => $"{value}", + GCodeShowImageTypes.FilenamePng0Started => $"{value}.png", + GCodeShowImageTypes.FilenamePng1Started => $"{value}.png", + GCodeShowImageTypes.LayerIndex0Started => $"{value}", + GCodeShowImageTypes.LayerIndex1Started => $"{value}", _ => throw new InvalidExpressionException($"Unhandled image type for {_gCodeShowImageType}") }; @@ -752,8 +749,8 @@ namespace UVtools.Core.GCode if (match.Success && match.Groups.Count >= 2) // Begin new layer { var layerIndex = uint.Parse(match.Groups[1].Value); - if (_gCodeShowImageType is GCodeShowImageTypes.FilenameNonZeroPNG or GCodeShowImageTypes - .LayerIndexNonZero) layerIndex--; + if (_gCodeShowImageType is GCodeShowImageTypes.FilenamePng1Started or GCodeShowImageTypes + .LayerIndex1Started) layerIndex--; if (layerIndex > slicerFile.LayerCount) { throw new FileLoadException( diff --git a/UVtools.Core/Layer/Layer.cs b/UVtools.Core/Layer/Layer.cs index c8ed94d..7ae01d0 100644 --- a/UVtools.Core/Layer/Layer.cs +++ b/UVtools.Core/Layer/Layer.cs @@ -561,16 +561,16 @@ namespace UVtools.Core $"{nameof(BoundingRectangle)}: {BoundingRectangle}, " + $"{nameof(IsBottomLayer)}: {IsBottomLayer}, " + $"{nameof(IsNormalLayer)}: {IsNormalLayer}, " + - $"{nameof(LayerHeight)}: {LayerHeight}, " + - $"{nameof(PositionZ)}: {PositionZ}, " + - $"{nameof(LightOffDelay)}: {LightOffDelay}, " + - $"{nameof(WaitTimeBeforeCure)}: {WaitTimeBeforeCure}, " + - $"{nameof(ExposureTime)}: {ExposureTime}, " + - $"{nameof(WaitTimeAfterCure)}: {WaitTimeAfterCure}, " + - $"{nameof(LiftHeight)}: {LiftHeight}, " + - $"{nameof(LiftSpeed)}: {LiftSpeed}, " + - $"{nameof(WaitTimeAfterLift)}: {WaitTimeAfterLift}, " + - $"{nameof(RetractSpeed)}: {RetractSpeed}, " + + $"{nameof(LayerHeight)}: {LayerHeight}mm, " + + $"{nameof(PositionZ)}: {PositionZ}mm, " + + $"{nameof(LightOffDelay)}: {LightOffDelay}s, " + + $"{nameof(WaitTimeBeforeCure)}: {WaitTimeBeforeCure}s, " + + $"{nameof(ExposureTime)}: {ExposureTime}s, " + + $"{nameof(WaitTimeAfterCure)}: {WaitTimeAfterCure}s, " + + $"{nameof(LiftHeight)}: {LiftHeight}mm, " + + $"{nameof(LiftSpeed)}: {LiftSpeed}mm/mim, " + + $"{nameof(WaitTimeAfterLift)}: {WaitTimeAfterLift}s, " + + $"{nameof(RetractSpeed)}: {RetractSpeed}mm/mim, " + $"{nameof(LightPWM)}: {LightPWM}, " + $"{nameof(IsModified)}: {IsModified}, " + $"{nameof(HaveGlobalParameters)}: {HaveGlobalParameters}"; @@ -606,21 +606,21 @@ namespace UVtools.Core WaitTimeAfterLift = 0; } - public string FormatFileName(string prepend, byte padDigits, bool layerIndexZeroStarted = true) + public string FormatFileName(string prepend, byte padDigits, bool layerIndexZeroStarted = true, string appendExt = ".png") { var index = Index; if (!layerIndexZeroStarted) { index++; } - return $"{prepend}{index.ToString().PadLeft(padDigits, '0')}.png"; + return $"{prepend}{index.ToString().PadLeft(padDigits, '0')}{appendExt}"; } - public string FormatFileName(string prepend = "", bool layerIndexZeroStarted = true) - => FormatFileName(prepend, ParentLayerManager.LayerDigits, layerIndexZeroStarted); + public string FormatFileName(string prepend = "", bool layerIndexZeroStarted = true, string appendExt = ".png") + => FormatFileName(prepend, ParentLayerManager.LayerDigits, layerIndexZeroStarted, appendExt); - public string FormatFileName(byte padDigits, bool layerIndexZeroStarted = true) - => FormatFileName(string.Empty, padDigits, layerIndexZeroStarted); + public string FormatFileName(byte padDigits, bool layerIndexZeroStarted = true, string appendExt = ".png") + => FormatFileName(string.Empty, padDigits, layerIndexZeroStarted, appendExt); public Rectangle GetBoundingRectangle(Mat mat = null, bool reCalculate = false) diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs index 307b1df..4d4c2a4 100644 --- a/UVtools.Core/Layer/LayerManager.cs +++ b/UVtools.Core/Layer/LayerManager.cs @@ -595,12 +595,15 @@ namespace UVtools.Core /// <param name="zeroLightOffDelay">If true also set light off to 0, otherwise current value will be kept.</param> /// </summary> public void SetNoLiftForSamePositionedLayers(bool zeroLightOffDelay = false) + => SetLiftForSamePositionedLayers(0, zeroLightOffDelay); + + public void SetLiftForSamePositionedLayers(float liftHeight = 0, bool zeroLightOffDelay = false) { for (int layerIndex = 1; layerIndex < LayerCount; layerIndex++) { var layer = this[layerIndex]; if (this[layerIndex - 1].PositionZ != layer.PositionZ) continue; - layer.LiftHeight = 0; + layer.LiftHeight = liftHeight; layer.WaitTimeAfterLift = 0; if (zeroLightOffDelay) { diff --git a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs index 855a2f8..14744f8 100644 --- a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs +++ b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs @@ -370,7 +370,7 @@ namespace UVtools.Core.Operations if (_bottomLayers <= 0) _bottomLayers = (ushort) Slicer.Slicer.MillimetersToLayers(1M, _layerHeight); if (_normalLayers <= 0) _normalLayers = (ushort) Slicer.Slicer.MillimetersToLayers(3.5M, _layerHeight); - _mirrorOutput = SlicerFile.MirrorDisplay; + _mirrorOutput = SlicerFile.DisplayMirror; } #endregion diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index 5ba5bd5..608b8ec 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -1144,7 +1144,7 @@ namespace UVtools.Core.Operations { base.InitWithSlicerFile(); - _mirrorOutput = SlicerFile.MirrorDisplay; + _mirrorOutput = SlicerFile.DisplayMirror; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; @@ -2224,7 +2224,7 @@ namespace UVtools.Core.Operations if (_mirrorOutput) { - new OperationFlip(SlicerFile) { FlipDirection = Enumerations.FlipDirection.Horizontally }.Execute(progress); + new OperationFlip(SlicerFile) { FlipDirection = FlipType.Horizontal }.Execute(progress); } } diff --git a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs index 9810dee..b92d339 100644 --- a/UVtools.Core/Operations/OperationCalibrateGrayscale.cs +++ b/UVtools.Core/Operations/OperationCalibrateGrayscale.cs @@ -112,7 +112,7 @@ namespace UVtools.Core.Operations if(_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if(_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if(_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.MirrorDisplay; + _mirrorOutput = SlicerFile.DisplayMirror; } #endregion diff --git a/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs b/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs new file mode 100644 index 0000000..4caf862 --- /dev/null +++ b/UVtools.Core/Operations/OperationCalibrateLiftHeight.cs @@ -0,0 +1,442 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Drawing; +using System.Text; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; + +namespace UVtools.Core.Operations +{ + [Serializable] + public sealed class OperationCalibrateLiftHeight : Operation + { + #region Members + private decimal _layerHeight; + private ushort _bottomLayers = 3; + private ushort _normalLayers = 2; + private decimal _bottomExposure; + private decimal _normalExposure; + private decimal _bottomLiftHeight; + private decimal _liftHeight; + private decimal _bottomLiftSpeed; + private decimal _liftSpeed; + private decimal _retractSpeed; + private ushort _leftRightMargin = 200; + private ushort _topBottomMargin = 200; + private bool _decreaseImage = true; + private byte _decreaseImageFactor = 10; + private byte _minimumImageFactor = 10; + + #endregion + + #region Overrides + + public override bool CanROI => false; + + public override bool CanCancel => false; + + public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + + public override string Title => "Lift height"; + public override string Description => + "Generates test models with various strategies and increments to measure the optimal lift height or peel forces for layers given the printed area.\n" + + "You must have a tool to measure the lift height / forces as it moves up, record the values to determine the lowest safe value for the lift.\n" + + "After find the height where it peels, you must give from 1mm to 2mm more for safeness.\n" + + "You must repeat this test when change any of the following: printer, LEDs, resin and exposure times.\n" + + "Note: The current opened file will be overwritten with this test, use a dummy or a not needed file."; + + public override string ConfirmationText => + $"generate the lift height test?"; + + public override string ProgressTitle => + $"Generating the lift height test"; + + public override string ProgressAction => "Generated"; + + public override string ValidateInternally() + { + var sb = new StringBuilder(); + + if (SlicerFile.ResolutionX - _leftRightMargin * 2 <= 0) + sb.AppendLine("The top/bottom margin is too big, it overlaps the screen resolution."); + + if (SlicerFile.ResolutionY - _topBottomMargin * 2 <= 0) + sb.AppendLine("The top/bottom margin is too big, it overlaps the screen resolution."); + + if (_decreaseImage) + { + if(_decreaseImageFactor is 0 or >= 100) + sb.AppendLine("The image decrease factor must be between 1 and 99%"); + + if(_minimumImageFactor is 0 or >= 100) + sb.AppendLine("The minimum image decrease factor must be between 1 and 99%"); + } + + return sb.ToString(); + } + + public override string ToString() + { + var result = $"[Layer Height: {_layerHeight}] " + + $"[Layers: {_bottomLayers}/{_normalLayers}] " + + $"[Exposure: {_bottomExposure}/{_normalExposure}s] " + + $"[Lift: {_bottomLiftHeight}/{_liftHeight}mm @ {_bottomLiftSpeed}/{_liftSpeed}mm/min]" + + $"[Retract speed: {_retractSpeed}mm/min]" + + $"[Decrease image: {_decreaseImage} @ {_decreaseImageFactor}-{_minimumImageFactor}%]"; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + + #endregion + + #region Properties + + public decimal LayerHeight + { + get => _layerHeight; + set + { + if(!RaiseAndSetIfChanged(ref _layerHeight, Layer.RoundHeight(value))) return; + RaisePropertyChanged(nameof(BottomHeight)); + RaisePropertyChanged(nameof(NormalHeight)); + RaisePropertyChanged(nameof(TotalHeight)); + } + } + + public ushort Microns => (ushort) (LayerHeight * 1000); + + public ushort BottomLayers + { + get => _bottomLayers; + set + { + if(!RaiseAndSetIfChanged(ref _bottomLayers, value)) return; + RaisePropertyChanged(nameof(BottomHeight)); + RaisePropertyChanged(nameof(TotalHeight)); + RaisePropertyChanged(nameof(LayerCount)); + } + } + + public ushort NormalLayers + { + get => _normalLayers; + set + { + if (!RaiseAndSetIfChanged(ref _normalLayers, value)) return; + RaisePropertyChanged(nameof(NormalHeight)); + RaisePropertyChanged(nameof(TotalHeight)); + RaisePropertyChanged(nameof(LayerCount)); + } + } + + public uint LayerCount + { + get + { + uint layerCount = (uint)(_bottomLayers + _normalLayers); + if (_decreaseImage) + { + layerCount += (100u - _minimumImageFactor) / _decreaseImageFactor; + //layerCount += (uint)Math.Ceiling((100.0 - _minimumImageFactor - _decreaseImageFactor) / _decreaseImageFactor); + //for (int factor = 100 - _decreaseImageFactor; factor >= _minimumImageFactor; factor -= _decreaseImageFactor) + // layerCount++; + } + return layerCount; + } + } + + public decimal BottomHeight => Layer.RoundHeight(_layerHeight * _bottomLayers); + public decimal NormalHeight => Layer.RoundHeight(_layerHeight * (LayerCount - _bottomLayers)); + + public decimal TotalHeight => BottomHeight + NormalHeight; + + public decimal BottomExposure + { + get => _bottomExposure; + set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2)); + } + + public decimal NormalExposure + { + get => _normalExposure; + set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2)); + } + + public decimal BottomLiftHeight + { + get => _bottomLiftHeight; + set => RaiseAndSetIfChanged(ref _bottomLiftHeight, value); + } + + public decimal LiftHeight + { + get => _liftHeight; + set => RaiseAndSetIfChanged(ref _liftHeight, value); + } + + public decimal BottomLiftSpeed + { + get => _bottomLiftSpeed; + set => RaiseAndSetIfChanged(ref _bottomLiftSpeed, value); + } + + public decimal LiftSpeed + { + get => _liftSpeed; + set => RaiseAndSetIfChanged(ref _liftSpeed, value); + } + + public decimal RetractSpeed + { + get => _retractSpeed; + set => RaiseAndSetIfChanged(ref _retractSpeed, value); + } + + public ushort LeftRightMargin + { + get => _leftRightMargin; + set => RaiseAndSetIfChanged(ref _leftRightMargin, value); + } + + public ushort MaxLeftRightMargin => (ushort)((SlicerFile.ResolutionX - 100) / 2); + + public ushort TopBottomMargin + { + get => _topBottomMargin; + set => RaiseAndSetIfChanged(ref _topBottomMargin, value); + } + + public ushort MaxTopBottomMargin => (ushort) ((SlicerFile.ResolutionY - 100) / 2); + + public bool DecreaseImage + { + get => _decreaseImage; + set + { + RaiseAndSetIfChanged(ref _decreaseImage, value); + RaisePropertyChanged(nameof(TotalHeight)); + RaisePropertyChanged(nameof(LayerCount)); + } + } + + public byte DecreaseImageFactor + { + get => _decreaseImageFactor; + set + { + RaiseAndSetIfChanged(ref _decreaseImageFactor, value); + RaisePropertyChanged(nameof(TotalHeight)); + RaisePropertyChanged(nameof(LayerCount)); + } + } + + public byte MinimumImageFactor + { + get => _minimumImageFactor; + set + { + RaiseAndSetIfChanged(ref _minimumImageFactor, value); + RaisePropertyChanged(nameof(TotalHeight)); + RaisePropertyChanged(nameof(LayerCount)); + } + } + + public Rectangle WhiteBlock + { + get + { + int width = (int) (SlicerFile.ResolutionX - _leftRightMargin * 2); + int height = (int) (SlicerFile.ResolutionY - _topBottomMargin * 2); + int x = (int) (SlicerFile.ResolutionX - width) / 2; + int y = (int) (SlicerFile.ResolutionY - height) / 2; + + return new Rectangle(x, y, width, height); + } + } + + #endregion + + #region Constructor + + public OperationCalibrateLiftHeight() { } + + public OperationCalibrateLiftHeight(FileFormat slicerFile) : base(slicerFile) + { } + + public override void InitWithSlicerFile() + { + base.InitWithSlicerFile(); + if(_layerHeight <= 0) _layerHeight = (decimal)SlicerFile.LayerHeight; + if(_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; + if(_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; + if (_bottomLiftHeight <= 0) _bottomLiftHeight = (decimal)SlicerFile.BottomLiftHeight; + if (_liftHeight <= 0) _liftHeight = (decimal)SlicerFile.LiftHeight; + if (_bottomLiftSpeed <= 0) _bottomLiftSpeed = (decimal) SlicerFile.BottomLiftSpeed; + if (_liftSpeed <= 0) _liftSpeed = (decimal) SlicerFile.LiftSpeed; + if (_retractSpeed <= 0) _retractSpeed = (decimal) SlicerFile.RetractSpeed; + } + + #endregion + + #region Equality + + private bool Equals(OperationCalibrateLiftHeight other) + { + return _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _normalLayers == other._normalLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _bottomLiftHeight == other._bottomLiftHeight && _liftHeight == other._liftHeight && _bottomLiftSpeed == other._bottomLiftSpeed && _liftSpeed == other._liftSpeed && _retractSpeed == other._retractSpeed && _leftRightMargin == other._leftRightMargin && _topBottomMargin == other._topBottomMargin && _decreaseImage == other._decreaseImage && _decreaseImageFactor == other._decreaseImageFactor && _minimumImageFactor == other._minimumImageFactor; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is OperationCalibrateLiftHeight other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(_layerHeight); + hashCode.Add(_bottomLayers); + hashCode.Add(_normalLayers); + hashCode.Add(_bottomExposure); + hashCode.Add(_normalExposure); + hashCode.Add(_bottomLiftHeight); + hashCode.Add(_liftHeight); + hashCode.Add(_bottomLiftSpeed); + hashCode.Add(_liftSpeed); + hashCode.Add(_retractSpeed); + hashCode.Add(_leftRightMargin); + hashCode.Add(_topBottomMargin); + hashCode.Add(_decreaseImage); + hashCode.Add(_decreaseImageFactor); + hashCode.Add(_minimumImageFactor); + return hashCode.ToHashCode(); + } + + #endregion + + #region Methods + + /// <summary> + /// Gets the bottom and normal layers, 0 = bottom | 1 = normal + /// </summary> + /// <returns></returns> + public Mat[] GetLayers() + { + var layers = new Mat[1]; + + layers[0] = EmguExtensions.InitMat(SlicerFile.Resolution); + CvInvoke.Rectangle(layers[0], WhiteBlock, EmguExtensions.WhiteColor, -1); + + return layers; + } + + public Mat GetThumbnail() + { + Mat thumbnail = EmguExtensions.InitMat(new Size(400, 200), 3); + var fontFace = FontFace.HersheyDuplex; + var fontScale = 1; + var fontThickness = 2; + const byte xSpacing = 45; + const byte ySpacing = 45; + CvInvoke.PutText(thumbnail, "UVtools", new Point(140, 35), fontFace, fontScale, new MCvScalar(255, 27, 245), fontThickness + 1); + CvInvoke.Line(thumbnail, new Point(xSpacing, 0), new Point(xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3); + CvInvoke.Line(thumbnail, new Point(xSpacing, ySpacing + 5), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3); + CvInvoke.Line(thumbnail, new Point(thumbnail.Width - xSpacing, 0), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3); + CvInvoke.PutText(thumbnail, "Lift Height Cal.", new Point(xSpacing, ySpacing * 2), fontFace, fontScale, new MCvScalar(0, 255, 255), fontThickness); + CvInvoke.PutText(thumbnail, $"{Microns}um @ {BottomExposure}s/{NormalExposure}s", new Point(xSpacing, ySpacing * 3), fontFace, fontScale, EmguExtensions.WhiteColor, fontThickness); + //CvInvoke.PutText(thumbnail, $"{ObjectCount} Objects", new Point(xSpacing, ySpacing * 4), fontFace, fontScale, EmguExtensions.WhiteColor, fontThickness); + + return thumbnail; + } + + protected override bool ExecuteInternally(OperationProgress progress) + { + progress.ItemCount = LayerCount; + + var newLayers = new Layer[LayerCount]; + + var layers = GetLayers(); + progress++; + + var layer = new Layer(0, layers[0], SlicerFile.LayerManager) + { + IsModified = true + }; + + uint layerIndex = 0; + for (; layerIndex < _bottomLayers + _normalLayers; layerIndex++) + { + newLayers[layerIndex] = layer.Clone(); + progress++; + } + + if (_decreaseImage) + { + var rect = WhiteBlock; + for (int factor = 100 - _decreaseImageFactor; factor >= _minimumImageFactor; factor -= _decreaseImageFactor) + { + using var mat = layers[0].NewBlank(); + + // size - 100 + // x - factor + + int width = rect.Width * factor / 100; + int height = rect.Height * factor / 100; + int x = (int)(SlicerFile.ResolutionX - width) / 2; + int y = (int)(SlicerFile.ResolutionY - height) / 2; + + CvInvoke.Rectangle(mat, + new Rectangle(x, y, width, height), + EmguExtensions.WhiteColor, -1); + + newLayers[layerIndex] = new Layer(0, mat, SlicerFile.LayerManager) + { + IsModified = true + }; + + layerIndex++; + progress++; + } + } + + foreach (var mat in layers) + { + mat.Dispose(); + } + + + if (SlicerFile.ThumbnailsCount > 0) + SlicerFile.SetThumbnails(GetThumbnail()); + + progress++; + + SlicerFile.SuppressRebuildPropertiesWork(() => + { + SlicerFile.LayerHeight = (float)_layerHeight; + SlicerFile.BottomExposureTime = (float)_bottomExposure; + SlicerFile.ExposureTime = (float)_normalExposure; + SlicerFile.BottomLiftHeight = (float)_bottomLiftHeight; + SlicerFile.LiftHeight = (float)_liftHeight; + SlicerFile.BottomLiftSpeed = (float)_bottomLiftSpeed; + SlicerFile.LiftSpeed = (float)_liftSpeed; + SlicerFile.RetractSpeed = (float)_retractSpeed; + SlicerFile.BottomLayerCount = _bottomLayers; + + SlicerFile.LayerManager.Layers = newLayers; + }, true); + + return !progress.Token.IsCancellationRequested; + } + + #endregion + } +} diff --git a/UVtools.Core/Operations/OperationCalibrateStressTower.cs b/UVtools.Core/Operations/OperationCalibrateStressTower.cs index a11f630..2ca4b29 100644 --- a/UVtools.Core/Operations/OperationCalibrateStressTower.cs +++ b/UVtools.Core/Operations/OperationCalibrateStressTower.cs @@ -111,7 +111,7 @@ namespace UVtools.Core.Operations if(_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if(_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if(_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.MirrorDisplay; + _mirrorOutput = SlicerFile.DisplayMirror; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; diff --git a/UVtools.Core/Operations/OperationCalibrateTolerance.cs b/UVtools.Core/Operations/OperationCalibrateTolerance.cs index 033c487..20036a7 100644 --- a/UVtools.Core/Operations/OperationCalibrateTolerance.cs +++ b/UVtools.Core/Operations/OperationCalibrateTolerance.cs @@ -431,7 +431,7 @@ namespace UVtools.Core.Operations if (_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if (_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if (_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.MirrorDisplay; + _mirrorOutput = SlicerFile.DisplayMirror; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; diff --git a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs index 54f195a..392cdf2 100644 --- a/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs +++ b/UVtools.Core/Operations/OperationCalibrateXYZAccuracy.cs @@ -463,7 +463,7 @@ namespace UVtools.Core.Operations if (_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if (_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if (_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; - _mirrorOutput = SlicerFile.MirrorDisplay; + _mirrorOutput = SlicerFile.DisplayMirror; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; diff --git a/UVtools.Core/Operations/OperationFlip.cs b/UVtools.Core/Operations/OperationFlip.cs index 047117d..3510d3e 100644 --- a/UVtools.Core/Operations/OperationFlip.cs +++ b/UVtools.Core/Operations/OperationFlip.cs @@ -19,7 +19,7 @@ namespace UVtools.Core.Operations { #region Members private bool _makeCopy; - private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.Horizontally; + private FlipType _flipDirection = FlipType.Horizontal; #endregion #region Overrides @@ -29,14 +29,14 @@ namespace UVtools.Core.Operations "Note: Before perform this operation, un-rotate the layer preview to see the real orientation."; public override string ConfirmationText => - FlipDirection == Enumerations.FlipDirection.Both - ? $"flip {(MakeCopy == true? "and blend ":"")}layers {LayerIndexStart} through {LayerIndexEnd} Horizontally and Vertically?" - : $"flip {(MakeCopy == true ? "and blend " : "")}layers {LayerIndexStart} through {LayerIndexEnd} {FlipDirection}?"; + FlipDirection == FlipType.Both + ? $"flip {(_makeCopy ? "and blend ":"")}layers {LayerIndexStart} through {LayerIndexEnd} Horizontally and Vertically?" + : $"flip {(_makeCopy ? "and blend " : "")}layers {LayerIndexStart} through {LayerIndexEnd} {FlipDirection}?"; public override string ProgressTitle => - FlipDirection == Enumerations.FlipDirection.Both - ? $"Flipping {(MakeCopy == true ? "and blending " : "")}layers {LayerIndexStart} through {LayerIndexEnd} Horizontally and Vertically" - : $"Flipping {(MakeCopy == true ? "and blending " : "")}layers {LayerIndexStart} through {LayerIndexEnd} {FlipDirection}"; + FlipDirection == FlipType.Both + ? $"Flipping {(_makeCopy ? "and blending " : "")}layers {LayerIndexStart} through {LayerIndexEnd} Horizontally and Vertically" + : $"Flipping {(_makeCopy ? "and blending " : "")}layers {LayerIndexStart} through {LayerIndexEnd} {FlipDirection}"; public override string ProgressAction => "Flipped layers"; @@ -50,7 +50,7 @@ namespace UVtools.Core.Operations #region Properties - public Enumerations.FlipDirection FlipDirection + public FlipType FlipDirection { get => _flipDirection; set => RaiseAndSetIfChanged(ref _flipDirection, value); @@ -62,29 +62,6 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _makeCopy, value); } - public static Array FlipDirections => Enum.GetValues(typeof(Enumerations.FlipDirection)); - - public FlipType FlipTypeOpenCV - { - get - { - var flipType = FlipType.Horizontal; - switch (FlipDirection) - { - case Enumerations.FlipDirection.Horizontally: - flipType = FlipType.Horizontal; - break; - case Enumerations.FlipDirection.Vertically: - flipType = FlipType.Vertical; - break; - case Enumerations.FlipDirection.Both: - flipType = FlipType.Horizontal | FlipType.Vertical; - break; - } - - return flipType; - } - } #endregion #region Constructor @@ -144,12 +121,12 @@ namespace UVtools.Core.Operations if (MakeCopy) { using Mat dst = new(); - CvInvoke.Flip(target, dst, FlipTypeOpenCV); + CvInvoke.Flip(target, dst, _flipDirection); CvInvoke.Add(target, dst, target); } else { - CvInvoke.Flip(target, target, FlipTypeOpenCV); + CvInvoke.Flip(target, target, _flipDirection); } ApplyMask(original, target); diff --git a/UVtools.Core/Operations/OperationLayerExportGif.cs b/UVtools.Core/Operations/OperationLayerExportGif.cs index 0762608..ae93843 100644 --- a/UVtools.Core/Operations/OperationLayerExportGif.cs +++ b/UVtools.Core/Operations/OperationLayerExportGif.cs @@ -24,30 +24,6 @@ namespace UVtools.Core.Operations [Serializable] public sealed class OperationLayerExportGif : Operation { - #region Enums - public enum ExportGifRotateDirection : byte - { - None = 9, - /// <summary>Rotate 90 degrees clockwise</summary> - [Description("Rotate 90º CW")] - Rotate90Clockwise = 0, - /// <summary>Rotate 180 degrees clockwise</summary> - [Description("Rotate 180º")] - Rotate180 = 1, - /// <summary>Rotate 270 degrees clockwise</summary> - [Description("Rotate 90º CCW")] - Rotate90CounterClockwise = 2, - } - - public enum ExportGifFlipDirection : byte - { - None, - Horizontally, - Vertically, - Both, - } - #endregion - #region Members private string _filePath; private bool _clipByVolumeBounds; @@ -56,8 +32,8 @@ namespace UVtools.Core.Operations private ushort _repeats; private ushort _skip; private decimal _scale = 50; - private ExportGifRotateDirection _rotateDirection = ExportGifRotateDirection.None; - private ExportGifFlipDirection _flipDirection = ExportGifFlipDirection.None; + private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; + private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; #endregion @@ -188,13 +164,13 @@ namespace UVtools.Core.Operations public float ScaleFactor => (float)_scale / 100f; - public ExportGifRotateDirection RotateDirection + public Enumerations.RotateDirection RotateDirection { get => _rotateDirection; set => RaiseAndSetIfChanged(ref _rotateDirection, value); } - public ExportGifFlipDirection FlipDirection + public Enumerations.FlipDirection FlipDirection { get => _flipDirection; set => RaiseAndSetIfChanged(ref _flipDirection, value); @@ -257,23 +233,14 @@ namespace UVtools.Core.Operations CvInvoke.Resize(matRoi, matRoi, new Size((int) (matRoi.Width * ScaleFactor), (int)(matRoi.Height * ScaleFactor))); } - if (_flipDirection != ExportGifFlipDirection.None) + if (_flipDirection != Enumerations.FlipDirection.None) { - FlipType? flipType = _flipDirection switch - { - ExportGifFlipDirection.Horizontally => FlipType.Horizontal, - ExportGifFlipDirection.Vertically => FlipType.Vertical, - ExportGifFlipDirection.Both => FlipType.Both, - _ => null - }; - - if (flipType is not null) - CvInvoke.Flip(matRoi, matRoi, flipType.Value); + CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); } - if (_rotateDirection != ExportGifRotateDirection.None) + if (_rotateDirection != Enumerations.RotateDirection.None) { - CvInvoke.Rotate(matRoi, matRoi, (RotateFlags) _rotateDirection); + CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); } if (_renderLayerCount) diff --git a/UVtools.Core/Operations/OperationLayerExportImage.cs b/UVtools.Core/Operations/OperationLayerExportImage.cs new file mode 100644 index 0000000..7e52359 --- /dev/null +++ b/UVtools.Core/Operations/OperationLayerExportImage.cs @@ -0,0 +1,303 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Util; +using UVtools.Core.FileFormats; + +namespace UVtools.Core.Operations +{ + [Serializable] + public sealed class OperationLayerExportImage : Operation + { + #region Enums + + public enum LayerExportImageTypes : byte + { + [Description("PNG: Portable Network Graphics")] + PNG, + [Description("JPG: Joint Photographic Experts Group")] + JPG, + [Description("JPEG: Joint Photographic Experts Group")] + JPEG, + [Description("JP2: Joint Photographic Experts Group (JPEG 2000)")] + JP2, + [Description("TIF: Tag Image File Format")] + TIF, + [Description("TIFF: Tag Image File Format")] + TIFF, + [Description("BMP: Bitmap")] + BMP, + [Description("PBM: Portable Bitmap")] + PBM, + [Description("PGM: Portable Greymap")] + PGM, + //[Description("PPM: Portable Pixmap")] + //PPM, + [Description("SR: Sun raster")] + SR, + [Description("RAS: Sun raster")] + RAS, + [Description("SVG: Scalable Vector Graphics")] + SVG + } + #endregion + + #region Members + + private string _outputFolder; + private string _filename = "layer"; + private LayerExportImageTypes _imageType = LayerExportImageTypes.PNG; + private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; + private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; + private bool _padLayerIndex = true; + private bool _cropByRoi = true; + + #endregion + + #region Overrides + + public override bool CanHaveProfiles => false; + public override string Title => "Export layers to images"; + + public override string Description => + "Export a layer range to images."; + + public override string ConfirmationText => + $"export {_imageType} images from layers {LayerIndexStart} through {LayerIndexEnd}?"; + + public override string ProgressTitle => + $"Exporting {_imageType} images from layers {LayerIndexStart} through {LayerIndexEnd}"; + + public override string ProgressAction => $"Exported {_imageType} images"; + + /*public override string ValidateInternally() + { + var sb = new StringBuilder(); + + if (LayerRangeCount < 2) + { + sb.AppendLine("To generate a heat map at least two layers are required."); + } + + return sb.ToString(); + }*/ + + public override string ToString() + { + var result = $"[Crop by ROI: {_cropByRoi}] [Pad index: {_padLayerIndex}]" + + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + + #endregion + + #region Properties + + public string OutputFolder + { + get => _outputFolder; + set => RaiseAndSetIfChanged(ref _outputFolder, value); + } + + public string Filename + { + get => _filename; + set => RaiseAndSetIfChanged(ref _filename, value); + } + + public LayerExportImageTypes ImageType + { + get => _imageType; + set => RaiseAndSetIfChanged(ref _imageType, value); + } + + public Enumerations.RotateDirection RotateDirection + { + get => _rotateDirection; + set => RaiseAndSetIfChanged(ref _rotateDirection, value); + } + + public Enumerations.FlipDirection FlipDirection + { + get => _flipDirection; + set => RaiseAndSetIfChanged(ref _flipDirection, value); + } + + public bool PadLayerIndex + { + get => _padLayerIndex; + set => RaiseAndSetIfChanged(ref _padLayerIndex, value); + } + + public bool CropByROI + { + get => _cropByRoi; + set => RaiseAndSetIfChanged(ref _cropByRoi, value); + } + + #endregion + + #region Constructor + + public OperationLayerExportImage() + { } + + public OperationLayerExportImage(FileFormat slicerFile) : base(slicerFile) + { + _outputFolder = Path.Combine(Path.GetDirectoryName(SlicerFile.FileFullPath) ?? string.Empty, FileFormat.GetFileNameStripExtensions(SlicerFile.FileFullPath)); + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + if (!Directory.Exists(_outputFolder)) + { + Directory.CreateDirectory(_outputFolder); + } + + var slicedFileNameNoExt = SlicerFile.FilenameNoExt; + + Parallel.For(LayerIndexStart, LayerIndexEnd+1, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + + using var mat = SlicerFile[layerIndex].LayerMat; + var matRoi = mat; + if (_cropByRoi && HaveROI) + { + matRoi = GetRoiOrDefault(mat); + } + + if (_flipDirection != Enumerations.FlipDirection.None) + { + CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); + } + + if (_rotateDirection != Enumerations.RotateDirection.None) + { + CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); + } + + var filename = SlicerFile[layerIndex].FormatFileName(_filename, _padLayerIndex ? SlicerFile.LayerManager.LayerDigits : byte.MinValue, true, string.Empty); + var fileFullPath = Path.Combine(_outputFolder, $"{filename}.{_imageType.ToString().ToLower()}"); + + if (_imageType != LayerExportImageTypes.SVG) + { + matRoi.Save(fileFullPath); + } + else + { + // SVG + + CvInvoke.Threshold(matRoi, matRoi, 127, byte.MaxValue, ThresholdType.Binary); // Remove AA + + using var contours = new VectorOfVectorOfPoint(); + using var hierarchy = new Mat(); + CvInvoke.FindContours(matRoi, contours, hierarchy, RetrType.Tree, ChainApproxMethod.ChainApproxSimple); + var hierarchyJagged = hierarchy.GetData(); + + using TextWriter tw = new StreamWriter(fileFullPath); + tw.WriteLine("<!--"); + tw.WriteLine($"# Generated by {About.Software} v{About.VersionStr} {About.Arch} @ {DateTime.UtcNow} #"); + tw.WriteLine($"File: {SlicerFile.Filename}"); + tw.WriteLine($"{SlicerFile[layerIndex].ToString().Replace(", ", "\n")}"); + tw.WriteLine("-->"); + tw.WriteLine("<svg " + + "xmlns=\"http://www.w3.org/2000/svg\" " + + //"xmlns:xlink=\"http://www.w3.org/1999/xlink\" " + + //"version=\"1.1\" " + + $"id=\"{slicedFileNameNoExt}_{filename}\" " + + $"data-name=\"{slicedFileNameNoExt}_{filename}\" " + + //"x=\"0\" " + + //"y=\"0\" " + + $"width=\"{matRoi.Width}\" " + + $"height=\"{matRoi.Height}\" " + + $"viewBox=\"0 0 {matRoi.Width} {matRoi.Height}\">"); + tw.WriteLine("\t<defs>"); + tw.WriteLine("\t\t<style>"); + //tw.WriteLine("\t\tsvg { background-color: #000000; }"); + tw.WriteLine("\t\t.background { fill: #000000; fill-rule: evenodd; }"); + tw.WriteLine("\t\t.black { fill: #000000; fill-rule: evenodd; }"); + tw.WriteLine("\t\t.white { fill: #FFFFFF; fill-rule: evenodd; }"); + tw.WriteLine("\t\t</style>"); + tw.WriteLine("\t</defs>"); + tw.WriteLine($"\t<title>{slicedFileNameNoExt} #{layerIndex}</title>"); + tw.WriteLine($"\t<rect class=\"background\" width=\"{mat.Width}\" height=\"{mat.Height}\"/>"); + + // + //hierarchy[i][0]: the index of the next contour of the same level + //hierarchy[i][1]: the index of the previous contour of the same level + //hierarchy[i][2]: the index of the first child + //hierarchy[i][3]: the index of the parent + // + + for (int i = 0; i < contours.Size; i++) + { + if (contours[i].Size == 0) continue; + + var style = "white"; + + int parentIndex = i; + int count = 0; + while ((parentIndex = (int)hierarchyJagged.GetValue(0, parentIndex, 3)) != -1) + { + count++; + } + + if (count % 2 != 0) + style = "black"; + + tw.Write($"\t<path class=\"{style}\" d=\"M{contours[i][0].X} {contours[i][0].Y}"); + for (int x = 1; x < contours[i].Size; x++) + { + tw.Write($",L{contours[i][x].X} {contours[i][x].Y}"); + } + tw.WriteLine("Z\"/>"); + } + + tw.WriteLine("</svg>"); + } + + progress.LockAndIncrement(); + }); + + return !progress.Token.IsCancellationRequested; + } + + #endregion + + #region Equality + + private bool Equals(OperationLayerExportImage other) + { + return _outputFolder == other._outputFolder && _filename == other._filename && _imageType == other._imageType && _rotateDirection == other._rotateDirection && _flipDirection == other._flipDirection && _padLayerIndex == other._padLayerIndex && _cropByRoi == other._cropByRoi; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is OperationLayerExportImage other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(_outputFolder, _filename, (int) _imageType, (int) _rotateDirection, (int) _flipDirection, _padLayerIndex, _cropByRoi); + } + + #endregion + } +} diff --git a/UVtools.Core/Operations/OperationProgress.cs b/UVtools.Core/Operations/OperationProgress.cs index 732434a..a567638 100644 --- a/UVtools.Core/Operations/OperationProgress.cs +++ b/UVtools.Core/Operations/OperationProgress.cs @@ -15,11 +15,13 @@ namespace UVtools.Core.Operations { public sealed class OperationProgress : BindableBase { - public const string StatusDecodeThumbnails = "Decoded Thumbnails"; + public const string StatusDecodePreviews = "Decoded Previews"; public const string StatusGatherLayers = "Gathered Layers"; public const string StatusDecodeLayers = "Decoded Layers"; + public const string StatusEncodePreviews = "Encoded Previews"; public const string StatusEncodeLayers = "Encoded Layers"; public const string StatusWritingFile = "Writing File"; + public const string StatusDecodeGcode = "Decoding GCode"; public const string StatusEncodeGcode = "Encoding GCode"; public const string StatusOptimizingBounds = "Gathering Bounds"; diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 51050a2..b2d41a2 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>2.16.0</Version> + <Version>2.17.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> |