diff options
45 files changed, 2750 insertions, 301 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c928c80..4a19d1c 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 91d56c0..db07aef 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 0000000..6100507 --- /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 <fgcolor=cBlack, bgcolor=cRed>; + ubyte PreviewData[PreviewLength] <fgcolor=cBlack, bgcolor=cGreen>; +}; + +struct HEADER { + uint HeaderLength <fgcolor=cBlack, bgcolor=cRed>; + ushort Version <fgcolor=cBlack, bgcolor=cRed>; // 1 + ubyte ImageLog <fgcolor=cBlack, bgcolor=cRed>; // log 2 + + + PREVIEW preview; + PREVIEW preview; + PREVIEW preview; + PREVIEW preview; + + ushort ResolutionX <fgcolor=cBlack, bgcolor=cRed>; + ushort ResolutionY <fgcolor=cBlack, bgcolor=cRed>; + ushort PixelUmMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (um, magnification 100 times: such as 100um write 10000, the same below) + ubyte Mirror <fgcolor=cBlack, bgcolor=cRed>; // (0x00 not mirrored, 0x01 X-axis mirroring, 0x02 Y-axis mirroring, 0x03 XY-axis mirroring) + ubyte BottomLightPWM <fgcolor=cBlack, bgcolor=cRed>; + ubyte LightPWM <fgcolor=cBlack, bgcolor=cRed>; + ubyte AntiAliasEnabled <fgcolor=cBlack, bgcolor=cRed>; + ubyte DistortionEnabled <fgcolor=cBlack, bgcolor=cRed>; + ubyte DelayedExposureActivationEnabled <fgcolor=cBlack, bgcolor=cRed>; + uint LayerCount <fgcolor=cBlack, bgcolor=cYellow>; + ushort NumberParameterSets <fgcolor=cBlack, bgcolor=cRed>; // 1 + uint LastLayerIndex <fgcolor=cBlack, bgcolor=cYellow>; + uint LayerHeightUmMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (um magnification 100 times) + ubyte BottomLayersCount <fgcolor=cBlack, bgcolor=cRed>; + + uint ExposureTimeMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + uint BottomExposureTimeMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + uint SupportDelayTimeMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + uint BottomSupportDelayTimeMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + ubyte TransitionLayers <fgcolor=cBlack, bgcolor=cRed>; + ubyte TransitionType <fgcolor=cBlack, bgcolor=cRed>; // (0x00 linear transition) + uint TransitionLayerIntervalTimeDifferenceMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + uint WaitTimeAfterCureMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + uint WaitTimeAfterLiftMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + uint WaitTimeBeforeCureMagnified100Times:24 <fgcolor=cBlack, bgcolor=cRed>; // s * 100 + + uint BottomLiftHeightSlowMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + uint BottomLiftHeightTotalMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + uint LiftHeightSlowMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + uint LiftHeightTotalMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + uint BottomRetractHeightTotalMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + uint RetractHeightSlowMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + uint RetractHeightTotalMagnified1000Times:24 <fgcolor=cBlack, bgcolor=cRed>; // (magnification 1000 times) + + ubyte AccelerationType <fgcolor=cBlack, bgcolor=cRed>; // (0x00: S-shaped acceleration, 0x01: T-shaped acceleration, Default Value: S-shaped acceleration, currently only supports S-shaped acceleration) + + ushort BottomLiftSpeedStartMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort BottomLiftSpeedSlowMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort BottomLiftSpeedFastMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ubyte BottomLiftAccelerationChange <fgcolor=cBlack, bgcolor=cRed>; // 5 + + ushort LiftSpeedStartMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort LiftSpeedSlowMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort LiftSpeedFastMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ubyte LiftAccelerationChange <fgcolor=cBlack, bgcolor=cRed>; // 5 + + ushort BottomRetractSpeedStartMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort BottomRetractSpeedSlowMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort BottomRetractFastMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ubyte BottomRetractAccelerationChange <fgcolor=cBlack, bgcolor=cRed>; // 5 + + ushort RetractSpeedStartMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort RetractSpeedSlowMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ushort RetractFastMagnified100Times <fgcolor=cBlack, bgcolor=cRed>; // (magnification 100 times) + ubyte RetractAccelerationChange <fgcolor=cBlack, bgcolor=cRed>; // 5 + + ubyte Reserved[23] <fgcolor=cWhite, bgcolor=cBlack>; + + ubyte ProtocolType <fgcolor=cBlack, bgcolor=cRed>; // 0 +} header; + + +struct LAYER_DEF { + ushort Mark <fgcolor=cBlack, bgcolor=cRed>; // (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 <fgcolor=cBlack, bgcolor=cRed>; + ushort StartY <fgcolor=cBlack, bgcolor=cRed>; +}; + + +struct LAYERS { + + local int i = 0; + for( i = 0; i < header.LayerCount; i++ ){ + LAYER_DEF layerDef <fgcolor=cBlack, bgcolor=cYellow>; + } + +} layers; diff --git a/UVtools.Cmd/Program.cs b/UVtools.Cmd/Program.cs index b75ff02..e6c815b 100644 --- a/UVtools.Cmd/Program.cs +++ b/UVtools.Cmd/Program.cs @@ -169,7 +169,7 @@ internal class Program { if (Quiet) return; Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine(obj); + Console.Write(obj); Console.ResetColor(); } @@ -190,12 +190,12 @@ internal class Program 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 class Program } 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 3d78239..89a4fdb 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 class ConvertCommand 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 class ConvertCommand 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 0f7493f..edd17c2 100644 --- a/UVtools.Cmd/UVtools.Cmd.csproj +++ b/UVtools.Cmd/UVtools.Cmd.csproj @@ -5,7 +5,7 @@ <TargetFramework>net6.0</TargetFramework> <AssemblyName>UVtoolsCmd</AssemblyName> <ApplicationIcon>UVtools.ico</ApplicationIcon> - <Version>1.0.0</Version> + <Version>1.0.1</Version> <Authors>Tiago Conceição, sn4k3</Authors> <Company>PTRTECH</Company> <PackageLicenseFile>LICENSE</PackageLicenseFile> diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 07120bd..5c506ae 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -351,7 +351,8 @@ public static class EmguExtensions 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 class EmguExtensions /// </summary> /// <param name="mat"></param> /// <param name="value"></param> - 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); + } /// <summary> /// Gets PNG byte array @@ -477,6 +480,23 @@ public static class EmguExtensions 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 d303793..2ac979a 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -659,16 +659,10 @@ public class CXDLPFile : FileFormat { 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 20c6358..532832d 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 @@ public sealed class FileExtension : IEquatable<FileExtension>, IEquatable<string return FileFormat.FindExtension(extension); } - + public static IEnumerable<FileExtension> FindAll(string extension) + { + if (string.IsNullOrWhiteSpace(extension)) return Enumerable.Empty<FileExtension>(); + 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 d8b1a2a..69ec05a 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -437,13 +437,26 @@ public abstract class FileFormat : BindableBase, IDisposable, IEquatable<FileFor /// <param name="extensionOrFilePath"> name to find</param> /// <param name="createNewInstance">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</param> /// <returns><see cref="FileFormat"/> object or null if not found</returns> - public static FileFormat? FindByExtensionOrFilePath(string extensionOrFilePath, bool createNewInstance = false) + public static FileFormat? FindByExtensionOrFilePath(string extensionOrFilePath, bool createNewInstance = false) => + FindByExtensionOrFilePath(extensionOrFilePath, out _, createNewInstance); + + /// <summary> + /// Find <see cref="FileFormat"/> by an extension + /// </summary> + /// <param name="extensionOrFilePath"> name to find</param> + /// <param name="fileFormatsSharingExt">Number of file formats sharing the input extension</param> + /// <param name="createNewInstance">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</param> + /// <returns><see cref="FileFormat"/> object or null if not found</returns> + 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 abstract class FileFormat : BindableBase, IDisposable, IEquatable<FileFor ? 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 abstract class FileFormat : BindableBase, IDisposable, IEquatable<FileFor return AvailableFormats.SelectMany(format => format.FileExtensions).FirstOrDefault(ext => ext.Equals(extension)); } + public static IEnumerable<FileExtension> 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 dc26d71..c0a51eb 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 0000000..f6fc560 --- /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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Drawing; +using System.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 + /// <summary> + /// Gets the index of this aperture + /// </summary> + public int Index { get; set; } + + /// <summary> + /// Gets the aperture name + /// </summary> + 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 0000000..1d8e5b5 --- /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. <https://fsf.org/> + * 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 0000000..3174609 --- /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. <https://fsf.org/> + * 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 0000000..54c199e --- /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. <https://fsf.org/> + * 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 0000000..08ac091 --- /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. <https://fsf.org/> + * 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 0000000..4f58eca --- /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. <https://fsf.org/> + * 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 0000000..0e38c42 --- /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. <https://fsf.org/> + * 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 0000000..1b29fb3 --- /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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.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<int, Aperture> Apertures { get; } = new(); + public Dictionary<string, Macro> 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<Point>(); + 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<KeyValuePair<Point, int>>(); + + 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<Point, int>(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 0000000..6d9a013 --- /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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UVtools.Core.Gerber.Primitives; + +namespace UVtools.Core.Gerber; + +public class Macro : IReadOnlyList<Primitive> +{ + #region Properties + + /// <summary> + /// Gets the macro name + /// </summary> + public string Name { get; set; } = string.Empty; + + public List<Primitive> 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<Primitive> 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 0000000..d882eea --- /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. <https://fsf.org/> + * 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; + +/// <summary> +/// A vector line is a rectangle defined by its line width, start and end points. The line ends are rectangular. +/// </summary> +public class CenterLinePrimitive : Primitive +{ + #region Constants + public const byte Code = 21; + #endregion + + #region Properties + public override string Name => "VectorLine"; + + /// <summary> + /// Exposure off/on (0/1) + /// 1 + /// </summary> + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// <summary> + /// Width ≥ 0 + /// 2 + /// </summary> + public string WidthExpression { get; set; } = "0"; + public float Width { get; set; } + + /// <summary> + /// Height ≥ 0 + /// 3 + /// </summary> + public string HeightExpression { get; set; } = "0"; + public float Height { get; set; } + + /// <summary> + /// Center point X coordinate + /// 4 + /// </summary> + public string CenterXExpression { get; set; } = "0"; + + public float CenterX { get; set; } + + /// <summary> + /// Center point Y coordinate + /// 5 + /// </summary> + public string CenterYExpression { get; set; } = "0"; + + public float CenterY { get; set; } + + /// <summary> + /// 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 + /// </summary> + 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 0000000..73eb04d --- /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. <https://fsf.org/> + * 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; + +/// <summary> +/// A circle primitive is defined by its center point and diameter. +/// </summary> +public class CirclePrimitive : Primitive +{ + #region Constants + public const byte Code = 1; + #endregion + + #region Properties + public override string Name => "Circle"; + + /// <summary> + /// Exposure off/on (0/1) + /// 1 + /// </summary> + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// <summary> + /// Diameter ≥ 0 + /// 2 + /// </summary> + public string DiameterExpression { get; set; } = "0"; + public float Diameter { get; set; } + + /// <summary> + /// Center X coordinate. + /// 3 + /// </summary> + public string CenterXExpression { get; set; } = "0"; + + public float CenterX { get; set; } + + /// <summary> + /// Center Y coordinate. + /// 4 + /// </summary> + public string CenterYExpression { get; set; } = "0"; + + public float CenterY { get; set; } + + /// <summary> + /// 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 + /// </summary> + 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 0000000..66596c3 --- /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. <https://fsf.org/> + * 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; + +/// <summary> +/// 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. +/// </summary> +public class CommentPrimitive : Primitive +{ + #region Constants + public const byte Code = 0; + #endregion + #region Properties + public override string Name => "Comment"; + + /// <summary> + /// The comment + /// 1 + /// </summary> + 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 0000000..0addbcf --- /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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.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; + +/// <summary> +/// 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. +/// </summary> +public class OutlinePrimitive : Primitive +{ + #region Constants + public const byte Code = 4; + #endregion + + #region Properties + public override string Name => "Outline"; + + /// <summary> + /// Exposure off/on (0/1) + /// 1 + /// </summary> + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// <summary> + /// The number of vertices of the outline = the number of coordinate pairs minus one. An integer ≥3. + /// 2 + /// </summary> + public string VerticesCountExpression { get; set; } = string.Empty; + public ushort VerticesCount => (ushort) Coordinates.Length; + + /// <summary> + /// 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 + /// </summary> + public string[] CoordinatesExpression { get; set; } = Array.Empty<string>(); + + public PointF[] Coordinates { get; set; } = Array.Empty<PointF>(); + + /// <summary> + /// 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. + /// </summary> + 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<Point>(); + 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<PointF>(); + 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 0000000..88d6fb8 --- /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. <https://fsf.org/> + * 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; + +/// <summary> +/// A polygon primitive is a regular polygon defined by the number of vertices n, the center point and the diameter of the circumscribed circle. +/// </summary> +public class PolygonPrimitive : Primitive +{ + #region Constants + public const byte Code = 5; + #endregion + + #region Properties + public override string Name => "Polygon"; + + /// <summary> + /// Exposure off/on (0/1) + /// 1 + /// </summary> + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// <summary> + /// Diameter ≥ 0 + /// 2 + /// </summary> + public string VerticesCountExpression { get; set; } = "0"; + public byte VerticesCount { get; set; } + + /// <summary> + /// Center X coordinate. + /// 3 + /// </summary> + public string CenterXExpression { get; set; } = "0"; + public float CenterX { get; set; } + + /// <summary> + /// Center Y coordinate. + /// 4 + /// </summary> + public string CenterYExpression { get; set; } = "0"; + public float CenterY { get; set; } + + /// <summary> + /// Diameter ≥ 0 + /// 5 + /// </summary> + public string DiameterExpression { get; set; } = "0"; + public float Diameter { get; set; } + + /// <summary> + /// 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 + /// </summary> + 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 0000000..51dd0d9 --- /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. <https://fsf.org/> + * 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 0000000..6c165dd --- /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. <https://fsf.org/> + * 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; + +/// <summary> +/// A vector line is a rectangle defined by its line width, start and end points. The line ends are rectangular. +/// </summary> +public class VectorLinePrimitive : Primitive +{ + #region Constants + public const byte Code = 20; + #endregion + + #region Properties + public override string Name => "VectorLine"; + + /// <summary> + /// Exposure off/on (0/1) + /// 1 + /// </summary> + public string ExposureExpression { get; set; } = "1"; + public byte Exposure { get; set; } = 1; + + /// <summary> + /// Width of the line ≥ 0 + /// 2 + /// </summary> + public string LineWidthExpression { get; set; } = "0"; + public float LineWidth { get; set; } + + /// <summary> + /// Start point X coordinate + /// 3 + /// </summary> + public string StartXExpression { get; set; } = "0"; + + public float StartX { get; set; } + + /// <summary> + /// Start point Y coordinate + /// 4 + /// </summary> + public string StartYExpression { get; set; } = "0"; + + public float StartY { get; set; } + + /// <summary> + /// End point X coordinate + /// 5 + /// </summary> + public string EndXExpression { get; set; } = "0"; + + public float EndX { get; set; } + + /// <summary> + /// Start point Y coordinate + /// 6 + /// </summary> + public string EndYExpression { get; set; } = "0"; + + public float EndY { get; set; } + + /// <summary> + /// 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 + /// </summary> + 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 ff34f58..c2fe015 100644 --- a/UVtools.Core/Managers/IssueManager.cs +++ b/UVtools.Core/Managers/IssueManager.cs @@ -187,6 +187,8 @@ public sealed class IssueManager : RangeObservableCollection<MainIssue> { progress.Reset(OperationProgress.StatusIslands, SlicerFile.LayerCount); + var firstLayer = SlicerFile.FirstLayer; + // Detect contours Parallel.For(0, SlicerFile.LayerCount, CoreSettings.ParallelOptions, layerIndexInt => { @@ -203,8 +205,8 @@ public sealed class IssueManager : RangeObservableCollection<MainIssue> // 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 @@ public sealed class IssueManager : RangeObservableCollection<MainIssue> } } - 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<byte> previousSpan = null; @@ -488,7 +490,7 @@ public sealed class IssueManager : RangeObservableCollection<MainIssue> // 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 b584cc9..d79ebcd 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 class ValueDescription : BindableBase, IEquatable<ValueDescription> 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 a1c51ee..0bafa08 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -597,9 +597,10 @@ public abstract class Operation : BindableBase, IDisposable 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 d22c732..be2655b 100644 --- a/UVtools.Core/Operations/OperationLayerImport.cs +++ b/UVtools.Core/Operations/OperationLayerImport.cs @@ -15,7 +15,6 @@ using System.Drawing; 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 sealed class OperationLayerImport : Operation public override uint LayerIndexEnd => _startLayerIndex + Count - 1; - public override bool CanHaveProfiles => false; - public override string? ValidateInternally() { /*var result = new ConcurrentBag<StringTag>(); @@ -126,6 +123,16 @@ public sealed class OperationLayerImport : Operation { 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 sealed class OperationLayerImport : Operation set => RaiseAndSetIfChanged(ref _stackMargin, value); } - [XmlIgnore] public RangeObservableCollection<ValueDescription> Files { get => _files; @@ -224,7 +230,7 @@ public sealed class OperationLayerImport : Operation 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 e0e6786..798b67b 100644 --- a/UVtools.Core/Operations/OperationLithophane.cs +++ b/UVtools.Core/Operations/OperationLithophane.cs @@ -85,11 +85,11 @@ public class OperationLithophane : Operation 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 class OperationLithophane : Operation } } - 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 @@ public class OperationLithophane : Operation 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 @@ public class OperationLithophane : Operation } 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 0000000..8ec2f99 --- /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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.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 6b451da..21691d6 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>3.3.2</Version> + <Version>3.4.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> diff --git a/UVtools.Installer/Code/Product.wxs b/UVtools.Installer/Code/Product.wxs index f4f2892..d392cc8 100644 --- a/UVtools.Installer/Code/Product.wxs +++ b/UVtools.Installer/Code/Product.wxs @@ -1,6 +1,6 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> - <!-- + <!-- MSIProductVersion is defined in UVtools.Installer.wixproj as 0.0.1 for local desktop builds. You should pass in the MSBuild Property 'MSIProductVersion' to override it during an automated build. See http://msdn.microsoft.com/en-us/library/windows/desktop/aa370859%28v=vs.85%29.aspx for information on allowable values. @@ -9,41 +9,42 @@ is a seamless uninstall/reinstall. Version="$(var.MSIProductVersion)" --> - <Product Id="*" Name="UVtools" Language="1033" Version="$(var.MSIProductVersion)" Manufacturer="PTRTECH" UpgradeCode="1ea6d212-15c0-425e-b2ec-4b6c60817552"> - <Package InstallerVersion="301" Compressed="yes" InstallScope="perMachine" Platform="x64" /> - <MediaTemplate EmbedCab="yes" /> - <!-- Major Upgrade Rule to disallow downgrades --> - <MajorUpgrade - AllowDowngrades="no" - AllowSameVersionUpgrades="yes" - IgnoreRemoveFailure="no" - DowngradeErrorMessage="A newer version of [ProductName] is already installed." - Schedule="afterInstallInitialize" /> - <!--Common Launch Condition--> - <!-- Examples at http://wixtoolset.org/documentation/manual/v3/customactions/wixnetfxextension.html --> - <!-- - <PropertyRef Id="NETFRAMEWORK45" /> - <Condition Message="[ProductName] requires .NET Framework 4.8.">Installed OR NETFRAMEWORK45</Condition> - --> - <!-- Include User Interface Experience --> - <Icon Id="Icon.ico" SourceFile="..\UVtools.CAD\UVtools.ico" /> - <Property Id="ARPPRODUCTICON" Value="Icon.ico"></Property> - <UIRef Id="UI" /> - <!-- Include Features and Directories Fragment --> - <DirectoryRef Id="INSTALLLOCATION"> - <Component Id="RegistryEntries" Guid="C3603223-A8C1-4393-8C06-36B48DED2652"> - <RegistryKey - Root="HKCU" - Key="Software\UVtools" - ForceCreateOnInstall="yes" - ForceDeleteOnUninstall="yes" /> - <RegistryValue - Root="HKCU" - Key="Software\UVtools" - Name="InstallDir" - Type="string" - Value="[INSTALLLOCATION]" /> - </Component> - </DirectoryRef> - </Product> -</Wix>
\ No newline at end of file + <Product Id="*" Name="UVtools" Language="1033" Version="$(var.MSIProductVersion)" Manufacturer="PTRTECH" UpgradeCode="1ea6d212-15c0-425e-b2ec-4b6c60817552"> + <Package InstallerVersion="301" Compressed="yes" InstallScope="perMachine" Platform="x64" /> + <MediaTemplate EmbedCab="yes" /> + <!-- Major Upgrade Rule to disallow downgrades --> + <MajorUpgrade + AllowDowngrades="no" + AllowSameVersionUpgrades="yes" + IgnoreRemoveFailure="no" + DowngradeErrorMessage="A newer version of [ProductName] is already installed." + Schedule="afterInstallInitialize" /> + <!--Common Launch Condition--> + <!-- Examples at http://wixtoolset.org/documentation/manual/v3/customactions/wixnetfxextension.html --> + <!-- + <PropertyRef Id="NETFRAMEWORK45" /> + <Condition Message="[ProductName] requires .NET Framework 4.8.">Installed OR NETFRAMEWORK45</Condition> + --> + <!-- Include User Interface Experience --> + <Icon Id="Icon.ico" SourceFile="..\UVtools.CAD\UVtools.ico" /> + <Property Id="ARPPRODUCTICON" Value="Icon.ico"></Property> + <UIRef Id="UI" /> + <!-- Include Features and Directories Fragment --> + <DirectoryRef Id="INSTALLLOCATION"> + <Component Id="RegistryEntries" Guid="C3603223-A8C1-4393-8C06-36B48DED2652"> + <!-- Install directory --> + <RegistryKey Root="HKCU" Key="Software\UVtools" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes" /> + <RegistryValue Root="HKCU" Key="Software\UVtools" Name="InstallDir" Value="[INSTALLLOCATION]" Type="string" /> + + <!-- Open file with UVtools --> + <RegistryKey Root="HKCR" Key="*\shell\UVtools" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes" /> + <RegistryValue Root="HKCR" Key="*\shell\UVtools" Value="Open with UVtools" Type="string" /> + <RegistryValue Root="HKCR" Key="*\shell\UVtools" Name="Icon" Value="[INSTALLLOCATION]UVtools.exe" Type="string" /> + <RegistryValue Root="HKCR" Key="*\shell\UVtools" Name="Position" Value="Top" Type="string" /> + <RegistryValue Root="HKCR" Key="*\shell\UVtools" Name="AppliesTo" Value="System.FileName:"*.sl1" OR System.FileName:"*.sl1s" OR System.FileName:"*.zip" OR System.FileName:"*.photon" OR System.FileName:"*.cbddlp" OR System.FileName:"*.ctb" OR System.FileName:"*.photons" OR System.FileName:"*.phz" OR System.FileName:"*.fdg" OR System.FileName:"*.pws" OR System.FileName:"*.pw0" OR System.FileName:"*.pwx" OR System.FileName:"*.dlp" OR System.FileName:"*.pwmx" OR System.FileName:"*.pwmb" OR System.FileName:"*.pwmo" OR System.FileName:"*.pwms" OR System.FileName:"*.pwma" OR System.FileName:"*.pmsq" OR System.FileName:"*.pm3" OR System.FileName:"*.pm3m" OR System.FileName:"*.cws" OR System.FileName:"*.osla" OR System.FileName:"*.jxs" OR System.FileName:"*.zcode" OR System.FileName:"*.zcodex" OR System.FileName:"*.mdlp" OR System.FileName:"*.gr1" OR System.FileName:"*.cxdlp" OR System.FileName:"*.lgs" OR System.FileName:"*.lgs30" OR System.FileName:"*.lgs120" OR System.FileName:"*.lgs4k" OR System.FileName:"*.svgx" OR System.FileName:"*.vdt" OR System.FileName:"*.uvj" OR System.FileName:"*.png" OR System.FileName:"*.jpg" OR System.FileName:"*.jpeg" OR System.FileName:"*.jp2" OR System.FileName:"*.tif" OR System.FileName:"*.tiff" OR System.FileName:"*.bmp" OR System.FileName:"*.pbm" OR System.FileName:"*.pgm" OR System.FileName:"*.sr" OR System.FileName:"*.ras"" Type="string" /> + <RegistryKey Root="HKCR" Key="*\shell\UVtools\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes" /> + <RegistryValue Root="HKCR" Key="*\shell\UVtools\command" Value=""[INSTALLLOCATION]UVtools.exe" "%1"" Type="string" /> + </Component> + </DirectoryRef> + </Product> +</Wix> diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index c641e82..221dc6a 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -2,7 +2,7 @@ <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <?define ComponentRules="OneToOne"?> <!-- SourceDir instructs IsWiX the location of the directory that contains files for this merge module --> - <?define SourceDir="..\publish\UVtools_win-x64_v3.3.2"?> + <?define SourceDir="..\publish\UVtools_win-x64_v3.4.0"?> <Module Id="UVtools" Language="1033" Version="1.0.0.0"> <Package Id="12aaa1cf-ff06-4a02-abd5-2ac01ac4f83b" Manufacturer="PTRTECH" InstallerVersion="200" Keywords="MSLA, DLP" Description="MSLA/DLP, file analysis, repair, conversion and manipulation" InstallScope="perMachine" Platform="x64" /> <Directory Id="TARGETDIR" Name="SourceDir"> diff --git a/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml b/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml new file mode 100644 index 0000000..cf0e7d9 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml @@ -0,0 +1,80 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="UVtools.WPF.Controls.Tools.ToolPCBExposureControl"> + <Grid ColumnDefinitions="Auto,10,350"> + <StackPanel Spacing="10"> + <Grid + RowDefinitions="Auto,10,Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,400"> + + <TextBlock Grid.Row="0" Grid.Column="0" + VerticalAlignment="Center" + Text="Gerber file:"/> + + <Grid Grid.Row="0" Grid.Column="2" + ColumnDefinitions="*,Auto"> + <TextBox Grid.Column="0" + IsReadOnly="True" + VerticalAlignment="Center" + Text="{Binding Operation.FilePath}"/> + <Button Grid.Column="1" + VerticalAlignment="Stretch" + Command="{Binding SelectFile}" + i:Attached.Icon="fas fa-file-import"/> + </Grid> + + <TextBlock Grid.Row="2" Grid.Column="0" + VerticalAlignment="Center" + Text="Layer height:"/> + <NumericUpDown Grid.Row="2" Grid.Column="2" + Classes="ValueLabel ValueLabel_mm" + Increment="0.01" + Minimum="0.01" + Maximum="500" + FormatString="F3" + Value="{Binding Operation.LayerHeight}"/> + + + <TextBlock Grid.Row="4" Grid.Column="0" + VerticalAlignment="Center" + Text="Exposure time:"/> + <NumericUpDown Grid.Row="4" Grid.Column="2" + Classes="ValueLabel ValueLabel_s" + Increment="0.5" + Minimum="0.1" + Maximum="200" + FormatString="F2" + Value="{Binding Operation.ExposureTime}"/> + + <StackPanel Grid.Row="6" Grid.Column="2" + Orientation="Horizontal" Spacing="20"> + <CheckBox VerticalAlignment="Center" + IsChecked="{Binding Operation.Mirror}" + Content="Mirror"/> + + <CheckBox VerticalAlignment="Center" + IsChecked="{Binding Operation.InvertColor}" + Content="Invert color"/> + + <CheckBox VerticalAlignment="Center" + IsChecked="{Binding Operation.EnableAntiAliasing}" + Content="Enable Anti-Aliasing"/> + </StackPanel> + + + </Grid> + + </StackPanel> + + <StackPanel Grid.Column="2" Orientation="Vertical" Spacing="10"> + <Image Stretch="Uniform" + Source="{Binding PreviewImage}"/> + + <TextBlock Text="{Binding PreviewImage.Size, StringFormat=Size: {0}}" HorizontalAlignment="Center"/> + </StackPanel> + </Grid> +</UserControl> diff --git a/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml.cs new file mode 100644 index 0000000..d2c4781 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Timers; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using UVtools.Core.Extensions; +using UVtools.Core.Operations; +using UVtools.WPF.Extensions; +using UVtools.WPF.Windows; +using Bitmap = Avalonia.Media.Imaging.Bitmap; + +namespace UVtools.WPF.Controls.Tools +{ + public partial class ToolPCBExposureControl : ToolControl + { + + public OperationPCBExposure Operation => BaseOperation as OperationPCBExposure; + + private readonly Timer _timer; + + private Bitmap _previewImage; + public Bitmap PreviewImage + { + get => _previewImage; + set => RaiseAndSetIfChanged(ref _previewImage, value); + } + + public ToolPCBExposureControl() + { + BaseOperation = new OperationPCBExposure(SlicerFile); + if (!ValidateSpawn()) return; + InitializeComponent(); + + _timer = new Timer(20) + { + AutoReset = false + }; + _timer.Elapsed += (sender, e) => Dispatcher.UIThread.InvokeAsync(UpdatePreview); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public override void Callback(ToolWindow.Callbacks callback) + { + if (App.SlicerFile is null) return; + switch (callback) + { + case ToolWindow.Callbacks.Init: + case ToolWindow.Callbacks.Loaded: + Operation.PropertyChanged += (sender, e) => + { + _timer.Stop(); + _timer.Start(); + }; + _timer.Stop(); + _timer.Start(); + break; + } + } + + public void UpdatePreview() + { + try + { + using var mat = Operation.GetMat(); + using var matCropped = mat.CropByBounds(20); + _previewImage?.Dispose(); + PreviewImage = matCropped.ToBitmap(); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + } + + public async void SelectFile() + { + var dialog = new OpenFileDialog + { + AllowMultiple = false, + Filters = new List<FileDialogFilter> + { + new() + { + Name = "Gerber files", + Extensions = new List<string>{ "gbr" } + }, + }, + }; + + var files = await dialog.ShowAsync(ParentWindow); + if (files is null || files.Length == 0) return; + + Operation.FilePath = files[0]; + } + } +} diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index d2b2fb5..1c24f95 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -58,198 +58,61 @@ public partial class MainWindow : WindowEx public static MenuItem[] MenuTools { get; } = { - new() - { - Tag = new OperationEditParameters(), - }, - new() - { - Tag = new OperationRepairLayers(), - }, - new() - { - Tag = new OperationMove(), - }, - new() - { - Tag = new OperationResize(), - }, - new() - { - Tag = new OperationFlip(), - }, - new() - { - Tag = new OperationRotate(), - }, - new() - { - Tag = new OperationSolidify(), - }, - new() - { - Tag = new OperationMorph(), - }, - new() - { - Tag = new OperationRaftRelief(), - }, - new() - { - Tag = new OperationRedrawModel(), - }, - /*new() - { - Tag = new OperationThreshold(), - },*/ - new() - { - Tag = new OperationLayerArithmetic(), - }, - new() - { - Tag = new OperationPixelArithmetic(), - }, - new() - { - Tag = new OperationMask(), - }, - /*new() - { - Tag = new OperationPixelDimming(), - },*/ - new() - { - Tag = new OperationLightBleedCompensation(), - }, - new() - { - Tag = new OperationInfill(), - }, - new() - { - Tag = new OperationBlur(), - }, - new() - { - Tag = new OperationPattern(), - }, - new() - { - Tag = new OperationFadeExposureTime(), - }, - new() - { - Tag = new OperationDoubleExposure(), - }, - new() - { - Tag = new OperationDynamicLifts(), - }, - new() - { - Tag = new OperationDynamicLayerHeight(), - }, - new() - { - Tag = new OperationLayerReHeight(), - }, - new() - { - Tag = new OperationRaiseOnPrintFinish(), - }, - new() - { - Tag = new OperationChangeResolution(), - }, - new() - { - Tag = new OperationTimelapse(), - }, - new() - { - Tag = new OperationLithophane(), - }, - new() - { - Tag = new OperationScripting(), - }, - new() - { - Tag = new OperationCalculator(), - }, + new() { Tag = new OperationEditParameters()}, + new() { Tag = new OperationRepairLayers()}, + new() { Tag = new OperationMove()}, + new() { Tag = new OperationResize()}, + new() { Tag = new OperationFlip()}, + new() { Tag = new OperationRotate()}, + new() { Tag = new OperationSolidify()}, + new() { Tag = new OperationMorph()}, + new() { Tag = new OperationRaftRelief()}, + new() { Tag = new OperationRedrawModel()}, + /*new() { Tag = new OperationThreshold()},*/ + new() { Tag = new OperationLayerArithmetic()}, + new() { Tag = new OperationPixelArithmetic()}, + new() { Tag = new OperationMask()}, + /*new() { Tag = new OperationPixelDimming()},*/ + new() { Tag = new OperationLightBleedCompensation()}, + new() { Tag = new OperationInfill()}, + new() { Tag = new OperationBlur()}, + new() { Tag = new OperationPattern()}, + new() { Tag = new OperationFadeExposureTime()}, + new() { Tag = new OperationDoubleExposure()}, + new() { Tag = new OperationDynamicLifts()}, + new() { Tag = new OperationDynamicLayerHeight()}, + new() { Tag = new OperationLayerReHeight()}, + new() { Tag = new OperationRaiseOnPrintFinish()}, + new() { Tag = new OperationChangeResolution()}, + new() { Tag = new OperationTimelapse()}, + new() { Tag = new OperationLithophane()}, + new() { Tag = new OperationPCBExposure()}, + new() { Tag = new OperationScripting()}, + new() { Tag = new OperationCalculator()}, }; public static MenuItem[] MenuCalibration { get; } = { - new() - { - Tag = new OperationCalibrateExposureFinder(), - }, - new() - { - Tag = new OperationCalibrateElephantFoot(), - }, - new() - { - Tag = new OperationCalibrateXYZAccuracy(), - }, - new() - { - Tag = new OperationCalibrateLiftHeight(), - }, - new() - { - Tag = new OperationCalibrateTolerance(), - }, - new() - { - Tag = new OperationCalibrateGrayscale(), - }, - new() - { - Tag = new OperationCalibrateStressTower(), - }, - new() - { - Tag = new OperationCalibrateExternalTests(), - }, + new() { Tag = new OperationCalibrateExposureFinder()}, + new() { Tag = new OperationCalibrateElephantFoot()}, + new() { Tag = new OperationCalibrateXYZAccuracy()}, + new() { Tag = new OperationCalibrateLiftHeight()}, + new() { Tag = new OperationCalibrateTolerance()}, + new() { Tag = new OperationCalibrateGrayscale()}, + new() { Tag = new OperationCalibrateStressTower()}, + new() { Tag = new OperationCalibrateExternalTests()}, }; public static MenuItem[] LayerActionsMenu { get; } = { - new() - { - Tag = new OperationLayerImport(), - }, - new() - { - Tag = new OperationLayerClone(), - }, - new() - { - Tag = new OperationLayerRemove(), - }, - new() - { - Tag = new OperationLayerExportImage(), - }, - new() - { - Tag = new OperationLayerExportGif(), - }, - new() - { - Tag = new OperationLayerExportSkeleton(), - }, - new() - { - Tag = new OperationLayerExportHeatMap(), - }, - new() - { - Tag = new OperationLayerExportMesh(), - }, + new() { Tag = new OperationLayerImport()}, + new() { Tag = new OperationLayerClone()}, + new() { Tag = new OperationLayerRemove()}, + new() { Tag = new OperationLayerExportImage()}, + new() { Tag = new OperationLayerExportGif()}, + new() { Tag = new OperationLayerExportSkeleton()}, + new() { Tag = new OperationLayerExportHeatMap()}, + new() { Tag = new OperationLayerExportMesh()}, }; diff --git a/UVtools.WPF/Program.cs b/UVtools.WPF/Program.cs index 67389bd..2e71d42 100644 --- a/UVtools.WPF/Program.cs +++ b/UVtools.WPF/Program.cs @@ -1,11 +1,16 @@ using System; using System.Diagnostics; +using System.Drawing; using System.Globalization; using System.Runtime.ExceptionServices; using Avalonia; +using Emgu.CV; +using Emgu.CV.Structure; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.MaterialDesign; +using UVtools.Core.Extensions; +using UVtools.Core.Gerber; using UVtools.WPF.Extensions; namespace UVtools.WPF; diff --git a/UVtools.WPF/Structures/OperationProfiles.cs b/UVtools.WPF/Structures/OperationProfiles.cs index 774abcb..b7dc3b9 100644 --- a/UVtools.WPF/Structures/OperationProfiles.cs +++ b/UVtools.WPF/Structures/OperationProfiles.cs @@ -48,6 +48,7 @@ public class OperationProfiles //: IList<Operation> [XmlElement(typeof(OperationChangeResolution))] [XmlElement(typeof(OperationTimelapse))] [XmlElement(typeof(OperationLithophane))] + [XmlElement(typeof(OperationPCBExposure))] [XmlElement(typeof(OperationScripting))] [XmlElement(typeof(OperationLayerExportGif))] diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 5cec53c..b40b1f4 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ <PackageLicenseFile>LICENSE</PackageLicenseFile> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <RepositoryType>Git</RepositoryType> - <Version>3.3.2</Version> + <Version>3.4.0</Version> <Platforms>AnyCPU;x64</Platforms> <PackageIcon>UVtools.png</PackageIcon> <PackageReadmeFile>README.md</PackageReadmeFile> diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs index a7772fa..fea7a28 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml.cs +++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs @@ -857,7 +857,7 @@ public class ToolWindow : WindowEx try { - ToolControl.BaseOperation.Serialize(file); + ToolControl.BaseOperation.Serialize(file, true); } catch (Exception e) { diff --git a/build/createRelease.ps1 b/build/createRelease.ps1 index 3b5a59f..ab4359a 100644 --- a/build/createRelease.ps1 +++ b/build/createRelease.ps1 @@ -26,7 +26,6 @@ class FixedEncoder : System.Text.UTF8Encoding { } } - function wixCleanUpElement([System.Xml.XmlElement]$element, [string]$rootPath) { $files = Get-ChildItem -Path $rootPath -File -Force -ErrorAction SilentlyContinue @@ -216,6 +215,7 @@ if([string]::IsNullOrWhiteSpace($version)){ $installers = @("UVtools.InstallerMM", "UVtools.Installer") $msiOutputFile = "$rootFolder\UVtools.Installer\bin\x64\Release\UVtools.msi" $msiComponentsFile = "$rootFolder\UVtools.InstallerMM\UVtools.InstallerMM.wxs" +$msiProductFile = "$rootFolder\UVtools.Installer\Code\Product.wxs" $msiSourceFiles = "$rootFolder\$publishFolder\${software}_win-x64_v$version" $msbuild = "`"${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe`" /t:Build /p:Configuration=$buildWith /p:MSIProductVersion=$version" @@ -329,8 +329,11 @@ foreach ($obj in $runtimes.GetEnumerator()) { $deployStopWatch.Restart() $runtime = $obj.Name; # runtime name $extraCmd = $obj.extraCmd; # extra cmd to run with dotnet - $publishName="${software}_${runtime}_v$version" + + #dotnet build "UVtools.Cmd" -c $buildWith + #dotnet build $project -c $buildWith + if($runtime.StartsWith("win-")) { $targetZip = "$publishFolder/${software}_${runtime}_v$version.zip" # Target zip filename @@ -372,14 +375,13 @@ foreach ($obj in $runtimes.GetEnumerator()) { } else { + $args = '-b' # Bundle if($null -ne $zipPackages -and $zipPackages) { - wsl bash "build/createRelease.sh" -b -z $runtime - } - else - { - wsl bash "build/createRelease.sh" -b $runtime + $args += ' -z' # Zip } + bash -c "'build/createRelease.sh' $args $runtime" + #Start-Job { bash -c "'build/createRelease.sh' $using:args $using:runtime" } } $deployStopWatch.Stop() @@ -388,6 +390,26 @@ foreach ($obj in $runtimes.GetEnumerator()) { " } +<# +$deployStopWatch.Restart() +#Wait for all jobs to finish. +While ($(Get-Job -State Running).count -gt 0){ + Write-Host -NoNewline "." + Start-Sleep 1 +} + +Write-Output " Took: $($deployStopWatch.Elapsed)" + +#Get information from each job. +foreach($job in Get-Job){ + $job + #Receive-Job -Id ($job.Id) +} + +#Remove all jobs created. +Get-Job | Remove-Job +#> + # Universal package <# $deployStopWatch.Restart() @@ -411,14 +433,15 @@ if($null -ne $enableMSI -and $enableMSI) { $deployStopWatch.Restart() $runtime = 'win-x64' + $publishName = "${software}_${runtime}_v$version"; if ((Test-Path -Path $msiSourceFiles) -and ((Get-ChildItem "$msiSourceFiles" | Measure-Object).Count) -gt 0) { - $msiTargetFile = "$publishFolder\${software}_${runtime}_v$version.msi" + $msiTargetFile = "$publishFolder\$publishName.msi" Write-Output "################################" Write-Output "Clean and build MSI components manifest file" Remove-Item "$msiTargetFile" -ErrorAction Ignore - (Get-Content "$msiComponentsFile") -replace 'SourceDir="\.\.\\publish\\.+"', "SourceDir=`"..\publish\${software}_win-x64_v$version`"" | Out-File "$msiComponentsFile" + (Get-Content "$msiComponentsFile") -replace 'SourceDir="\.\.\\publish\\.+"', "SourceDir=`"..\publish\$publishName`"" | Out-File "$msiComponentsFile" $msiComponentsXml = [Xml] (Get-Content "$msiComponentsFile") foreach($element in $msiComponentsXml.Wix.Module.Directory.Directory) @@ -431,6 +454,30 @@ if($null -ne $enableMSI -and $enableMSI) } } + if(Test-Path "$publishFolder\$publishName\UVtools.Core.dll" -PathType Leaf){ + Add-Type -Path "$publishFolder\$publishName\UVtools.Core.dll" + } else { + Write-Error "Unable to find UVtools.Core.dll" + return + } + + # Add edit with UVtools possible extensions + $extensions = [UVtools.Core.FileFormats.FileFormat]::AllFileExtensions; + $extensionList = New-Object Collections.Generic.List[String] + foreach($ext in $extensions) + { + if($ext.Extension.Contains('.')) { continue; } # Virtual extension + + $extKey = "System.FileName:"*.$($ext.Extension.ToLowerInvariant())""; + if($extensionList.Contains($extKey)) { continue; } # Already here + $extensionList.Add($extKey); + } + if($extensionList.Count -gt 0) + { + $regValue = [String]::Join(' OR ', $extensionList) + (Get-Content "$msiProductFile") -replace '(?<A><RegistryValue Root="HKCR".+Name="AppliesTo".+Value=").+(?<B>" Type=.+)', "`${A}$regValue`${B}" | Out-File "$msiProductFile" + } + Write-Output "Building: $runtime MSI Installer" foreach($installer in $installers) diff --git a/build/createRelease.sh b/build/createRelease.sh index 22a6b3d..c988ecd 100644 --- a/build/createRelease.sh +++ b/build/createRelease.sh @@ -31,7 +31,7 @@ projectDir="$rootDir/$buildProject" cmdProjectDir="$rootDir/$cmdProject" netVersion="6.0" -if [ $runtime == "clean" ]; then +if [ "$runtime" == "clean" ]; then echo "Cleaning publish directory" rm -rf "$publishDir" 2>/dev/null exit @@ -72,13 +72,13 @@ if [ -z "$runtime" ]; then exit -1 fi -if [[ $runtime != *-* && $runtime != *-arm && $runtime != *-x64 && $runtime != *-x86 ]]; then +if [[ "$runtime" != *-* && $runtime != *-arm && $runtime != *-x64 && $runtime != *-x86 ]]; then echo "Error: The runtime '$runtime' is not valid, please pick one of the following:" ls "$platformsDir" exit -1 fi -if [ $runtime != "win-x64" -a ! -d "$platformsDir/$runtime" ]; then +if [ "$runtime" != "win-x64" -a ! -d "$platformsDir/$runtime" ]; then echo "Error: The runtime '$runtime' is not valid, please pick one of the following:" ls "$platformsDir" exit -1 @@ -111,9 +111,9 @@ chmod -fv a+x "$publishRuntimeDir/UVtools" chmod -fv a+x "$publishRuntimeDir/UVtoolsCmd" chmod -fv a+x "$publishRuntimeDir/UVtools.sh" -if [[ $runtime == win-* ]]; then +if [[ "$runtime" == win-* ]]; then echo "6. Windows should be published in a windows machine!" -elif [[ $runtime == osx-* ]]; then +elif [[ "$runtime" == osx-* ]]; then if [ $bundlePublish == true ]; then echo "6. macOS: Creating app bundle" osxApp="$publishDir/$publishName.app" @@ -134,13 +134,16 @@ elif [[ $runtime == osx-* ]]; then # Packing AppImage if [ "$zipPackage" == true -a -d "$osxApp" ] ; then echo "7. Compressing '$publishName.app' to '$publishName.zip'" - cd "$publishDir" + tempFolder="$publishDir/$publishName.app.ln" + mkdir "$tempFolder" + cd "$tempFolder" + #mv "$publishName.app" "UVtools.app" - ln -s "$publishName.app" "UVtools.app" + ln -s "$publishDir/$publishName.app" "UVtools.app" zip -rq "$publishDir/$publishName.zip" "UVtools.app" #mv "UVtools.app" "$publishName.app" - rm -f "UVtools.app" cd "$rootDir" + rm -rf "$tempFolder" zipPackage=false fi fi |