diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-06-12 05:56:20 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-06-12 05:56:20 +0300 |
commit | 5d61e95331329a842c8547c04f40c68bcc922693 (patch) | |
tree | b1f23594e678af6ec4c379d85282c5c374d306cf | |
parent | 470f5ec1cf87fff04ed88044e3b3beeeabd55609 (diff) |
v2.13.3v2.13.3
- **File formats:**
- (Add) CXDLP v2
- (Improved) GR1, MDLP, CXDLP decode and encode performance and memory optimization
- (Remove) CXDLP v1 from available formats
- (Add) Pixel editor - Drawing: New brushes of shapes/polygons
- (Upgrade) .NET from 5.0.6 to 5.0.7
- (Fix) When there are issues on the list, executing any operation will navigate to the last layer
- (Fix) PrusaSlicer printer: Rename "Creality HALOT-SKY CL-60" to "Creality HALOT-ONE CL-60"
38 files changed, 1888 insertions, 403 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a5be9d..e04a2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 12/06/2021 - v2.13.3 + +- **File formats:** + - (Add) CXDLP v2 + - (Improved) GR1, MDLP, CXDLP decode and encode performance and memory optimization + - (Remove) CXDLP v1 from available formats +- (Add) Pixel editor - Drawing: New brushes of shapes/polygons +- (Upgrade) .NET from 5.0.6 to 5.0.7 +- (Fix) When there are issues on the list, executing any operation will navigate to the last layer +- (Fix) PrusaSlicer printer: Rename "Creality HALOT-SKY CL-60" to "Creality HALOT-ONE CL-60" + ## 06/06/2021 - v2.13.2 - (Upgrade) AvaloniaUI from 0.10.5 to 0.10.6 @@ -54,4 +54,5 @@ * Dennis Hansen * Evert Goor * James Kao -* Finn Newick
\ No newline at end of file +* Finn Newick +* Thomas Wilbert
\ No newline at end of file diff --git a/PrusaSlicer/printer/Creality HALOT-SKY CL-60.ini b/PrusaSlicer/printer/Creality HALOT-ONE CL-60.ini index 0a013c2..7e49667 100644 --- a/PrusaSlicer/printer/Creality HALOT-SKY CL-60.ini +++ b/PrusaSlicer/printer/Creality HALOT-ONE CL-60.ini @@ -1,4 +1,4 @@ -# generated by PrusaSlicer 2.3.0+win64 on 2021-04-14 at 04:57:50 UTC +# generated by PrusaSlicer 2.3.1+win64 on 2021-06-08 at 21:10:32 UTC absolute_correction = 0 area_fill = 50 bed_custom_model = diff --git a/Scripts/ImportPrusaSlicerData.bat b/Scripts/ImportPrusaSlicerData.bat index 525dfcf..4fe6b5d 100644 --- a/Scripts/ImportPrusaSlicerData.bat +++ b/Scripts/ImportPrusaSlicerData.bat @@ -58,8 +58,8 @@ SET files[47]=Wanhao CGR Mono.ini SET files[48]=Creality LD-002R.ini SET files[49]=Creality LD-002H.ini SET files[50]=Creality LD-006.ini -SET files[51]=Creality HALOT-SKY CL-89.ini -SET files[52]=Creality HALOT-SKY CL-60.ini +SET files[51]=Creality HALOT-ONE CL-60.ini +SET files[52]=Creality HALOT-SKY CL-89.ini SET files[53]=Voxelab Polaris 5.5.ini SET files[54]=Voxelab Proxima 6.ini SET files[55]=Voxelab Ceres 8.9.ini diff --git a/Scripts/cxdlp_v2.bt b/Scripts/cxdlp_v2.bt index aaccf46..0c27e76 100644 --- a/Scripts/cxdlp_v2.bt +++ b/Scripts/cxdlp_v2.bt @@ -4,6 +4,11 @@ // File: Creality // Authors: Julien Delnatte //------------------------------------------------ +// CHANGELOG: +// Add uint16 unknown after header (Set to 2) +// Add uint32 modelSize and BYTE model[modelSize] after unknown +// Add byte offset[64] after resolutionY (Zeros) + typedef struct { BitfieldDisablePadding();
ushort startY:13;
ushort endY:13;
ushort x:14; @@ -39,6 +44,8 @@ struct HEADER { uint16 resolutionX <fgcolor=cBlack, bgcolor=cWhite>; uint16 resolutionY <fgcolor=cBlack, bgcolor=cWhite>; + byte offset[64]; + rgbPreviewImageRawData preview(116*116*2); BYTE rn0[2] <fgcolor=cBlack, bgcolor=cRed>; @@ -46,9 +53,7 @@ struct HEADER { BYTE rn1[2] <fgcolor=cBlack, bgcolor=cRed>; rgbPreviewImageRawData preview2(290*290*2); - BYTE rn2[2] <fgcolor=cBlack, bgcolor=cRed>; - - byte offset[64]; + BYTE rn2[2] <fgcolor=cBlack, bgcolor=cRed>; uint32 plateformXLength <fgcolor=cBlack, bgcolor=cWhite>; wchar_t plateformX[plateformXLength/2]; diff --git a/UVtools.Core/Extensions/BitExtensions.cs b/UVtools.Core/Extensions/BitExtensions.cs index 7df1876..8cb7dbd 100644 --- a/UVtools.Core/Extensions/BitExtensions.cs +++ b/UVtools.Core/Extensions/BitExtensions.cs @@ -11,5 +11,78 @@ namespace UVtools.Core.Extensions { public static ushort ToUShortLittleEndian(byte byte1, byte byte2) => (ushort)(byte1 + (byte2 << 8)); public static ushort ToUShortBigEndian(byte byte1, byte byte2) => (ushort)((byte1 << 8) + byte2); + + public static ushort ToUShortLittleEndian(byte[] buffer, int offset = 0) + => (ushort)(buffer[offset] + (buffer[offset+1] << 8)); + public static ushort ToUShortBigEndian(byte[] buffer, int offset = 0) + => (ushort)((buffer[offset] << 8) + buffer[offset+1]); + + public static uint ToUIntLittleEndian(byte byte1, byte byte2, byte byte3, byte byte4) + => (uint)(byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); + public static uint ToUIntBigEndian(byte byte1, byte byte2, byte byte3, byte byte4) + => (uint)((byte1 << 24) + (byte1 << 16) + (byte1 << 8) + byte2); + + public static uint ToUIntLittleEndian(byte[] buffer, int offset = 0) + => (uint)(buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24)); + public static uint ToUIntBigEndian(byte[] buffer, int offset = 0) + => (uint)((buffer[offset] << 24) + (buffer[offset+1] << 16) + (buffer[offset+2] << 8) + buffer[offset+3]); + + public static byte[] ToBytesLittleEndian(ushort value) + { + var bytes = new byte[2]; + ToBytesLittleEndian(value, bytes); + return bytes; + } + + public static void ToBytesLittleEndian(ushort value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + } + + public static byte[] ToBytesBigEndian(ushort value) + { + var bytes = new byte[2]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(ushort value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)(value >> 8); + buffer[offset + 1] = (byte)value; + } + + public static byte[] ToBytesLittleEndian(uint value) + { + var bytes = new byte[4]; + ToBytesLittleEndian(value, bytes); + return bytes; + } + + public static void ToBytesLittleEndian(uint value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + buffer[offset + 2] = (byte)(value >> 16); + buffer[offset + 3] = (byte)(value >> 24); + } + + public static byte[] ToBytesBigEndian(uint value) + { + var bytes = new byte[4]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(uint value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)value; + } + + } } diff --git a/UVtools.Core/Extensions/DrawingExtensions.cs b/UVtools.Core/Extensions/DrawingExtensions.cs index db5ec69..d8f4385 100644 --- a/UVtools.Core/Extensions/DrawingExtensions.cs +++ b/UVtools.Core/Extensions/DrawingExtensions.cs @@ -21,88 +21,23 @@ namespace UVtools.Core.Extensions return Color.FromArgb(r, g, b); } - public static double CalculateSideLength(int sides, int radius) + public static double CalculatePolygonSideLengthFromRadius(double radius, int sides) { return 2 * radius * Math.Sin(Math.PI / sides); } - /*public static Point[] GetPolygonVertices(int sides, int radius, Point center, double startingAngle = 0) + public static double CalculatePolygonVerticalLengthFromRadius(double radius, int sides) { - if (sides < 3) - throw new ArgumentException("Polygons can't have less than 3 sides...", nameof(sides)); - - - // Fix rotation - switch (sides) - { - case 3: - startingAngle += 90; - break; - case 4: - startingAngle += 45; - break; - case 5: - startingAngle += 22.5; - break; - } - - var points = new Point[sides]; - var step = 360.0 / sides; - int i = 0; - for (var angle = startingAngle; angle < startingAngle + 360.0; angle += step) //go in a circle - { - if (i == sides) break; // Fix floating problem - double radians = angle * Math.PI / 180.0; - points[i++] = new( - (int) Math.Round(Math.Cos(radians) * radius + center.X), - (int) Math.Round(Math.Sin(-radians) * radius + center.Y) - ); - } - - return points; - }*/ + return radius * Math.Cos(Math.PI / sides); + } - /*public static Point[] GetPolygonVertices(int sides, int radius, Point center, double startingAngle = 0) + public static double CalculatePolygonRadiusFromSideLength(double length, int sides) { - startingAngle = -45; - if (sides < 3) - throw new ArgumentException("Polygons can't have less than 3 sides...", nameof(sides)); - - var vertex = new Point[sides]; - //var deg = (180.0 * (sides - 2)) / sides + startingAngle; - var deg = ((180.0 * (sides - 2) / sides) - 180) / 2 + startingAngle; - var step = 360.0 / sides; - var rad = deg * (Math.PI / 180); - - double nSinDeg = Math.Sin(rad); - double nCosDeg = Math.Cos(rad); - - vertex[0] = center; - //vertex[0].X += radius; - vertex[0].Y -= radius; - //vertex[0].X += (int)Math.Cos(deg) / 2 *radius; - //vertex[0].Y -= (int)Math.Sin(deg) / 2 *radius; - int length = (int)Math.Round(CalculateSideLength(sides, radius)); - - for (int i = 1; i < vertex.Length; i++) - { - vertex[i] = new( - (int)Math.Round(vertex[i - 1].X - nCosDeg * length), - (int)Math.Round(vertex[i - 1].Y - nSinDeg * length)); - - - //recalculate the degree for the next vertex - deg -= step; - rad = deg * (Math.PI / 180); - - nSinDeg = Math.Sin(rad); - nCosDeg = Math.Cos(rad); - - } - return vertex; - }*/ + var theta = 360.0 / sides; + return length / (2 * Math.Cos((90 - theta / 2) * Math.PI / 180.0)); + } - public static Point[] GetPolygonVertices(int sides, int radius, Point center, double startingAngle = 0) + public static Point[] GetPolygonVertices(int sides, int radius, Point center, double startingAngle = 0, bool flipHorizontally = false, bool flipVertically = false) { if (sides < 3) throw new ArgumentException("Polygons can't have less than 3 sides...", nameof(sides)); @@ -110,18 +45,23 @@ namespace UVtools.Core.Extensions var vertices = new Point[sides]; double deg = 360.0 / sides;//calculate the rotation angle - //double a = radius * Math.Cos(Math.PI / sides);//calculate vertical length - //double s = CalculateSideLength(sides, radius);//calculate the side length var rad = Math.PI / 180.0; + var x0 = center.X + radius * Math.Cos(-(((180 - deg) / 2) + startingAngle) * rad); + var y0 = center.Y - radius * Math.Sin(-(((180 - deg) / 2) + startingAngle) * rad); + + var x1 = center.X + radius * Math.Cos(-(((180 - deg) / 2) + deg + startingAngle) * rad); + var y1 = center.Y - radius * Math.Sin(-(((180 - deg) / 2) + deg + startingAngle) * rad); + vertices[0] = new( - (int) Math.Round(center.X + radius * Math.Cos(-(((180 - deg) / 2) + startingAngle) * rad)), - (int) Math.Round(center.Y - radius * Math.Sin(-(((180 - deg) / 2) + startingAngle) * rad))); + (int) Math.Round(x0), + (int) Math.Round(y0) + ); vertices[1] = new( - (int) Math.Round(center.X + radius * Math.Cos(-(((180 - deg) / 2) + deg + startingAngle) * rad)), - (int) Math.Round(center.Y - radius * Math.Sin(-(((180 - deg) / 2) + deg + startingAngle) * rad) - )); + (int) Math.Round(x1), + (int) Math.Round(y1) + ); for (int i = 0; i < sides - 2; i++) { @@ -129,11 +69,31 @@ namespace UVtools.Core.Extensions double dcosrot = Math.Cos((deg * (i + 1)) * rad); vertices[i + 2] = new( - (int)Math.Round(center.X + dcosrot * (vertices[1].X - center.X) - dsinrot * (vertices[1].Y - center.Y)), - (int)Math.Round(center.Y + dsinrot * (vertices[1].X - center.X) + dcosrot * (vertices[1].Y - center.Y)) + (int)Math.Round(center.X + dcosrot * (x1 - center.X) - dsinrot * (y1 - center.Y)), + (int)Math.Round(center.Y + dsinrot * (x1 - center.X) + dcosrot * (y1 - center.Y)) ); } + if (flipHorizontally) + { + var startX = center.X - radius; + var endX = center.X + radius; + for (int i = 0; i < sides; i++) + { + vertices[i].X = endX - (vertices[i].X - startX); + } + } + + if (flipVertically) + { + var startY = center.Y - radius; + var endY = center.Y + radius; + for (int i = 0; i < sides; i++) + { + vertices[i].Y = endY - (vertices[i].Y - startY); + } + } + return vertices; } } diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 1963528..0399fa6 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -608,7 +608,8 @@ namespace UVtools.Core.Extensions /// <param name="startingAngle"></param> /// <param name="thickness"></param> /// <param name="lineType"></param> - public static void DrawPolygon(this Mat src, int sides, int radius, Point center, MCvScalar color, double startingAngle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) + /// <param name="flip"></param> + public static void DrawPolygon(this Mat src, int sides, int radius, Point center, MCvScalar color, double startingAngle = 0, int thickness = -1, LineType lineType = LineType.EightConnected, FlipType flip = FlipType.None) { if (sides == 1) { @@ -625,7 +626,9 @@ namespace UVtools.Core.Extensions return; } - var points = DrawingExtensions.GetPolygonVertices(sides, radius, center, startingAngle); + var points = DrawingExtensions.GetPolygonVertices(sides, radius, center, startingAngle, + (flip & FlipType.Horizontal) != 0, (flip & FlipType.Vertical) != 0); + if (thickness <= 0) { using var vec = new VectorOfPoint(points); diff --git a/UVtools.Core/Extensions/FileStreamExtensions.cs b/UVtools.Core/Extensions/FileStreamExtensions.cs index 4e13028..b0370f2 100644 --- a/UVtools.Core/Extensions/FileStreamExtensions.cs +++ b/UVtools.Core/Extensions/FileStreamExtensions.cs @@ -17,6 +17,13 @@ namespace UVtools.Core.Extensions return (uint)fs.Read(bytes, offset, bytes.Length); } + public static byte[] ReadBytes(this FileStream fs, int length, int offset = 0) + { + var buffer = new byte[length]; + fs.Read(buffer, offset, length); + return buffer; + } + public static uint WriteStream(this FileStream fs, MemoryStream stream, int offset = 0) { return fs.WriteBytes(stream.ToArray(), offset); diff --git a/UVtools.Core/Extensions/PointExtensions.cs b/UVtools.Core/Extensions/PointExtensions.cs index ac81e26..01bcc96 100644 --- a/UVtools.Core/Extensions/PointExtensions.cs +++ b/UVtools.Core/Extensions/PointExtensions.cs @@ -42,13 +42,35 @@ namespace UVtools.Core.Extensions return new((float) x, (float) y); } - public static Point Half(this Point point)=> new(point.X / 2, point.Y / 2); + public static void Rotate(Point[] points, double angleDegree, Point pivot = default) + { + for (int i = 0; i < points.Length; i++) + { + points[i] = points[i].Rotate(angleDegree, pivot); + } + } + + public static void Rotate(PointF[] points, double angleDegree, PointF pivot = default) + { + for (int i = 0; i < points.Length; i++) + { + points[i] = points[i].Rotate(angleDegree, pivot); + } + } + + public static Point OffsetBy(this Point point, int value)=> new(point.X + value, point.Y + value); + public static Point OffsetBy(this Point point, int x, int y) => new(point.X + x, point.Y + y); + public static Point OffsetBy(this Point point, Point other) => new(point.X + other.X, point.Y + other.Y); + + public static Point Half(this Point point) => new(point.X / 2, point.Y / 2); public static PointF Half(this PointF point) => new(point.X / 2, point.Y / 2); public static Size ToSize(this Point point) => new(point.X, point.Y); public static SizeF ToSize(this PointF point) => new(point.X, point.Y); + + } } diff --git a/UVtools.Core/FileFormats/CXDLPFile.cs b/UVtools.Core/FileFormats/CXDLPFile.cs index 305cd08..4e88af8 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -6,8 +6,6 @@ * of this license document, but changing it is not allowed. */ -// https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc - using System; using System.Collections.Generic; using System.Diagnostics; @@ -21,6 +19,7 @@ using BinarySerialization; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; +using MoreLinq; using UVtools.Core.Extensions; using UVtools.Core.Operations; @@ -31,15 +30,14 @@ namespace UVtools.Core.FileFormats #region Constants private const byte HEADER_SIZE = 9; // CXSW3DV2 private const string HEADER_VALUE = "CXSW3DV2"; - - - private const uint SlicerInfoAddress = 4 + HEADER_SIZE + 6 + 290 * 290 * 4 + 116 * 116 * 2 + 6; #endregion #region Sub Classes #region Header public sealed class Header { + private string _printerModel = "CL-89"; + /// <summary> /// Gets the size of the header /// </summary> @@ -55,26 +53,68 @@ namespace UVtools.Core.FileFormats [SerializeAs(SerializedType.TerminatedString)] public string HeaderValue { get; set; } = HEADER_VALUE; + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public ushort Unknown { get; set; } = 2; + + /// <summary> + /// Gets the size of the printer model + /// </summary> + [FieldOrder(3)] + [FieldEndianness(Endianness.Big)] + public uint PrinterModelSize { get; set; } = 6; + + /// <summary> + /// Gets the printer model + /// </summary> + /*[FieldOrder(4)] + [FieldLength(nameof(PrinterModelSize), BindingMode = BindingMode.OneWay)] + [SerializeAs(SerializedType.TerminatedString)] + public string PrinterModel + { + get => _printerModel; + set + { + _printerModel = value; + PrinterModelSize = string.IsNullOrEmpty(value) ? 0 : (uint)value.Length+1; + } + }*/ + + [FieldOrder(4)] + [FieldLength(nameof(PrinterModelSize))] + public byte[] PrinterModelArray { get; set; } + + [Ignore] + public string PrinterModel + { + get => Encoding.ASCII.GetString(PrinterModelArray).TrimEnd(char.MinValue); + set => PrinterModelArray = Encoding.ASCII.GetBytes(value+char.MinValue); + } + /// <summary> /// Gets the number of records in the layer table /// </summary> - [FieldOrder(2)] + [FieldOrder(5)] [FieldEndianness(Endianness.Big)] public ushort LayerCount { get; set; } /// <summary> /// Gets the printer resolution along X axis, in pixels. This information is critical to correctly decoding layer images. /// </summary> - [FieldOrder(3)] + [FieldOrder(6)] [FieldEndianness(Endianness.Big)] public ushort ResolutionX { get; set; } /// <summary> /// Gets the printer resolution along Y axis, in pixels. This information is critical to correctly decoding layer images. /// </summary> - [FieldOrder(4)] + [FieldOrder(7)] [FieldEndianness(Endianness.Big)] public ushort ResolutionY { get; set; } + + [FieldOrder(8)] + [FieldLength(64)] + public byte[] Offset { get; set; } = new byte[64]; public void Validate() { @@ -83,12 +123,17 @@ namespace UVtools.Core.FileFormats throw new FileLoadException("Not a valid CXDLP file!"); } } + + public override string ToString() + { + return $"{nameof(HeaderSize)}: {HeaderSize}, {nameof(HeaderValue)}: {HeaderValue}, {nameof(Unknown)}: {Unknown}, {nameof(PrinterModelSize)}: {PrinterModelSize}, {nameof(PrinterModelArray)}: {PrinterModelArray}, {nameof(PrinterModel)}: {PrinterModel}, {nameof(LayerCount)}: {LayerCount}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(Offset)}: {Offset}"; + } } #endregion #region SlicerInfo - // Address: 363337 + // Address: 363407 public sealed class SlicerInfo { [FieldOrder(0)] @@ -158,6 +203,11 @@ namespace UVtools.Core.FileFormats [FieldOrder(16)] [FieldEndianness(Endianness.Big)] public ushort LightPWM { get; set; } = 255; + + public override string ToString() + { + return $"{nameof(DisplayWidthDataSize)}: {DisplayWidthDataSize}, {nameof(DisplayWidthBytes)}: {DisplayWidthBytes}, {nameof(DisplayHeightDataSize)}: {DisplayHeightDataSize}, {nameof(DisplayHeightBytes)}: {DisplayHeightBytes}, {nameof(LayerHeightDataSize)}: {LayerHeightDataSize}, {nameof(LayerHeightBytes)}: {LayerHeightBytes}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(BottomLayers)}: {BottomLayers}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(LightPWM)}: {LightPWM}"; + } } #endregion @@ -186,6 +236,14 @@ namespace UVtools.Core.FileFormats [FieldOrder(2)] [FieldCount(nameof(LineCount))] public LayerLine[] Lines { get; set; } [FieldOrder(3)] public PageBreak PageBreak { get; set; } = new(); + public static byte[] GetHeaderBytes(uint unknown, uint lineCount) + { + var bytes = new byte[8]; + BitExtensions.ToBytesBigEndian(unknown, bytes); + BitExtensions.ToBytesBigEndian(lineCount, bytes, 4); + return bytes; + } + public LayerDef() { } public LayerDef(uint unknown, uint lineCount, LayerLine[] lines) @@ -212,6 +270,18 @@ namespace UVtools.Core.FileFormats [Ignore] public ushort StartX => (ushort)(((Coordinates[3] << 8) + Coordinates[4]) & 0x3FFF); // 14 bits [Ignore] public ushort Length => (ushort) (EndY - StartY); + public static byte[] GetBytes(ushort startY, ushort endY, ushort startX, byte gray) + { + var bytes = new byte[CoordinateCount + 1]; + bytes[0] = (byte)((startY >> 5) & 0xFF); + bytes[1] = (byte)(((startY << 3) + (endY >> 10)) & 0xFF); + bytes[2] = (byte)((endY >> 2) & 0xFF); + bytes[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); + bytes[4] = (byte)startX; + bytes[5] = gray; + return bytes; + } + public LayerLine() { } public LayerLine(ushort startY, ushort endY, ushort startX, byte gray) @@ -226,10 +296,17 @@ namespace UVtools.Core.FileFormats StartX = startX;*/ Gray = gray; } + + public override string ToString() + { + return $"{nameof(Gray)}: {Gray}, {nameof(StartY)}: {StartY}, {nameof(EndY)}: {EndY}, {nameof(StartX)}: {StartX}, {nameof(Length)}: {Length}"; + } } public sealed class PageBreak { + public static byte[] Bytes => new byte[] {0x0D, 0x0A}; + [FieldOrder(0)] public byte Line { get; set; } = 0x0D; [FieldOrder(1)] public byte Break { get; set; } = 0x0A; } @@ -460,6 +537,12 @@ namespace UVtools.Core.FileFormats set => base.LightPWM = (byte) (SlicerInfoSettings.LightPWM = value); } + public override string MachineName + { + get => HeaderSettings.PrinterModel; + set => base.MachineName = HeaderSettings.PrinterModel = value; + } + public override object[] Configs => new object[] { HeaderSettings, SlicerInfoSettings, FooterSettings }; #endregion @@ -473,8 +556,18 @@ namespace UVtools.Core.FileFormats { using var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write); + if (ResolutionX == 2560 && ResolutionY == 1620) + { + MachineName = "CL-60"; + } + else if (ResolutionX == 3840 && ResolutionY == 2400) + { + MachineName = "CL-89"; + } + + var pageBreak = PageBreak.Bytes; + Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); - var pageBreak = new PageBreak(); byte[][] previews = new byte[ThumbnailsOriginalSize.Length][]; for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) @@ -509,30 +602,103 @@ namespace UVtools.Core.FileFormats for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) { Helpers.SerializeWriteFileStream(outputFile, previews[i]); - Helpers.SerializeWriteFileStream(outputFile, pageBreak); + outputFile.WriteBytes(pageBreak); + //Helpers.SerializeWriteFileStream(outputFile, pageBreak); } Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); - var preLayers = new PreLayer[LayerCount]; - var layerDefs = new LayerDef[LayerCount]; + //var preLayers = new PreLayer[LayerCount]; + //var layerDefs = new LayerDef[LayerCount]; + //var layersStreams = new MemoryStream[LayerCount]; + for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) { - var layer = this[layerIndex]; - preLayers[layerIndex] = new(layer.NonZeroPixelCount); + //var layer = this[layerIndex]; + outputFile.WriteBytes(BitExtensions.ToBytesBigEndian(this[layerIndex].NonZeroPixelCount)); + //preLayers[layerIndex] = new(layer.NonZeroPixelCount); } - Helpers.SerializeWriteFileStream(outputFile, preLayers); - Helpers.SerializeWriteFileStream(outputFile, pageBreak); + //Helpers.SerializeWriteFileStream(outputFile, preLayers); + //Helpers.SerializeWriteFileStream(outputFile, pageBreak); + outputFile.WriteBytes(pageBreak); - Parallel.For(0, LayerCount, layerIndex => + var range = Enumerable.Range(0, (int) LayerCount); + + var layerBytes = new List<byte>[LayerCount]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) + { + progress.Token.ThrowIfCancellationRequested(); + + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + var span = mat.GetDataByteSpan(); + + layerBytes[layerIndex] = new(); + + uint lineCount = 0; + + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) + { + int y = layer.BoundingRectangle.Y; + int startY = -1; + byte lastColor = 0; + for (; y < layer.BoundingRectangle.Bottom; y++) + { + int pos = mat.GetPixelPos(x, y); + byte color = span[pos]; + + if (lastColor == color && color != 0) continue; + + if (startY >= 0) + { + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + lineCount++; + } + + startY = color == 0 ? -1 : y; + + lastColor = color; + } + + if (startY >= 0) + { + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + lineCount++; + } + } + + layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layer.NonZeroPixelCount, lineCount)); + layerBytes[layerIndex].AddRange(pageBreak); + + progress.LockAndIncrement(); + }); + + progress.Token.ThrowIfCancellationRequested(); + + foreach (var layerIndex in batch) + { + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + layerBytes[layerIndex] = null; + } + } + + + /*Parallel.For(0, LayerCount, + //new ParallelOptions{MaxDegreeOfParallelism = 1}, + layerIndex => { if (progress.Token.IsCancellationRequested) return; - List<LayerLine> layerLines = new(); + //List<LayerLine> layerLines = new(); var layer = this[layerIndex]; using var mat = layer.LayerMat; var span = mat.GetDataByteSpan(); + layerBytes[layerIndex] = new(); + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) { int y = layer.BoundingRectangle.Y; @@ -547,7 +713,9 @@ namespace UVtools.Core.FileFormats if (startY >= 0) { - layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + //Debug.WriteLine(layerLines[^1]); } startY = color == 0 ? -1 : y; @@ -557,11 +725,19 @@ namespace UVtools.Core.FileFormats if (startY >= 0) { - layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + //Debug.WriteLine(layerLines[^1]); } } - layerDefs[layerIndex] = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); + //layerDefs[layerIndex] = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); + //var layerDef = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); + //layersStreams[layerIndex] = new MemoryStream(); + //Helpers.Serializer.Serialize(layersStreams[layerIndex], layerDef); + + //layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layer.NonZeroPixelCount, (uint) layerBytes[layerIndex].Count)); + //layerBytes[layerIndex].AddRange(PageBreak.Bytes); progress.LockAndIncrement(); }); @@ -570,9 +746,14 @@ namespace UVtools.Core.FileFormats for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) { progress.Token.ThrowIfCancellationRequested(); - Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); + //Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); + //outputFile.WriteStream(layersStreams[layerIndex]); + //layersStreams[layerIndex].Dispose(); + outputFile.WriteBytes(LayerDef.GetHeaderBytes(this[layerIndex].NonZeroPixelCount, (uint)layerBytes[layerIndex].Count)); + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + outputFile.WriteBytes(pageBreak); progress++; - } + }*/ Helpers.SerializeWriteFileStream(outputFile, FooterSettings); @@ -587,6 +768,8 @@ namespace UVtools.Core.FileFormats HeaderSettings = Helpers.Deserialize<Header>(inputFile); HeaderSettings.Validate(); + Debug.WriteLine(HeaderSettings); + byte[][] previews = new byte[ThumbnailsOriginalSize.Length][]; for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) { @@ -618,45 +801,79 @@ namespace UVtools.Core.FileFormats SlicerInfoSettings = Helpers.Deserialize<SlicerInfo>(inputFile); + Debug.WriteLine(SlicerInfoSettings); LayerManager.Init(HeaderSettings.LayerCount); - progress.ItemCount = LayerCount; - var preLayers = new PreLayer[LayerCount]; + inputFile.Seek(LayerCount * 4 + 2, SeekOrigin.Current); // Skip pre layers + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); + /*var preLayers = new PreLayer[LayerCount]; for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) { progress.Token.ThrowIfCancellationRequested(); preLayers[layerIndex] = Helpers.Deserialize<PreLayer>(inputFile); - } + progress++; + }*/ - inputFile.Seek(2, SeekOrigin.Current); - var layerDefs = new LayerDef[LayerCount]; - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + //inputFile.Seek(2, SeekOrigin.Current); + var range = Enumerable.Range(0, (int)LayerCount); + + var linesBytes = new byte[LayerCount][]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) { progress.Token.ThrowIfCancellationRequested(); - layerDefs[layerIndex] = Helpers.Deserialize<LayerDef>(inputFile); - progress++; + + foreach (var layerIndex in batch) + { + inputFile.Seek(4, SeekOrigin.Current); + var lineCount = BitExtensions.ToUIntBigEndian(inputFile.ReadBytes(4)); + + linesBytes[layerIndex] = new byte[lineCount * 6]; + inputFile.ReadBytes(linesBytes[layerIndex]); + inputFile.Seek(2, SeekOrigin.Current); + + progress.Token.ThrowIfCancellationRequested(); + } + + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using var mat = EmguExtensions.InitMat(Resolution); + + for (int i = 0; i < linesBytes[layerIndex].Length; i++) + { + LayerLine line = new() + { + Coordinates = + { + [0] = linesBytes[layerIndex][i++], + [1] = linesBytes[layerIndex][i++], + [2] = linesBytes[layerIndex][i++], + [3] = linesBytes[layerIndex][i++], + [4] = linesBytes[layerIndex][i++] + }, + Gray = linesBytes[layerIndex][i] + }; + + CvInvoke.Line(mat, new Point(line.StartX, line.StartY), new Point(line.StartX, line.EndY), new MCvScalar(line.Gray)); + } + + linesBytes[layerIndex] = null; + + this[layerIndex] = new Layer((uint)layerIndex, mat, this); + + progress.LockAndIncrement(); + }); } - /*using TextWriter file = new StreamWriter("D:\\dump.txt"); + progress.Token.ThrowIfCancellationRequested(); + + /*var layerDefs = new LayerDef[LayerCount]; for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) { - file.WriteLine($"Layer: {layerIndex}"); - for (int lineIndex = 0; lineIndex < layerDefs[layerIndex].LineCount; lineIndex++) - { - file.WriteLine($"\tLine: {lineIndex}"); - var line = layerDefs[layerIndex].Lines[lineIndex]; - file.WriteLine($"\t\tb1: {line.Coordinates[0]}"); - file.WriteLine($"\t\tb2: {line.Coordinates[1]}"); - file.WriteLine($"\t\tb3: {line.Coordinates[2]}"); - file.WriteLine($"\t\tb4: {line.Coordinates[3]}"); - file.WriteLine($"\t\tb5: {line.Coordinates[4]}"); - file.WriteLine($"\t\tstartY: {line.StartY}"); - file.WriteLine($"\t\tendY: {line.EndY}"); - file.WriteLine($"\t\tstartX: {line.StartX}"); - file.WriteLine($"\t\tgray: {line.Gray}"); - } + progress.Token.ThrowIfCancellationRequested(); + layerDefs[layerIndex] = Helpers.Deserialize<LayerDef>(inputFile); + progress++; } - file.Close();*/ progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); Parallel.For(0, LayerCount, layerIndex => @@ -670,7 +887,7 @@ namespace UVtools.Core.FileFormats this[layerIndex] = new Layer((uint)layerIndex, mat, this); progress.LockAndIncrement(); - }); + });*/ FooterSettings = Helpers.Deserialize<Footer>(inputFile); FooterSettings.Validate(); @@ -694,8 +911,14 @@ namespace UVtools.Core.FileFormats FileFullPath = filePath; } + var offset = Helpers.Serializer.SizeOf(HeaderSettings); + foreach (var size in ThumbnailsOriginalSize) + { + offset += size.Area() * 2 + 2; // + page break + } + using var outputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Write); - outputFile.Seek(SlicerInfoAddress, SeekOrigin.Begin); + outputFile.Seek(offset, SeekOrigin.Begin); Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); } diff --git a/UVtools.Core/FileFormats/CXDLPv1File.cs b/UVtools.Core/FileFormats/CXDLPv1File.cs new file mode 100644 index 0000000..cf3a2a9 --- /dev/null +++ b/UVtools.Core/FileFormats/CXDLPv1File.cs @@ -0,0 +1,774 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BinarySerialization; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using MoreLinq; +using UVtools.Core.Extensions; +using UVtools.Core.Operations; + +namespace UVtools.Core.FileFormats +{ + public class CXDLPv1File : FileFormat + { + #region Constants + private const byte HEADER_SIZE = 9; // CXSW3DV2 + private const string HEADER_VALUE = "CXSW3DV2"; + #endregion + + #region Sub Classes + #region Header + public sealed class Header + { + /// <summary> + /// Gets the size of the header + /// </summary> + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint HeaderSize { get; set; } = HEADER_SIZE; + + /// <summary> + /// Gets the header name + /// </summary> + [FieldOrder(1)] + [FieldLength(HEADER_SIZE)] + [SerializeAs(SerializedType.TerminatedString)] + public string HeaderValue { get; set; } = HEADER_VALUE; + + /// <summary> + /// Gets the number of records in the layer table + /// </summary> + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public ushort LayerCount { get; set; } + + /// <summary> + /// Gets the printer resolution along X axis, in pixels. This information is critical to correctly decoding layer images. + /// </summary> + [FieldOrder(3)] + [FieldEndianness(Endianness.Big)] + public ushort ResolutionX { get; set; } + + /// <summary> + /// Gets the printer resolution along Y axis, in pixels. This information is critical to correctly decoding layer images. + /// </summary> + [FieldOrder(4)] + [FieldEndianness(Endianness.Big)] + public ushort ResolutionY { get; set; } + + public void Validate() + { + if (HeaderSize != HEADER_SIZE || HeaderValue != HEADER_VALUE) + { + throw new FileLoadException("Not a valid CXDLP file!"); + } + } + + public override string ToString() + { + return $"{nameof(HeaderSize)}: {HeaderSize}, {nameof(HeaderValue)}: {HeaderValue}, {nameof(LayerCount)}: {LayerCount}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}"; + } + } + + #endregion + + #region SlicerInfo + // Address: 363407 + public sealed class SlicerInfo + { + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint DisplayWidthDataSize { get; set; } = 20; + + [FieldOrder(1)] + [FieldLength(nameof(DisplayWidthDataSize))] + public byte[] DisplayWidthBytes { get; set; } + + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public uint DisplayHeightDataSize { get; set; } = 20; + + [FieldOrder(3)] + [FieldLength(nameof(DisplayHeightDataSize))] + public byte[] DisplayHeightBytes { get; set; } + + [FieldOrder(4)] + [FieldEndianness(Endianness.Big)] + public uint LayerHeightDataSize { get; set; } = 16; + + [FieldOrder(5)] + [FieldLength(nameof(LayerHeightDataSize))] + public byte[] LayerHeightBytes { get; set; } + + [FieldOrder(6)] + [FieldEndianness(Endianness.Big)] + public ushort LightOffDelay { get; set; } + + [FieldOrder(7)] + [FieldEndianness(Endianness.Big)] + public ushort ExposureTime { get; set; } + + [FieldOrder(8)] + [FieldEndianness(Endianness.Big)] + public ushort BottomExposureTime { get; set; } + + [FieldOrder(9)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLayers { get; set; } + + [FieldOrder(10)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLiftHeight { get; set; } + + [FieldOrder(11)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLiftSpeed { get; set; } + + [FieldOrder(12)] + [FieldEndianness(Endianness.Big)] + public ushort LiftHeight { get; set; } + + [FieldOrder(13)] + [FieldEndianness(Endianness.Big)] + public ushort LiftSpeed { get; set; } + + [FieldOrder(14)] + [FieldEndianness(Endianness.Big)] + public ushort RetractSpeed { get; set; } + + [FieldOrder(15)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLightPWM { get; set; } = 255; + + [FieldOrder(16)] + [FieldEndianness(Endianness.Big)] + public ushort LightPWM { get; set; } = 255; + + public override string ToString() + { + return $"{nameof(DisplayWidthDataSize)}: {DisplayWidthDataSize}, {nameof(DisplayWidthBytes)}: {DisplayWidthBytes}, {nameof(DisplayHeightDataSize)}: {DisplayHeightDataSize}, {nameof(DisplayHeightBytes)}: {DisplayHeightBytes}, {nameof(LayerHeightDataSize)}: {LayerHeightDataSize}, {nameof(LayerHeightBytes)}: {LayerHeightBytes}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(BottomLayers)}: {BottomLayers}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(LightPWM)}: {LightPWM}"; + } + } + #endregion + + #region Layer Def + + public sealed class PreLayer + { + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint Unknown { get; set; } + + public PreLayer() + { + } + + public PreLayer(uint unknown) + { + Unknown = unknown; + } + } + + public sealed class LayerDef + { + [FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint Unknown { get; set; } + [FieldOrder(1)] [FieldEndianness(Endianness.Big)] public uint LineCount { get; set; } + [FieldOrder(2)] [FieldCount(nameof(LineCount))] public LayerLine[] Lines { get; set; } + [FieldOrder(3)] public PageBreak PageBreak { get; set; } = new(); + + public static byte[] GetHeaderBytes(uint unknown, uint lineCount) + { + var bytes = new byte[8]; + BitExtensions.ToBytesBigEndian(unknown, bytes); + BitExtensions.ToBytesBigEndian(lineCount, bytes, 4); + return bytes; + } + + public LayerDef() { } + + public LayerDef(uint unknown, uint lineCount, LayerLine[] lines) + { + Unknown = unknown; + LineCount = lineCount; + Lines = lines; + } + } + + public sealed class LayerLine + { + public const byte CoordinateCount = 5; + [FieldOrder(0)] [FieldCount(CoordinateCount)] public byte[] Coordinates { get; set; } = new byte[CoordinateCount]; + //[FieldOrder(0)] [FieldEndianness(Endianness.Big)] [FieldBitLength(13)] public ushort StartY { get; set; } + //[FieldOrder(1)] [FieldEndianness(Endianness.Big)] [FieldBitLength(13)] public ushort EndY { get; set; } + //[FieldOrder(2)] [FieldEndianness(Endianness.Big)] [FieldBitLength(14)] public ushort StartX { get; set; } + [FieldOrder(1)] public byte Gray { get; set; } + + [Ignore] public ushort StartY => (ushort)((((Coordinates[0] << 8) + Coordinates[1]) >> 3) & 0x1FFF); // 13 bits + + [Ignore] public ushort EndY => (ushort)((((Coordinates[1] << 16) + (Coordinates[2] << 8) + Coordinates[3]) >> 6) & 0x1FFF); // 13 bits + + [Ignore] public ushort StartX => (ushort)(((Coordinates[3] << 8) + Coordinates[4]) & 0x3FFF); // 14 bits + [Ignore] public ushort Length => (ushort)(EndY - StartY); + + public static byte[] GetBytes(ushort startY, ushort endY, ushort startX, byte gray) + { + var bytes = new byte[CoordinateCount + 1]; + bytes[0] = (byte)((startY >> 5) & 0xFF); + bytes[1] = (byte)(((startY << 3) + (endY >> 10)) & 0xFF); + bytes[2] = (byte)((endY >> 2) & 0xFF); + bytes[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); + bytes[4] = (byte)startX; + bytes[5] = gray; + return bytes; + } + + public LayerLine() { } + + public LayerLine(ushort startY, ushort endY, ushort startX, byte gray) + { + Coordinates[0] = (byte)((startY >> 5) & 0xFF); + Coordinates[1] = (byte)(((startY << 3) + (endY >> 10)) & 0xFF); + Coordinates[2] = (byte)((endY >> 2) & 0xFF); + Coordinates[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); + Coordinates[4] = (byte)startX; + /*StartY = startY; + EndY = endY; + StartX = startX;*/ + Gray = gray; + } + + public override string ToString() + { + return $"{nameof(Gray)}: {Gray}, {nameof(StartY)}: {StartY}, {nameof(EndY)}: {EndY}, {nameof(StartX)}: {StartX}, {nameof(Length)}: {Length}"; + } + } + + public sealed class PageBreak + { + public static byte[] Bytes => new byte[] { 0x0D, 0x0A }; + + [FieldOrder(0)] public byte Line { get; set; } = 0x0D; + [FieldOrder(1)] public byte Break { get; set; } = 0x0A; + } + + #endregion + + #region Footer + public sealed class Footer + { + /// <summary> + /// Gets the size of the header + /// </summary> + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint FooterSize { get; set; } = HEADER_SIZE; + + /// <summary> + /// Gets the header name + /// </summary> + [FieldOrder(1)] + [FieldLength(HEADER_SIZE)] + [SerializeAs(SerializedType.TerminatedString)] + public string FooterValue { get; set; } = HEADER_VALUE; + + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public uint Unknown { get; set; } = 7; + + public void Validate() + { + if (FooterSize != HEADER_SIZE || FooterValue != HEADER_VALUE) + { + throw new FileLoadException("Not a valid CXDLP file!"); + } + } + } + #endregion + + #endregion + + #region Properties + + public Header HeaderSettings { get; protected internal set; } = new(); + public SlicerInfo SlicerInfoSettings { get; protected internal set; } = new(); + public Footer FooterSettings { get; protected internal set; } = new(); + + public override FileFormatType FileType => FileFormatType.Binary; + + public override FileExtension[] FileExtensions { get; } = { + new("v1.cxdlp", "Creality CXDLP v1"), + }; + + public override PrintParameterModifier[] PrintParameterModifiers { get; } = + { + PrintParameterModifier.BottomLayerCount, + PrintParameterModifier.BottomExposureSeconds, + PrintParameterModifier.ExposureSeconds, + + PrintParameterModifier.BottomLiftHeight, + PrintParameterModifier.BottomLiftSpeed, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.RetractSpeed, + PrintParameterModifier.LightOffDelay, + + PrintParameterModifier.BottomLightPWM, + PrintParameterModifier.LightPWM, + }; + + public override Size[] ThumbnailsOriginalSize { get; } = + { + new(116, 116), + new(290, 290), + new(290, 290) + }; + + public override uint ResolutionX + { + get => HeaderSettings.ResolutionX; + set + { + HeaderSettings.ResolutionX = (ushort)value; + RaisePropertyChanged(); + } + } + + public override uint ResolutionY + { + get => HeaderSettings.ResolutionY; + set + { + HeaderSettings.ResolutionY = (ushort)value; + RaisePropertyChanged(); + } + } + + public override float DisplayWidth + { + get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.DisplayWidthBytes.Where(b => b != 0).ToArray())); + set + { + string str = Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); + SlicerInfoSettings.DisplayWidthDataSize = (uint)(str.Length * 2); + var data = new byte[SlicerInfoSettings.DisplayWidthDataSize]; + for (var i = 0; i < str.Length; i++) + { + data[i * 2 + 1] = System.Convert.ToByte(str[i]); + } + + SlicerInfoSettings.DisplayWidthBytes = data; + RaisePropertyChanged(); + } + } + + public override float DisplayHeight + { + get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.DisplayHeightBytes.Where(b => b != 0).ToArray())); + set + { + string str = Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); + SlicerInfoSettings.DisplayHeightDataSize = (uint)(str.Length * 2); + var data = new byte[SlicerInfoSettings.DisplayHeightDataSize]; + for (var i = 0; i < str.Length; i++) + { + data[i * 2 + 1] = System.Convert.ToByte(str[i]); + } + + SlicerInfoSettings.DisplayHeightBytes = data; + RaisePropertyChanged(); + } + } + + public override byte AntiAliasing + { + get => 8; + set { } + } + + public override float LayerHeight + { + get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.LayerHeightBytes.Where(b => b != 0).ToArray())); + set + { + string str = Layer.RoundHeight(value).ToString(CultureInfo.InvariantCulture); + SlicerInfoSettings.LayerHeightDataSize = (uint)(str.Length * 2); + var data = new byte[SlicerInfoSettings.LayerHeightDataSize]; + for (var i = 0; i < str.Length; i++) + { + data[i * 2 + 1] = System.Convert.ToByte(str[i]); + } + + SlicerInfoSettings.LayerHeightBytes = data; + RaisePropertyChanged(); + } + } + + public override uint LayerCount + { + get => base.LayerCount; + set => base.LayerCount = HeaderSettings.LayerCount = (ushort)base.LayerCount; + } + + public override ushort BottomLayerCount + { + get => SlicerInfoSettings.BottomLayers; + set => base.BottomLayerCount = SlicerInfoSettings.BottomLayers = value; + } + + public override float BottomExposureTime + { + get => SlicerInfoSettings.BottomExposureTime; + set => base.BottomExposureTime = SlicerInfoSettings.BottomExposureTime = (ushort)value; + } + + public override float ExposureTime + { + get => SlicerInfoSettings.ExposureTime; + set => base.ExposureTime = SlicerInfoSettings.ExposureTime = (ushort)value; + } + + public override float BottomLiftHeight + { + get => SlicerInfoSettings.BottomLiftHeight; + set => base.BottomLiftHeight = SlicerInfoSettings.BottomLiftHeight = (ushort)value; + } + + public override float LiftHeight + { + get => SlicerInfoSettings.LiftHeight; + set => base.LiftHeight = SlicerInfoSettings.LiftHeight = (ushort)value; + } + + public override float BottomLiftSpeed + { + get => SlicerInfoSettings.BottomLiftSpeed; + set => base.BottomLiftSpeed = SlicerInfoSettings.BottomLiftSpeed = (ushort)value; + } + + public override float LiftSpeed + { + get => SlicerInfoSettings.LiftSpeed; + set => base.LiftSpeed = SlicerInfoSettings.LiftSpeed = (ushort)value; + } + + public override float RetractSpeed + { + get => SlicerInfoSettings.RetractSpeed; + set => base.RetractSpeed = SlicerInfoSettings.RetractSpeed = (ushort)value; + } + + public override float BottomLightOffDelay => SlicerInfoSettings.LightOffDelay; + + public override float LightOffDelay + { + get => SlicerInfoSettings.LightOffDelay; + set => base.LightOffDelay = SlicerInfoSettings.LightOffDelay = (ushort)value; + } + + public override byte BottomLightPWM + { + get => (byte)SlicerInfoSettings.BottomLightPWM; + set => base.BottomLightPWM = (byte)(SlicerInfoSettings.BottomLightPWM = value); + } + + public override byte LightPWM + { + get => (byte)SlicerInfoSettings.LightPWM; + set => base.LightPWM = (byte)(SlicerInfoSettings.LightPWM = value); + } + + public override object[] Configs => new object[] { HeaderSettings, SlicerInfoSettings, FooterSettings }; + + #endregion + + #region Constructors + #endregion + + #region Methods + + protected override void EncodeInternally(string fileFullPath, OperationProgress progress) + { + using var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write); + + if (ResolutionX == 2560 && ResolutionY == 1620) + { + MachineName = "CL-60"; + } + else if (ResolutionX == 3840 && ResolutionY == 2400) + { + MachineName = "CL-89"; + } + + var pageBreak = PageBreak.Bytes; + + Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); + + byte[][] previews = new byte[ThumbnailsOriginalSize.Length][]; + for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) + { + previews[i] = new byte[ThumbnailsOriginalSize[i].Area() * 2]; + } + // Previews + Parallel.For(0, previews.Length, previewIndex => + { + if (progress.Token.IsCancellationRequested) return; + if (Thumbnails[previewIndex] is null) return; + var span = Thumbnails[previewIndex].GetDataByteSpan(); + int index = 0; + for (int i = 0; i < span.Length; i += 3) + { + byte b = span[i]; + byte g = span[i + 1]; + byte r = span[i + 2]; + + ushort rgb15 = (ushort)(((r >> 3) << 11) | ((g >> 2) << 5) | ((b >> 3) << 0)); + + previews[previewIndex][index++] = (byte)(rgb15 >> 8); + previews[previewIndex][index++] = (byte)(rgb15 & 0xff); + } + + if (index != previews[previewIndex].Length) + { + throw new FileLoadException($"Preview encode incomplete encode, expected: {previews[previewIndex].Length}, encoded: {index}"); + } + }); + + for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) + { + Helpers.SerializeWriteFileStream(outputFile, previews[i]); + outputFile.WriteBytes(pageBreak); + } + Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); + + progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); + + + for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + outputFile.WriteBytes(BitExtensions.ToBytesBigEndian(this[layerIndex].NonZeroPixelCount)); + } + outputFile.WriteBytes(pageBreak); + + var range = Enumerable.Range(0, (int)LayerCount); + + var layerBytes = new List<byte>[LayerCount]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) + { + progress.Token.ThrowIfCancellationRequested(); + + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + var span = mat.GetDataByteSpan(); + + layerBytes[layerIndex] = new(); + + uint lineCount = 0; + + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) + { + int y = layer.BoundingRectangle.Y; + int startY = -1; + byte lastColor = 0; + for (; y < layer.BoundingRectangle.Bottom; y++) + { + int pos = mat.GetPixelPos(x, y); + byte color = span[pos]; + + if (lastColor == color && color != 0) continue; + + if (startY >= 0) + { + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + lineCount++; + } + + startY = color == 0 ? -1 : y; + + lastColor = color; + } + + if (startY >= 0) + { + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + lineCount++; + } + } + + layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layer.NonZeroPixelCount, lineCount)); + layerBytes[layerIndex].AddRange(pageBreak); + + progress.LockAndIncrement(); + }); + + progress.Token.ThrowIfCancellationRequested(); + + foreach (var layerIndex in batch) + { + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + layerBytes[layerIndex] = null; + } + } + + + Helpers.SerializeWriteFileStream(outputFile, FooterSettings); + + Debug.WriteLine("Encode Results:"); + Debug.WriteLine(HeaderSettings); + Debug.WriteLine(SlicerInfoSettings); + Debug.WriteLine("-End-"); + } + + protected override void DecodeInternally(string fileFullPath, OperationProgress progress) + { + using var inputFile = new FileStream(fileFullPath, FileMode.Open, FileAccess.Read); + HeaderSettings = Helpers.Deserialize<Header>(inputFile); + HeaderSettings.Validate(); + + Debug.WriteLine(HeaderSettings); + + byte[][] previews = new byte[ThumbnailsOriginalSize.Length][]; + for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) + { + previews[i] = new byte[ThumbnailsOriginalSize[i].Area() * 2]; + inputFile.ReadBytes(previews[i]); + inputFile.Seek(2, SeekOrigin.Current); + } + + Parallel.For(0, previews.Length, previewIndex => + { + var mat = new Mat(ThumbnailsOriginalSize[previewIndex], DepthType.Cv8U, 3); + var span = mat.GetDataByteSpan(); + + int spanIndex = 0; + for (int i = 0; i < previews[previewIndex].Length; i += 2) + { + ushort rgb15 = (ushort)((ushort)(previews[previewIndex][i + 0] << 8) | previews[previewIndex][i + 1]); + byte r = (byte)((rgb15 >> 11) << 3); + byte g = (byte)((rgb15 >> 5) << 2); + byte b = (byte)((rgb15 >> 0) << 3); + + span[spanIndex++] = b; + span[spanIndex++] = g; + span[spanIndex++] = r; + } + + Thumbnails[previewIndex] = mat; + }); + + + SlicerInfoSettings = Helpers.Deserialize<SlicerInfo>(inputFile); + Debug.WriteLine(SlicerInfoSettings); + + LayerManager.Init(HeaderSettings.LayerCount); + inputFile.Seek(LayerCount * 4 + 2, SeekOrigin.Current); // Skip pre layers + + + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); + + var range = Enumerable.Range(0, (int)LayerCount); + + var linesBytes = new byte[LayerCount][]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) + { + progress.Token.ThrowIfCancellationRequested(); + + foreach (var layerIndex in batch) + { + inputFile.Seek(4, SeekOrigin.Current); + var lineCount = BitExtensions.ToUIntBigEndian(inputFile.ReadBytes(4)); + + linesBytes[layerIndex] = new byte[lineCount * 6]; + inputFile.ReadBytes(linesBytes[layerIndex]); + inputFile.Seek(2, SeekOrigin.Current); + + progress.Token.ThrowIfCancellationRequested(); + } + + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using var mat = EmguExtensions.InitMat(Resolution); + + for (int i = 0; i < linesBytes[layerIndex].Length; i++) + { + LayerLine line = new() + { + Coordinates = + { + [0] = linesBytes[layerIndex][i++], + [1] = linesBytes[layerIndex][i++], + [2] = linesBytes[layerIndex][i++], + [3] = linesBytes[layerIndex][i++], + [4] = linesBytes[layerIndex][i++] + }, + Gray = linesBytes[layerIndex][i] + }; + + CvInvoke.Line(mat, new Point(line.StartX, line.StartY), new Point(line.StartX, line.EndY), new MCvScalar(line.Gray)); + } + + linesBytes[layerIndex] = null; + + this[layerIndex] = new Layer((uint)layerIndex, mat, this); + + progress.LockAndIncrement(); + }); + } + + progress.Token.ThrowIfCancellationRequested(); + + FooterSettings = Helpers.Deserialize<Footer>(inputFile); + FooterSettings.Validate(); + } + + 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; + } + + var offset = Helpers.Serializer.SizeOf(HeaderSettings); + foreach (var size in ThumbnailsOriginalSize) + { + offset += size.Area() * 2 + 2; // + page break + } + + using var outputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Write); + outputFile.Seek(offset, SeekOrigin.Begin); + Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); + } + + #endregion + } +} diff --git a/UVtools.Core/FileFormats/ChituboxFile.cs b/UVtools.Core/FileFormats/ChituboxFile.cs index 470bbb9..4e0cbc6 100644 --- a/UVtools.Core/FileFormats/ChituboxFile.cs +++ b/UVtools.Core/FileFormats/ChituboxFile.cs @@ -380,6 +380,13 @@ namespace UVtools.Core.FileFormats var image = new Mat(new Size((int) ResolutionX, (int) ResolutionY), DepthType.Cv8U, 3); var span = image.GetBytePointer(); + /*var previewSize = ResolutionX * ResolutionY * 2; + if (previewSize != rawImageData.Length) + { + throw new FileLoadException($"Thumbnail out of size, expecting {previewSize} bytes, got {rawImageData.Length}"); + return null; + }*/ + int pixel = 0; for (int n = 0; n < rawImageData.Length; n++) { diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index c667d84..322073b 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -214,6 +214,7 @@ namespace UVtools.Core.FileFormats new ZCodexFile(), // zcodex new MDLPFile(), // MKS v1 new GR1File(), // GR1 Workshop + //new CXDLPv1File(), // Creality Box v1 new CXDLPFile(), // Creality Box new LGSFile(), // LGS, LGS30 new VDAFile(), // VDA diff --git a/UVtools.Core/FileFormats/GR1File.cs b/UVtools.Core/FileFormats/GR1File.cs index da26561..b9b64ec 100644 --- a/UVtools.Core/FileFormats/GR1File.cs +++ b/UVtools.Core/FileFormats/GR1File.cs @@ -6,8 +6,6 @@ * of this license document, but changing it is not allowed. */ -// https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc - using System; using System.Collections.Generic; using System.Diagnostics; @@ -20,6 +18,7 @@ using System.Threading.Tasks; using BinarySerialization; using Emgu.CV; using Emgu.CV.CvEnum; +using MoreLinq.Extensions; using UVtools.Core.Extensions; using UVtools.Core.Operations; @@ -111,6 +110,15 @@ namespace UVtools.Core.FileFormats //[FieldOrder(3)] [FieldEndianness(Endianness.Big)] public byte Gray { get; set; } + public static byte[] GetBytes(ushort startY, ushort endY, ushort startX) + { + var bytes = new byte[6]; + BitExtensions.ToBytesBigEndian(startY, bytes); + BitExtensions.ToBytesBigEndian(endY, bytes, 2); + BitExtensions.ToBytesBigEndian(startX, bytes, 4); + return bytes; + } + public LayerLine() { } @@ -124,11 +132,13 @@ namespace UVtools.Core.FileFormats public sealed class PageBreak { + public static byte[] Bytes => new byte[] {0x0D, 0x0A}; [FieldOrder(0)] [FieldEndianness(Endianness.Big)] public byte Line { get; set; } = 0x0D; [FieldOrder(1)] [FieldEndianness(Endianness.Big)] public byte Break { get; set; } = 0x0A; } #endregion + #endregion #region Properties @@ -332,7 +342,7 @@ namespace UVtools.Core.FileFormats protected override void EncodeInternally(string fileFullPath, OperationProgress progress) { using var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write); - var pageBreak = new PageBreak(); + var pageBreak = PageBreak.Bytes; Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); @@ -369,59 +379,74 @@ namespace UVtools.Core.FileFormats for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) { Helpers.SerializeWriteFileStream(outputFile, previews[i]); - Helpers.SerializeWriteFileStream(outputFile, pageBreak); + outputFile.WriteBytes(pageBreak); } Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); - var layerDefs = new LayerDef[LayerCount]; - Parallel.For(0, LayerCount, layerIndex => + + var range = Enumerable.Range(0, (int)LayerCount); + + var layerBytes = new List<byte>[LayerCount]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) { - if (progress.Token.IsCancellationRequested) return; - List<LayerLine> layerLines = new(); - var layer = this[layerIndex]; - using var mat = layer.LayerMat; - var span = mat.GetDataByteSpan(); + progress.Token.ThrowIfCancellationRequested(); - for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) + Parallel.ForEach(batch, layerIndex => { - int y = layer.BoundingRectangle.Y; - int startY = -1; - for (; y < layer.BoundingRectangle.Bottom; y++) + if (progress.Token.IsCancellationRequested) return; + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + var span = mat.GetDataByteSpan(); + + layerBytes[layerIndex] = new(); + + uint lineCount = 0; + + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) { - int pos = mat.GetPixelPos(x, y); - if (span[pos] < 128) // Black pixel + int y = layer.BoundingRectangle.Y; + int startY = -1; + for (; y < layer.BoundingRectangle.Bottom; y++) { - if(startY == -1) continue; // Keep ignoring - layerLines.Add(new LayerLine((ushort) startY, (ushort) (y-1), (ushort) x)); - startY = -1; + int pos = mat.GetPixelPos(x, y); + if (span[pos] < 128) // Black pixel + { + if (startY == -1) continue; // Keep ignoring + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x)); + startY = -1; + lineCount++; + } + else + { + if (startY >= 0) continue; // Keep sum + startY = y; + } } - else // White pixel + + if (startY >= 0) { - if (startY >= 0) continue; // Keep sum - startY = y; + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x)); + lineCount++; } } - if (startY >= 0) - { - layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort) x)); - } - } + layerBytes[layerIndex].InsertRange(0, BitExtensions.ToBytesBigEndian(lineCount)); + layerBytes[layerIndex].AddRange(pageBreak); - layerDefs[layerIndex] = new LayerDef((uint) layerLines.Count, layerLines.ToArray()); + progress.LockAndIncrement(); + }); - progress.LockAndIncrement(); - }); - - progress.Reset(OperationProgress.StatusWritingFile, LayerCount); - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { progress.Token.ThrowIfCancellationRequested(); - Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); - progress++; + + foreach (var layerIndex in batch) + { + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + layerBytes[layerIndex] = null; + } } + Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); Debug.WriteLine("Encode Results:"); @@ -473,28 +498,49 @@ namespace UVtools.Core.FileFormats SlicerInfoSettings = Helpers.Deserialize<SlicerInfo>(inputFile); LayerManager.Init(SlicerInfoSettings.LayerCount); - progress.ItemCount = LayerCount; - LayerDef[] layerDefs = new LayerDef[LayerCount]; - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - progress.Token.ThrowIfCancellationRequested(); - layerDefs[layerIndex] = Helpers.Deserialize<LayerDef>(inputFile); - progress++; - } + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); - Parallel.For(0, LayerCount, layerIndex => + + var range = Enumerable.Range(0, (int)LayerCount); + + var linesBytes = new byte[LayerCount][]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) { - if (progress.Token.IsCancellationRequested) return; - using var mat = EmguExtensions.InitMat(Resolution); - foreach (var line in layerDefs[layerIndex].Lines) + progress.Token.ThrowIfCancellationRequested(); + + foreach (var layerIndex in batch) { - CvInvoke.Line(mat, new Point(line.StartX, line.StartY), new Point(line.StartX, line.EndY), EmguExtensions.WhiteColor); + var lineCount = BitExtensions.ToUIntBigEndian(inputFile.ReadBytes(4)); + + linesBytes[layerIndex] = new byte[lineCount * 6]; + inputFile.ReadBytes(linesBytes[layerIndex]); + inputFile.Seek(2, SeekOrigin.Current); + + progress.Token.ThrowIfCancellationRequested(); } - this[layerIndex] = new Layer((uint) layerIndex, mat, this); - progress.LockAndIncrement(); - }); + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using var mat = EmguExtensions.InitMat(Resolution); + + for (int i = 0; i < linesBytes[layerIndex].Length; i++) + { + var startY = BitExtensions.ToUShortBigEndian(linesBytes[layerIndex][i++], linesBytes[layerIndex][i++]); + var endY = BitExtensions.ToUShortBigEndian(linesBytes[layerIndex][i++], linesBytes[layerIndex][i++]); + var startX = BitExtensions.ToUShortBigEndian(linesBytes[layerIndex][i++], linesBytes[layerIndex][i]); + + CvInvoke.Line(mat, new Point(startX, startY), new Point(startX, endY), EmguExtensions.WhiteColor); + } + + linesBytes[layerIndex] = null; + + this[layerIndex] = new Layer((uint)layerIndex, mat, this); + + progress.LockAndIncrement(); + }); + } HeaderSettings = Helpers.Deserialize<Header>(inputFile); if (HeaderSettings.HeaderValue != Header.HEADER_VALUE) diff --git a/UVtools.Core/FileFormats/MDLPFile.cs b/UVtools.Core/FileFormats/MDLPFile.cs index 0b86e11..21b61cd 100644 --- a/UVtools.Core/FileFormats/MDLPFile.cs +++ b/UVtools.Core/FileFormats/MDLPFile.cs @@ -6,8 +6,6 @@ * of this license document, but changing it is not allowed. */ -// https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc - using System; using System.Collections.Generic; using System.Diagnostics; @@ -20,6 +18,7 @@ using System.Threading.Tasks; using BinarySerialization; using Emgu.CV; using Emgu.CV.CvEnum; +using MoreLinq; using UVtools.Core.Extensions; using UVtools.Core.Operations; @@ -113,6 +112,15 @@ namespace UVtools.Core.FileFormats [FieldOrder(1)] [FieldEndianness(Endianness.Big)] public ushort EndY { get; set; } [FieldOrder(2)] [FieldEndianness(Endianness.Big)] public ushort StartX { get; set; } + public static byte[] GetBytes(ushort StartY, ushort EndY, ushort StartX) + { + var bytes = new byte[6]; + BitExtensions.ToBytesBigEndian(StartY, bytes); + BitExtensions.ToBytesBigEndian(EndY, bytes, 2); + BitExtensions.ToBytesBigEndian(StartX, bytes, 4); + return bytes; + } + public LayerLine() { } @@ -127,11 +135,13 @@ namespace UVtools.Core.FileFormats public sealed class PageBreak { + public static byte[] Bytes => new byte[] {0x0D, 0x0A}; [FieldOrder(0)] [FieldEndianness(Endianness.Big)] public byte Line { get; set; } = 0x0D; [FieldOrder(1)] [FieldEndianness(Endianness.Big)] public byte Break { get; set; } = 0x0A; } #endregion + #endregion #region Properties @@ -298,7 +308,7 @@ namespace UVtools.Core.FileFormats protected override void EncodeInternally(string fileFullPath, OperationProgress progress) { using var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write); - var pageBreak = new PageBreak(); + var pageBreak = PageBreak.Bytes; Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); @@ -335,57 +345,70 @@ namespace UVtools.Core.FileFormats for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) { Helpers.SerializeWriteFileStream(outputFile, previews[i]); - Helpers.SerializeWriteFileStream(outputFile, pageBreak); + outputFile.WriteBytes(pageBreak); } Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); - var layerDefs = new LayerDef[LayerCount]; - Parallel.For(0, LayerCount, layerIndex => + var range = Enumerable.Range(0, (int)LayerCount); + + var layerBytes = new List<byte>[LayerCount]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) { - if (progress.Token.IsCancellationRequested) return; - List<LayerLine> layerLines = new(); - var layer = this[layerIndex]; - using var mat = layer.LayerMat; - var span = mat.GetDataByteSpan(); + progress.Token.ThrowIfCancellationRequested(); - for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) + Parallel.ForEach(batch, layerIndex => { - int y = layer.BoundingRectangle.Y; - int startY = -1; - for (; y < layer.BoundingRectangle.Bottom; y++) + if (progress.Token.IsCancellationRequested) return; + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + var span = mat.GetDataByteSpan(); + + layerBytes[layerIndex] = new(); + + uint lineCount = 0; + + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) { - int pos = mat.GetPixelPos(x, y); - if (span[pos] < 128) // Black pixel + int y = layer.BoundingRectangle.Y; + int startY = -1; + for (; y < layer.BoundingRectangle.Bottom; y++) { - if(startY == -1) continue; // Keep ignoring - layerLines.Add(new LayerLine((ushort) startY, (ushort) (y-1), (ushort) x)); - startY = -1; + int pos = mat.GetPixelPos(x, y); + if (span[pos] < 128) // Black pixel + { + if (startY == -1) continue; // Keep ignoring + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x)); + startY = -1; + lineCount++; + } + else + { + if (startY >= 0) continue; // Keep sum + startY = y; + } } - else // White pixel + + if (startY >= 0) { - if (startY >= 0) continue; // Keep sum - startY = y; + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x)); + lineCount++; } } - if (startY >= 0) - { - layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort) x)); - } - } - - layerDefs[layerIndex] = new LayerDef((uint) layerLines.Count, layerLines.ToArray()); + layerBytes[layerIndex].InsertRange(0, BitExtensions.ToBytesBigEndian(lineCount)); + layerBytes[layerIndex].AddRange(pageBreak); - progress.LockAndIncrement(); - }); + progress.LockAndIncrement(); + }); - progress.Reset(OperationProgress.StatusWritingFile, LayerCount); - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { progress.Token.ThrowIfCancellationRequested(); - Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); - progress++; + + foreach (var layerIndex in batch) + { + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + layerBytes[layerIndex] = null; + } } Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); @@ -438,28 +461,49 @@ namespace UVtools.Core.FileFormats SlicerInfoSettings = Helpers.Deserialize<SlicerInfo>(inputFile); LayerManager.Init(SlicerInfoSettings.LayerCount); - progress.ItemCount = LayerCount; - LayerDef[] layerDefs = new LayerDef[LayerCount]; - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - progress.Token.ThrowIfCancellationRequested(); - layerDefs[layerIndex] = Helpers.Deserialize<LayerDef>(inputFile); - progress++; - } + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); - Parallel.For(0, LayerCount, layerIndex => + + var range = Enumerable.Range(0, (int)LayerCount); + + var linesBytes = new byte[LayerCount][]; + foreach (var batch in range.Batch(Environment.ProcessorCount * 10)) { - if (progress.Token.IsCancellationRequested) return; - using var mat = EmguExtensions.InitMat(Resolution); - foreach (var line in layerDefs[layerIndex].Lines) + progress.Token.ThrowIfCancellationRequested(); + + foreach (var layerIndex in batch) { - CvInvoke.Line(mat, new Point(line.StartX, line.StartY), new Point(line.StartX, line.EndY), EmguExtensions.WhiteColor); + var lineCount = BitExtensions.ToUIntBigEndian(inputFile.ReadBytes(4)); + + linesBytes[layerIndex] = new byte[lineCount * 6]; + inputFile.ReadBytes(linesBytes[layerIndex]); + inputFile.Seek(2, SeekOrigin.Current); + + progress.Token.ThrowIfCancellationRequested(); } - this[layerIndex] = new Layer((uint) layerIndex, mat, this); - progress.LockAndIncrement(); - }); + Parallel.ForEach(batch, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using var mat = EmguExtensions.InitMat(Resolution); + + for (int i = 0; i < linesBytes[layerIndex].Length; i++) + { + var startY = BitExtensions.ToUShortBigEndian(linesBytes[layerIndex][i++], linesBytes[layerIndex][i++]); + var endY = BitExtensions.ToUShortBigEndian(linesBytes[layerIndex][i++], linesBytes[layerIndex][i++]); + var startX = BitExtensions.ToUShortBigEndian(linesBytes[layerIndex][i++], linesBytes[layerIndex][i]); + + CvInvoke.Line(mat, new Point(startX, startY), new Point(startX, endY), EmguExtensions.WhiteColor); + } + + linesBytes[layerIndex] = null; + + this[layerIndex] = new Layer((uint)layerIndex, mat, this); + + progress.LockAndIncrement(); + }); + } HeaderSettings = Helpers.Deserialize<Header>(inputFile); HeaderSettings.Validate(); diff --git a/UVtools.Core/Helpers.cs b/UVtools.Core/Helpers.cs index b4b1dab..53c8b90 100644 --- a/UVtools.Core/Helpers.cs +++ b/UVtools.Core/Helpers.cs @@ -41,19 +41,17 @@ namespace UVtools.Core public static uint SerializeWriteFileStream(FileStream fs, object value, int offset = 0) { - using MemoryStream stream = Serialize(value); + using var stream = Serialize(value); return fs.WriteStream(stream, offset); } public static T JsonDeserializeObject<T>(Stream stream) { - using (TextReader tr = new StreamReader(stream)) - { - return JsonConvert.DeserializeObject<T>(tr.ReadToEnd()); - } + using TextReader tr = new StreamReader(stream); + return JsonConvert.DeserializeObject<T>(tr.ReadToEnd()); } - public static SHA1CryptoServiceProvider SHA1 { get; } = new SHA1CryptoServiceProvider(); + public static SHA1CryptoServiceProvider SHA1 { get; } = new(); public static string ComputeSHA1Hash(byte[] input) { return Convert.ToBase64String(SHA1.ComputeHash(input)); diff --git a/UVtools.Core/Layer/Layer.cs b/UVtools.Core/Layer/Layer.cs index fb58af7..c7aca95 100644 --- a/UVtools.Core/Layer/Layer.cs +++ b/UVtools.Core/Layer/Layer.cs @@ -625,6 +625,7 @@ namespace UVtools.Core return changed; } + /* /// <summary> /// Gets all islands start pixel location for this layer /// https://www.geeksforgeeks.org/find-number-of-islands/ @@ -651,17 +652,6 @@ namespace UVtools.Core var previousLayerImage = PreviousLayer()?.LayerMat; var previousBytes = previousLayerImage?.GetBytes(); - - /*var nextLayerImage = NextLayer()?.Image; - byte[] nextBytes = null; - if (!ReferenceEquals(nextLayerImage, null)) - { - if (nextLayerImage.TryGetSinglePixelSpan(out var nextPixelSpan)) - { - nextBytes = MemoryMarshal.AsBytes(nextPixelSpan).ToArray(); - } - }*/ - // Make a bool array to // mark visited cells. // Initially all cells @@ -688,12 +678,6 @@ namespace UVtools.Core { pixelIndex = y * mat.Width + x; - /*if (bytes[pixelIndex] == 0 && previousBytes?[pixelIndex] == byte.MaxValue && - nextBytes?[pixelIndex] == byte.MaxValue) - { - result.Add(new LayerIssue(this, LayerIssue.IssueType.HoleSandwich, new []{new Point(x, y)})); - }*/ - if (bytes[pixelIndex] > minPixel && !visited[x, y]) { // If a cell with value 1 is not @@ -798,7 +782,7 @@ namespace UVtools.Core pixels.Clear(); return result; - } + }*/ public Layer Clone() { diff --git a/UVtools.Core/Layer/LayerIssue.cs b/UVtools.Core/Layer/LayerIssue.cs index 812479f..8b46e51 100644 --- a/UVtools.Core/Layer/LayerIssue.cs +++ b/UVtools.Core/Layer/LayerIssue.cs @@ -11,7 +11,6 @@ using System.Collections; using System.Collections.Generic; using System.Drawing; using System.Linq; -using System.Runtime.InteropServices.ComTypes; using UVtools.Core.Objects; namespace UVtools.Core @@ -106,6 +105,12 @@ namespace UVtools.Core { Enabled = enabled; } + + public IslandDetectionConfiguration Clone() + { + var clone = MemberwiseClone() as IslandDetectionConfiguration; + return clone; + } } /// <summary> @@ -307,7 +312,7 @@ namespace UVtools.Core return (uint)(BoundingRectangle.Width * BoundingRectangle.Height); } - if (ReferenceEquals(Pixels, null)) return 0; + if (Pixels is null) return 0; return (uint)Pixels.Length; } } @@ -315,9 +320,9 @@ namespace UVtools.Core /// <summary> /// Check if this issue have a valid start point to show /// </summary> - public bool HaveValidPoint => !ReferenceEquals(Pixels, null) && Pixels.Length > 0; + public bool HaveValidPoint => Pixels?.Length > 0; - public LayerIssue(Layer layer, IssueType type, Point[] pixels = null, Rectangle boundingRectangle = new Rectangle()) + public LayerIssue(Layer layer, IssueType type, Point[] pixels = null, Rectangle boundingRectangle = default) { Layer = layer; Type = type; @@ -346,19 +351,19 @@ namespace UVtools.Core public bool Equals(LayerIssue other) { - if (ReferenceEquals(null, other)) return false; + if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Layer.Index == other.Layer.Index && Type == other.Type && PixelsCount == other.PixelsCount - && !(Pixels is null) && !(other.Pixels is null) && Pixels.SequenceEqual(other.Pixels) + && Pixels is not null && other.Pixels is not null && Pixels.SequenceEqual(other.Pixels) //&& BoundingRectangle.Equals(other.BoundingRectangle) ; } public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((LayerIssue) obj); diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs index 054807e..b9585c3 100644 --- a/UVtools.Core/Layer/LayerManager.cs +++ b/UVtools.Core/Layer/LayerManager.cs @@ -10,7 +10,6 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; @@ -728,7 +727,7 @@ namespace UVtools.Core Mat[] cachedLayers = new Mat[LayerCount]; const uint cacheCount = 300; - bool IsIgnored(LayerIssue issue) => !(ignoredIssues is null) && ignoredIssues.Count > 0 && ignoredIssues.Contains(issue); + bool IsIgnored(LayerIssue issue) => ignoredIssues is not null && ignoredIssues.Count > 0 && ignoredIssues.Contains(issue); bool AddIssue(LayerIssue issue) { if (IsIgnored(issue)) return false; @@ -931,7 +930,7 @@ namespace UVtools.Core var result = new ConcurrentBag<LayerIssue>(); var layerHollowAreas = new ConcurrentDictionary<uint, List<LayerHollowArea>>(); - bool IsIgnored(LayerIssue issue) => !(ignoredIssues is null) && ignoredIssues.Count > 0 && ignoredIssues.Contains(issue); + bool IsIgnored(LayerIssue issue) => ignoredIssues is not null && ignoredIssues.Count > 0 && ignoredIssues.Contains(issue); bool AddIssue(LayerIssue issue) { @@ -1115,9 +1114,10 @@ namespace UVtools.Core islandImage = image; } - using (Mat labels = new()) - using (Mat stats = new()) - using (Mat centroids = new()) + using ( + Mat labels = new(), + stats = new(), + centroids = new()) { var numLabels = CvInvoke.ConnectedComponentsWithStats(islandImage, labels, stats, centroids, @@ -1620,9 +1620,11 @@ namespace UVtools.Core continue; } - switch (operationDrawing.BrushShape) + mat.DrawPolygon((byte)operationDrawing.BrushShape, operationDrawing.BrushSize / 2, operationDrawing.Location, + new MCvScalar(operationDrawing.Brightness), operationDrawing.RotationAngle, operationDrawing.Thickness, operationDrawing.LineType); + /*switch (operationDrawing.BrushShape) { - case PixelDrawing.BrushShapeType.Rectangle: + case PixelDrawing.BrushShapeType.Square: CvInvoke.Rectangle(mat, operationDrawing.Rectangle, new MCvScalar(operationDrawing.Brightness), operationDrawing.Thickness, operationDrawing.LineType); break; case PixelDrawing.BrushShapeType.Circle: @@ -1631,7 +1633,7 @@ namespace UVtools.Core break; default: throw new ArgumentOutOfRangeException(); - } + }*/ } else if (operation.OperationType == PixelOperation.PixelOperationType.Text) { diff --git a/UVtools.Core/Objects/RangeObservableCollection.cs b/UVtools.Core/Objects/RangeObservableCollection.cs index 9e529d8..c6996ce 100644 --- a/UVtools.Core/Objects/RangeObservableCollection.cs +++ b/UVtools.Core/Objects/RangeObservableCollection.cs @@ -420,7 +420,7 @@ return; } - if (!(collection is IList<T> list)) + if (collection is not IList<T> list) list = new List<T>(collection); using (BlockReentrancy()) diff --git a/UVtools.Core/Operations/OperationRepairLayers.cs b/UVtools.Core/Operations/OperationRepairLayers.cs index 0eb6318..9227bef 100644 --- a/UVtools.Core/Operations/OperationRepairLayers.cs +++ b/UVtools.Core/Operations/OperationRepairLayers.cs @@ -9,9 +9,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Xml.Serialization; using Emgu.CV; @@ -19,7 +21,6 @@ using Emgu.CV.CvEnum; using Emgu.CV.Util; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; -using UVtools.Core.Objects; namespace UVtools.Core.Operations { @@ -32,8 +33,10 @@ namespace UVtools.Core.Operations private bool _removeEmptyLayers = true; private ushort _removeIslandsBelowEqualPixelCount = 5; private ushort _removeIslandsRecursiveIterations = 4; + private ushort _attachIslandsBelowLayers = 2; private uint _gapClosingIterations = 1; private uint _noiseRemovalIterations; + #endregion #region Overrides @@ -102,6 +105,12 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _removeIslandsRecursiveIterations, value); } + public ushort AttachIslandsBelowLayers + { + get => _attachIslandsBelowLayers; + set => RaiseAndSetIfChanged(ref _attachIslandsBelowLayers, value); + } + public uint GapClosingIterations { get => _gapClosingIterations; @@ -126,21 +135,20 @@ namespace UVtools.Core.Operations protected override bool ExecuteInternally(OperationProgress progress) { // Remove islands - if (!ReferenceEquals(Issues, null) - && !ReferenceEquals(IslandDetectionConfig, null) - && RepairIslands - && RemoveIslandsBelowEqualPixelCount > 0 - && RemoveIslandsRecursiveIterations != 1) + if (Issues is not null + && IslandDetectionConfig is not null + && _repairIslands + && _removeIslandsBelowEqualPixelCount > 0 + && _removeIslandsRecursiveIterations != 1) { progress.Reset("Removed recursive islands"); - ushort limit = RemoveIslandsRecursiveIterations == 0 + ushort limit = _removeIslandsRecursiveIterations == 0 ? ushort.MaxValue - : RemoveIslandsRecursiveIterations; + : _removeIslandsRecursiveIterations; var recursiveIssues = Issues; - ConcurrentBag<uint> islandsToRecompute = null; - - var islandConfig = IslandDetectionConfig; + var islandsToRecompute = new ConcurrentBag<uint>(); + var islandConfig = IslandDetectionConfig.Clone(); var overhangConfig = new OverhangDetectionConfiguration(false); var touchingBoundsConfig = new TouchingBoundDetectionConfiguration(false); var printHeightConfig = new PrintHeightDetectionConfiguration(false); @@ -148,7 +156,7 @@ namespace UVtools.Core.Operations var emptyLayersConfig = false; islandConfig.Enabled = true; - islandConfig.RequiredAreaToProcessCheck = (ushort) Math.Floor(RemoveIslandsBelowEqualPixelCount / 2m); + islandConfig.RequiredAreaToProcessCheck = (ushort) Math.Floor(_removeIslandsBelowEqualPixelCount / 2.0); for (uint i = 0; i < limit; i++) { @@ -169,7 +177,8 @@ namespace UVtools.Core.Operations .GroupBy(issue => issue.LayerIndex); if (!issuesGroup.Any()) break; // Nothing to process - islandsToRecompute = new ConcurrentBag<uint>(); + + islandsToRecompute.Clear(); Parallel.ForEach(issuesGroup, group => { if (progress.Token.IsCancellationRequested) return; @@ -193,17 +202,127 @@ namespace UVtools.Core.Operations layer.LayerMat = image; }); + // Remove from main list due the replicate below repair + Issues.RemoveAll(issue => issue.Type == LayerIssue.IssueType.Island && issue.Pixels.Length <= RemoveIslandsBelowEqualPixelCount); + if (islandsToRecompute.IsEmpty) break; // No more leftovers } } + if (_repairIslands && _attachIslandsBelowLayers > 0) + { + var islandsToProcess = Issues; + + if (islandsToProcess is null) + { + var islandConfig = IslandDetectionConfig.Clone(); + var overhangConfig = new OverhangDetectionConfiguration(false); + var touchingBoundsConfig = new TouchingBoundDetectionConfiguration(false); + var printHeightConfig = new PrintHeightDetectionConfiguration(false); + var resinTrapsConfig = new ResinTrapDetectionConfiguration(false); + var emptyLayersConfig = false; + + islandConfig.Enabled = true; + + islandsToProcess = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapsConfig, touchingBoundsConfig, printHeightConfig, emptyLayersConfig, null, progress); + } + + var issuesGroup = + islandsToProcess + .Where(issue => issue.Type == LayerIssue.IssueType.Island) + .GroupBy(issue => issue.LayerIndex); + + + progress.Reset("Attempt to attach islands below", (uint) islandsToProcess.Count); + Parallel.ForEach(issuesGroup, group => + { + using var mat = SlicerFile[group.Key].LayerMat; + var matSpan = mat.GetDataByteSpan(); + var matCache = new Dictionary<uint, Mat>(); + var matCacheModified = new Dictionary<uint, bool>(); + var startLayer = Math.Max(0, (int)group.Key - 2); + var lowestPossibleLayer = (uint)Math.Max(0, (int)group.Key - 1 - _attachIslandsBelowLayers); + + for (var layerIndex = startLayer+1; layerIndex >= lowestPossibleLayer; layerIndex--) + { + Debug.WriteLine(layerIndex); + Monitor.Enter(SlicerFile[layerIndex].Mutex); + matCache.Add((uint) layerIndex, SlicerFile[layerIndex].LayerMat); + matCacheModified.Add((uint) layerIndex, false); + } + + foreach (var issue in group) + { + int foundAt = startLayer == 0 ? 0 : - 1; + var requiredSupportingPixels = Math.Max(1, issue.PixelsCount * IslandDetectionConfig.RequiredPixelsToSupportMultiplier); + + for (var layerIndex = startLayer; layerIndex >= lowestPossibleLayer && foundAt < 0; layerIndex--) + { + uint pixelsSupportingIsland = 0; + + unsafe + { + var span = matCache[(uint) layerIndex].GetBytePointer(); + foreach (var point in issue.Pixels) + { + if (span[mat.GetPixelPos(point)] < + IslandDetectionConfig.RequiredPixelBrightnessToSupport) + { + continue; + } + + pixelsSupportingIsland++; + + if (pixelsSupportingIsland >= requiredSupportingPixels) + { + foundAt = layerIndex + 1; + break; + } + } + } + } + + // Copy pixels + if (foundAt >= 0) + { + for (var layerIndex = startLayer + 1; layerIndex >= foundAt; layerIndex--) + { + matCacheModified[(uint) layerIndex] = true; + unsafe + { + var span = matCache[(uint) layerIndex].GetBytePointer(); + foreach (var point in issue.Pixels) + { + var pos = mat.GetPixelPos(point); + span[pos] = (byte) Math.Min(span[pos] + matSpan[pos], byte.MaxValue); + } + } + } + } + + progress.LockAndIncrement(); + } + + foreach (var dict in matCache) + { + if (matCacheModified[dict.Key]) + { + SlicerFile[dict.Key].LayerMat = dict.Value; + } + dict.Value.Dispose(); + Monitor.Exit(SlicerFile[dict.Key].Mutex); + } + }); + + } + progress.Reset(ProgressAction, LayerRangeCount); - if (RepairIslands || RepairResinTraps) + if (_repairIslands || _repairResinTraps) { Parallel.For(LayerIndexStart, LayerIndexEnd, layerIndex => { if (progress.Token.IsCancellationRequested) return; - Layer layer = SlicerFile[layerIndex]; + var layer = SlicerFile[layerIndex]; Mat image = null; void initImage() @@ -213,7 +332,7 @@ namespace UVtools.Core.Operations if (Issues is not null) { - if (RepairIslands && RemoveIslandsBelowEqualPixelCount > 0 && RemoveIslandsRecursiveIterations == 1) + if (_repairIslands && _removeIslandsBelowEqualPixelCount > 0 && _removeIslandsRecursiveIterations == 1) { Span<byte> bytes = null; foreach (var issue in Issues) @@ -221,7 +340,7 @@ namespace UVtools.Core.Operations if ( issue.LayerIndex != layerIndex || issue.Type != LayerIssue.IssueType.Island || - issue.Pixels.Length > RemoveIslandsBelowEqualPixelCount) continue; + issue.Pixels.Length > _removeIslandsBelowEqualPixelCount) continue; initImage(); if (bytes == null) @@ -232,21 +351,9 @@ namespace UVtools.Core.Operations bytes[image.GetPixelPos(issuePixel)] = 0; } } - /*if (issues.TryGetValue((uint)layerIndex, out var issueList)) - { - var bytes = image.GetPixelSpan<byte>(); - foreach (var issue in issueList.Where(issue => - issue.Type == LayerIssue.IssueType.Island && issue.Pixels.Length <= removeIslandsBelowEqualPixels)) - { - foreach (var issuePixel in issue.Pixels) - { - bytes[image.GetPixelPos(issuePixel)] = 0; - } - } - }*/ } - if (RepairResinTraps) + if (_repairResinTraps) { foreach (var issue in Issues.Where(issue => issue.LayerIndex == layerIndex && issue.Type == LayerIssue.IssueType.ResinTrap)) { @@ -261,25 +368,25 @@ namespace UVtools.Core.Operations } } - if (RepairIslands && (GapClosingIterations > 0 || NoiseRemovalIterations > 0)) + if (_repairIslands && (_gapClosingIterations > 0 || _noiseRemovalIterations > 0)) { initImage(); using Mat kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), new Point(-1, -1)); - if (GapClosingIterations > 0) + if (_gapClosingIterations > 0) { CvInvoke.MorphologyEx(image, image, MorphOp.Close, kernel, new Point(-1, -1), - (int)GapClosingIterations, BorderType.Default, default); + (int)_gapClosingIterations, BorderType.Default, default); } - if (NoiseRemovalIterations > 0) + if (_noiseRemovalIterations > 0) { CvInvoke.MorphologyEx(image, image, MorphOp.Open, kernel, new Point(-1, -1), - (int)NoiseRemovalIterations, BorderType.Default, default); + (int)_noiseRemovalIterations, BorderType.Default, default); } } - if (!ReferenceEquals(image, null)) + if (image is not null) { layer.LayerMat = image; image.Dispose(); @@ -289,7 +396,7 @@ namespace UVtools.Core.Operations }); } - if (RemoveEmptyLayers) + if (_removeEmptyLayers) { List<uint> removeLayers = new(); for (uint layerIndex = LayerIndexStart; layerIndex <= LayerIndexEnd; layerIndex++) diff --git a/UVtools.Core/PixelEditor/PixelDrawing.cs b/UVtools.Core/PixelEditor/PixelDrawing.cs index 45cbb9e..9aa8269 100644 --- a/UVtools.Core/PixelEditor/PixelDrawing.cs +++ b/UVtools.Core/PixelEditor/PixelDrawing.cs @@ -13,16 +13,28 @@ namespace UVtools.Core.PixelEditor { public class PixelDrawing : PixelOperation { - private BrushShapeType _brushShape = BrushShapeType.Rectangle; + private BrushShapeType _brushShape = BrushShapeType.Square; private ushort _brushSize = 1; private short _thickness = -1; private byte _removePixelBrightness; + private double _rotationAngle; public const byte MinRectangleBrush = 1; public const byte MinCircleBrush = 7; public enum BrushShapeType : byte { - Rectangle = 0, - Circle + //Mask = 0, + Line = 1, + Triangle = 3, + Square = 4, + Pentagon = 5, + Hexagon = 6, + Heptagon = 7, + Octagon = 8, + Nonagon = 9, + Decagon = 10, + Hendecagon = 11, + Dodecagon = 12, + Circle = 100, } public override PixelOperationType OperationType => PixelOperationType.Drawing; @@ -42,6 +54,12 @@ namespace UVtools.Core.PixelEditor } } + public double RotationAngle + { + get => _rotationAngle; + set => RaiseAndSetIfChanged(ref _rotationAngle, Math.Round(value, 2)); + } + public ushort BrushSize { get => _brushSize; @@ -77,9 +95,10 @@ namespace UVtools.Core.PixelEditor } - public PixelDrawing(uint layerIndex, Point location, LineType lineType, BrushShapeType brushShape, ushort brushSize, short thickness, byte removePixelBrightness, byte pixelBrightness, bool isAdd) : base(layerIndex, location, lineType, pixelBrightness) + public PixelDrawing(uint layerIndex, Point location, LineType lineType, BrushShapeType brushShape, double rotationAngle, ushort brushSize, short thickness, byte removePixelBrightness, byte pixelBrightness, bool isAdd) : base(layerIndex, location, lineType, pixelBrightness) { _brushShape = brushShape; + _rotationAngle = rotationAngle; _brushSize = brushSize; _thickness = thickness; _removePixelBrightness = removePixelBrightness; diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index f6a08d3..a11e5bd 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.13.2</Version> + <Version>2.13.3</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index cd32d11..e924a15 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -308,8 +308,8 @@ <Component Id="owc6F31DEF09608AA645959666A4CB7FBBC" Guid="c94570a5-5bb4-a53f-e12f-9581bddf7d08"> <File Id="owf6F31DEF09608AA645959666A4CB7FBBC" Source="$(var.SourceDir)\mscordaccore.dll" KeyPath="yes" /> </Component> - <Component Id="owc4E313F8BFFE35360B32FE794558D37F8" Guid="3a8b626a-4506-c48d-46b6-b63a59616c3c"> - <File Id="owf4E313F8BFFE35360B32FE794558D37F8" Source="$(var.SourceDir)\mscordaccore_amd64_amd64_5.0.621.22011.dll" KeyPath="yes" /> + <Component Id="owc628E325F4699E7AEC74011370E41ACEE" Guid="1965c775-7da4-0dcd-7aa2-4bda54a68382"> + <File Id="owf628E325F4699E7AEC74011370E41ACEE" Source="$(var.SourceDir)\mscordaccore_amd64_amd64_5.0.721.25508.dll" KeyPath="yes" /> </Component> <Component Id="owc5FC34571A1AE47A011FC6C2A95B00DA6" Guid="2591451e-0fe5-ccad-abf6-1d1097251253"> <File Id="owf5FC34571A1AE47A011FC6C2A95B00DA6" Source="$(var.SourceDir)\mscordbi.dll" KeyPath="yes" /> @@ -897,8 +897,8 @@ <Component Id="owc1C3DE0BEF10A5791E37D4C3D97AED5E1" Guid="b1851a14-1200-d448-1acd-b229bf6ac918"> <File Id="owf1C3DE0BEF10A5791E37D4C3D97AED5E1" Source="$(var.SourceDir)\Assets\PrusaSlicer\printer\AnyCubic Photon.ini" KeyPath="yes" /> </Component> - <Component Id="owcFE63AE2337EB232D5C139086B8A44942" Guid="02bb8e1d-14a7-f786-5cb9-e7b37337918b"> - <File Id="owfFE63AE2337EB232D5C139086B8A44942" Source="$(var.SourceDir)\Assets\PrusaSlicer\printer\Creality HALOT-SKY CL-60.ini" KeyPath="yes" /> + <Component Id="owc64BDFB521E8E34CAE3547B3D1B8F3996" Guid="71d86e2a-c6b7-884a-65ff-2260e4edd56f"> + <File Id="owf64BDFB521E8E34CAE3547B3D1B8F3996" Source="$(var.SourceDir)\Assets\PrusaSlicer\printer\Creality HALOT-ONE CL-60.ini" KeyPath="yes" /> </Component> <Component Id="owc88F58B3D3A8E53CEFC3FE84F6C20FAB8" Guid="8f66ab9b-8ff1-f324-9ec5-5cbb688dd5f6"> <File Id="owf88F58B3D3A8E53CEFC3FE84F6C20FAB8" Source="$(var.SourceDir)\Assets\PrusaSlicer\printer\Creality HALOT-SKY CL-89.ini" KeyPath="yes" /> diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs b/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs index d4c9d89..3cdd6f6 100644 --- a/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs +++ b/UVtools.WPF/Controls/Calibrators/CalibrateElephantFootControl.axaml.cs @@ -75,7 +75,7 @@ namespace UVtools.WPF.Controls.Calibrators { Operation.ErodeKernel.Matrix = _kernelCtrl.GetMatrix(); Operation.ErodeKernel.Anchor = _kernelCtrl.Anchor; - return !(Operation.ErodeKernel.Matrix is null); + return Operation.ErodeKernel.Matrix is not null; } public void UpdatePreview() diff --git a/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs index dbb6600..baa6316 100644 --- a/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs @@ -89,7 +89,7 @@ namespace UVtools.WPF.Controls.Tools FilesListBox = this.Find<ListBox>("FilesListBox"); FilesListBox.DoubleTapped += (sender, args) => { - if (!(FilesListBox.SelectedItem is ValueDescription file)) return; + if (FilesListBox.SelectedItem is not ValueDescription file) return; App.StartProcess(file.ValueAsString); }; FilesListBox.KeyUp += (sender, e) => diff --git a/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml b/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml index fcc7036..84378c0 100644 --- a/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml +++ b/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml @@ -35,15 +35,17 @@ </StackPanel> <Grid ColumnDefinitions="Auto,10,150,5,Auto" - RowDefinitions="Auto,10,Auto"> + RowDefinitions="Auto,10,Auto,10,Auto"> <!-- Remove islands equal or smaller than --> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" + IsEnabled="{Binding Operation.RepairIslands}" ToolTip.Tip="The pixel area threshold above which islands will not be removed by this repair. - 
Islands remaining after repair will require supports to be added manually." +
Islands remaining after repair will require supports to be added manually. +
NOTE: This repair method requires the issues to be manually computed before hand." Text="Remove islands equal or smaller than:"/> <NumericUpDown @@ -52,9 +54,11 @@ Increment="1" Minimum="0" Maximum="65535" + IsEnabled="{Binding Operation.RepairIslands}" ToolTip.Tip="The pixel area threshold above which islands will not be removed by this repair. -
Islands remaining after repair will require supports to be added manually." +
Islands remaining after repair will require supports to be added manually. +
NOTE: This repair method requires the issues to be manually computed before hand." Value="{Binding Operation.RemoveIslandsBelowEqualPixelCount}" /> @@ -62,6 +66,7 @@ Grid.Row="0" Grid.Column="4" VerticalAlignment="Center" + IsEnabled="{Binding Operation.RepairIslands}" Text="px"/> @@ -70,10 +75,11 @@ Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" + IsEnabled="{Binding Operation.RepairIslands}" ToolTip.Tip="If the removal of an island in the current layer results in a new island being introduce in the layer above, the island in the layer above will also be automatically removed. 
This process will repeat for up to the number of layers specified. Set to 0 to repeat until there are no more valid islands to remove. -
 
NOTE: Use with caution as this can remove large portions of your model if proper supports have not been added beforehand. +
This repair method requires the issues to be manually computed before hand. 
Using this function with high values can be extremely slow depending on resolution and issue count." Text="Recursively remove islands for up to:"/> @@ -83,22 +89,54 @@ Increment="1" Minimum="0" Maximum="65535" - + IsEnabled="{Binding Operation.RepairIslands}" ToolTip.Tip="If the removal of an island in the current layer results in a new island being introduce in the layer above, the island in the layer above will also be automatically removed. 
This process will repeat for up to the number of layers specified. Set to 0 to repeat until there are no more valid islands to remove. -
 
NOTE: Use with caution as this can remove large portions of your model if proper supports have not been added beforehand. +
This repair method requires the issues to be manually computed before hand. 
Using this function with high values can be extremely slow depending on resolution and issue count." Value="{Binding Operation.RemoveIslandsRecursiveIterations}" /> - <TextBlock Grid.Row="2" Grid.Column="4" VerticalAlignment="Center" + IsEnabled="{Binding Operation.RepairIslands}" Text="layers"/> + + <!-- Attempt to attach islands below --> + <TextBlock + Grid.Row="4" + Grid.Column="0" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.RepairIslands}" + ToolTip.Tip="Attempt to attach the islands down to n layers when found a safe mass below to support it. +
NOTE: Use with caution and with a low value, this can mostly be used when there are some missed support layers in between as observed from the issues. +
This repair method requires the issues to be manually computed before hand. +
Use 0 to disable this repair method." + Text="Attempt to attach islands down to:"/> + + <NumericUpDown + Grid.Row="4" + Grid.Column="2" + Increment="1" + Minimum="0" + Maximum="65535" + IsEnabled="{Binding Operation.RepairIslands}" + ToolTip.Tip="Attempt to attach the islands down to n layers when found a safe mass below to support it. +
NOTE: Use with caution and with a low value, this can mostly be used when there are some missed support layers in between as observed from the issues. +
This repair method requires the issues to be manually computed before hand. +
Use 0 to disable this repair method." + Value="{Binding Operation.AttachIslandsBelowLayers}"/> + + <TextBlock + Grid.Row="4" + Grid.Column="4" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.RepairIslands}" + Text="layers"/> </Grid> diff --git a/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml.cs index 6b038b4..a285344 100644 --- a/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolRepairLayersControl.axaml.cs @@ -26,6 +26,7 @@ namespace UVtools.WPF.Controls.Tools RemoveEmptyLayers = UserSettings.Instance.LayerRepair.RemoveEmptyLayers, RemoveIslandsBelowEqualPixelCount = UserSettings.Instance.LayerRepair.RemoveIslandsBelowEqualPixels, RemoveIslandsRecursiveIterations = UserSettings.Instance.LayerRepair.RemoveIslandsRecursiveIterations, + AttachIslandsBelowLayers = UserSettings.Instance.LayerRepair.AttachIslandsBelowLayers, GapClosingIterations = UserSettings.Instance.LayerRepair.ClosingIterations, NoiseRemovalIterations = UserSettings.Instance.LayerRepair.OpeningIterations, }; @@ -37,6 +38,7 @@ namespace UVtools.WPF.Controls.Tools Operation.RemoveEmptyLayers = UserSettings.Instance.LayerRepair.RemoveEmptyLayers; Operation.RemoveIslandsBelowEqualPixelCount = UserSettings.Instance.LayerRepair.RemoveIslandsBelowEqualPixels; Operation.RemoveIslandsRecursiveIterations = UserSettings.Instance.LayerRepair.RemoveIslandsRecursiveIterations; + Operation.AttachIslandsBelowLayers = UserSettings.Instance.LayerRepair.AttachIslandsBelowLayers; Operation.GapClosingIterations = UserSettings.Instance.LayerRepair.ClosingIterations; Operation.NoiseRemovalIterations = UserSettings.Instance.LayerRepair.OpeningIterations; } diff --git a/UVtools.WPF/MainWindow.Issues.cs b/UVtools.WPF/MainWindow.Issues.cs index 403f17a..0e9def1 100644 --- a/UVtools.WPF/MainWindow.Issues.cs +++ b/UVtools.WPF/MainWindow.Issues.cs @@ -335,14 +335,15 @@ namespace UVtools.WPF private void IssuesGridOnSelectionChanged(object? sender, SelectionChangedEventArgs e) { + if (DataContext is null) return; + if (IssuesGrid.SelectedItem is not LayerIssue issue) { ShowLayer(); return; } - if (issue.Type == LayerIssue.IssueType.TouchingBound || issue.Type == LayerIssue.IssueType.EmptyLayer || - (issue.X == -1 && issue.Y == -1)) + if (issue.Type is LayerIssue.IssueType.TouchingBound or LayerIssue.IssueType.EmptyLayer || (issue.X == -1 && issue.Y == -1)) { ZoomToFit(); } diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index c0a304a..d5789b9 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; -using System.Data; using System.Diagnostics; using System.Drawing; using System.IO; @@ -24,7 +23,6 @@ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; -using Emgu.CV.XImgproc; using UVtools.Core; using UVtools.Core.Extensions; using UVtools.Core.PixelEditor; @@ -991,9 +989,11 @@ namespace UVtools.WPF continue; } - switch (operationDrawing.BrushShape) + LayerCache.ImageBgr.DrawPolygon((byte)operationDrawing.BrushShape, operationDrawing.BrushSize / 2, operationDrawing.Location, + new MCvScalar(color.B, color.G, color.R), operationDrawing.RotationAngle, operationDrawing.Thickness, operationDrawing.LineType); + /*switch (operationDrawing.BrushShape) { - case PixelDrawing.BrushShapeType.Rectangle: + case PixelDrawing.BrushShapeType.Square: CvInvoke.Rectangle(LayerCache.ImageBgr, operationDrawing.Rectangle, new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness, operationDrawing.LineType); @@ -1005,7 +1005,7 @@ namespace UVtools.WPF break; default: throw new ArgumentOutOfRangeException(); - } + }*/ } else if (operation.OperationType == PixelOperation.PixelOperationType.Text) { @@ -1861,10 +1861,45 @@ namespace UVtools.WPF if (DrawingPixelDrawing.BrushSize > 1) { - cursor = EmguExtensions.InitMat(new Size(DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.BrushSize), 4); - switch (DrawingPixelDrawing.BrushShape) + if ((byte)DrawingPixelDrawing.BrushShape >= 1) + { + int cursorSize = DrawingPixelDrawing.BrushSize; + if (DrawingPixelDrawing.Thickness > 1) + { + cursorSize += DrawingPixelDrawing.Thickness; + } + cursor = EmguExtensions.InitMat(new Size(cursorSize, cursorSize), 4); + + cursor.DrawPolygon((byte) DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.BrushSize / 2, cursor.Size.ToPoint().Half(), + _pixelEditorCursorColor, DrawingPixelDrawing.RotationAngle, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.LineType); + + if (DrawingPixelDrawing.BrushShape != PixelDrawing.BrushShapeType.Circle) + { + if (_showLayerImageFlipped) + { + var flipType = FlipType.None; + if (_showLayerImageFlippedHorizontally) + flipType |= FlipType.Horizontal; + if (_showLayerImageFlippedVertically) + flipType |= FlipType.Vertical; + + if (flipType != FlipType.None) + CvInvoke.Flip(cursor, cursor, flipType); + } + + if (_showLayerImageRotated) + { + CvInvoke.Rotate(cursor, cursor, + _showLayerImageRotateCcwDirection + ? RotateFlags.Rotate90CounterClockwise + : RotateFlags.Rotate90Clockwise); + } + } + } + + /*switch (DrawingPixelDrawing.BrushShape) { - case PixelDrawing.BrushShapeType.Rectangle: + case PixelDrawing.BrushShapeType.Square: CvInvoke.Rectangle(cursor, new Rectangle(Point.Empty, new Size(DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.BrushSize)), _pixelEditorCursorColor, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.LineType); @@ -1889,7 +1924,7 @@ namespace UVtools.WPF 1, DrawingPixelDrawing.LineType ); break; - } + }*/ } break; case PixelOperation.PixelOperationType.Text: diff --git a/UVtools.WPF/MainWindow.PixelEditor.cs b/UVtools.WPF/MainWindow.PixelEditor.cs index 43be784..6652947 100644 --- a/UVtools.WPF/MainWindow.PixelEditor.cs +++ b/UVtools.WPF/MainWindow.PixelEditor.cs @@ -25,6 +25,7 @@ using UVtools.Core; using UVtools.Core.Extensions; using UVtools.Core.PixelEditor; using UVtools.WPF.Extensions; +using DrawingExtensions = UVtools.Core.Extensions.DrawingExtensions; namespace UVtools.WPF { @@ -182,7 +183,7 @@ namespace UVtools.WPF for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++) { var operationDrawing = new PixelDrawing(layerIndex, realLocation, DrawingPixelDrawing.LineType, - DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.RemovePixelBrightness, DrawingPixelDrawing.PixelBrightness, isAdd); + DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.RotationAngle, DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.RemovePixelBrightness, DrawingPixelDrawing.PixelBrightness, isAdd); //if (PixelHistory.Contains(operation)) continue; AddDrawing(operationDrawing); @@ -195,26 +196,57 @@ namespace UVtools.WPF if (operationDrawing.BrushSize == 1) { - unsafe + /*unsafe { using var framebuffer = bitmap.Lock(); var data = (uint*)framebuffer.Address.ToPointer(); data[bitmap.GetPixelPos(location)] = color.ToUint32(); - } - - LayerImageBox.InvalidateArrange(); + }*/ + + LayerCache.Canvas.DrawPoint(location.X, location.Y, new SKColor(color.ToUint32())); + + + LayerImageBox.InvalidateVisual(); // LayerCache.ImageBgr.SetByte(operationDrawing.Location.X, operationDrawing.Location.Y, // new[] { color.B, color.G, color.R }); continue; } + int halfBrush = operationDrawing.BrushSize / 2; switch (operationDrawing.BrushShape) { - case PixelDrawing.BrushShapeType.Rectangle: - - int shiftPos = operationDrawing.BrushSize / 2; - LayerCache.Canvas.DrawRect(location.X - shiftPos, location.Y - shiftPos, + case PixelDrawing.BrushShapeType.Line: + Point point1 = new(location.X - halfBrush, location.Y); + Point point2 = new(location.X + halfBrush, location.Y); + point1 = point1.Rotate(operationDrawing.RotationAngle, location); + point2 = point2.Rotate(operationDrawing.RotationAngle, location); + + if (_showLayerImageRotated) + { + if (_showLayerImageRotateCcwDirection) + { + point1 = point1.Rotate(90, location); + point2 = point2.Rotate(90, location); + } + else + { + point1 = point1.Rotate(-90, location); + point2 = point2.Rotate(-90, location); + } + } + + LayerCache.Canvas.DrawLine(point1.X, point1.Y, point2.X, point2.Y, new SKPaint + { + IsAntialias = operationDrawing.LineType == LineType.AntiAlias, + Color = new SKColor(color.ToUint32()), + IsStroke = operationDrawing.Thickness >= 0, + StrokeWidth = operationDrawing.Thickness, + StrokeCap = SKStrokeCap.Round + }); + break; + /*case PixelDrawing.BrushShapeType.Square: + LayerCache.Canvas.DrawRect(location.X - halfBrush, location.Y - halfBrush, operationDrawing.BrushSize, operationDrawing.BrushSize, new SKPaint @@ -227,10 +259,8 @@ namespace UVtools.WPF /*CvInvoke.Rectangle(LayerCache.ImageBgr, GetTransposedRectangle(operationDrawing.Rectangle), new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness, operationDrawing.LineType);*/ - break; + //break; case PixelDrawing.BrushShapeType.Circle: - - LayerCache.Canvas.DrawCircle(location.X, location.Y, operationDrawing.BrushSize / 2f, new SKPaint { @@ -239,13 +269,69 @@ namespace UVtools.WPF IsStroke = operationDrawing.Thickness >= 0, StrokeWidth = operationDrawing.Thickness }); - + /*CvInvoke.Circle(LayerCache.ImageBgr, location, operationDrawing.BrushSize / 2, new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness, operationDrawing.LineType);*/ break; default: - throw new ArgumentOutOfRangeException(); + var angle = operationDrawing.RotationAngle; + if (_showLayerImageRotated) + { + if (!_showLayerImageFlipped || _showLayerImageFlippedHorizontally && _showLayerImageFlippedVertically) + { + if (_showLayerImageRotateCcwDirection) + { + angle -= 90; + } + else + { + angle += 90; + } + } + else + { + if (_showLayerImageRotateCcwDirection) + { + angle += 90; + } + else + { + angle -= 90; + } + } + } + + + var vertices = DrawingExtensions.GetPolygonVertices((byte) operationDrawing.BrushShape, + operationDrawing.BrushSize / 2, location, angle, _showLayerImageFlipped && _showLayerImageFlippedHorizontally, _showLayerImageFlipped && _showLayerImageFlippedVertically); + + //if(angle % 360 != 0) PointExtensions.Rotate(vertices, angle, location); + + var path = new SKPath(); + path.MoveTo(vertices[0].X, vertices[0].Y); + for (var i = 1; i < vertices.Length; i++) + { + path.LineTo(vertices[i].X, vertices[i].Y); + } + path.Close(); + + LayerCache.Canvas.DrawPath(path, new SKPaint + { + IsAntialias = operationDrawing.LineType == LineType.AntiAlias, + Color = new SKColor(color.ToUint32()), + IsStroke = operationDrawing.Thickness >= 0, + StrokeWidth = operationDrawing.Thickness + }); + /*LayerCache.Canvas.DrawPoints(SKPointMode.Polygon, points, + new SKPaint + { + IsAntialias = operationDrawing.LineType == LineType.AntiAlias, + Color = new SKColor(color.ToUint32()), + IsStroke = operationDrawing.Thickness >= 0, + StrokeWidth = operationDrawing.Thickness + });*/ + break; } LayerImageBox.InvalidateVisual(); //RefreshLayerImage(); diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml index 1eedf95..ecc8aa8 100644 --- a/UVtools.WPF/MainWindow.axaml +++ b/UVtools.WPF/MainWindow.axaml @@ -823,7 +823,7 @@ </Border> <Grid - RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto" + RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto" ColumnDefinitions="Auto,10,130,5,40"> <TextBlock @@ -853,12 +853,30 @@ SelectedItem="{Binding DrawingPixelDrawing.BrushShape}"/> <TextBlock - Grid.Row="4" + Grid.Row="4" + Grid.Column="0" + VerticalAlignment="Center" + Text="Rotation angle:" /> + <NumericUpDown + Grid.Row="4" + Grid.Column="2" + FormatString="F2" + Minimum="-360" + Maximum="360" + Value="{Binding DrawingPixelDrawing.RotationAngle}"/> + <TextBlock + Grid.Row="4" + Grid.Column="4" + VerticalAlignment="Center" + Text="º" /> + + <TextBlock + Grid.Row="6" Grid.Column="0" VerticalAlignment="Center" Text="Brush diameter:" /> <NumericUpDown - Grid.Row="4" + Grid.Row="6" Grid.Column="2" Minimum="1" Maximum="4000" @@ -866,18 +884,18 @@ HorizontalAlignment="Stretch" Value="{Binding DrawingPixelDrawing.BrushSize}"/> <TextBlock - Grid.Row="4" + Grid.Row="6" Grid.Column="4" VerticalAlignment="Center" Text="px" /> <TextBlock - Grid.Row="6" + Grid.Row="8" Grid.Column="0" VerticalAlignment="Center" Text="Thickness:" /> <NumericUpDown - Grid.Row="6" + Grid.Row="8" Grid.Column="2" Minimum="-1" Maximum="255" @@ -885,18 +903,18 @@ HorizontalAlignment="Stretch" Value="{Binding DrawingPixelDrawing.Thickness}"/> <TextBlock - Grid.Row="6" + Grid.Row="8" Grid.Column="4" VerticalAlignment="Center" Text="px" /> <TextBlock - Grid.Row="8" + Grid.Row="10" Grid.Column="0" VerticalAlignment="Center" Text="Remove pixel brightness:" /> <NumericUpDown - Grid.Row="8" + Grid.Row="10" Grid.Column="2" Minimum="0" Maximum="255" @@ -904,18 +922,18 @@ HorizontalAlignment="Stretch" Value="{Binding DrawingPixelDrawing.RemovePixelBrightness}"/> <TextBlock - Grid.Row="8" + Grid.Row="10" Grid.Column="4" VerticalAlignment="Center" Text="{Binding DrawingPixelDrawing.RemovePixelBrightnessPercent, StringFormat=\{0:0\}%}" /> <TextBlock - Grid.Row="10" + Grid.Row="12" Grid.Column="0" VerticalAlignment="Center" Text="Add pixel brightness:" /> <NumericUpDown - Grid.Row="10" + Grid.Row="12" Grid.Column="2" Minimum="1" Maximum="255" @@ -923,18 +941,18 @@ HorizontalAlignment="Stretch" Value="{Binding DrawingPixelDrawing.PixelBrightness}"/> <TextBlock - Grid.Row="10" + Grid.Row="12" Grid.Column="4" VerticalAlignment="Center" Text="{Binding DrawingPixelDrawing.PixelBrightnessPercent, StringFormat=\{0:0\}%}" /> <TextBlock - Grid.Row="12" + Grid.Row="14" Grid.Column="0" VerticalAlignment="Center" Text="Layers depth below:" /> <NumericUpDown - Grid.Row="12" + Grid.Row="14" Grid.Column="2" Grid.ColumnSpan="3" Minimum="0" @@ -944,12 +962,12 @@ <TextBlock - Grid.Row="14" + Grid.Row="16" Grid.Column="0" VerticalAlignment="Center" Text="Layers depth above:" /> <NumericUpDown - Grid.Row="14" + Grid.Row="16" Grid.Column="2" Grid.ColumnSpan="3" Minimum="0" diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 67fc9b7..37a3d02 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -623,7 +623,7 @@ namespace UVtools.WPF if (e.Handled || !IsFileLoaded || LayerImageBox.IsPanning - || !(LayerImageBox.TrackerImage is null) + || LayerImageBox.TrackerImage is not null || LayerImageBox.Cursor == StaticControls.CrossCursor || LayerImageBox.Cursor == StaticControls.HandCursor || LayerImageBox.SelectionMode == AdvancedImageBox.SelectionModes.Rectangle diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 85c39b9..e53c7c8 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ <PackageLicenseFile>LICENSE</PackageLicenseFile> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <RepositoryType>Git</RepositoryType> - <Version>2.13.2</Version> + <Version>2.13.3</Version> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs index 38ebee4..23dab0c 100644 --- a/UVtools.WPF/UserSettings.cs +++ b/UVtools.WPF/UserSettings.cs @@ -1070,6 +1070,7 @@ namespace UVtools.WPF private bool _removeEmptyLayers = true; private ushort _removeIslandsBelowEqualPixels = 5; private ushort _removeIslandsRecursiveIterations = 4; + private ushort _attachIslandsBelowLayers = 2; private byte _closingIterations = 2; private byte _openingIterations = 0; @@ -1103,6 +1104,12 @@ namespace UVtools.WPF set => RaiseAndSetIfChanged(ref _removeIslandsRecursiveIterations, value); } + public ushort AttachIslandsBelowLayers + { + get => _attachIslandsBelowLayers; + set => RaiseAndSetIfChanged(ref _attachIslandsBelowLayers, value); + } + public byte ClosingIterations { get => _closingIterations; diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml b/UVtools.WPF/Windows/SettingsWindow.axaml index 3ff30f1..48ede09 100644 --- a/UVtools.WPF/Windows/SettingsWindow.axaml +++ b/UVtools.WPF/Windows/SettingsWindow.axaml @@ -1387,6 +1387,15 @@ </StackPanel> <StackPanel Orientation="Horizontal" Margin="10,0" Spacing="10"> + <NumericUpDown + Value="{Binding Settings.LayerRepair.AttachIslandsBelowLayers}" + Width="70" + Minimum="0" + Maximum="65535"/> + <TextBlock VerticalAlignment="Center" Text="Attempt to attach islands down to this layers (0 = Disabled)"/> + </StackPanel> + + <StackPanel Orientation="Horizontal" Margin="10,0" Spacing="10"> <NumericUpDown Value="{Binding Settings.LayerRepair.ClosingIterations}" Width="70" diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs index 60e0ff4..2d1da9c 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml.cs +++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs @@ -1,9 +1,6 @@ using System; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Drawing; -using System.IO; -using System.Xml.Serialization; using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -147,7 +144,7 @@ namespace UVtools.WPF.Windows get => _layerIndexEnd; set { - if (!(ToolControl?.BaseOperation is null)) + if (ToolControl?.BaseOperation is not null) { ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; ToolControl.BaseOperation.LayerIndexEnd = value; @@ -178,14 +175,14 @@ namespace UVtools.WPF.Windows { LayerIndexStart = 0; LayerIndexEnd = MaximumLayerIndex; - if(!(ToolControl is null)) + if(ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.All; } public void SelectCurrentLayer() { LayerIndexStart = LayerIndexEnd = App.MainWindow.ActualLayer; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Current; } @@ -193,7 +190,7 @@ namespace UVtools.WPF.Windows { LayerIndexEnd = App.MainWindow.ActualLayer; LayerIndexStart = 0; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; } @@ -201,7 +198,7 @@ namespace UVtools.WPF.Windows { LayerIndexStart = App.MainWindow.ActualLayer; LayerIndexEnd = SlicerFile.LastLayerIndex; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; } @@ -209,7 +206,7 @@ namespace UVtools.WPF.Windows { LayerIndexStart = 0; LayerIndexEnd = SlicerFile.BottomLayerCount-1u; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Bottom; } @@ -217,21 +214,21 @@ namespace UVtools.WPF.Windows { LayerIndexStart = SlicerFile.BottomLayerCount; LayerIndexEnd = MaximumLayerIndex; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Normal; } public void SelectFirstLayer() { LayerIndexStart = LayerIndexEnd = 0; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.First; } public void SelectLastLayer() { LayerIndexStart = LayerIndexEnd = MaximumLayerIndex; - if (!(ToolControl is null)) + if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Last; } @@ -401,7 +398,7 @@ namespace UVtools.WPF.Windows { var name = string.IsNullOrWhiteSpace(_profileText) ? null : _profileText.Trim(); var operation = OperationProfiles.FindByName(ToolControl.BaseOperation, name); - if (!(operation is null)) + if (operation is not null) { if (await this.MessageBoxQuestion( $"A profile with same name or settings already exists.\nDo you want to overwrite:\n{operation}", |