diff --git a/CHANGELOG.md b/CHANGELOG.md index c928c804..4a19d1c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 02/05/2022 - v3.4.0 + +- **Tools:** + - (Add) PCB exposure: Converts a gerber file to a pixel perfect image given your printer LCD/resolution to exposure the copper traces. + - (Improvement) Export settings now indent the XML to be more user friendly to edit + - (Improvement) Layer import: Allow to have profiles + - (Improvement) Layer import: Validates if selected files exists before execute + - (Fix) Lithophane: Disallow having start threshold equal to end threshold +- (Add) Windows explorer: Right-click on files will show "Open with UVtools" on context menu which opens the selected file on UVtools (Windows MSI only) +- (Improvement) Island and overhang detection: Ignore detection on all layers that are in direct contact with the plate (On same first layer position) +- (Improvement) Cmd: Better error messages for convert command when using shared extensions and no extension + ## 14/04/2022 - v3.3.2 - **UI:** diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 91d56c0f..db07aefc 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,11 +1,10 @@ -- **UI:** - - (Add) Setting: Restore window last position - If enabled, it will restore the main window last known client position on startup (#460) - - (Add) Setting: Restore window last size - If enabled, it will restore the main window last known client size on startup (#460) - - (Improvement) If there are missing dependencies it will show a proper window with information instead of crashing application without any visuals - - (Improvement) Start maximized is set before windows spawn to prevent the flicker effect on main window -- (Add) File formats: Property `IsUsingTSMC` - Indicates whatever file globals are using TSMC or not -- (Change) Lithophane: Noise removal and gap closing iterations defaults to 0 -- (Fix) Anycubic files: Printers are unable to use TSMC values after save (#457) -- (Fix) Pixel Editor button is hidden when using screens with scalling > 100% [dirty-fix] (#458) -- (Upgrade) .NET from 6.0.3 to 6.0.4 +- **Tools:** + - (Add) PCB exposure: Converts a gerber file to a pixel perfect image given your printer LCD/resolution to exposure the copper traces. + - (Improvement) Export settings now indent the XML to be more user friendly to edit + - (Improvement) Layer import: Allow to have profiles + - (Improvement) Layer import: Validates if selected files exists before execute + - (Fix) Lithophane: Disallow having start threshold equal to end threshold +- (Add) Windows explorer: Right-click on files will show "Open with UVtools" on context menu which opens the selected file on UVtools (Windows MSI only) +- (Improvement) Island and overhang detection: Ignore detection on all layers that are in direct contact with the plate (On same first layer position) +- (Improvement) Cmd: Better error messages for convert command when using shared extensions and no extension diff --git a/Scripts/010 Editor/osf.bt b/Scripts/010 Editor/osf.bt new file mode 100644 index 00000000..61005077 --- /dev/null +++ b/Scripts/010 Editor/osf.bt @@ -0,0 +1,103 @@ +//------------------------------------------------ +//--- 010 Editor v8.0.1 Binary Template +// +// File: osf (vlare) +// Authors: Tiago Conceição +//------------------------------------------------ + +BigEndian(); + +struct PREVIEW { + BitfieldDisablePadding(); + uint PreviewLength:24 ; + ubyte PreviewData[PreviewLength] ; +}; + +struct HEADER { + uint HeaderLength ; + ushort Version ; // 1 + ubyte ImageLog ; // log 2 + + + PREVIEW preview; + PREVIEW preview; + PREVIEW preview; + PREVIEW preview; + + ushort ResolutionX ; + ushort ResolutionY ; + ushort PixelUmMagnified100Times ; // (um, magnification 100 times: such as 100um write 10000, the same below) + ubyte Mirror ; // (0x00 not mirrored, 0x01 X-axis mirroring, 0x02 Y-axis mirroring, 0x03 XY-axis mirroring) + ubyte BottomLightPWM ; + ubyte LightPWM ; + ubyte AntiAliasEnabled ; + ubyte DistortionEnabled ; + ubyte DelayedExposureActivationEnabled ; + uint LayerCount ; + ushort NumberParameterSets ; // 1 + uint LastLayerIndex ; + uint LayerHeightUmMagnified100Times:24 ; // (um magnification 100 times) + ubyte BottomLayersCount ; + + uint ExposureTimeMagnified100Times:24 ; // s * 100 + uint BottomExposureTimeMagnified100Times:24 ; // s * 100 + uint SupportDelayTimeMagnified100Times:24 ; // s * 100 + uint BottomSupportDelayTimeMagnified100Times:24 ; // s * 100 + ubyte TransitionLayers ; + ubyte TransitionType ; // (0x00 linear transition) + uint TransitionLayerIntervalTimeDifferenceMagnified100Times:24 ; // s * 100 + uint WaitTimeAfterCureMagnified100Times:24 ; // s * 100 + uint WaitTimeAfterLiftMagnified100Times:24 ; // s * 100 + uint WaitTimeBeforeCureMagnified100Times:24 ; // s * 100 + + uint BottomLiftHeightSlowMagnified1000Times:24 ; // (magnification 1000 times) + uint BottomLiftHeightTotalMagnified1000Times:24 ; // (magnification 1000 times) + uint LiftHeightSlowMagnified1000Times:24 ; // (magnification 1000 times) + uint LiftHeightTotalMagnified1000Times:24 ; // (magnification 1000 times) + uint BottomRetractHeightTotalMagnified1000Times:24 ; // (magnification 1000 times) + uint RetractHeightSlowMagnified1000Times:24 ; // (magnification 1000 times) + uint RetractHeightTotalMagnified1000Times:24 ; // (magnification 1000 times) + + ubyte AccelerationType ; // (0x00: S-shaped acceleration, 0x01: T-shaped acceleration, Default Value: S-shaped acceleration, currently only supports S-shaped acceleration) + + ushort BottomLiftSpeedStartMagnified100Times ; // (magnification 100 times) + ushort BottomLiftSpeedSlowMagnified100Times ; // (magnification 100 times) + ushort BottomLiftSpeedFastMagnified100Times ; // (magnification 100 times) + ubyte BottomLiftAccelerationChange ; // 5 + + ushort LiftSpeedStartMagnified100Times ; // (magnification 100 times) + ushort LiftSpeedSlowMagnified100Times ; // (magnification 100 times) + ushort LiftSpeedFastMagnified100Times ; // (magnification 100 times) + ubyte LiftAccelerationChange ; // 5 + + ushort BottomRetractSpeedStartMagnified100Times ; // (magnification 100 times) + ushort BottomRetractSpeedSlowMagnified100Times ; // (magnification 100 times) + ushort BottomRetractFastMagnified100Times ; // (magnification 100 times) + ubyte BottomRetractAccelerationChange ; // 5 + + ushort RetractSpeedStartMagnified100Times ; // (magnification 100 times) + ushort RetractSpeedSlowMagnified100Times ; // (magnification 100 times) + ushort RetractFastMagnified100Times ; // (magnification 100 times) + ubyte RetractAccelerationChange ; // 5 + + ubyte Reserved[23] ; + + ubyte ProtocolType ; // 0 +} header; + + +struct LAYER_DEF { + ushort Mark ; // (OD OA begins, indicating that the model + support is included; the beginning of 0D 0B, indicating that the layer only has support data) + uint NumberOfPixels ; + ushort StartY ; +}; + + +struct LAYERS { + + local int i = 0; + for( i = 0; i < header.LayerCount; i++ ){ + LAYER_DEF layerDef ; + } + +} layers; diff --git a/UVtools.Cmd/Program.cs b/UVtools.Cmd/Program.cs index b75ff020..e6c815b3 100644 --- a/UVtools.Cmd/Program.cs +++ b/UVtools.Cmd/Program.cs @@ -169,7 +169,7 @@ internal static void WriteWarning(object? obj) { if (Quiet) return; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(obj); + Console.Write(obj); Console.ResetColor(); } @@ -190,12 +190,12 @@ internal static void WriteError(object? obj, bool exit = false) return; } Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(obj); + Console.Write(obj); Console.ResetColor(); if (exit) Environment.Exit(-1); } - internal static void WriteLineError(object? obj, bool exit = true) + internal static void WriteLineError(object? obj, bool exit = true, bool prependError = true) { if (Quiet) { @@ -204,7 +204,7 @@ internal static void WriteLineError(object? obj, bool exit = true) } var str = obj?.ToString(); - if(str is not null && !str.StartsWith("Error:")) str = $"Error: {str}"; + if(str is not null && prependError && !str.StartsWith("Error:")) str = $"Error: {str}"; Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(str); diff --git a/UVtools.Cmd/Symbols/ConvertCommand.cs b/UVtools.Cmd/Symbols/ConvertCommand.cs index 3d782394..89a4fdb7 100644 --- a/UVtools.Cmd/Symbols/ConvertCommand.cs +++ b/UVtools.Cmd/Symbols/ConvertCommand.cs @@ -8,6 +8,8 @@ using System; using System.CommandLine; using System.IO; +using System.Linq; +using UVtools.Core.Extensions; using UVtools.Core.FileFormats; namespace UVtools.Cmd.Symbols; @@ -28,22 +30,40 @@ internal static Command CreateCommand() command.SetHandler((FileInfo inputFile, string targetTypeExt, FileInfo? outputFile, ushort version, bool noOverwrite) => { + var targetType = FileFormat.FindByType(targetTypeExt); + if (targetType is null) + { + targetType = FileFormat.FindByExtensionOrFilePath(targetTypeExt, out var fileFormatsSharingExt); + if (targetType is not null && fileFormatsSharingExt > 1) + { + Program.WriteLineError($"The extension '{targetTypeExt}' is shared by multiple encoders, use the strict encoder name instead.", false); + Program.WriteLineError($"Available {FileFormat.AvailableFormats.Length} encoders:", false, false); + foreach (var fileFormat in FileFormat.AvailableFormats) + { + Program.WriteLineError($"{fileFormat.GetType().Name.RemoveFromEnd("File".Length)} ({string.Join(", ", fileFormat.FileExtensions.Select(extension => extension.Extension))})", false, false); + } + Environment.Exit(-1); + return; + } + } - var targetType = FileFormat.FindByAnyMeans(targetTypeExt); if (targetType is null) { - Program.WriteLineError($"Unable to find a valid convert type candidate from {targetTypeExt}."); + Program.WriteLineError($"Unable to find a valid encoder from {targetTypeExt}.", false); + Program.WriteLineError($"Available {FileFormat.AvailableFormats.Length} encoders:", false, false); + foreach (var fileFormat in FileFormat.AvailableFormats) + { + Program.WriteLineError($"{fileFormat.GetType().Name.RemoveFromEnd("File".Length)} ({string.Join(", ", fileFormat.FileExtensions.Select(extension => extension.Extension))})", false, false); + } + Environment.Exit(-1); return; } string? outputFilePath; - if (outputFile is not null) + string inputFileName = FileFormat.GetFileNameStripExtensions(inputFile.Name)!; + if (outputFile is null) { - outputFilePath = outputFile.FullName; - } - else - { - outputFilePath = FileFormat.GetFileNameStripExtensions(inputFile.Name)!; + outputFilePath = inputFileName; if (targetType.FileExtensions.Length == 1) { outputFilePath = Path.Combine(inputFile.DirectoryName!, $"{outputFilePath}.{targetType.FileExtensions[0].Extension}"); @@ -53,22 +73,62 @@ internal static Command CreateCommand() var ext = FileExtension.Find(targetTypeExt); if (ext is null) { - Program.WriteLineError($"Unable to construct the output filename from {targetTypeExt}, there are {targetType.FileExtensions.Length} extensions on this format, please specify an output file."); + Program.WriteLineError($"Unable to construct the output filename and guess the extension from the {targetTypeExt} encoder.", false); + Program.WriteLineError($"There are {targetType.FileExtensions.Length} possible extensions on this format ({string.Join(", ", targetType.FileExtensions.Select(extension => extension.Extension))}), please specify an output file.", false, false); return; } outputFilePath = Path.Combine(inputFile.DirectoryName!, $"{outputFilePath}.{ext.Extension}"); } } + else + { + outputFilePath = string.Format(outputFile.FullName, inputFileName); + } var outputFileName = Path.GetFileName(outputFilePath); + var outputFileDirectory = Path.GetDirectoryName(outputFilePath)!; + + if (outputFileName == string.Empty) + { + Program.WriteLineError("No output file was specified."); + return; + } + + if (!outputFileName.Contains('.')) + { + if (targetType.IsExtensionValid(outputFileName)) + { + outputFileName = $"{inputFileName}.{outputFileName}"; + outputFilePath = Path.Combine(outputFileDirectory, outputFileName); + } + else if (targetType.FileExtensions.Length == 1) + { + outputFileName = $"{outputFileName}.{targetType.FileExtensions[0].Extension}"; + outputFilePath = Path.Combine(outputFileDirectory, outputFileName); + } + } + + if (!targetType.IsExtensionValid(outputFileName, true)) + { + Program.WriteLineError($"The extension on '{outputFileName}' file is not valid for the {targetType.GetType().Name} encoder.", false); + Program.WriteLineError($"Available {targetType.FileExtensions.Length} extension(s):", false, false); + foreach (var fileExtension in targetType.FileExtensions) + { + Program.WriteLineError(fileExtension.Extension, false, false); + } + + Environment.Exit(-1); + + return; + } if (noOverwrite && File.Exists(outputFilePath)) { Program.WriteLineError($"{outputFileName} already exits! --no-overwrite is enabled."); return; } - + var slicerFile = Program.OpenInputFile(inputFile); Program.ProgressBarWork($"Converting to {outputFileName}", diff --git a/UVtools.Cmd/UVtools.Cmd.csproj b/UVtools.Cmd/UVtools.Cmd.csproj index 0f7493f4..edd17c27 100644 --- a/UVtools.Cmd/UVtools.Cmd.csproj +++ b/UVtools.Cmd/UVtools.Cmd.csproj @@ -5,7 +5,7 @@ net6.0 UVtoolsCmd UVtools.ico - 1.0.0 + 1.0.1 Tiago Conceição, sn4k3 PTRTECH LICENSE diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 07120bd0..5c506ae9 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -351,7 +351,8 @@ public static int GetPixelPos(this Mat mat, Point point) public static byte[] GetBytes(this Mat mat) { var data = new byte[mat.GetLength()]; - Marshal.Copy(mat.DataPointer, data, 0, data.Length); + //Marshal.Copy(mat.DataPointer, data, 0, data.Length); + mat.CopyTo(data); return data; } @@ -428,8 +429,10 @@ public static void SetByte(this Mat mat, int x, int y, byte[] value) => /// /// /// - public static void SetBytes(this Mat mat, byte[] value) => - Marshal.Copy(value, 0, mat.DataPointer, value.Length); + public static void SetBytes(this Mat mat, byte[] value) + { + mat.SetTo(value); + } /// /// Gets PNG byte array @@ -477,6 +480,23 @@ public static Mat CropByBounds(this Mat src, bool cloneInsteadRoi = false) return roi; } + public static Mat CropByBounds(this Mat src, ushort margin) => src.CropByBounds(new Size(margin, margin)); + + public static Mat CropByBounds(this Mat src, Size margin) + { + var rect = CvInvoke.BoundingRectangle(src); + if (rect.Size == Size.Empty) return src.New(); + using var roi = src.Size == rect.Size ? src.Roi(src.Size) : src.Roi(rect); + + var numberOfChannels = roi.NumberOfChannels; + var cropped = new Mat(roi.Rows + margin.Height * 2, roi.Cols + margin.Width * 2, roi.Depth, numberOfChannels); + + using var dest = new Mat(cropped, new Rectangle(margin.Width, margin.Height, roi.Width, roi.Height)); + roi.CopyTo(dest); + + return cropped; + } + public static void CropByBounds(this Mat src, Mat dst) { using var mat = src.CropByBounds(); diff --git a/UVtools.Core/FileFormats/CXDLPFile.cs b/UVtools.Core/FileFormats/CXDLPFile.cs index d303793e..2ac979a7 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -659,16 +659,10 @@ protected override void EncodeInternally(OperationProgress progress) { using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.ReadWrite); - if (string.IsNullOrWhiteSpace(MachineName)) - { - throw new InvalidDataException("Unable to detect the printer model from resolution, check if resolution is well defined on slicer for your printer model."); - } - - - if (!MachineName.StartsWith("CL-") && !MachineName.StartsWith("CT")) + if (string.IsNullOrWhiteSpace(MachineName) || (!MachineName.StartsWith("CL-") && !MachineName.StartsWith("CT"))) { bool found = false; - foreach (var machine in Printer.Machine.Machines + foreach (var machine in Machine.Machines .Where(machine => machine.Brand == PrinterBrand.Creality && (machine.Model.StartsWith("CL-") || machine.Model.StartsWith("CT")) )) diff --git a/UVtools.Core/FileFormats/FileExtension.cs b/UVtools.Core/FileFormats/FileExtension.cs index 20c63581..532832d3 100644 --- a/UVtools.Core/FileFormats/FileExtension.cs +++ b/UVtools.Core/FileFormats/FileExtension.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Linq; namespace UVtools.Core.FileFormats; @@ -136,6 +137,13 @@ FileFormatType is null return FileFormat.FindExtension(extension); } - + public static IEnumerable FindAll(string extension) + { + if (string.IsNullOrWhiteSpace(extension)) return Enumerable.Empty(); + if (extension.StartsWith('.')) extension = extension.Remove(1); + return FileFormat.FindExtensions(extension); + } + + #endregion } \ No newline at end of file diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index d8b1a2a6..69ec05a6 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -437,13 +437,26 @@ public static List AllFileExtensions /// name to find /// True to create a new instance of found file format, otherwise will return a pre created one which should be used for read-only purpose /// object or null if not found - public static FileFormat? FindByExtensionOrFilePath(string extensionOrFilePath, bool createNewInstance = false) + public static FileFormat? FindByExtensionOrFilePath(string extensionOrFilePath, bool createNewInstance = false) => + FindByExtensionOrFilePath(extensionOrFilePath, out _, createNewInstance); + + /// + /// Find by an extension + /// + /// name to find + /// Number of file formats sharing the input extension + /// True to create a new instance of found file format, otherwise will return a pre created one which should be used for read-only purpose + /// object or null if not found + public static FileFormat? FindByExtensionOrFilePath(string extensionOrFilePath, out byte fileFormatsSharingExt, bool createNewInstance = false) { + fileFormatsSharingExt = 0; if (string.IsNullOrWhiteSpace(extensionOrFilePath)) return null; bool isFilePath = false; // Test for ext first var fileFormats = AvailableFormats.Where(fileFormat => fileFormat.IsExtensionValid(extensionOrFilePath)).ToArray(); + fileFormatsSharingExt = (byte)fileFormats.Length; + if (fileFormats.Length == 0) // Extension not found, can be filepath, try to find it { GetFileNameStripExtensions(extensionOrFilePath, out var extension); @@ -460,7 +473,7 @@ public static List AllFileExtensions ? Activator.CreateInstance(fileFormats[0].GetType()) as FileFormat : fileFormats[0]; } - + // Multiple instances using Check for valid candidate foreach (var fileFormat in fileFormats) @@ -533,6 +546,11 @@ public static List AllFileExtensions return AvailableFormats.SelectMany(format => format.FileExtensions).FirstOrDefault(ext => ext.Equals(extension)); } + public static IEnumerable FindExtensions(string extension) + { + return AvailableFormats.SelectMany(format => format.FileExtensions).Where(ext => ext.Equals(extension)); + } + public static string? GetFileNameStripExtensions(string? filepath) { if (filepath is null) return null; diff --git a/UVtools.Core/FileFormats/ImageFile.cs b/UVtools.Core/FileFormats/ImageFile.cs index dc26d711..c0a51eb1 100644 --- a/UVtools.Core/FileFormats/ImageFile.cs +++ b/UVtools.Core/FileFormats/ImageFile.cs @@ -25,7 +25,7 @@ public class ImageFile : FileFormat new (typeof(ImageFile), "pgm", "PGM: Portable Greymap"), //new (typeof(ImageFile), "gif", "GIF"), new (typeof(ImageFile), "sr", "SR: Sun raster"), - new (typeof(ImageFile), "RAS", "RAS: Sun raster"), + new (typeof(ImageFile), "ras", "RAS: Sun raster"), }; public override PrintParameterModifier[]? PrintParameterModifiers => null; diff --git a/UVtools.Core/Gerber/Apertures/Aperture.cs b/UVtools.Core/Gerber/Apertures/Aperture.cs new file mode 100644 index 00000000..f6fc560e --- /dev/null +++ b/UVtools.Core/Gerber/Apertures/Aperture.cs @@ -0,0 +1,102 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Drawing; +using System.Linq; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; + +namespace UVtools.Core.Gerber.Apertures; + +public abstract class Aperture +{ + #region Properties + /// + /// Gets the index of this aperture + /// + public int Index { get; set; } + + /// + /// Gets the aperture name + /// + public string Name { get; set; } = string.Empty; + + #endregion + + protected Aperture() { } + + protected Aperture(int index) { Index = index; } + protected Aperture(string name) { Name = name; } + protected Aperture(int index, string name) : this(index) { Name = name; } + + public abstract void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected); + + public static Aperture? Parse(string line, GerberDocument document) + { + var match = Regex.Match(line, @"\%ADD(\d+)(\S+),(\S+)\*\%"); + if (!match.Success || match.Groups.Count < 4) return null; + + if (!int.TryParse(match.Groups[1].Value, out var index)) return null; + //if (!char.TryParse(match.Groups[2].Value, out var type)) return null; + + + switch (match.Groups[2].Value) + { + case "C": + { + if (!double.TryParse(match.Groups[3].Value, out var diameter)) return null; + return new CircleAperture(index, diameter); + } + case "O": + { + var split = match.Groups[3].Value.Split('X', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length < 2) return null; + if (!float.TryParse(split[0], out var width)) return null; + if (!float.TryParse(split[1], out var height)) return null; + + return new EllipseAperture(index, width, height); + } + case "R": + { + var split = match.Groups[3].Value.Split('X', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length < 2) return null; + if (!float.TryParse(split[0], out var width)) return null; + if (!float.TryParse(split[1], out var height)) return null; + + return new RectangleAperture(index, width, height); + } + case "P": + { + var split = match.Groups[3].Value.Split('X', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length < 2) return null; + if (!double.TryParse(split[0], out var diameter)) return null; + if (!ushort.TryParse(split[1], out var vertices)) return null; + + return new PolygonAperture(index, diameter, vertices); + } + default: // macro + { + if (!document.Macros.TryGetValue(match.Groups[2].Value, out var macro)) return null; + var parseLine = line.TrimEnd('%', '*'); + var commaIndex = parseLine.IndexOf(',')+1; + if (commaIndex == 0) return null; + parseLine = parseLine[commaIndex..]; + var args = new[] {"0"}.Concat(parseLine.Split('X', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)).ToArray(); + foreach (var primitive in macro) + { + primitive.ParseExpressions(args); + } + + return new MacroAperture(index, macro); + } + } + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Apertures/CircleAperture.cs b/UVtools.Core/Gerber/Apertures/CircleAperture.cs new file mode 100644 index 00000000..1d8e5b5f --- /dev/null +++ b/UVtools.Core/Gerber/Apertures/CircleAperture.cs @@ -0,0 +1,37 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Apertures; + +public class CircleAperture : Aperture +{ + #region Properties + public double Diameter { get; set; } + #endregion + + #region Constructor + public CircleAperture() : base("Circle") { } + + public CircleAperture(int index, double diameter) : base(index, "Circle") + { + Diameter = diameter; + } + #endregion + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, + LineType lineType = LineType.EightConnected) + { + CvInvoke.Circle(mat, at, (int)(Diameter * xyPpmm.Max() / 2), color, -1, lineType); + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Apertures/EllipseAperture.cs b/UVtools.Core/Gerber/Apertures/EllipseAperture.cs new file mode 100644 index 00000000..3174609a --- /dev/null +++ b/UVtools.Core/Gerber/Apertures/EllipseAperture.cs @@ -0,0 +1,42 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; + +namespace UVtools.Core.Gerber.Apertures; + +public class EllipseAperture : Aperture +{ + #region Properties + public SizeF Axes { get; set; } + #endregion + + #region Constructor + public EllipseAperture() : base("Ellipse") { } + + public EllipseAperture(int index, float width, float height) : base(index, "Ellipse") + { + Axes = new SizeF(width, height); + } + + public EllipseAperture(int index, SizeF axes) : base(index, "Ellipse") + { + Axes = axes; + } + #endregion + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, + LineType lineType = LineType.EightConnected) + { + var axes = new Size((int) (Axes.Width * xyPpmm.Width / 2), (int) (Axes.Height * xyPpmm.Height / 2)); + CvInvoke.Ellipse(mat, at, axes, 0, 0, 360, color, -1, lineType); + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Apertures/MacroAperture.cs b/UVtools.Core/Gerber/Apertures/MacroAperture.cs new file mode 100644 index 00000000..54c199e3 --- /dev/null +++ b/UVtools.Core/Gerber/Apertures/MacroAperture.cs @@ -0,0 +1,38 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; + +namespace UVtools.Core.Gerber.Apertures; + +public class MacroAperture : Aperture +{ + #region Properties + public Macro Macro { get; set; } + #endregion + + #region Constructor + public MacroAperture() : base("Macro") { } + + public MacroAperture(int index, Macro macro) : base(index, "Macro") + { + Macro = macro; + } + #endregion + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected) + { + foreach (var macro in Macro) + { + macro.DrawFlashD3(mat, xyPpmm, at, color, lineType); + } + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Apertures/PoygonAperture.cs b/UVtools.Core/Gerber/Apertures/PoygonAperture.cs new file mode 100644 index 00000000..08ac091a --- /dev/null +++ b/UVtools.Core/Gerber/Apertures/PoygonAperture.cs @@ -0,0 +1,40 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Apertures; + +public class PolygonAperture : Aperture +{ + #region Properties + public double Diameter { get; set; } + public ushort Vertices { get; set; } + #endregion + + #region Constructor + public PolygonAperture() : base("Polygon") { } + + public PolygonAperture(int index, double diameter, ushort vertices) : base(index, "Polygon") + { + Diameter = diameter; + Vertices = vertices; + } + #endregion + + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, + LineType lineType = LineType.EightConnected) + { + mat.DrawPolygon(Vertices, (int)(Diameter * xyPpmm.Max() / 2), at, color, 0, -1, lineType); + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Apertures/RectangleAperture.cs b/UVtools.Core/Gerber/Apertures/RectangleAperture.cs new file mode 100644 index 00000000..4f58eca6 --- /dev/null +++ b/UVtools.Core/Gerber/Apertures/RectangleAperture.cs @@ -0,0 +1,43 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; + +namespace UVtools.Core.Gerber.Apertures; + +public class RectangleAperture : Aperture +{ + #region Properties + public SizeF Size { get; set; } + #endregion + + #region Constructor + public RectangleAperture() : base("Rectangle") { } + + public RectangleAperture(int index, float width, float height) : base(index, "Rectangle") + { + Size = new SizeF(width, height); + } + + public RectangleAperture(int index, SizeF size) : base(index, "Rectangle") + { + Size = size; + } + #endregion + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, + LineType lineType = LineType.EightConnected) + { + var size = new Size((int) (Size.Width * xyPpmm.Width), (int) (Size.Height * xyPpmm.Height)); + at.Offset(-size.Width / 2, -size.Height / 2); + CvInvoke.Rectangle(mat, new Rectangle(at, size), color, -1, lineType); + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Enumerations.cs b/UVtools.Core/Gerber/Enumerations.cs new file mode 100644 index 00000000..0e38c42b --- /dev/null +++ b/UVtools.Core/Gerber/Enumerations.cs @@ -0,0 +1,37 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +namespace UVtools.Core.Gerber; + +public enum GerberPositionType : byte +{ + Absolute, + Relative +} + +public enum GerberUnitType : byte +{ + Millimeter, + Inch +} + +public enum GerberPolarityType : byte +{ + Dark, + Clear +} + + +public enum GerberMoveType : byte +{ + // G01 + Linear, + // G02 + Arc, + // G03 + ArcCounterClockwise +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/GerberDocument.cs b/UVtools.Core/Gerber/GerberDocument.cs new file mode 100644 index 00000000..1b29fb3b --- /dev/null +++ b/UVtools.Core/Gerber/GerberDocument.cs @@ -0,0 +1,537 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Emgu.CV.Util; +using UVtools.Core.Extensions; +using UVtools.Core.Gerber.Apertures; + +namespace UVtools.Core.Gerber +{ + public class GerberDocument + { + #region Properties + + public GerberPositionType PositionType { get; set; } = GerberPositionType.Absolute; + public GerberUnitType UnitType { get; set; } = GerberUnitType.Millimeter; + public GerberPolarityType Polarity { get; set; } = GerberPolarityType.Dark; + public GerberMoveType MoveType { get; set; } = GerberMoveType.Linear; + + public bool LeadingZeroOmitted { get; set; } = true; + + public byte CoordinateXIntegers { get; set; } = 3; + public byte CoordinateXFractionalDigits { get; set; } = 6; + + public byte CoordinateYIntegers { get; set; } = 3; + public byte CoordinateYFractionalDigits { get; set; } = 6; + + public Dictionary Apertures { get; } = new(); + public Dictionary Macros { get; } = new(); + + #endregion + + + public GerberDocument() + { + } + + public GerberDocument(string filePath) + { + } + + public static GerberDocument ParseAndDraw(string filePath, Mat mat, bool enableAntialiasing = false) + { + using var file = new StreamReader(filePath); + var document = new GerberDocument(); + + int FSlength = "%FSLAX46Y46*%".Length; + int MOlength = "%MOMM*%".Length; + int LPlength = "%LPD*%".Length; + + SizeF xyPpmm = new SizeF(20, 20); + + double currentX = 0; + double currentY = 0; + Aperture? currentAperture = null; + Macro? currentMacro = null; + var regionPoints = new List(); + bool insideRegion = false; + string? line; + while ((line = file.ReadLine()) is not null) + { + line = line.Trim(); + if(line == string.Empty) continue; + if (line.StartsWith("M02")) break; + + var accumulatedLine = line; + while (!accumulatedLine.Contains('*') && (line = file.ReadLine()) is not null) + { + line = line.Trim(); + if (line == string.Empty) continue; + accumulatedLine += line; + } + + line = accumulatedLine; + + if(currentMacro is not null) + { + currentMacro.ParsePrimitive(line); + if (line[^1] == '%') currentMacro = null; + continue; + } + + if (line.StartsWith("%MO") && line.Length >= MOlength) + { + if(line[3] == 'M' && line[4] == 'M') document.UnitType = GerberUnitType.Millimeter; + else if(line[3] == 'I' && line[4] == 'N') document.UnitType = GerberUnitType.Inch; + continue; + } + + if (line.StartsWith("%FS") && line.Length >= FSlength) + { + // %FSLAX34Y34*% + // 0123456789 + document.LeadingZeroOmitted = line[3] switch + { + 'L' => true, + 'T' => false, + _ => document.LeadingZeroOmitted + }; + document.PositionType = line[4] switch + { + 'A' => GerberPositionType.Absolute, + 'I' => GerberPositionType.Relative, + _ => document.PositionType + }; + if (line[5] != 'X') continue; + if (byte.TryParse(line[6].ToString(), out var x1)) document.CoordinateXIntegers = x1; + if (byte.TryParse(line[7].ToString(), out var x2)) document.CoordinateXFractionalDigits = x2; + if (line[8] != 'Y') continue; + if (byte.TryParse(line[9].ToString(), out var y1)) document.CoordinateYIntegers = y1; + if (byte.TryParse(line[10].ToString(), out var y2)) document.CoordinateYFractionalDigits = y2; + continue; + } + + if (line.StartsWith("%LP") && line.Length >= LPlength) + { + document.Polarity = line[3] switch + { + 'D' => GerberPolarityType.Dark, + 'C' => GerberPolarityType.Clear, + _ => document.Polarity + }; + + continue; + } + + if (line.StartsWith("G01")) + { + document.MoveType = GerberMoveType.Linear; + continue; + } + + if (line.StartsWith("G02")) + { + document.MoveType = GerberMoveType.Arc; + continue; + } + + if (line.StartsWith("G03")) + { + document.MoveType = GerberMoveType.ArcCounterClockwise; + continue; + } + + if (line.StartsWith("G36")) + { + insideRegion = true; + regionPoints.Clear(); + continue; + } + + if (line.StartsWith("G37")) + { + insideRegion = false; + if (regionPoints.Count > 0) + { + using var vec = new VectorOfPoint(regionPoints.ToArray()); + CvInvoke.FillPoly(mat, vec, document.Polarity == GerberPolarityType.Dark ? EmguExtensions.WhiteColor : EmguExtensions.BlackColor, enableAntialiasing ? LineType.AntiAlias : LineType.EightConnected); + } + regionPoints.Clear(); + continue; + } + + if (line.StartsWith("%AM")) + { + currentMacro = Macro.Parse(line); + if (currentMacro is null) continue; + document.Macros.Add(currentMacro.Name, currentMacro); + //document.Apertures.Add(aperture.Index, aperture); + continue; + } + + if (line.StartsWith("%ADD")) + { + var aperture = Aperture.Parse(line, document); + if (aperture is null) continue; + currentAperture = aperture; + document.Apertures.Add(aperture.Index, aperture); + continue; + } + + if (line[0] == 'D') + { + var matchD = Regex.Match(line, @"D(\d+)"); + if (!matchD.Success || matchD.Groups.Count < 2) continue; + + if (!int.TryParse(matchD.Groups[1].Value, out var d)) continue; + + if (!document.Apertures.TryGetValue(d, out currentAperture)) continue; + + continue; + } + + if (line[0] == 'X' || line[0] == 'Y') + { + var matchX = Regex.Match(line, @"X-?(\d+)"); + var matchY = Regex.Match(line, @"Y-?(\d+)"); + var matchD = Regex.Match(line, @"D(\d+)"); + + double nowX = 0; + double nowY = 0; + + if (!matchD.Success || matchD.Groups.Count < 2) continue; + if (!int.TryParse(matchD.Groups[1].Value, out var d)) continue; + + if (matchX.Success && matchX.Groups.Count >= 2) + { + if (double.TryParse(matchX.Groups[1].Value, out nowX)) + { + if (nowX != 0) + { + var integers = matchX.Groups[1].Value[..^document.CoordinateXFractionalDigits]; + var fraction = matchX.Groups[1].Value.Substring(matchX.Groups[1].Value.Length - document.CoordinateXFractionalDigits, document.CoordinateXFractionalDigits); + double.TryParse($"{integers}.{fraction}", out nowX); + } + } + } + + if (matchY.Success && matchY.Groups.Count >= 2) + { + if (double.TryParse(matchY.Groups[1].Value, out nowY)) + { + if (nowY != 0) + { + var integers = matchY.Groups[1].Value[..^document.CoordinateYFractionalDigits]; + var fraction = matchY.Groups[1].Value.Substring(matchY.Groups[1].Value.Length - document.CoordinateYFractionalDigits, document.CoordinateYFractionalDigits); + double.TryParse($"{integers}.{fraction}", out nowY); + } + } + } + + if (insideRegion) + { + if (d == 2) + { + if (regionPoints.Count > 0) + { + using var vec = new VectorOfPoint(regionPoints.ToArray()); + CvInvoke.FillPoly(mat, vec, document.Polarity == GerberPolarityType.Dark ? EmguExtensions.WhiteColor : EmguExtensions.BlackColor, enableAntialiasing ? LineType.AntiAlias : LineType.EightConnected); + } + regionPoints.Clear(); + } + + var pt = new Point((int) (nowX * xyPpmm.Width), (int) (nowY * xyPpmm.Height)); + if (regionPoints.Count == 0 || (regionPoints.Count > 0 && regionPoints[^1] != pt)) regionPoints.Add(pt); + } + else if(currentAperture is not null) + { + if (d == 1) + { + if (currentAperture is CircleAperture circleAperture) + { + if (document.MoveType is GerberMoveType.Arc or GerberMoveType.ArcCounterClockwise) + { + var matchI = Regex.Match(line, @"I(-?\d+)"); + var matchJ = Regex.Match(line, @"J(-?\d+)"); + if(!matchI.Success || !matchJ.Success || matchI.Groups.Count < 2 || matchJ.Groups.Count < 2) continue; + + + if (double.TryParse(matchI.Groups[1].Value, out var xOffset)) + { + if (xOffset != 0) + { + var integers = matchI.Groups[1].Value[..^document.CoordinateXFractionalDigits]; + var fraction = matchI.Groups[1].Value.Substring(matchI.Groups[1].Value.Length - document.CoordinateXFractionalDigits, document.CoordinateXFractionalDigits); + double.TryParse($"{integers}.{fraction}", out xOffset); + } + } + + if (double.TryParse(matchJ.Groups[1].Value, out var yOffset)) + { + if(yOffset != 0) + { + var integers = matchJ.Groups[1].Value[..^document.CoordinateYFractionalDigits]; + var fraction = matchJ.Groups[1].Value.Substring(matchJ.Groups[1].Value.Length - document.CoordinateYFractionalDigits, document.CoordinateYFractionalDigits); + double.TryParse($"{integers}.{fraction}", out yOffset); + } + } + + if (currentX == nowX && currentY == nowY) // Closed circle + { + CvInvoke.Ellipse(mat, new Point((int)((nowX + xOffset) * xyPpmm.Width), (int)((nowY + yOffset) * xyPpmm.Height)), + new Size((int)(Math.Abs(xOffset) * xyPpmm.Width), (int)(Math.Abs(xOffset) * xyPpmm.Width)), + 0, 0, 360.0, document.Polarity == GerberPolarityType.Dark ? EmguExtensions.WhiteColor : EmguExtensions.BlackColor, + (int)(circleAperture.Diameter * xyPpmm.Max()), + enableAntialiasing ? LineType.AntiAlias : LineType.EightConnected + ); + } + else + { + // TODO: Fix this + throw new NotImplementedException("Partial arcs are not yet implemented (G03)\nTo be fixed in the future..."); + CvInvoke.Ellipse(mat, new Point((int)((nowX + xOffset) * xyPpmm.Width), (int)((currentY) * xyPpmm.Height)), + new Size((int)(Math.Abs(xOffset) * xyPpmm.Width), (int)(Math.Abs(yOffset) * xyPpmm.Width)), + 0, Math.Abs(currentY - nowY), 360.0 / Math.Abs(currentX - nowX), document.Polarity == GerberPolarityType.Dark ? EmguExtensions.WhiteColor : EmguExtensions.BlackColor, + (int)(circleAperture.Diameter * xyPpmm.Max()), + enableAntialiasing ? LineType.AntiAlias : LineType.EightConnected + ); + } + + } + else + { + CvInvoke.Line(mat, + new Point((int)(currentX * xyPpmm.Width), (int)(currentY * xyPpmm.Height)), + new Point((int)(nowX * xyPpmm.Width), (int)(nowY * xyPpmm.Height)), + document.Polarity == GerberPolarityType.Dark ? EmguExtensions.WhiteColor : EmguExtensions.BlackColor, + (int)(circleAperture.Diameter * xyPpmm.Max()), + enableAntialiasing ? LineType.AntiAlias : LineType.EightConnected); + } + + } + } + else if (d == 3) + { + currentAperture.DrawFlashD3(mat, xyPpmm, new Point((int)(nowX * xyPpmm.Width), (int)(nowY * xyPpmm.Height)), document.Polarity == GerberPolarityType.Dark ? EmguExtensions.WhiteColor : EmguExtensions.BlackColor, enableAntialiasing ? LineType.AntiAlias : LineType.EightConnected); + } + } + + currentX = nowX; + currentY = nowY; + continue; + } + } + + return document; + } + } +} + + + +/* KIDCAD + var document = File.ReadAllLines(@"D:\Tiago\Desktop\kisample\kisample.kicad_pcb"); + System.Drawing.PointF location = PointF.Empty; + + using var mat = EmguExtensions.InitMat(new System.Drawing.Size(2440, 1440)); + const byte pixelsPerMm = 20; + + var drillPoints = new List>(); + + foreach (var line in document) + { + var parseLine = line.Trim(); + if (parseLine.StartsWith("(footprint ")) + { + location = PointF.Empty; + continue; + } + if (location.IsEmpty && parseLine.StartsWith("(at ")) + { + parseLine = parseLine.Substring(4, parseLine.Length-5); + var split = parseLine.Split(' '); + location = new PointF(float.Parse(split[0]), float.Parse(split[1])); + continue; + } + if (parseLine.StartsWith("(segment ") || parseLine.StartsWith("(gr_line ")) + { + var layerMatch = Regex.Match(parseLine, @"\S.Cu"); + if (!layerMatch.Success || layerMatch.Groups.Count < 1) continue; + + var startMatch = Regex.Match(parseLine, @"\(start\s+(\S+)\s+(\S+)\)"); + if(!startMatch.Success || startMatch.Groups.Count < 3) continue; + + var endMatch = Regex.Match(parseLine, @"\(end\s+(\S+)\s+(\S+)\)"); + if (!endMatch.Success || endMatch.Groups.Count < 3) continue; + + var widthMatch = Regex.Match(parseLine, @"\(width\s+(\S+)\)"); + if (!widthMatch.Success || widthMatch.Groups.Count < 2) continue; + + var startXf = new PointF(float.Parse(startMatch.Groups[1].Value), float.Parse(startMatch.Groups[2].Value)); + var endXf = new PointF(float.Parse(endMatch.Groups[1].Value), float.Parse(endMatch.Groups[2].Value)); + var widthf = float.Parse(widthMatch.Groups[1].Value); + + var startX = new System.Drawing.Point((int)(startXf.X * pixelsPerMm), (int)(startXf.Y * pixelsPerMm)); + var endX = new System.Drawing.Point((int)(endXf.X * pixelsPerMm), (int)(endXf.Y * pixelsPerMm)); + var width = (int) (widthf * pixelsPerMm); + + CvInvoke.Line(mat, startX, endX, EmguExtensions.WhiteColor, width); + + continue; + } + + if (parseLine.StartsWith("(via ")) + { + var layerMatches = Regex.Matches(parseLine, @"\S.Cu"); + if (layerMatches.Count < 1) continue; + + var atMatch = Regex.Match(parseLine, @"\(at\s+(\S+)\s+(\S+)\)"); + if (!atMatch.Success || atMatch.Groups.Count < 3) continue; + + var drillMatch = Regex.Match(parseLine, @"\(drill\s+(\S+)\)"); + + + var atf = new PointF(float.Parse(atMatch.Groups[1].Value), float.Parse(atMatch.Groups[2].Value)); + //var sizef = new SizeF(float.Parse(sizeMatch.Groups[1].Value), float.Parse(sizeMatch.Groups[2].Value)); + + var at = new System.Drawing.Point((int)(atf.X * pixelsPerMm), (int)(atf.Y * pixelsPerMm)); + if (!drillMatch.Success || drillMatch.Groups.Count < 2) continue; + var drillf = float.Parse(drillMatch.Groups[1].Value); + var drill = (int) (drillf * pixelsPerMm / 2); + + CvInvoke.Circle(mat, at, drill, EmguExtensions.WhiteColor, -1); + + + continue; + } + + if (parseLine.StartsWith("(gr_circle ")) + { + var layerMatch = Regex.Match(parseLine, @"\S.Cu"); + if (!layerMatch.Success || layerMatch.Groups.Count < 1) continue; + + var atMatch = Regex.Match(parseLine, @"\(center\s+(\S+)\s+(\S+)\)"); + if (!atMatch.Success || atMatch.Groups.Count < 3) continue; + + var endMatch = Regex.Match(parseLine, @"\(end\s+(\S+)\s+(\S+)\)"); + if (!endMatch.Success || endMatch.Groups.Count < 3) continue; + + var widthMatch = Regex.Match(parseLine, @"\(width\s+(\S+)\)"); + if (!widthMatch.Success || widthMatch.Groups.Count < 2) continue; + + var atf = new PointF(float.Parse(atMatch.Groups[1].Value), float.Parse(atMatch.Groups[2].Value)); + var at = new System.Drawing.Point((int)(atf.X * pixelsPerMm), (int)(atf.Y * pixelsPerMm)); + var endf = new PointF(float.Parse(endMatch.Groups[1].Value), float.Parse(endMatch.Groups[2].Value)); + var radius = (int)(Math.Max(Math.Abs(atf.X - endf.X), Math.Abs(atf.Y - endf.Y)) * pixelsPerMm); + var widthf = float.Parse(widthMatch.Groups[1].Value); + var width = (int)(widthf * pixelsPerMm); + + CvInvoke.Circle(mat, at, radius, EmguExtensions.WhiteColor, width); + if (parseLine.Contains("fill solid")) + { + CvInvoke.Circle(mat, at, radius, EmguExtensions.WhiteColor, -1); + } + + + continue; + } + if (parseLine.StartsWith("(gr_rect ")) + { + var layerMatch = Regex.Match(parseLine, @"\S.Cu"); + if (!layerMatch.Success || layerMatch.Groups.Count < 1) continue; + + var startMatch = Regex.Match(parseLine, @"\(start\s+(\S+)\s+(\S+)\)"); + if (!startMatch.Success || startMatch.Groups.Count < 3) continue; + + var endMatch = Regex.Match(parseLine, @"\(end\s+(\S+)\s+(\S+)\)"); + if (!endMatch.Success || endMatch.Groups.Count < 3) continue; + + var widthMatch = Regex.Match(parseLine, @"\(width\s+(\S+)\)"); + if (!widthMatch.Success || widthMatch.Groups.Count < 2) continue; + + var startf = new PointF(float.Parse(startMatch.Groups[1].Value), float.Parse(startMatch.Groups[2].Value)); + var endf = new PointF(float.Parse(endMatch.Groups[1].Value), float.Parse(endMatch.Groups[2].Value)); + var widthf = float.Parse(widthMatch.Groups[1].Value); + + var start = new System.Drawing.Point((int)(startf.X * pixelsPerMm), (int)(startf.Y * pixelsPerMm)); + var end = new System.Drawing.Point((int)(endf.X * pixelsPerMm), (int)(endf.Y * pixelsPerMm)); + var width = (int)(widthf * pixelsPerMm); + + CvInvoke.Rectangle(mat, new Rectangle(start, new System.Drawing.Size(end.X - start.X, end.Y - start.Y)), EmguExtensions.WhiteColor, width); + if (parseLine.Contains("fill solid")) + { + CvInvoke.Rectangle(mat, new Rectangle(start, new System.Drawing.Size(end.X - start.X, end.Y - start.Y)), EmguExtensions.WhiteColor, -1); + } + + continue; + } + + if (location.IsEmpty) continue; + + if (parseLine.StartsWith("(pad ")) + { + var layerMatch = Regex.Match(parseLine, @"\S.Cu"); + if (!layerMatch.Success || layerMatch.Groups.Count < 1) continue; + + var atMatch = Regex.Match(parseLine, @"\(at\s+(\S+)\s+(\S+)\)"); + if (!atMatch.Success || atMatch.Groups.Count < 3) continue; + + var sizeMatch = Regex.Match(parseLine, @"\(size\s+(\S+)\s+(\S+)\)"); + if (!sizeMatch.Success || sizeMatch.Groups.Count < 3) continue; + + var drillMatch = Regex.Match(parseLine, @"\(drill\s+(\S+)\)"); + + + var atf = new PointF(float.Parse(atMatch.Groups[1].Value), float.Parse(atMatch.Groups[2].Value)); + var sizef = new SizeF(float.Parse(sizeMatch.Groups[1].Value), float.Parse(sizeMatch.Groups[2].Value)); + + var at = new System.Drawing.Point((int)(location.X * pixelsPerMm + atf.X * pixelsPerMm), (int)(location.Y * pixelsPerMm + atf.Y * pixelsPerMm)); + + if (parseLine.Contains(" rect ") || parseLine.Contains(" roundrect ")) + { + var size = new System.Drawing.Size((int)(sizef.Width * pixelsPerMm), (int)(sizef.Height * pixelsPerMm)); + var rect = new Rectangle(at, size); + rect.Offset(-size.Width / 2, -size.Height / 2); + CvInvoke.Rectangle(mat, rect, EmguExtensions.WhiteColor, -1); + } + else if (parseLine.Contains(" oval ") || parseLine.Contains(" circle ")) + { + var size = new System.Drawing.Size((int)(sizef.Width / 2 * pixelsPerMm), (int)(sizef.Height / 2 * pixelsPerMm)); + CvInvoke.Ellipse(mat, at, size, 0, 0, 360, EmguExtensions.WhiteColor, -1); + } + + if (drillMatch.Success && drillMatch.Groups.Count >= 2) + { + var drillf = float.Parse(drillMatch.Groups[1].Value); + var drill = (int)(drillf * pixelsPerMm / 2); + + drillPoints.Add(new KeyValuePair(at, drill)); + } + + + continue; + } + } + + foreach (var pair in drillPoints) + { + CvInvoke.Circle(mat, pair.Key, pair.Value, EmguExtensions.BlackColor, -1); + } + + CvInvoke.Imshow("asd", mat); + CvInvoke.WaitKey(); + return; + */ \ No newline at end of file diff --git a/UVtools.Core/Gerber/Macro.cs b/UVtools.Core/Gerber/Macro.cs new file mode 100644 index 00000000..6d9a0137 --- /dev/null +++ b/UVtools.Core/Gerber/Macro.cs @@ -0,0 +1,114 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UVtools.Core.Gerber.Primitives; + +namespace UVtools.Core.Gerber; + +public class Macro : IReadOnlyList +{ + #region Properties + + /// + /// Gets the macro name + /// + public string Name { get; set; } = string.Empty; + + public List Primitives { get; } = new(); + #endregion + + public Macro() { } + + public Macro(string name) + { + Name = name; + } + + public void ParsePrimitive(string line) + { + line = line.TrimEnd('%', '*'); + + // 0 Comment: A comment string + if (line[0] == '0') + { + if(line.Length > 2) Primitives.Add(new CommentPrimitive(line[2..])); + return; + } + + var commaSplit = line.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if(!byte.TryParse(commaSplit[0], out var code)) return; + switch (code) + { + // 1 Circle: Exposure, Diameter, Center X, Center Y[, Rotation] + case CirclePrimitive.Code: + { + var primitive = new CirclePrimitive(commaSplit[1], commaSplit[2], commaSplit[3], commaSplit[4]); + if (commaSplit.Length > 5) primitive.RotationExpression = commaSplit[5]; + Primitives.Add(primitive); + break; + } + // 20 Vector Line: Exposure, Width, Start X, Start Y, End X, End Y, Rotation + case VectorLinePrimitive.Code: + { + var primitive = new VectorLinePrimitive(commaSplit[1], commaSplit[2], commaSplit[3], commaSplit[4], commaSplit[5], commaSplit[6]); + if (commaSplit.Length > 7) primitive.RotationExpression = commaSplit[7]; + Primitives.Add(primitive); + break; + } + // 21 Center Line: Exposure, Width, Hight, Center X, Center Y, Rotation + case CenterLinePrimitive.Code: + { + var primitive = new CenterLinePrimitive(commaSplit[1], commaSplit[2], commaSplit[3], commaSplit[4], commaSplit[5]); + if (commaSplit.Length > 6) primitive.RotationExpression = commaSplit[6]; + Primitives.Add(primitive); + break; + } + // 4 Outline: Exposure, # vertices, Start X, Start Y, Subsequent points..., Rotation + case OutlinePrimitive.Code: + { + Primitives.Add(new OutlinePrimitive(commaSplit[1], commaSplit[3..^1], commaSplit[^1])); + break; + } + // 5 Outline: Exposure, # vertices, Start X, Start Y, Subsequent points..., Rotation + case PolygonPrimitive.Code: + { + var primitive = new PolygonPrimitive(commaSplit[1], commaSplit[2], commaSplit[3], commaSplit[4], commaSplit[5]); + if (commaSplit.Length > 6) primitive.RotationExpression = commaSplit[6]; + Primitives.Add(primitive); + break; + } + } + } + + + public static Macro? Parse(string line) + { + var match = Regex.Match(line, @"\%AM(\S+)\*"); + if (!match.Success || match.Groups.Count < 2) return null; + + return new Macro(match.Groups[1].Value); + } + + public IEnumerator GetEnumerator() + { + return Primitives.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) Primitives).GetEnumerator(); + } + + public int Count => Primitives.Count; + + public Primitive this[int index] => Primitives[index]; +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/CenterLinePrimitive.cs b/UVtools.Core/Gerber/Primitives/CenterLinePrimitive.cs new file mode 100644 index 00000000..d882eea2 --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/CenterLinePrimitive.cs @@ -0,0 +1,166 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Data; +using System.Drawing; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Primitives; + +/// +/// A vector line is a rectangle defined by its line width, start and end points. The line ends are rectangular. +/// +public class CenterLinePrimitive : Primitive +{ + #region Constants + public const byte Code = 21; + #endregion + + #region Properties + public override string Name => "VectorLine"; + + /// + /// Exposure off/on (0/1) + /// 1 + /// + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// + /// Width ≥ 0 + /// 2 + /// + public string WidthExpression { get; set; } = "0"; + public float Width { get; set; } + + /// + /// Height ≥ 0 + /// 3 + /// + public string HeightExpression { get; set; } = "0"; + public float Height { get; set; } + + /// + /// Center point X coordinate + /// 4 + /// + public string CenterXExpression { get; set; } = "0"; + + public float CenterX { get; set; } + + /// + /// Center point Y coordinate + /// 5 + /// + public string CenterYExpression { get; set; } = "0"; + + public float CenterY { get; set; } + + /// + /// Rotation angle, in degrees counterclockwise, a decimal. + /// The primitive is rotated around the origin of the macro definition, i.e. the (0, 0) point of macro coordinates. + /// 6 + /// + public string RotationExpression { get; set; } = "0"; + public float Rotation { get; set; } = 0; + #endregion + + protected CenterLinePrimitive() { } + + public CenterLinePrimitive(string exposureExpression, string widthExpression = "0", string heightExpression = "0", string centerXExpression = "0", string centerYExpression = "0", string rotationExpression = "0") + { + ExposureExpression = exposureExpression; + WidthExpression = widthExpression; + HeightExpression = heightExpression; + CenterXExpression = centerXExpression; + CenterYExpression = centerYExpression; + RotationExpression = rotationExpression; + } + + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected) + { + if (!IsParsed) return; + if (Width <= 0 || Height <= 0) return; + + if (Exposure == 0) color = EmguExtensions.BlackColor; + else if(color.V0 == 0) color = EmguExtensions.WhiteColor; + + var halfWidth = Width / 2; + var pt1 = new Point(at.X + (int)((CenterX - halfWidth) * xyPpmm.Width), at.Y + (int)(CenterY * xyPpmm.Height)); + var pt2 = new Point(at.X + (int)((CenterX + halfWidth) * xyPpmm.Width), at.Y + (int)(CenterY * xyPpmm.Height)); + CvInvoke.Line(mat, pt1, pt2, color, (int)(Height * xyPpmm.Height), lineType); + //CvInvoke.Rectangle(mat, rectangle, color, -1, lineType); + } + + public override void ParseExpressions(params string[] args) + { + string csharpExp, result; + float num; + var exp = new DataTable(); + + if (byte.TryParse(ExposureExpression, out var exposure)) Exposure = exposure; + else + { + csharpExp = string.Format(Regex.Replace(ExposureExpression, @"\$(\d+)", "{$1}"), args); + result = exp.Compute(csharpExp, null).ToString()!; + if (byte.TryParse(result, out var val)) Exposure = val; + } + + if (float.TryParse(WidthExpression, out num)) Width = num; + else + { + csharpExp = Regex.Replace(WidthExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out var val)) Width = val; + } + + if (float.TryParse(HeightExpression, out num)) Height = num; + else + { + csharpExp = Regex.Replace(HeightExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out var val)) Height = val; + } + + if (float.TryParse(CenterXExpression, out num)) CenterX = num; + else + { + csharpExp = Regex.Replace(CenterXExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) CenterX = num; + } + + if (float.TryParse(CenterYExpression, out num)) CenterY = num; + else + { + csharpExp = Regex.Replace(CenterYExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) CenterY = num; + } + + if (float.TryParse(RotationExpression, out num)) Rotation = (short)num; + else + { + csharpExp = Regex.Replace(RotationExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Rotation = num; + } + + IsParsed = true; + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/CirclePrimitive.cs b/UVtools.Core/Gerber/Primitives/CirclePrimitive.cs new file mode 100644 index 00000000..73eb04d1 --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/CirclePrimitive.cs @@ -0,0 +1,149 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Data; +using System.Drawing; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Primitives; + +/// +/// A circle primitive is defined by its center point and diameter. +/// +public class CirclePrimitive : Primitive +{ + #region Constants + public const byte Code = 1; + #endregion + + #region Properties + public override string Name => "Circle"; + + /// + /// Exposure off/on (0/1) + /// 1 + /// + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// + /// Diameter ≥ 0 + /// 2 + /// + public string DiameterExpression { get; set; } = "0"; + public float Diameter { get; set; } + + /// + /// Center X coordinate. + /// 3 + /// + public string CenterXExpression { get; set; } = "0"; + + public float CenterX { get; set; } + + /// + /// Center Y coordinate. + /// 4 + /// + public string CenterYExpression { get; set; } = "0"; + + public float CenterY { get; set; } + + /// + /// Rotation angle, in degrees counterclockwise, a decimal. + /// The primitive is rotated around the origin of the macro definition, i.e. the (0, 0) point of macro coordinates. + /// 5 + /// + public string RotationExpression { get; set; } = "0"; + public float Rotation { get; set; } = 0; + #endregion + + protected CirclePrimitive() { } + + public CirclePrimitive(string exposureExpression = "1", string diameterExpression = "0", string centerXExpression = "0", string centerYExpression = "0", string rotationExpression = "0") + { + ExposureExpression = exposureExpression; + DiameterExpression = diameterExpression; + CenterXExpression = centerXExpression; + CenterYExpression = centerYExpression; + RotationExpression = rotationExpression; + } + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected) + { + if (!IsParsed) return; + if (Diameter <= 0) return; + + if (Exposure == 0) color = EmguExtensions.BlackColor; + else if (color.V0 == 0) color = EmguExtensions.WhiteColor; + + var position = new Point( + (int) (at.X + CenterX * xyPpmm.Width), + (int) (at.Y + CenterY * xyPpmm.Height) + ); + CvInvoke.Circle(mat, position, (int)(Diameter * xyPpmm.Max() / 2), color, -1, lineType); + } + + public override void ParseExpressions(params string[] args) + { + string csharpExp, result; + float num; + var exp = new DataTable(); + + if (byte.TryParse(ExposureExpression, out var exposure)) Exposure = exposure; + else + { + csharpExp = string.Format(Regex.Replace(ExposureExpression, @"\$(\d+)", "{$1}"), args); + result = exp.Compute(csharpExp, null).ToString()!; + if (byte.TryParse(result, out var val)) Exposure = val; + } + + if (float.TryParse(DiameterExpression, out num)) Diameter = num; + else + { + csharpExp = Regex.Replace(DiameterExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Diameter = num; + } + + if (float.TryParse(CenterXExpression, out num)) CenterX = num; + else + { + csharpExp = Regex.Replace(CenterXExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) CenterX = num; + } + + if (float.TryParse(CenterYExpression, out num)) CenterY = num; + else + { + csharpExp = Regex.Replace(CenterYExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) CenterY = num; + } + + + if (float.TryParse(RotationExpression, out num)) Rotation = (short)num; + else + { + csharpExp = Regex.Replace(RotationExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Rotation = num; + } + + IsParsed = true; + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/CommentPrimitive.cs b/UVtools.Core/Gerber/Primitives/CommentPrimitive.cs new file mode 100644 index 00000000..66596c39 --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/CommentPrimitive.cs @@ -0,0 +1,57 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; + +namespace UVtools.Core.Gerber.Primitives; + +/// +/// The comment primitive has no effect on the image but adds human-readable comments in an AM command. +/// The comment primitive starts with the ‘0’ code followed by a space and then a single-line text string. +/// The text string follows the syntax for strings in section 3.4.3. +/// +public class CommentPrimitive : Primitive +{ + #region Constants + public const byte Code = 0; + #endregion + #region Properties + public override string Name => "Comment"; + + /// + /// The comment + /// 1 + /// + public string Comment { get; set; } = string.Empty; + #endregion + + public CommentPrimitive() + { + IsParsed = true; + } + + public CommentPrimitive(string comment) + { + Comment = comment; + IsParsed = true; + } + + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, + LineType lineType = LineType.EightConnected) + { + + } + + public override void ParseExpressions(params string[] args) + { + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/OutlinePrimitive.cs b/UVtools.Core/Gerber/Primitives/OutlinePrimitive.cs new file mode 100644 index 00000000..0addbcfa --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/OutlinePrimitive.cs @@ -0,0 +1,150 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Emgu.CV.Util; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Primitives; + +/// +/// An outline primitive is an area defined by its outline or contour. +/// The outline is a polygon, consisting of linear segments only, defined by its start vertex and n subsequent vertices. +/// The outline must be closed, i.e. the last vertex must be equal to the start vertex. +/// The outline must comply with all the requirements of a contour according to 4.10.3. +/// +public class OutlinePrimitive : Primitive +{ + #region Constants + public const byte Code = 4; + #endregion + + #region Properties + public override string Name => "Outline"; + + /// + /// Exposure off/on (0/1) + /// 1 + /// + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// + /// The number of vertices of the outline = the number of coordinate pairs minus one. An integer ≥3. + /// 2 + /// + public string VerticesCountExpression { get; set; } = string.Empty; + public ushort VerticesCount => (ushort) Coordinates.Length; + + /// + /// subsequent X and Y coordinates. + /// The X and Y coordinates are not modal: both X and Y must be specified for all points. + /// 2+n + /// + public string[] CoordinatesExpression { get; set; } = Array.Empty(); + + public PointF[] Coordinates { get; set; } = Array.Empty(); + + /// + /// Rotation angle, in degrees counterclockwise, a decimal. + /// The primitive is rotated around the origin of the macro definition, i.e. the (0, 0) point of macro coordinates. + /// + public string RotationExpression { get; set; } = "0"; + public float Rotation { get; set; } = 0; + #endregion + + protected OutlinePrimitive() { } + + public OutlinePrimitive(string exposureExpression, string[] coordinatesExpression, string rotationExpression) + { + ExposureExpression = exposureExpression; + CoordinatesExpression = coordinatesExpression; + RotationExpression = rotationExpression; + } + + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected) + { + if (Coordinates.Length < 3) return; + + if (Exposure == 0) color = EmguExtensions.BlackColor; + else if (color.V0 == 0) color = EmguExtensions.WhiteColor; + + var points = new List(); + for (int i = 0; i < Coordinates.Length-1; i++) + { + var pt = new Point( + at.X + (int)(Coordinates[i].X * xyPpmm.Width), + at.Y + (int)(Coordinates[i].Y * xyPpmm.Height) + ); + + if(i > 0 && points[i-1] == pt) continue; // Prevent duplicates + points.Add(pt); + } + + using var vec = new VectorOfPoint(points.ToArray()); + CvInvoke.FillPoly(mat, vec, color, lineType); + } + + public override void ParseExpressions(params string[] args) + { + string csharpExp, result; + float num; + var exp = new DataTable(); + + if (byte.TryParse(ExposureExpression, out var exposure)) Exposure = exposure; + else + { + csharpExp = string.Format(Regex.Replace(ExposureExpression, @"\$(\d+)", "{$1}"), args); + result = exp.Compute(csharpExp, null).ToString()!; + if (byte.TryParse(result, out var val)) Exposure = val; + } + + float? x = null; + var coordinates = new List(); + foreach (var coordinate in CoordinatesExpression) + { + if (!float.TryParse(coordinate, out num)) + { + csharpExp = string.Format(Regex.Replace(coordinate, @"\$(\d+)", "{$1}"), args); + result = exp.Compute(csharpExp, null).ToString()!; + float.TryParse(result, out num); + } + + if (x is null) + { + x = num; + } + else + { + coordinates.Add(new PointF(x.Value, num)); + x = null; + } + } + + Coordinates = coordinates.ToArray(); + + if (float.TryParse(RotationExpression, out num)) Rotation = (short)num; + else + { + csharpExp = Regex.Replace(RotationExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Rotation = num; + } + + IsParsed = true; + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/PolygonPrimitive.cs b/UVtools.Core/Gerber/Primitives/PolygonPrimitive.cs new file mode 100644 index 00000000..88d6fb81 --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/PolygonPrimitive.cs @@ -0,0 +1,165 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Data; +using System.Drawing; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Primitives; + +/// +/// A polygon primitive is a regular polygon defined by the number of vertices n, the center point and the diameter of the circumscribed circle. +/// +public class PolygonPrimitive : Primitive +{ + #region Constants + public const byte Code = 5; + #endregion + + #region Properties + public override string Name => "Polygon"; + + /// + /// Exposure off/on (0/1) + /// 1 + /// + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// + /// Diameter ≥ 0 + /// 2 + /// + public string VerticesCountExpression { get; set; } = "0"; + public byte VerticesCount { get; set; } + + /// + /// Center X coordinate. + /// 3 + /// + public string CenterXExpression { get; set; } = "0"; + public float CenterX { get; set; } + + /// + /// Center Y coordinate. + /// 4 + /// + public string CenterYExpression { get; set; } = "0"; + public float CenterY { get; set; } + + /// + /// Diameter ≥ 0 + /// 5 + /// + public string DiameterExpression { get; set; } = "0"; + public float Diameter { get; set; } + + /// + /// Rotation angle, in degrees counterclockwise, a decimal. + /// The primitive is rotated around the origin of the macro definition, i.e. the (0, 0) point of macro coordinates. + /// 6 + /// + public string RotationExpression { get; set; } = "0"; + public float Rotation { get; set; } = 0; + #endregion + + protected PolygonPrimitive() { } + + public PolygonPrimitive(string exposureExpression, string verticesCountExpression, string centerXExpression = "0", string centerYExpression = "0", string diameterExpression = "0", string rotationExpression = "0") + { + ExposureExpression = exposureExpression; + VerticesCountExpression = verticesCountExpression; + CenterXExpression = centerXExpression; + CenterYExpression = centerYExpression; + DiameterExpression = diameterExpression; + RotationExpression = rotationExpression; + } + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected) + { + if (!IsParsed) return; + if (Diameter <= 0) return; + + if (Exposure == 0) color = EmguExtensions.BlackColor; + else if (color.V0 == 0) color = EmguExtensions.WhiteColor; + + var position = new Point( + (int) (at.X + CenterX * xyPpmm.Width), + (int) (at.Y + CenterY * xyPpmm.Height) + ); + + mat.DrawPolygon(VerticesCount, (int)(Diameter * xyPpmm.Max() / 2), position, color, 0, -1, lineType); + } + + public override void ParseExpressions(params string[] args) + { + string csharpExp, result; + float num; + var exp = new DataTable(); + + if (byte.TryParse(ExposureExpression, out var exposure)) Exposure = exposure; + else + { + csharpExp = string.Format(Regex.Replace(ExposureExpression, @"\$(\d+)", "{$1}"), args); + result = exp.Compute(csharpExp, null).ToString()!; + if (byte.TryParse(result, out var val)) Exposure = val; + } + + if (byte.TryParse(DiameterExpression, out var vertices)) VerticesCount = vertices; + else + { + csharpExp = Regex.Replace(DiameterExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (byte.TryParse(result, out var vertices1)) VerticesCount = vertices1; + } + + if (float.TryParse(CenterXExpression, out num)) CenterX = num; + else + { + csharpExp = Regex.Replace(CenterXExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) CenterX = num; + } + + if (float.TryParse(CenterYExpression, out num)) CenterY = num; + else + { + csharpExp = Regex.Replace(CenterYExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) CenterY = num; + } + + if (float.TryParse(DiameterExpression, out num)) Diameter = num; + else + { + csharpExp = Regex.Replace(DiameterExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Diameter = num; + } + + + if (float.TryParse(RotationExpression, out num)) Rotation = (short)num; + else + { + csharpExp = Regex.Replace(RotationExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Rotation = num; + } + + IsParsed = true; + } +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/Primitive.cs b/UVtools.Core/Gerber/Primitives/Primitive.cs new file mode 100644 index 00000000..51dd0d93 --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/Primitive.cs @@ -0,0 +1,31 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Drawing; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; + +namespace UVtools.Core.Gerber.Primitives; + +public abstract class Primitive +{ + #region Properties + public abstract string Name { get; } + + public bool IsParsed { get; protected set; } = false; + + #endregion + + protected Primitive() { } + + public abstract void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected); + + public abstract void ParseExpressions(params string[] args); +} \ No newline at end of file diff --git a/UVtools.Core/Gerber/Primitives/VectorLinePrimitive.cs b/UVtools.Core/Gerber/Primitives/VectorLinePrimitive.cs new file mode 100644 index 00000000..6c165dd8 --- /dev/null +++ b/UVtools.Core/Gerber/Primitives/VectorLinePrimitive.cs @@ -0,0 +1,184 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Data; +using System.Drawing; +using System.Text.RegularExpressions; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using UVtools.Core.Extensions; + +namespace UVtools.Core.Gerber.Primitives; + +/// +/// A vector line is a rectangle defined by its line width, start and end points. The line ends are rectangular. +/// +public class VectorLinePrimitive : Primitive +{ + #region Constants + public const byte Code = 20; + #endregion + + #region Properties + public override string Name => "VectorLine"; + + /// + /// Exposure off/on (0/1) + /// 1 + /// + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// + /// Width of the line ≥ 0 + /// 2 + /// + public string LineWidthExpression { get; set; } = "0"; + public float LineWidth { get; set; } + + /// + /// Start point X coordinate + /// 3 + /// + public string StartXExpression { get; set; } = "0"; + + public float StartX { get; set; } + + /// + /// Start point Y coordinate + /// 4 + /// + public string StartYExpression { get; set; } = "0"; + + public float StartY { get; set; } + + /// + /// End point X coordinate + /// 5 + /// + public string EndXExpression { get; set; } = "0"; + + public float EndX { get; set; } + + /// + /// Start point Y coordinate + /// 6 + /// + public string EndYExpression { get; set; } = "0"; + + public float EndY { get; set; } + + /// + /// Rotation angle, in degrees counterclockwise, a decimal. + /// The primitive is rotated around the origin of the macro definition, i.e. the (0, 0) point of macro coordinates. + /// 7 + /// + public string RotationExpression { get; set; } = "0"; + public float Rotation { get; set; } = 0; + #endregion + + protected VectorLinePrimitive() { } + + public VectorLinePrimitive(string exposureExpression, string lineWidthExpression, string startXExpression, string startYExpression, string endXExpression, string endYExpression, string rotationExpression = "0") + { + ExposureExpression = exposureExpression; + LineWidthExpression = lineWidthExpression; + StartXExpression = startXExpression; + StartYExpression = startYExpression; + EndXExpression = endXExpression; + EndYExpression = endYExpression; + RotationExpression = rotationExpression; + } + + public override void DrawFlashD3(Mat mat, SizeF xyPpmm, Point at, MCvScalar color, LineType lineType = LineType.EightConnected) + { + if (!IsParsed) return; + if (LineWidth <= 0) return; + + if (Exposure == 0) color = EmguExtensions.BlackColor; + else if (color.V0 == 0) color = EmguExtensions.WhiteColor; + + var pt1 = new Point(at.X + (int) (StartX * xyPpmm.Width), at.Y + (int) (StartY * xyPpmm.Height)); + var pt2 = new Point(at.X + (int) (EndX * xyPpmm.Height), at.Y + (int) (EndY * xyPpmm.Height)); + CvInvoke.Line(mat, pt1, pt2, color, (int)(LineWidth * xyPpmm.Height), lineType); + //CvInvoke.Rectangle(mat, rectangle, color, -1, lineType); + } + + public override void ParseExpressions(params string[] args) + { + string csharpExp, result; + float num; + var exp = new DataTable(); + + if (byte.TryParse(ExposureExpression, out var exposure)) Exposure = exposure; + else + { + csharpExp = string.Format(Regex.Replace(ExposureExpression, @"\$(\d+)", "{$1}"), args); + result = exp.Compute(csharpExp, null).ToString()!; + if (byte.TryParse(result, out var val)) Exposure = val; + } + + if (float.TryParse(LineWidthExpression, out num)) LineWidth = num; + else + { + csharpExp = Regex.Replace(LineWidthExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out var val)) LineWidth = val; + } + + if (float.TryParse(StartXExpression, out num)) StartX = num; + else + { + csharpExp = Regex.Replace(StartXExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) StartX = num; + } + + if (float.TryParse(EndXExpression, out num)) EndX = num; + else + { + csharpExp = Regex.Replace(EndXExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) EndX = num; + } + + if (float.TryParse(StartYExpression, out num)) StartY = num; + else + { + csharpExp = Regex.Replace(StartYExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) StartY = num; + } + + if (float.TryParse(EndYExpression, out num)) EndY = num; + else + { + csharpExp = Regex.Replace(EndYExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) EndY = num; + } + + + if (float.TryParse(RotationExpression, out num)) Rotation = (short)num; + else + { + csharpExp = Regex.Replace(RotationExpression, @"\$(\d+)", "{$1}"); + csharpExp = string.Format(csharpExp, args); + result = exp.Compute(csharpExp, null).ToString()!; + if (float.TryParse(result, out num)) Rotation = num; + } + + IsParsed = true; + } +} \ No newline at end of file diff --git a/UVtools.Core/Managers/IssueManager.cs b/UVtools.Core/Managers/IssueManager.cs index ff34f581..c2fe015d 100644 --- a/UVtools.Core/Managers/IssueManager.cs +++ b/UVtools.Core/Managers/IssueManager.cs @@ -187,6 +187,8 @@ void GenerateAirMap(IInputArray input, IInputOutputArray output, VectorOfVectorO { progress.Reset(OperationProgress.StatusIslands, SlicerFile.LayerCount); + var firstLayer = SlicerFile.FirstLayer; + // Detect contours Parallel.For(0, SlicerFile.LayerCount, CoreSettings.ParallelOptions, layerIndexInt => { @@ -203,8 +205,8 @@ void GenerateAirMap(IInputArray input, IInputOutputArray output, VectorOfVectorO // Spare a decoding cycle if (!touchBoundConfig.Enabled && !resinTrapConfig.Enabled && - (!overhangConfig.Enabled || overhangConfig.Enabled && (layerIndex == 0 || overhangConfig.WhiteListLayers is not null && !overhangConfig.WhiteListLayers.Contains(layerIndex))) && - (!islandConfig.Enabled || islandConfig.Enabled && (layerIndex == 0 || islandConfig.WhiteListLayers is not null && !islandConfig.WhiteListLayers.Contains(layerIndex))) + (!overhangConfig.Enabled || overhangConfig.Enabled && (layerIndex == 0 || layer.PositionZ <= firstLayer!.PositionZ || overhangConfig.WhiteListLayers is not null && !overhangConfig.WhiteListLayers.Contains(layerIndex))) && + (!islandConfig.Enabled || islandConfig.Enabled && (layerIndex == 0 || layer.PositionZ <= firstLayer!.PositionZ || islandConfig.WhiteListLayers is not null && !islandConfig.WhiteListLayers.Contains(layerIndex))) ) { progress.LockAndIncrement(); @@ -322,7 +324,7 @@ void GenerateAirMap(IInputArray input, IInputOutputArray output, VectorOfVectorO } } - if (layerIndex > 0) // No islands nor overhangs for layer 0 + if (layerIndex > 0 && layer.PositionZ > firstLayer!.PositionZ) // No islands nor overhangs for layer 0 or on plate { Mat? previousImage = null; Span previousSpan = null; @@ -488,7 +490,7 @@ island is null // Overhangs if (!islandConfig.Enabled && overhangConfig.Enabled || (islandConfig.Enabled && overhangConfig.Enabled && - overhangConfig.IndependentFromIslands)) + overhangConfig.IndependentFromIslands) ) { bool canProcessCheck = true; if (overhangConfig.WhiteListLayers is not null) // Check white list diff --git a/UVtools.Core/Objects/ValueDescription.cs b/UVtools.Core/Objects/ValueDescription.cs index b584cc99..d79ebcd4 100644 --- a/UVtools.Core/Objects/ValueDescription.cs +++ b/UVtools.Core/Objects/ValueDescription.cs @@ -7,6 +7,8 @@ */ using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; namespace UVtools.Core.Objects; @@ -27,11 +29,17 @@ public string? Description set => RaiseAndSetIfChanged(ref _description, value); } + [XmlIgnore] + [JsonIgnore] public string ValueAsString { get => Value?.ToString() ?? string.Empty; set => Value = value; - } + } + + public ValueDescription() + { + } public ValueDescription(object value, string? description = null) { diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index a1c51ee3..0bafa089 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -597,9 +597,10 @@ public void CopyConfigurationTo(Operation operation) operation.MaskPoints = MaskPoints; } - public void Serialize(string path) + public void Serialize(string path, bool indent = false) { - XmlExtensions.SerializeToFile(this, path); + if(indent) XmlExtensions.SerializeToFile(this, path, XmlExtensions.SettingsIndent); + else XmlExtensions.SerializeToFile(this, path); } public virtual Operation Clone() diff --git a/UVtools.Core/Operations/OperationLayerImport.cs b/UVtools.Core/Operations/OperationLayerImport.cs index d22c7329..be2655b5 100644 --- a/UVtools.Core/Operations/OperationLayerImport.cs +++ b/UVtools.Core/Operations/OperationLayerImport.cs @@ -15,7 +15,6 @@ using System.IO; using System.Text; using System.Threading.Tasks; -using System.Xml.Serialization; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Layers; @@ -85,8 +84,6 @@ public enum ImportTypes : byte public override uint LayerIndexEnd => _startLayerIndex + Count - 1; - public override bool CanHaveProfiles => false; - public override string? ValidateInternally() { /*var result = new ConcurrentBag(); @@ -126,6 +123,16 @@ public enum ImportTypes : byte { sb.AppendLine("No files to import."); } + else + { + foreach (var keyValue in _files) + { + if (!File.Exists(keyValue.ValueAsString)) + { + sb.AppendLine($"The file '{keyValue.ValueAsString}' does not exists."); + } + } + } return sb.ToString(); } @@ -175,7 +182,6 @@ public ushort StackMargin set => RaiseAndSetIfChanged(ref _stackMargin, value); } - [XmlIgnore] public RangeObservableCollection Files { get => _files; @@ -224,7 +230,7 @@ public void Sort() public override string ToString() { - var result = $"[Files: {Count}]" + LayerRangeString; + var result = $"[{_importType}] [Start at: {_startLayerIndex}] [Files: {Count}]"; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; } diff --git a/UVtools.Core/Operations/OperationLithophane.cs b/UVtools.Core/Operations/OperationLithophane.cs index e0e67860..798b67b1 100644 --- a/UVtools.Core/Operations/OperationLithophane.cs +++ b/UVtools.Core/Operations/OperationLithophane.cs @@ -85,11 +85,11 @@ public enum LithophaneBaseType : byte var sb = new StringBuilder(); if (string.IsNullOrWhiteSpace(_filePath)) { - sb.AppendLine("The selected file is empty"); + sb.AppendLine("The input file is empty"); } else if(!File.Exists(_filePath)) { - sb.AppendLine("The selected file does not exists"); + sb.AppendLine("The input file does not exists"); } else { @@ -113,7 +113,11 @@ public enum LithophaneBaseType : byte } } - if (_startThresholdRange > _endThresholdRange) + if (_startThresholdRange == _endThresholdRange) + { + sb.AppendLine($"Start threshold can't be equal than end threshold ({_endThresholdRange})"); + } + else if (_startThresholdRange > _endThresholdRange) { sb.AppendLine("Start threshold can't be higher than end threshold"); } @@ -405,6 +409,11 @@ protected override bool ExecuteInternally(OperationProgress progress) progress.LockAndIncrement(); }); + if (layersBag.Count == 0) + { + throw new InvalidOperationException("Unable to continue due to no threshold layers was generated, either by lack of pixels or by using a short range."); + } + var thresholdLayers = layersBag.OrderBy(pair => pair.Key).Select(pair => pair.Value).ToArray(); if (!_oneLayerPerThreshold) @@ -444,7 +453,7 @@ protected override bool ExecuteInternally(OperationProgress progress) } else if (layerIncrementF < 1) { - var layerIncrement = (uint)(1/layerIncrementF); + var layerIncrement = (uint)Math.Ceiling(1/layerIncrementF); if (layerIncrement > 1) { progress.Reset("Packed layers"); diff --git a/UVtools.Core/Operations/OperationPCBExposure.cs b/UVtools.Core/Operations/OperationPCBExposure.cs new file mode 100644 index 00000000..8ec2f991 --- /dev/null +++ b/UVtools.Core/Operations/OperationPCBExposure.cs @@ -0,0 +1,213 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.IO; +using System.Text; +using Emgu.CV; +using Emgu.CV.CvEnum; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; +using UVtools.Core.Gerber; +using UVtools.Core.Layers; + +namespace UVtools.Core.Operations; + +[Serializable] +public class OperationPCBExposure : Operation +{ + #region Enum + + public enum LithophaneBaseType : byte + { + None, + Square, + Model + } + + #endregion + + #region Members + private string? _filePath; + + private decimal _layerHeight; + private decimal _exposureTime; + private bool _mirror; + private bool _invertColor; + private bool _enableAntiAliasing; + + #endregion + + #region Overrides + + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; + public override string IconClass => "fas fa-microchip"; + public override string Title => "PCB exposure"; + public override string Description => + "Converts a gerber file to a pixel perfect image given your printer LCD/resolution to exposure the copper traces.\n" + + "Note: The current opened file will be overwritten with this gerber image, use a dummy or a not needed file."; + + public override string ConfirmationText => + "generate the PCB traces?"; + + public override string ProgressTitle => + "Generating PCB traces"; + + public override string ProgressAction => "Tracing"; + + public override string? ValidateInternally() + { + var sb = new StringBuilder(); + if (string.IsNullOrWhiteSpace(_filePath)) + { + sb.AppendLine("The input file is empty"); + } + else if(!File.Exists(_filePath)) + { + sb.AppendLine("The input file does not exists"); + } + + return sb.ToString(); + } + + public override string ToString() + { + var result = $"{(FileExists ? $"{Path.GetFileName(_filePath)} [Exposure: {_exposureTime}s] [Invert: {_invertColor}]" : $"[Exposure: {_exposureTime}s] [Invert: {_invertColor}]")}"; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + #endregion + + #region Constructor + + public OperationPCBExposure() { } + + public OperationPCBExposure(FileFormat slicerFile) : base(slicerFile) + { + if (_layerHeight <= 0) _layerHeight = (decimal)SlicerFile.LayerHeight; + if (_exposureTime <= 0) _exposureTime = (decimal)SlicerFile.BottomExposureTime; + _mirror = SlicerFile.DisplayMirror != FlipDirection.None; + } + + #endregion + + #region Properties + public string? FilePath + { + get => _filePath; + set => RaiseAndSetIfChanged(ref _filePath, value); + } + public bool FileExists => !string.IsNullOrWhiteSpace(_filePath) && File.Exists(_filePath); + + public decimal LayerHeight + { + get => _layerHeight; + set => RaiseAndSetIfChanged(ref _layerHeight, Layer.RoundHeight(value)); + } + + public decimal ExposureTime + { + get => _exposureTime; + set => RaiseAndSetIfChanged(ref _exposureTime, Math.Round(value, 2)); + } + + public bool Mirror + { + get => _mirror; + set => RaiseAndSetIfChanged(ref _mirror, value); + } + + public bool InvertColor + { + get => _invertColor; + set => RaiseAndSetIfChanged(ref _invertColor, value); + } + + public bool EnableAntiAliasing + { + get => _enableAntiAliasing; + set => RaiseAndSetIfChanged(ref _enableAntiAliasing, value); + } + + #endregion + + #region Equality + + protected bool Equals(OperationPCBExposure other) + { + return _filePath == other._filePath && _layerHeight == other._layerHeight && _exposureTime == other._exposureTime && _mirror == other._mirror && _invertColor == other._invertColor && _enableAntiAliasing == other._enableAntiAliasing; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((OperationPCBExposure) obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(_filePath, _layerHeight, _exposureTime, _mirror, _invertColor, _enableAntiAliasing); + } + + #endregion + + #region Methods + + public Mat GetMat() + { + var mat = SlicerFile.CreateMat(); + if (!FileExists) return mat; + GerberDocument.ParseAndDraw(_filePath!, mat, _enableAntiAliasing); + + //var boundingRectangle = CvInvoke.BoundingRectangle(mat); + //var cropped = mat.Roi(new Size(boundingRectangle.Right, boundingRectangle.Bottom)); + var cropped = mat.CropByBounds(); + + if (_invertColor) CvInvoke.BitwiseNot(cropped, cropped); + if (_mirror) + { + var flip = SlicerFile.DisplayMirror; + if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; + CvInvoke.Flip(cropped, cropped, (FlipType)flip); + } + + return mat; + } + + protected override bool ExecuteInternally(OperationProgress progress) + { + using var mat = GetMat(); + var layer = new Layer(mat, SlicerFile); + layer.SetNoDelays(); + + SlicerFile.SuppressRebuildPropertiesWork(() => + { + SlicerFile.LayerHeight = (float) _layerHeight; + SlicerFile.BottomLayerCount = 1; + SlicerFile.BottomExposureTime = (float) _exposureTime; + SlicerFile.ExposureTime = (float)_exposureTime; + SlicerFile.LiftHeightTotal = 0; + SlicerFile.SetNoDelays(); + + SlicerFile.Layers = new[] { layer }; + }, true); + + + using var croppedMat = mat.CropByBounds(20); + using var bgrMat = new Mat(); + CvInvoke.CvtColor(croppedMat, bgrMat, ColorConversion.Gray2Bgr); + SlicerFile.SetThumbnails(bgrMat); + + return !progress.Token.IsCancellationRequested; + } + + + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 6b451da0..21691d6a 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ https://github.com/sn4k3/UVtools https://github.com/sn4k3/UVtools MSLA/DLP, file analysis, calibration, repair, conversion and manipulation - 3.3.2 + 3.4.0 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 diff --git a/UVtools.Installer/Code/Product.wxs b/UVtools.Installer/Code/Product.wxs index f4f2892d..d392cc84 100644 --- a/UVtools.Installer/Code/Product.wxs +++ b/UVtools.Installer/Code/Product.wxs @@ -1,6 +1,6 @@ - + - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index c641e826..221dc6a9 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -2,7 +2,7 @@ - + diff --git a/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml b/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml new file mode 100644 index 00000000..cf0e7d97 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml @@ -0,0 +1,80 @@ + + + + + + + + + +