diff --git a/UVtools.ScriptSample/ScriptSample.cs b/UVtools.ScriptSample/ScriptSample.cs
new file mode 100644
index 0000000..83f5ca0
--- /dev/null
+++ b/UVtools.ScriptSample/ScriptSample.cs
@@ -0,0 +1,131 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+using System;
+using System.Drawing;
+using System.Text;
+using System.Threading.Tasks;
+using UVtools.Core.Scripting;
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+
+namespace UVtools.ScriptSample
+{
+ public class ScriptSample : ScriptGlobals
+ {
+ ScriptNumericalInput InsetMarginFromEdge = new()
+ {
+ Label = "Inset from edge",
+ ToolTip = "Margin in pixels to inset from object edge",
+ Unit = "px",
+ Minimum = 1,
+ Maximum = ushort.MaxValue,
+ Increment = 1,
+ Value = 20
+ };
+
+ ScriptNumericalInput InsetThickness = new()
+ {
+ Label = "Inset line thickness",
+ ToolTip = "Inset line thickness in pixels",
+ Unit = "px",
+ Minimum = 1,
+ Maximum = ushort.MaxValue,
+ Increment = 1,
+ Value = 10
+ };
+
+ ///
+ /// Set configurations here, this function trigger just after load a script
+ ///
+ public void ScriptInit()
+ {
+ Script.Name = "Inset";
+ Script.Description = "Perform a black inset around objects";
+ Script.Author = "Tiago Conceição";
+ Script.Version = new Version(0, 1);
+ Script.Inputs.AddRange(new[]
+ {
+ InsetMarginFromEdge,
+ InsetThickness
+ });
+ }
+
+ ///
+ /// Validate your user inputs here, this function trigger when user click on execute
+ ///
+ /// A error message, empty or null if validation passes.
+ public string ScriptValidate()
+ {
+ StringBuilder sb = new();
+
+ if (InsetMarginFromEdge.Value < InsetMarginFromEdge.Minimum)
+ {
+ sb.AppendLine($"Inset edge margin must be at least {InsetMarginFromEdge.Minimum}{InsetMarginFromEdge.Unit}");
+ }
+ if (InsetThickness.Value < InsetThickness.Minimum)
+ {
+ sb.AppendLine($"Inset thickness must be at least {InsetThickness.Minimum}{InsetThickness.Unit}");
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Execute the script, this function trigger when when user click on execute and validation passes
+ ///
+ /// True if executes successfully to the end, otherwise false.
+ public bool ScriptExecute()
+ {
+ var anchor = new Point(-1, -1); // Kernel anchor, -1, -1 = center
+ var kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), anchor); // Rectangle 3x3 kernel
+ Progress.Reset("Inset layers", Operation.LayerRangeCount); // Sets the progress name and number of items to process
+
+ // Loop user selected layers in parallel, this will put each core of CPU working here on parallel
+ Parallel.For(Operation.LayerIndexStart, Operation.LayerIndexEnd, layerIndex =>
+ {
+ if (Progress.Token.IsCancellationRequested) return; // Abort operation, user requested cancellation
+
+ var layer = SlicerFile[layerIndex]; // Unpack and expose layer variable for easier use
+ using var mat = layer.LayerMat; // Gets this layer mat/image
+ var original = mat.Clone(); // Keep a original mat copy
+ using var erodeMat = new Mat(); // Creates a temporary mat for the eroded image
+ using var wallMat = new Mat(); // Creates a temporary mat for the wall image
+
+ var target = Operation.GetRoiOrDefault(mat); // Get ROI from mat if user selected an region
+
+ // Erode original image by InsetMarginFromEdge pixels, so we get the offset margin from image and put new image on erodeMat
+ CvInvoke.Erode(target, erodeMat, kernel, anchor, InsetMarginFromEdge.Value, BorderType.Reflect101, default);
+
+ // Now erode the eroded image with InsetThickness pixels, so we get the original-margin-thickness image and put the new image on wallMat
+ CvInvoke.Erode(erodeMat, wallMat, kernel, anchor, InsetThickness.Value, BorderType.Reflect101, default);
+
+ // Subtract walls image from eroded image, so we get only the inset line pixels in white and put back into wallMat
+ CvInvoke.Subtract(erodeMat, wallMat, wallMat);
+
+ // Invert pixels of wallMat so the whites will become black and blacks whites
+ CvInvoke.BitwiseNot(wallMat, wallMat);
+
+ // Bitwise And original image with the modified image and put back into mat
+ // This will keep only the pixels that are positive in both mat's, so mat[n] & wallMat[n] must both have a positive pixel value (> 0)
+ CvInvoke.BitwiseAnd(target, wallMat, target);
+
+ // Apply the results only to the selected masked area, if user selected one
+ Operation.ApplyMask(original, target);
+
+ // Set current layer image with the modified mat we just manipulated
+ layer.LayerMat = mat;
+
+ // Increment progress bar by 1
+ Progress.LockAndIncrement();
+ });
+
+ return true;
+ }
+ }
+}
diff --git a/UVtools.ScriptSample/UVtools.ScriptSample.csproj b/UVtools.ScriptSample/UVtools.ScriptSample.csproj
new file mode 100644
index 0000000..d2db2bd
--- /dev/null
+++ b/UVtools.ScriptSample/UVtools.ScriptSample.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
diff --git a/UVtools.sln b/UVtools.sln
index f59ab22..65c5cd1 100644
--- a/UVtools.sln
+++ b/UVtools.sln
@@ -9,6 +9,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UVtools.GUI", "UVtools.GUI\
EndProjectSection
EndProject
Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "UVtools.InstallerMM", "UVtools.InstallerMM\UVtools.InstallerMM.wixproj", "{E53BAA5D-29A8-4287-B3AA-1AFF5A4BDC6C}"
+ ProjectSection(ProjectDependencies) = postProject
+ {B1B40A7B-D30B-4056-AE6F-C4F0519FD8D4} = {B1B40A7B-D30B-4056-AE6F-C4F0519FD8D4}
+ EndProjectSection
EndProject
Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "UVtools.Installer", "UVtools.Installer\UVtools.Installer.wixproj", "{41224896-08E9-4F22-9E56-5F9D46DEC7D2}"
EndProject
@@ -24,6 +27,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UVtools.WPF", "UVtools.WPF\
{7C9927F8-132E-4A37-B894-440E0FD5AA3D} = {7C9927F8-132E-4A37-B894-440E0FD5AA3D}
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UVtools.ScriptSample", "UVtools.ScriptSample\UVtools.ScriptSample.csproj", "{6CCAA18A-8810-42DF-B75B-4160B40B9DD5}"
+ ProjectSection(ProjectDependencies) = postProject
+ {7C9927F8-132E-4A37-B894-440E0FD5AA3D} = {7C9927F8-132E-4A37-B894-440E0FD5AA3D}
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -98,6 +106,18 @@ Global
{B1B40A7B-D30B-4056-AE6F-C4F0519FD8D4}.Release|x64.Build.0 = Release|Any CPU
{B1B40A7B-D30B-4056-AE6F-C4F0519FD8D4}.Release|x86.ActiveCfg = Release|Any CPU
{B1B40A7B-D30B-4056-AE6F-C4F0519FD8D4}.Release|x86.Build.0 = Release|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Debug|x64.Build.0 = Debug|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Debug|x86.Build.0 = Debug|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Release|x64.ActiveCfg = Release|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Release|x64.Build.0 = Release|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Release|x86.ActiveCfg = Release|Any CPU
+ {6CCAA18A-8810-42DF-B75B-4160B40B9DD5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE