Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/sn4k3/UVtools.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTiago Conceição <Tiago_caza@hotmail.com>2022-05-02 03:02:13 +0300
committerTiago Conceição <Tiago_caza@hotmail.com>2022-05-02 03:02:13 +0300
commit3130ebe0f21d34f3d50c5ef05b7d0144ce674aa9 (patch)
tree5aa893183909e4822d82852a1ca40466698a74f6
parent2625c13cc3179a55865e5594180050258ab60a95 (diff)
v3.4.0v3.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
-rw-r--r--CHANGELOG.md12
-rw-r--r--RELEASE_NOTES.md19
-rw-r--r--Scripts/010 Editor/osf.bt103
-rw-r--r--UVtools.Cmd/Program.cs8
-rw-r--r--UVtools.Cmd/Symbols/ConvertCommand.cs80
-rw-r--r--UVtools.Cmd/UVtools.Cmd.csproj2
-rw-r--r--UVtools.Core/Extensions/EmguExtensions.cs26
-rw-r--r--UVtools.Core/FileFormats/CXDLPFile.cs10
-rw-r--r--UVtools.Core/FileFormats/FileExtension.cs10
-rw-r--r--UVtools.Core/FileFormats/FileFormat.cs22
-rw-r--r--UVtools.Core/FileFormats/ImageFile.cs2
-rw-r--r--UVtools.Core/Gerber/Apertures/Aperture.cs102
-rw-r--r--UVtools.Core/Gerber/Apertures/CircleAperture.cs37
-rw-r--r--UVtools.Core/Gerber/Apertures/EllipseAperture.cs42
-rw-r--r--UVtools.Core/Gerber/Apertures/MacroAperture.cs38
-rw-r--r--UVtools.Core/Gerber/Apertures/PoygonAperture.cs40
-rw-r--r--UVtools.Core/Gerber/Apertures/RectangleAperture.cs43
-rw-r--r--UVtools.Core/Gerber/Enumerations.cs37
-rw-r--r--UVtools.Core/Gerber/GerberDocument.cs537
-rw-r--r--UVtools.Core/Gerber/Macro.cs114
-rw-r--r--UVtools.Core/Gerber/Primitives/CenterLinePrimitive.cs166
-rw-r--r--UVtools.Core/Gerber/Primitives/CirclePrimitive.cs149
-rw-r--r--UVtools.Core/Gerber/Primitives/CommentPrimitive.cs57
-rw-r--r--UVtools.Core/Gerber/Primitives/OutlinePrimitive.cs150
-rw-r--r--UVtools.Core/Gerber/Primitives/PolygonPrimitive.cs165
-rw-r--r--UVtools.Core/Gerber/Primitives/Primitive.cs31
-rw-r--r--UVtools.Core/Gerber/Primitives/VectorLinePrimitive.cs184
-rw-r--r--UVtools.Core/Managers/IssueManager.cs10
-rw-r--r--UVtools.Core/Objects/ValueDescription.cs10
-rw-r--r--UVtools.Core/Operations/Operation.cs5
-rw-r--r--UVtools.Core/Operations/OperationLayerImport.cs16
-rw-r--r--UVtools.Core/Operations/OperationLithophane.cs17
-rw-r--r--UVtools.Core/Operations/OperationPCBExposure.cs213
-rw-r--r--UVtools.Core/UVtools.Core.csproj2
-rw-r--r--UVtools.Installer/Code/Product.wxs81
-rw-r--r--UVtools.InstallerMM/UVtools.InstallerMM.wxs2
-rw-r--r--UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml80
-rw-r--r--UVtools.WPF/Controls/Tools/ToolPCBExposureControl.axaml.cs104
-rw-r--r--UVtools.WPF/MainWindow.axaml.cs231
-rw-r--r--UVtools.WPF/Program.cs5
-rw-r--r--UVtools.WPF/Structures/OperationProfiles.cs1
-rw-r--r--UVtools.WPF/UVtools.WPF.csproj2
-rw-r--r--UVtools.WPF/Windows/ToolWindow.axaml.cs2
-rw-r--r--build/createRelease.ps165
-rw-r--r--build/createRelease.sh19
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:&quot;*.sl1&quot; OR System.FileName:&quot;*.sl1s&quot; OR System.FileName:&quot;*.zip&quot; OR System.FileName:&quot;*.photon&quot; OR System.FileName:&quot;*.cbddlp&quot; OR System.FileName:&quot;*.ctb&quot; OR System.FileName:&quot;*.photons&quot; OR System.FileName:&quot;*.phz&quot; OR System.FileName:&quot;*.fdg&quot; OR System.FileName:&quot;*.pws&quot; OR System.FileName:&quot;*.pw0&quot; OR System.FileName:&quot;*.pwx&quot; OR System.FileName:&quot;*.dlp&quot; OR System.FileName:&quot;*.pwmx&quot; OR System.FileName:&quot;*.pwmb&quot; OR System.FileName:&quot;*.pwmo&quot; OR System.FileName:&quot;*.pwms&quot; OR System.FileName:&quot;*.pwma&quot; OR System.FileName:&quot;*.pmsq&quot; OR System.FileName:&quot;*.pm3&quot; OR System.FileName:&quot;*.pm3m&quot; OR System.FileName:&quot;*.cws&quot; OR System.FileName:&quot;*.osla&quot; OR System.FileName:&quot;*.jxs&quot; OR System.FileName:&quot;*.zcode&quot; OR System.FileName:&quot;*.zcodex&quot; OR System.FileName:&quot;*.mdlp&quot; OR System.FileName:&quot;*.gr1&quot; OR System.FileName:&quot;*.cxdlp&quot; OR System.FileName:&quot;*.lgs&quot; OR System.FileName:&quot;*.lgs30&quot; OR System.FileName:&quot;*.lgs120&quot; OR System.FileName:&quot;*.lgs4k&quot; OR System.FileName:&quot;*.svgx&quot; OR System.FileName:&quot;*.vdt&quot; OR System.FileName:&quot;*.uvj&quot; OR System.FileName:&quot;*.png&quot; OR System.FileName:&quot;*.jpg&quot; OR System.FileName:&quot;*.jpeg&quot; OR System.FileName:&quot;*.jp2&quot; OR System.FileName:&quot;*.tif&quot; OR System.FileName:&quot;*.tiff&quot; OR System.FileName:&quot;*.bmp&quot; OR System.FileName:&quot;*.pbm&quot; OR System.FileName:&quot;*.pgm&quot; OR System.FileName:&quot;*.sr&quot; OR System.FileName:&quot;*.ras&quot;" Type="string" />
+ <RegistryKey Root="HKCR" Key="*\shell\UVtools\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes" />
+ <RegistryValue Root="HKCR" Key="*\shell\UVtools\command" Value="&quot;[INSTALLLOCATION]UVtools.exe&quot; &quot;%1&quot;" 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:&quot;*.$($ext.Extension.ToLowerInvariant())&quot;";
+ 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