diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68b0ffa..5e0126b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
# Changelog
+## 28/03/2021 - v2.8.0
+
+* **Scripting:**
+ * Add scripting capability, this allow to run external scripts inside the GUI and take advantage of visual layout to display user input fields
+ * Scripts run under the "Roslyn Scripting API" and can make use of the whole C# language, this mean a huge boost compared to PowerShell scripts
+ * Scripts are written in the same way UVtools is, by learning and programing scripts you are learning the UVtools core
+ * For more information see the script sample: https://github.com/sn4k3/UVtools/tree/master/UVtools.ScriptSample
+ * To run scripts go to: Tools - Scripting
+* **File formats:**
+ * Add a check and warning when opening an file that have a diferent layer and file resolution
+* **Issues:**
+ * Add "Print height" as new type of issue detection, all layers that goes beyond maximum printer Z height will be flagged as PrintHeight issue
+ * Print height issues will not be automatical fixed, however user can fix it by remove some layers to counter the problem, still is recommended to resize object on slicer
+ * Fix unable to compute issues when only islands or overhangs are selected to be detected alone (#177)
+* **Settings:**
+ * Add default directory for scripts on "General - File dialogs"
+ * Add checkbox on "Issues - Compute - Print height" to enable or disable this type of detection
+ * Add numerical on "Issues - Print height - Offset" to define a custom offset from Z top
+ * Fix default directories input width to not grow with text, it was overflowing on large strings
+* **Menu - Help:**
+ * Add web link to "Wiki & tutorials"
+ * Add web link to "Facebook group"
+ * Add web link to "Report a issue"
+ * Add web link to "Ask a question"
+ * Add web link to "Suggest an improvement or new features"
+
## 28/03/2021 - v2.7.2
* **Core:**
diff --git a/UVtools.Core/Layer/LayerIssue.cs b/UVtools.Core/Layer/LayerIssue.cs
index 5bac5e8..15f9cff 100644
--- a/UVtools.Core/Layer/LayerIssue.cs
+++ b/UVtools.Core/Layer/LayerIssue.cs
@@ -22,14 +22,21 @@ namespace UVtools.Core
public OverhangDetectionConfiguration OverhangConfig { get; }
public ResinTrapDetectionConfiguration ResinTrapConfig { get; }
public TouchingBoundDetectionConfiguration TouchingBoundConfig { get; }
+ public PrintHeightDetectionConfiguration PrintHeightConfig { get; }
public bool EmptyLayerConfig { get; }
- public IssuesDetectionConfiguration(IslandDetectionConfiguration islandConfig, OverhangDetectionConfiguration overhangConfig, ResinTrapDetectionConfiguration resinTrapConfig, TouchingBoundDetectionConfiguration touchingBoundConfig, bool emptyLayerConfig)
+ public IssuesDetectionConfiguration(IslandDetectionConfiguration islandConfig,
+ OverhangDetectionConfiguration overhangConfig,
+ ResinTrapDetectionConfiguration resinTrapConfig,
+ TouchingBoundDetectionConfiguration touchingBoundConfig,
+ PrintHeightDetectionConfiguration printHeightConfig,
+ bool emptyLayerConfig)
{
IslandConfig = islandConfig;
OverhangConfig = overhangConfig;
ResinTrapConfig = resinTrapConfig;
TouchingBoundConfig = touchingBoundConfig;
+ PrintHeightConfig = printHeightConfig;
EmptyLayerConfig = emptyLayerConfig;
}
}
@@ -211,6 +218,24 @@ namespace UVtools.Core
}
}
+ public sealed class PrintHeightDetectionConfiguration
+ {
+ ///
+ /// Gets if the detection is enabled
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Get the offset from top to sum to printer max Z height
+ ///
+ public float Offset { get; set; }
+
+ public PrintHeightDetectionConfiguration(bool enabled = true)
+ {
+ Enabled = enabled;
+ }
+ }
+
public class LayerIssue : IEquatable, IEnumerable
{
@@ -220,6 +245,7 @@ namespace UVtools.Core
Overhang,
ResinTrap,
TouchingBound,
+ PrintHeight,
EmptyLayer,
//HoleSandwich,
}
diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs
index 49a6730..ae4c9a0 100644
--- a/UVtools.Core/Layer/LayerManager.cs
+++ b/UVtools.Core/Layer/LayerManager.cs
@@ -596,6 +596,7 @@ namespace UVtools.Core
OverhangDetectionConfiguration overhangConfig = null,
ResinTrapDetectionConfiguration resinTrapConfig = null,
TouchingBoundDetectionConfiguration touchBoundConfig = null,
+ PrintHeightDetectionConfiguration printHeightConfig = null,
bool emptyLayersConfig = true,
List ignoredIssues = null,
OperationProgress progress = null)
@@ -604,6 +605,7 @@ namespace UVtools.Core
overhangConfig ??= new OverhangDetectionConfiguration();
resinTrapConfig ??= new ResinTrapDetectionConfiguration();
touchBoundConfig ??= new TouchingBoundDetectionConfiguration();
+ printHeightConfig ??= new PrintHeightDetectionConfiguration();
progress ??= new OperationProgress();
var result = new ConcurrentBag();
@@ -803,6 +805,7 @@ namespace UVtools.Core
OverhangDetectionConfiguration overhangConfig = null,
ResinTrapDetectionConfiguration resinTrapConfig = null,
TouchingBoundDetectionConfiguration touchBoundConfig = null,
+ PrintHeightDetectionConfiguration printHeightConfig = null,
bool emptyLayersConfig = true,
List ignoredIssues = null,
OperationProgress progress = null)
@@ -812,6 +815,7 @@ namespace UVtools.Core
overhangConfig ??= new OverhangDetectionConfiguration();
resinTrapConfig ??= new ResinTrapDetectionConfiguration();
touchBoundConfig ??= new TouchingBoundDetectionConfiguration();
+ printHeightConfig ??= new PrintHeightDetectionConfiguration();
progress ??= new OperationProgress();
var result = new ConcurrentBag();
@@ -826,7 +830,33 @@ namespace UVtools.Core
return true;
}
- if (islandConfig.Enabled || overhangConfig.Enabled || resinTrapConfig.Enabled || touchBoundConfig.Enabled || emptyLayersConfig)
+ if (printHeightConfig.Enabled && SlicerFile.MaxPrintHeight > 0)
+ {
+ float printHeightWithOffset = Layer.RoundHeight(SlicerFile.MaxPrintHeight + printHeightConfig.Offset);
+ if (SlicerFile.PrintHeight > printHeightWithOffset)
+ {
+ foreach (var layer in this)
+ {
+ if (layer.PositionZ > printHeightWithOffset)
+ {
+ AddIssue(new LayerIssue(layer, LayerIssue.IssueType.PrintHeight));
+ }
+ }
+ }
+ }
+
+ if (emptyLayersConfig)
+ {
+ foreach (var layer in this)
+ {
+ if (layer.IsEmpty)
+ {
+ AddIssue(new LayerIssue(layer, LayerIssue.IssueType.EmptyLayer));
+ }
+ }
+ }
+
+ if (islandConfig.Enabled || overhangConfig.Enabled || resinTrapConfig.Enabled || touchBoundConfig.Enabled)
{
progress.Reset(OperationProgress.StatusIslands, LayerCount);
@@ -838,21 +868,15 @@ namespace UVtools.Core
if (progress.Token.IsCancellationRequested) return;
if (layer.IsEmpty)
{
- if (emptyLayersConfig)
- {
- AddIssue(new LayerIssue(layer, LayerIssue.IssueType.EmptyLayer));
- }
-
progress.LockAndIncrement();
-
return;
}
// Spare a decoding cycle
if (!touchBoundConfig.Enabled &&
!resinTrapConfig.Enabled &&
- !overhangConfig.Enabled || overhangConfig.Enabled && (layer.Index == 0 || overhangConfig.WhiteListLayers is not null && !overhangConfig.WhiteListLayers.Contains(layer.Index)) &&
- !islandConfig.Enabled || islandConfig.Enabled && (layer.Index == 0 || islandConfig.WhiteListLayers is not null && !islandConfig.WhiteListLayers.Contains(layer.Index))
+ (!overhangConfig.Enabled || overhangConfig.Enabled && (layer.Index == 0 || overhangConfig.WhiteListLayers is not null && !overhangConfig.WhiteListLayers.Contains(layer.Index))) &&
+ (!islandConfig.Enabled || islandConfig.Enabled && (layer.Index == 0 || islandConfig.WhiteListLayers is not null && !islandConfig.WhiteListLayers.Contains(layer.Index)))
)
{
progress.LockAndIncrement();
diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs
index 168d629..f64f4c7 100644
--- a/UVtools.Core/Operations/Operation.cs
+++ b/UVtools.Core/Operations/Operation.cs
@@ -8,6 +8,7 @@
using System;
using System.Drawing;
+using System.Threading.Tasks;
using System.Xml.Serialization;
using Emgu.CV;
using Emgu.CV.Util;
@@ -398,6 +399,19 @@ namespace UVtools.Core.Operations
throw new NotImplementedException();
}
+ ///
+ /// Copy this operation base configuration to another operation.
+ /// Layer range, ROI, Masks
+ ///
+ ///
+ public void CopyConfigurationTo(Operation operation)
+ {
+ operation.LayerIndexStart = LayerIndexStart;
+ operation.LayerIndexEnd = LayerIndexEnd;
+ operation.ROI = ROI;
+ operation.MaskPoints = MaskPoints;
+ }
+
public virtual Operation Clone()
{
var operation = MemberwiseClone() as Operation;
diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs
index 539c270..c57a03b 100644
--- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs
+++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs
@@ -12,6 +12,7 @@ using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
diff --git a/UVtools.Core/Operations/OperationEditParameters.cs b/UVtools.Core/Operations/OperationEditParameters.cs
index d5e6e72..b6da2c2 100644
--- a/UVtools.Core/Operations/OperationEditParameters.cs
+++ b/UVtools.Core/Operations/OperationEditParameters.cs
@@ -9,6 +9,7 @@
using System;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using System.Xml.Serialization;
using UVtools.Core.FileFormats;
using UVtools.Core.Objects;
diff --git a/UVtools.Core/Operations/OperationIPrintedThisFile.cs b/UVtools.Core/Operations/OperationIPrintedThisFile.cs
index 602f624..c743aa7 100644
--- a/UVtools.Core/Operations/OperationIPrintedThisFile.cs
+++ b/UVtools.Core/Operations/OperationIPrintedThisFile.cs
@@ -8,6 +8,7 @@
using System;
using System.Text;
+using System.Threading.Tasks;
using UVtools.Core.FileFormats;
using UVtools.Core.Managers;
using UVtools.Core.Objects;
diff --git a/UVtools.Core/Operations/OperationLayerClone.cs b/UVtools.Core/Operations/OperationLayerClone.cs
index 5e82593..9d12ab5 100644
--- a/UVtools.Core/Operations/OperationLayerClone.cs
+++ b/UVtools.Core/Operations/OperationLayerClone.cs
@@ -11,6 +11,7 @@ using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using UVtools.Core.FileFormats;
using UVtools.Core.Objects;
diff --git a/UVtools.Core/Operations/OperationLayerReHeight.cs b/UVtools.Core/Operations/OperationLayerReHeight.cs
index 17b70db..9d214f2 100644
--- a/UVtools.Core/Operations/OperationLayerReHeight.cs
+++ b/UVtools.Core/Operations/OperationLayerReHeight.cs
@@ -9,6 +9,7 @@
using System;
using System.Collections.Generic;
using System.Text;
+using System.Threading.Tasks;
using Emgu.CV;
using UVtools.Core.Extensions;
using UVtools.Core.FileFormats;
diff --git a/UVtools.Core/Operations/OperationLayerRemove.cs b/UVtools.Core/Operations/OperationLayerRemove.cs
index b815c7f..653eef7 100644
--- a/UVtools.Core/Operations/OperationLayerRemove.cs
+++ b/UVtools.Core/Operations/OperationLayerRemove.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
using UVtools.Core.FileFormats;
namespace UVtools.Core.Operations
diff --git a/UVtools.Core/Operations/OperationRepairLayers.cs b/UVtools.Core/Operations/OperationRepairLayers.cs
index f1824b6..fc37c87 100644
--- a/UVtools.Core/Operations/OperationRepairLayers.cs
+++ b/UVtools.Core/Operations/OperationRepairLayers.cs
@@ -143,6 +143,7 @@ namespace UVtools.Core.Operations
var islandConfig = IslandDetectionConfig;
var overhangConfig = new OverhangDetectionConfiguration(false);
var touchingBoundsConfig = new TouchingBoundDetectionConfiguration(false);
+ var printHeightConfig = new PrintHeightDetectionConfiguration(false);
var resinTrapsConfig = new ResinTrapDetectionConfiguration(false);
var emptyLayersConfig = false;
@@ -157,7 +158,7 @@ namespace UVtools.Core.Operations
.Select(grp => grp.First())
.ToList();*/
islandConfig.WhiteListLayers = islandsToRecompute.ToList();
- recursiveIssues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapsConfig, touchingBoundsConfig, emptyLayersConfig);
+ recursiveIssues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapsConfig, touchingBoundsConfig, printHeightConfig, emptyLayersConfig);
//Debug.WriteLine(i);
}
diff --git a/UVtools.Core/Operations/OperationScripting.cs b/UVtools.Core/Operations/OperationScripting.cs
new file mode 100644
index 0000000..ea95c47
--- /dev/null
+++ b/UVtools.Core/Operations/OperationScripting.cs
@@ -0,0 +1,168 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using Microsoft.CodeAnalysis.CSharp.Scripting;
+using Microsoft.CodeAnalysis.Scripting;
+using UVtools.Core.FileFormats;
+using UVtools.Core.Objects;
+using UVtools.Core.Scripting;
+
+namespace UVtools.Core.Operations
+{
+ [Serializable]
+ public sealed class OperationScripting : Operation
+ {
+ #region Members
+
+ public event EventHandler OnScriptReload;
+ private string _filePath;
+ private string _scriptText;
+ private ScriptState _scriptState;
+
+ #endregion
+
+ #region Overrides
+
+ public override bool CanHaveProfiles => false;
+
+ public override string Title => "Scripting";
+
+ public override string Description =>
+ $"Run external scripts to manipulate the loaded file\n" +
+ $"The scripts have wide access to your system and able to do modifications, read/write files, etc. " +
+ $"Make sure to run only the scripts you trust! Or run UVtools in a sandbox while executing this.";
+
+ public override string ConfirmationText =>
+ $"Run script from layers {LayerIndexStart} through {LayerIndexEnd}?";
+
+ public override string ProgressTitle =>
+ $"Scripting from layers {LayerIndexStart} through {LayerIndexEnd}";
+
+ public override string ProgressAction => "Scripted layers";
+
+ public override StringTag Validate(params object[] parameters)
+ {
+ if (!CanExecute)
+ {
+ return new StringTag("Script is not loaded.");
+ }
+
+ var scriptValidation = _scriptState.ContinueWithAsync("return ScriptValidate();").Result;
+ return new StringTag(scriptValidation.ReturnValue);
+ }
+
+ #endregion
+
+ #region Enums
+
+ #endregion
+
+ #region Properties
+
+ public ScriptGlobals ScriptGlobals { get; private set; }
+
+ public string FilePath
+ {
+ get => _filePath;
+ set
+ {
+ if (value is null)
+ {
+ RaiseAndSetIfChanged(ref _filePath, null);
+ }
+ else
+ {
+ if (!value.EndsWith(".csx") && !value.EndsWith(".cs")) return;
+ if (!File.Exists(value)) return;
+ if (!RaiseAndSetIfChanged(ref _filePath, value)) return;
+ }
+
+ RaisePropertyChanged(nameof(HaveFile));
+ }
+ }
+
+ [XmlIgnore]
+ public string ScriptText
+ {
+ get => _scriptText;
+ set => RaiseAndSetIfChanged(ref _scriptText, value);
+ }
+
+ [XmlIgnore]
+ public bool CanExecute => !string.IsNullOrWhiteSpace(_filePath) && _scriptState is not null;
+
+ [XmlIgnore]
+ public bool HaveFile => !string.IsNullOrWhiteSpace(_filePath);
+
+ /*public override string ToString()
+ {
+ var result = $"[{_infillType}] [Wall: {_wallThickness}px] [B: {_infillBrightness}px] [T: {_infillThickness}px] [S: {_infillSpacing}px]" + LayerRangeString;
+ if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
+ return result;
+ }*/
+
+
+
+ #endregion
+
+ #region Constructor
+
+ public OperationScripting() { }
+
+ public OperationScripting(FileFormat slicerFile) : base(slicerFile)
+ { }
+
+ #endregion
+
+ #region Equality
+
+ #endregion
+
+ #region Methods
+
+ public void ReloadScriptFromFile(string filePath = null)
+ {
+ if (!string.IsNullOrWhiteSpace(filePath)) FilePath = filePath;
+ if (string.IsNullOrWhiteSpace(_filePath) || !File.Exists(_filePath)) return;
+ ReloadScriptFromText(File.ReadAllText(_filePath));
+ }
+
+ public void ReloadScriptFromText(string text = null)
+ {
+ if (!string.IsNullOrWhiteSpace(text)) ScriptText = text;
+ if (string.IsNullOrWhiteSpace(_scriptText)) return;
+
+
+ ScriptText = ScriptParser.ParseScriptFromText(_scriptText);
+
+ ScriptGlobals = new ScriptGlobals { SlicerFile = SlicerFile, Operation = this };
+ _scriptState = CSharpScript.RunAsync(_scriptText,
+ ScriptOptions.Default.AddReferences(typeof(About).Assembly).WithAllowUnsafe(true),
+ ScriptGlobals).Result;
+
+ var result = _scriptState.ContinueWithAsync("ScriptInit();").Result;
+
+ RaisePropertyChanged(nameof(CanExecute));
+ OnScriptReload?.Invoke(this, EventArgs.Empty);
+ }
+
+
+
+ protected override bool ExecuteInternally(OperationProgress progress)
+ {
+ ScriptGlobals.Progress = progress;
+ var scriptExecute = _scriptState.ContinueWithAsync("return ScriptExecute();").Result;
+ return !progress.Token.IsCancellationRequested && scriptExecute.ReturnValue;
+ }
+
+ #endregion
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptBaseInput.cs b/UVtools.Core/Scripting/ScriptBaseInput.cs
new file mode 100644
index 0000000..b7f78ff
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptBaseInput.cs
@@ -0,0 +1,36 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+namespace UVtools.Core.Scripting
+{
+ public abstract class ScriptBaseInput
+ {
+ ///
+ /// Gets the input label
+ ///
+ public string Label { get; init; }
+
+ ///
+ /// Gets the hover tooltip for this input
+ ///
+ public string ToolTip { get; init; }
+
+ ///
+ /// Gets the value representative unit name
+ ///
+ public string Unit { get; init; }
+ }
+
+ public abstract class ScriptBaseInput : ScriptBaseInput
+ {
+ ///
+ /// Gets or sets the value for this input
+ ///
+ public T Value { get; set; }
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptCheckBoxInput.cs b/UVtools.Core/Scripting/ScriptCheckBoxInput.cs
new file mode 100644
index 0000000..551e6f0
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptCheckBoxInput.cs
@@ -0,0 +1,14 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+namespace UVtools.Core.Scripting
+{
+ public class ScriptCheckBoxInput : ScriptBaseInput
+ {
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptConfiguration.cs b/UVtools.Core/Scripting/ScriptConfiguration.cs
new file mode 100644
index 0000000..321c49c
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptConfiguration.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+
+namespace UVtools.Core.Scripting
+{
+ public sealed class ScriptConfiguration
+ {
+ ///
+ /// Gets the script name
+ ///
+ public string Name { get; set; } = "Unnamed script";
+
+ ///
+ /// Gets the script description of what it does
+ ///
+ public string Description { get; set; } = "I don't know my purpose, do not run me!";
+
+ ///
+ /// Gets the script author name
+ ///
+ public string Author { get; set; } = "Undefined";
+
+ ///
+ /// Gets the script version
+ ///
+ public Version Version { get; set; } = new(0, 1);
+
+ ///
+ /// List of user inputs to show on GUI for configuration of the script
+ ///
+ public List UserInputs { get; } = new();
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptGlobals.cs b/UVtools.Core/Scripting/ScriptGlobals.cs
new file mode 100644
index 0000000..15bd062
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptGlobals.cs
@@ -0,0 +1,35 @@
+/*
+ * 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 UVtools.Core.FileFormats;
+using UVtools.Core.Operations;
+
+namespace UVtools.Core.Scripting
+{
+ public class ScriptGlobals
+ {
+ ///
+ /// Gets the loaded slicer file
+ ///
+ public FileFormat SlicerFile { get; init; }
+
+ ///
+ /// Gets the progress operation for loading bar
+ ///
+ public OperationProgress Progress { get; set; } = new("Unknown");
+
+ ///
+ /// Gets the current operation holding the layer range, mask, roi, etc
+ ///
+ public Operation Operation { get; init; }
+
+ ///
+ /// Gets the script configuration
+ ///
+ public ScriptConfiguration Script { get; } = new();
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptNumericalInput.cs b/UVtools.Core/Scripting/ScriptNumericalInput.cs
new file mode 100644
index 0000000..f894bfa
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptNumericalInput.cs
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+namespace UVtools.Core.Scripting
+{
+ public class ScriptNumericalInput : ScriptBaseInput
+ {
+ ///
+ /// Gets the minimum for this input
+ ///
+ public T Minimum { get; init; }
+
+ ///
+ /// Gets the minimum for this input
+ ///
+ public T Maximum { get; init; }
+
+ ///
+ /// Gets the increment value for this
+ ///
+ public T Increment { get; init; }
+
+ ///
+ /// Gets the number of decimal plates to round the value
+ ///
+ public byte DecimalPlates { get; init; } = 2;
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptParser.cs b/UVtools.Core/Scripting/ScriptParser.cs
new file mode 100644
index 0000000..b7c4923
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptParser.cs
@@ -0,0 +1,60 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+using System;
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace UVtools.Core.Scripting
+{
+ public static class ScriptParser
+ {
+ public static string ParseScriptFromFile(string path)
+ {
+ return ParseScriptFromText(File.ReadAllText(path));
+ }
+
+ ///
+ /// Parse the script and clean forbidden keywords
+ ///
+ /// Text to parse
+ /// The parsed text
+ public static string ParseScriptFromText(string text)
+ {
+ if(!Regex.Match(text, @"(void\s*ScriptInit\s*\(\s*\))").Success)
+ {
+ throw new ArgumentException("The method \"void ScriptInit()\" was not found on script, please verify the script.");
+ }
+ if (!Regex.Match(text, @"(string\s*ScriptValidate\s*\(\s*\))").Success)
+ {
+ throw new ArgumentException("The method \"string ScriptValidate()\" was not found on script, please verify the script.");
+ }
+ if (!Regex.Match(text, @"(bool\s*ScriptExecute\s*\(\s*\))").Success)
+ {
+ throw new ArgumentException("The method \"bool ScriptExecute()\" was not found on script, please verify the script.");
+ }
+
+ var textLength = text.Length;
+ sbyte bracketsToRemove = 0;
+ text = Regex.Replace(text, @"(namespace .*\n*.*{)", string.Empty);
+ if (textLength != text.Length) bracketsToRemove++;
+ textLength = text.Length;
+ text = Regex.Replace(text, "(.*class .*\n*.*{)", string.Empty);
+ if (textLength != text.Length) bracketsToRemove++;
+
+ if (bracketsToRemove <= 0) return text;
+
+ for (textLength = text.Length - 1; textLength >= 0 && bracketsToRemove > 0; textLength--)
+ {
+ if (text[textLength] == '}') bracketsToRemove--;
+ }
+
+ return text.Substring(0, textLength);
+ }
+ }
+}
diff --git a/UVtools.Core/Scripting/ScriptTextBoxInput.cs b/UVtools.Core/Scripting/ScriptTextBoxInput.cs
new file mode 100644
index 0000000..9a31173
--- /dev/null
+++ b/UVtools.Core/Scripting/ScriptTextBoxInput.cs
@@ -0,0 +1,18 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+namespace UVtools.Core.Scripting
+{
+ public class ScriptTextBoxInput : ScriptBaseInput
+ {
+ ///
+ /// Gets if this input accepts multi lines
+ ///
+ public bool MultiLine { get; init; }
+ }
+}
diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj
index 5f4d166..b5b8319 100644
--- a/UVtools.Core/UVtools.Core.csproj
+++ b/UVtools.Core/UVtools.Core.csproj
@@ -10,7 +10,7 @@
https://github.com/sn4k3/UVtools
https://github.com/sn4k3/UVtools
MSLA/DLP, file analysis, calibration, repair, conversion and manipulation
- 2.7.2
+ 2.8.0
Copyright © 2020 PTRTECH
UVtools.png
AnyCPU;x64
@@ -46,6 +46,7 @@
+
diff --git a/UVtools.ScriptSample/ScriptAutomateWorkflowSample.cs b/UVtools.ScriptSample/ScriptAutomateWorkflowSample.cs
new file mode 100644
index 0000000..77b729a
--- /dev/null
+++ b/UVtools.ScriptSample/ScriptAutomateWorkflowSample.cs
@@ -0,0 +1,115 @@
+/*
+ * GNU AFFERO GENERAL PUBLIC LICENSE
+ * Version 3, 19 November 2007
+ * Copyright (C) 2007 Free Software Foundation, Inc.
+ * Everyone is permitted to copy and distribute verbatim copies
+ * of this license document, but changing it is not allowed.
+ */
+
+using System;
+using System.Collections.Generic;
+using Emgu.CV.CvEnum;
+using UVtools.Core.Operations;
+using UVtools.Core.Scripting;
+
+namespace UVtools.ScriptSample
+{
+ ///
+ /// Performs a black inset around objects
+ ///
+ public class ScriptAutomateWorkflowSample : ScriptGlobals
+ {
+ private ScriptCheckBoxInput InputErode = new()
+ {
+ Label = "Erode base layers",
+ Value = true
+ };
+ private ScriptCheckBoxInput InputReliefRaft = new()
+ {
+ Label = "Relief raft",
+ Value = true
+ };
+ private ScriptCheckBoxInput InputPixelDimming = new()
+ {
+ Label = "Pixel dimming base layers",
+ Value = true
+ };
+ ///
+ /// Set configurations here, this function trigger just after load a script
+ ///
+ public void ScriptInit()
+ {
+ Script.Name = "Automate my workflow";
+ Script.Description = "A workflow automation sample";
+ Script.Author = "Tiago Conceição";
+ Script.Version = new Version(0, 1);
+ Script.UserInputs.AddRange(new []
+ {
+ InputErode,
+ InputReliefRaft,
+ InputPixelDimming
+ });
+ }
+
+ ///
+ /// Validate user inputs here, this function trigger when user click on execute
+ ///
+ /// A error message, empty or null if validation passes.
+ public string ScriptValidate()
+ {
+ return null;
+ }
+
+ ///
+ /// 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()
+ {
+ List operations = new();
+
+ // Morph bottom layers
+ if (InputErode.Value)
+ {
+ OperationMorph morph = new(SlicerFile)
+ {
+ MorphOperation = MorphOp.Erode,
+ Iterations = 4,
+ };
+ morph.SelectBottomLayers();
+ operations.Add(morph);
+ }
+
+ // Raft relief
+ if (InputReliefRaft.Value)
+ {
+ OperationRaftRelief raftRelief = new(SlicerFile)
+ {
+ ReliefType = OperationRaftRelief.RaftReliefTypes.Relief,
+ };
+ operations.Add(raftRelief);
+ }
+
+ // Dim and apply checkboard pattern to bottom layers
+ if (InputPixelDimming.Value)
+ {
+ OperationPixelDimming pixelDimming = new(SlicerFile);
+ pixelDimming.GeneratePixelDimming("Chessboard");
+ pixelDimming.SelectBottomLayers();
+ operations.Add(pixelDimming);
+
+ foreach (var operation in operations) // Loop all my created operations to execute them
+ {
+ Progress.Token.ThrowIfCancellationRequested(); // Abort operation, user requested cancellation
+ operation.ROI = Operation.ROI; // Copy user selected ROI to my operation
+ operation.MaskPoints = Operation.MaskPoints; // Copy user selected Masks to my operation
+ if (!operation.CanValidate()) continue; // If cant validate don't execute the operation
+ operation.Execute(Progress);
+ }
+ }
+
+ // return true if not cancelled by user
+ return !Progress.Token.IsCancellationRequested;
+ }
+ }
+}
diff --git a/UVtools.ScriptSample/ScriptInsetSample.cs b/UVtools.ScriptSample/ScriptInsetSample.cs
index 4519163..9bfdcba 100644
--- a/UVtools.ScriptSample/ScriptInsetSample.cs
+++ b/UVtools.ScriptSample/ScriptInsetSample.cs
@@ -29,7 +29,7 @@ namespace UVtools.ScriptSample
Minimum = 1,
Maximum = ushort.MaxValue,
Increment = 1,
- Value = 20
+ Value = 10
};
ScriptNumericalInput InsetThickness = new()
diff --git a/UVtools.ScriptSample/UVtools.ScriptSample.csproj b/UVtools.ScriptSample/UVtools.ScriptSample.csproj
index d2db2bd..6c0dcf5 100644
--- a/UVtools.ScriptSample/UVtools.ScriptSample.csproj
+++ b/UVtools.ScriptSample/UVtools.ScriptSample.csproj
@@ -2,10 +2,25 @@
net5.0
+ PTRTECH
+ Scripting samples
+ Tiago Conceição
+ LICENSE
+ https://github.com/sn4k3/UVtools
+ https://github.com/sn4k3/UVtools
+ Git
+ Copyright © 2020 PTRTECH
+
+
+ True
+
+
+
+
diff --git a/UVtools.WPF/Assets/Icons/GCode-16x16.png b/UVtools.WPF/Assets/Icons/GCode-16x16.png
deleted file mode 100644
index 127cecc..0000000
Binary files a/UVtools.WPF/Assets/Icons/GCode-16x16.png and /dev/null differ
diff --git a/UVtools.WPF/Assets/Icons/bug-16x16.png b/UVtools.WPF/Assets/Icons/bug-16x16.png
new file mode 100644
index 0000000..0f82fe9
Binary files /dev/null and b/UVtools.WPF/Assets/Icons/bug-16x16.png differ
diff --git a/UVtools.WPF/Assets/Icons/facebook-16x16.png b/UVtools.WPF/Assets/Icons/facebook-16x16.png
new file mode 100644
index 0000000..e03391e
Binary files /dev/null and b/UVtools.WPF/Assets/Icons/facebook-16x16.png differ
diff --git a/UVtools.WPF/Assets/Icons/file-code-16x16.png b/UVtools.WPF/Assets/Icons/file-code-16x16.png
new file mode 100644
index 0000000..1d1ebd7
Binary files /dev/null and b/UVtools.WPF/Assets/Icons/file-code-16x16.png differ
diff --git a/UVtools.WPF/Assets/Icons/lightbulb-16x16.png b/UVtools.WPF/Assets/Icons/lightbulb-16x16.png
new file mode 100644
index 0000000..01c3427
Binary files /dev/null and b/UVtools.WPF/Assets/Icons/lightbulb-16x16.png differ
diff --git a/UVtools.WPF/Assets/Icons/question-16x16.png b/UVtools.WPF/Assets/Icons/question-16x16.png
new file mode 100644
index 0000000..bb4e680
Binary files /dev/null and b/UVtools.WPF/Assets/Icons/question-16x16.png differ
diff --git a/UVtools.WPF/Assets/Icons/wikipedia-16x16.png b/UVtools.WPF/Assets/Icons/wikipedia-16x16.png
new file mode 100644
index 0000000..71ea4ec
Binary files /dev/null and b/UVtools.WPF/Assets/Icons/wikipedia-16x16.png differ
diff --git a/UVtools.WPF/Controls/Helpers.cs b/UVtools.WPF/Controls/Helpers.cs
index 28ec99f..ca66103 100644
--- a/UVtools.WPF/Controls/Helpers.cs
+++ b/UVtools.WPF/Controls/Helpers.cs
@@ -65,6 +65,19 @@ namespace UVtools.WPF.Controls
}
};
+ public static readonly List ScriptsFileFilter = new()
+ {
+ new FileDialogFilter
+ {
+ Name = "Script Files",
+ Extensions = new List
+ {
+ "csx",
+ "cs",
+ }
+ }
+ };
+
public static List ToAvaloniaFileFilter(List>> data)
{
var result = new List(data.Capacity);
diff --git a/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml b/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml
new file mode 100644
index 0000000..1a0a0a5
--- /dev/null
+++ b/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs
new file mode 100644
index 0000000..eaa58db
--- /dev/null
+++ b/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs
@@ -0,0 +1,498 @@
+using System;
+using System.IO;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+using UVtools.Core.Operations;
+using UVtools.Core.Scripting;
+using UVtools.WPF.Extensions;
+using UVtools.WPF.Windows;
+
+namespace UVtools.WPF.Controls.Tools
+{
+ public class ToolScriptingControl : ToolControl
+ {
+ public OperationScripting Operation => BaseOperation as OperationScripting;
+
+ private readonly StackPanel _scriptConfigurationPanel;
+ private readonly Grid _scriptVariablesGrid;
+
+
+ public ToolScriptingControl()
+ {
+ InitializeComponent();
+ _scriptConfigurationPanel = this.FindControl("ScriptConfigurationPanel");
+ _scriptVariablesGrid = this.FindControl("ScriptVariablesGrid");
+ BaseOperation = new OperationScripting(SlicerFile);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Callback(ToolWindow.Callbacks callback)
+ {
+ switch (callback)
+ {
+ case ToolWindow.Callbacks.Init:
+ ParentWindow.ButtonOkEnabled = Operation.CanExecute;
+ ReloadGUI();
+ Operation.PropertyChanged += (sender, e) =>
+ {
+ if (e.PropertyName == nameof(Operation.CanExecute))
+ {
+ ParentWindow.ButtonOkEnabled = Operation.CanExecute;
+ }
+ };
+ Operation.OnScriptReload += (sender, e) => ReloadGUI();
+ break;
+ }
+ }
+
+ public async void LoadScript()
+ {
+ var dialog = new OpenFileDialog
+ {
+ AllowMultiple = false,
+ Directory = UserSettings.Instance.General.DefaultDirectoryScripts,
+ Filters = Helpers.ScriptsFileFilter,
+ };
+
+ var files = await dialog.ShowAsync(ParentWindow);
+ if (files is null || files.Length == 0) return;
+
+ Operation.FilePath = files[0];
+ ReloadScript();
+ }
+
+ public async void ReloadScript()
+ {
+ try
+ {
+ Operation.ReloadScriptFromFile();
+ }
+ catch (Exception e)
+ {
+ await ParentWindow.MessageBoxError(e.Message);
+ }
+
+ }
+
+ public void OpenScriptFolder()
+ {
+ if (!Operation.HaveFile) return;
+ App.StartProcess(Path.GetDirectoryName(Operation.FilePath));
+ }
+
+ public void OpenScriptFile()
+ {
+ if (!Operation.HaveFile) return;
+ App.StartProcess(Operation.FilePath);
+ }
+
+ public void ReloadGUI()
+ {
+ if (!Operation.CanExecute) return;
+
+ _scriptConfigurationPanel.Children.Clear();
+ _scriptVariablesGrid.Children.Clear();
+ _scriptVariablesGrid.RowDefinitions.Clear();
+
+ TextBox tbScriptName = new()
+ {
+ IsReadOnly = true,
+ Text = $"{Operation.ScriptGlobals.Script.Name} v{Operation.ScriptGlobals.Script.Version} by {Operation.ScriptGlobals.Script.Author}",
+ UseFloatingWatermark = true,
+ Watermark = "Script name, Version and Author"
+ };
+
+ TextBox tbScriptDescription = new()
+ {
+ IsReadOnly = true,
+ Text = Operation.ScriptGlobals.Script.Description,
+ AcceptsReturn = true,
+ UseFloatingWatermark = true,
+ Watermark = "Script description"
+ };
+
+ _scriptConfigurationPanel.Children.Add(tbScriptName);
+ _scriptConfigurationPanel.Children.Add(tbScriptDescription);
+
+ //Operation.ScriptGlobals.Script.UserInputs.Add(new ScriptBoolInput() { Label = "Hellow" });
+ //Operation.ScriptGlobals.Script.UserInputs.Add(new ScriptTextBoxInput() { Label = "Hellow", Value = "m,e", MultiLine = true});
+ if (Operation.ScriptGlobals.Script.UserInputs.Count == 0)
+ {
+ return;
+ }
+
+
+ string rowDefinitions = string.Empty;
+ for (var i = 0; i < Operation.ScriptGlobals.Script.UserInputs.Count; i++)
+ {
+ if (i < Operation.ScriptGlobals.Script.UserInputs.Count - 1)
+ {
+ rowDefinitions += "Auto,10,";
+ }
+ else
+ {
+ rowDefinitions += "Auto";
+ }
+ }
+
+ _scriptVariablesGrid.RowDefinitions = RowDefinitions.Parse(rowDefinitions);
+
+ for (var i = 0; i < Operation.ScriptGlobals.Script.UserInputs.Count; i++)
+ {
+ var variable = Operation.ScriptGlobals.Script.UserInputs[i];
+
+ if (!string.IsNullOrWhiteSpace(variable.Label) && variable is not ScriptCheckBoxInput)
+ {
+ TextBlock tbLabel = new()
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ Text = $"{variable.Label}:"
+ };
+
+ if (!string.IsNullOrWhiteSpace(variable.ToolTip))
+ {
+ ToolTip.SetTip(tbLabel, variable.ToolTip);
+ }
+
+ _scriptVariablesGrid.Children.Add(tbLabel);
+ Grid.SetRow(tbLabel, i * 2);
+ Grid.SetColumn(tbLabel, 0);
+ }
+
+ if (!string.IsNullOrWhiteSpace(variable.Unit))
+ {
+ TextBlock control = new()
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ Text = variable.Unit
+ };
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 4);
+ }
+
+ switch (variable)
+ {
+ case ScriptNumericalInput numSBYTE:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numSBYTE.Minimum,
+ Maximum = numSBYTE.Maximum,
+ Value = numSBYTE.Value,
+ Increment = numSBYTE.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numSBYTE.Value = (sbyte)value;
+ control.Value = numSBYTE.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numBYTE:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numBYTE.Minimum,
+ Maximum = numBYTE.Maximum,
+ Value = numBYTE.Value,
+ Increment = numBYTE.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numBYTE.Value = (byte)value;
+ control.Value = numBYTE.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numSHORT:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numSHORT.Minimum,
+ Maximum = numSHORT.Maximum,
+ Value = numSHORT.Value,
+ Increment = numSHORT.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numSHORT.Value = (short)value;
+ control.Value = numSHORT.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numUSHORT:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numUSHORT.Minimum,
+ Maximum = numUSHORT.Maximum,
+ Value = numUSHORT.Value,
+ Increment = numUSHORT.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numUSHORT.Value = (ushort)value;
+ control.Value = numUSHORT.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numINT:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numINT.Minimum,
+ Maximum = numINT.Maximum,
+ Value = numINT.Value,
+ Increment = numINT.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numINT.Value = (int)value;
+ control.Value = numINT.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numUINT:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numUINT.Minimum,
+ Maximum = numUINT.Maximum,
+ Value = numUINT.Value,
+ Increment = numUINT.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numUINT.Value = (uint)value;
+ control.Value = numUINT.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numLONG:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numLONG.Minimum,
+ Maximum = numLONG.Maximum,
+ Value = numLONG.Value,
+ Increment = numLONG.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numLONG.Value = (long)value;
+ control.Value = numLONG.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numULONG:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numULONG.Minimum,
+ Maximum = numULONG.Maximum,
+ Value = numULONG.Value,
+ Increment = numULONG.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numULONG.Value = (ulong)value;
+ control.Value = numULONG.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numFLOAT:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numFLOAT.Minimum,
+ Maximum = numFLOAT.Maximum,
+ Value = numFLOAT.Value,
+ Increment = numFLOAT.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numFLOAT.Value = (float) Math.Round(value, numFLOAT.DecimalPlates);
+ control.Value = numFLOAT.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numDOUBLE:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = numDOUBLE.Minimum,
+ Maximum = numDOUBLE.Maximum,
+ Value = numDOUBLE.Value,
+ Increment = numDOUBLE.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numDOUBLE.Value = Math.Round(value, numDOUBLE.DecimalPlates);
+ control.Value = numDOUBLE.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptNumericalInput numDECIMAL:
+ {
+ NumericUpDown control = new()
+ {
+ Minimum = (double)numDECIMAL.Minimum,
+ Maximum = (double)numDECIMAL.Maximum,
+ Value = (double)numDECIMAL.Value,
+ Increment = (double)numDECIMAL.Increment,
+ MinWidth = 150
+ };
+
+ var valueProperty = control.GetObservable(NumericUpDown.ValueProperty);
+ valueProperty.Subscribe(value =>
+ {
+ numDECIMAL.Value = (decimal)Math.Round(value, numDECIMAL.DecimalPlates);
+ control.Value = (double)numDECIMAL.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ case ScriptCheckBoxInput inputCheckBox:
+ {
+ CheckBox control = new()
+ {
+ Content = variable.Label,
+ IsChecked = inputCheckBox.Value
+ };
+
+ var valueProperty = control.GetObservable(CheckBox.IsCheckedProperty);
+ valueProperty.Subscribe(value =>
+ {
+ if (value != null) inputCheckBox.Value = value.Value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ if (!string.IsNullOrWhiteSpace(variable.ToolTip))
+ {
+ ToolTip.SetTip(control, variable.ToolTip);
+ }
+
+ continue;
+ }
+ case ScriptTextBoxInput inputTextBox:
+ {
+ TextBox control = new()
+ {
+ AcceptsReturn = inputTextBox.MultiLine,
+ Text = inputTextBox.Value,
+ };
+
+ var valueProperty = control.GetObservable(TextBox.TextProperty);
+ valueProperty.Subscribe(value =>
+ {
+ inputTextBox.Value = value;
+ });
+
+ _scriptVariablesGrid.Children.Add(control);
+ Grid.SetRow(control, i * 2);
+ Grid.SetColumn(control, 2);
+
+ continue;
+ }
+ }
+ }
+
+ ParentWindow.FitToSize();
+ }
+ }
+}
diff --git a/UVtools.WPF/MainWindow.Issues.cs b/UVtools.WPF/MainWindow.Issues.cs
index 5e9c513..31668ef 100644
--- a/UVtools.WPF/MainWindow.Issues.cs
+++ b/UVtools.WPF/MainWindow.Issues.cs
@@ -265,8 +265,9 @@ namespace UVtools.WPF
if (whiteListLayers.Count == 0) return;
var islandConfig = GetIslandDetectionConfiguration();
var overhangConfig = GetOverhangDetectionConfiguration();
- var resinTrapConfig = new ResinTrapDetectionConfiguration { Enabled = false };
- var touchingBoundConfig = new TouchingBoundDetectionConfiguration { Enabled = false };
+ var resinTrapConfig = new ResinTrapDetectionConfiguration(false);
+ var touchingBoundConfig = new TouchingBoundDetectionConfiguration(false);
+ var printHeightConfig = new PrintHeightDetectionConfiguration(false);
islandConfig.Enabled = true;
islandConfig.WhiteListLayers = whiteListLayers;
overhangConfig.Enabled = true;
@@ -294,7 +295,7 @@ namespace UVtools.WPF
try
{
var issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapConfig,
- touchingBoundConfig, false, IgnoredIssues,
+ touchingBoundConfig, printHeightConfig, false, IgnoredIssues,
ProgressWindow.RestartProgress());
issues.RemoveAll(issue => issue.Type != LayerIssue.IssueType.Island && issue.Type != LayerIssue.IssueType.Overhang); // Remove all non islands and overhangs
@@ -435,13 +436,16 @@ namespace UVtools.WPF
GetOverhangDetectionConfiguration(),
GetResinTrapDetectionConfiguration(),
GetTouchingBoundsDetectionConfiguration(),
+ GetPrintHeightDetectionConfiguration(),
Settings.Issues.ComputeEmptyLayers);
}
private async Task ComputeIssues(IslandDetectionConfiguration islandConfig = null,
OverhangDetectionConfiguration overhangConfig = null,
ResinTrapDetectionConfiguration resinTrapConfig = null,
- TouchingBoundDetectionConfiguration touchingBoundConfig = null, bool emptyLayersConfig = true)
+ TouchingBoundDetectionConfiguration touchingBoundConfig = null,
+ PrintHeightDetectionConfiguration printHeightConfig = null,
+ bool emptyLayersConfig = true)
{
Issues.Clear();
@@ -454,7 +458,7 @@ namespace UVtools.WPF
try
{
var issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapConfig, touchingBoundConfig,
- emptyLayersConfig, IgnoredIssues, ProgressWindow.RestartProgress());
+ printHeightConfig, emptyLayersConfig, IgnoredIssues, ProgressWindow.RestartProgress());
return issues;
}
catch (OperationCanceledException)
@@ -578,6 +582,15 @@ namespace UVtools.WPF
};
}
+ public PrintHeightDetectionConfiguration GetPrintHeightDetectionConfiguration()
+ {
+ return new PrintHeightDetectionConfiguration
+ {
+ Enabled = Settings.Issues.ComputePrintHeight,
+ Offset = (float) Settings.Issues.PrintHeightOffset
+ };
+ }
+
#endregion
}
}
diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml
index 10587f1..815468f 100644
--- a/UVtools.WPF/MainWindow.axaml
+++ b/UVtools.WPF/MainWindow.axaml
@@ -164,11 +164,29 @@
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs
index 6512bea..2c5ec87 100644
--- a/UVtools.WPF/MainWindow.axaml.cs
+++ b/UVtools.WPF/MainWindow.axaml.cs
@@ -214,6 +214,14 @@ namespace UVtools.WPF
}
},
new()
+ {
+ Tag = new OperationScripting(),
+ Icon = new Avalonia.Controls.Image
+ {
+ Source = new Bitmap(App.GetAsset("/Assets/Icons/code-16x16.png"))
+ }
+ },
+ new()
{
Tag = new OperationCalculator(),
Icon = new Avalonia.Controls.Image
@@ -784,7 +792,7 @@ namespace UVtools.WPF
await settingsWindow.ShowDialog(this);
}
- public void OpenWebsite()
+ public void OpenHomePage()
{
App.OpenBrowser(About.Website);
}
@@ -794,6 +802,11 @@ namespace UVtools.WPF
App.OpenBrowser(About.Donate);
}
+ public void OpenWebsite(string url)
+ {
+ App.OpenBrowser(url);
+ }
+
public async void MenuHelpAboutClicked()
{
await new AboutWindow().ShowDialog(this);
@@ -1098,7 +1111,7 @@ namespace UVtools.WPF
MenuFileConvertItems = menuItems.ToArray();
}
- using Mat mat = SlicerFile[0].LayerMat;
+ using var mat = SlicerFile[0].LayerMat;
VisibleThumbnailIndex = 1;
@@ -1126,6 +1139,15 @@ namespace UVtools.WPF
ZoomToFit();
}
+ if (mat.Size != SlicerFile.Resolution)
+ {
+ await this.MessageBoxWaring($"Layer image resolution of {mat.Size} mismatch with printer resolution of {SlicerFile.Resolution}.\n" +
+ "Printing this file can lead to problems or malformed model, please verify your slicing settings;\n" +
+ "Processing this file with some of the tools can lead to program crash or misfunction;\n" +
+ "If you used PrusaSlicer to slice this file, you must use it with compatible UVtools printer profiles (Help - Install profiles into PrusaSlicer).",
+ "File and layer resolution mismatch!");
+ }
+
if (Settings.Issues.ComputeIssuesOnLoad)
{
_firstTimeOnIssues = false;
@@ -1448,14 +1470,15 @@ namespace UVtools.WPF
{
var islandConfig = GetIslandDetectionConfiguration();
islandConfig.Enabled = operation.RepairIslands && operation.RemoveIslandsBelowEqualPixelCount > 0;
- var overhangConfig = new OverhangDetectionConfiguration { Enabled = false };
+ var overhangConfig = new OverhangDetectionConfiguration(false);
var resinTrapConfig = GetResinTrapDetectionConfiguration();
resinTrapConfig.Enabled = operation.RepairResinTraps;
- var touchingBoundConfig = new TouchingBoundDetectionConfiguration { Enabled = false };
+ var touchingBoundConfig = new TouchingBoundDetectionConfiguration(false);
+ var printHeightConfig = new PrintHeightDetectionConfiguration(false);
if (islandConfig.Enabled || resinTrapConfig.Enabled)
{
- ComputeIssues(islandConfig, overhangConfig, resinTrapConfig, touchingBoundConfig, Settings.Issues.ComputeEmptyLayers);
+ await ComputeIssues(islandConfig, overhangConfig, resinTrapConfig, touchingBoundConfig, printHeightConfig, Settings.Issues.ComputeEmptyLayers);
}
}
diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj
index bd98a45..1500c93 100644
--- a/UVtools.WPF/UVtools.WPF.csproj
+++ b/UVtools.WPF/UVtools.WPF.csproj
@@ -12,7 +12,7 @@
LICENSE
https://github.com/sn4k3/UVtools
Git
- 2.7.2
+ 2.8.0
diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs
index 853c257..9d2123f 100644
--- a/UVtools.WPF/UserSettings.cs
+++ b/UVtools.WPF/UserSettings.cs
@@ -43,6 +43,7 @@ namespace UVtools.WPF
private string _defaultDirectorySaveFile;
private string _defaultDirectoryExtractFile;
private string _defaultDirectoryConvertFile;
+ private string _defaultDirectoryScripts;
private bool _promptOverwriteFileSave = true;
private string _fileSaveNamePrefix;
private string _fileSaveNameSuffix = "_copy";
@@ -121,6 +122,13 @@ namespace UVtools.WPF
set => RaiseAndSetIfChanged(ref _defaultDirectoryConvertFile, value);
}
+ public string DefaultDirectoryScripts
+ {
+ get => _defaultDirectoryScripts;
+ set => RaiseAndSetIfChanged(ref _defaultDirectoryScripts, value);
+ }
+
+
public bool PromptOverwriteFileSave
{
get => _promptOverwriteFileSave;
@@ -618,6 +626,7 @@ namespace UVtools.WPF
private bool _computeOverhangs = true;
private bool _computeResinTraps = true;
private bool _computeTouchingBounds = true;
+ private bool _computePrintHeight = true;
private bool _computeEmptyLayers = true;
private bool _islandEnhancedDetection = true;
private bool _islandAllowDiagonalBonds;
@@ -639,6 +648,7 @@ namespace UVtools.WPF
private byte _touchingBoundMarginRight = 5;
private byte _touchingBoundMarginBottom = 5;
private bool _touchingBoundSyncMargins = true;
+ private decimal _printHeightOffset;
public bool ComputeIssuesOnLoad
{
@@ -682,6 +692,12 @@ namespace UVtools.WPF
set => RaiseAndSetIfChanged(ref _computeTouchingBounds, value);
}
+ public bool ComputePrintHeight
+ {
+ get => _computePrintHeight;
+ set => RaiseAndSetIfChanged(ref _computePrintHeight, value);
+ }
+
public bool ComputeEmptyLayers
{
get => _computeEmptyLayers;
@@ -836,6 +852,12 @@ namespace UVtools.WPF
set => RaiseAndSetIfChanged(ref _touchingBoundSyncMargins, value);
}
+ public decimal PrintHeightOffset
+ {
+ get => _printHeightOffset;
+ set => RaiseAndSetIfChanged(ref _printHeightOffset, value);
+ }
+
public IssuesUserSettings Clone()
{
return MemberwiseClone() as IssuesUserSettings;
diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml b/UVtools.WPF/Windows/SettingsWindow.axaml
index 5a2cdc6..cd66987 100644
--- a/UVtools.WPF/Windows/SettingsWindow.axaml
+++ b/UVtools.WPF/Windows/SettingsWindow.axaml
@@ -95,7 +95,9 @@
-
+
@@ -196,6 +198,29 @@
+
+
+
+
+
@@ -738,19 +763,40 @@
IsChecked="{Binding Settings.Issues.ComputeIssuesOnClickTab}"
/>
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -760,10 +806,12 @@
Margin="5">
-
+
@@ -892,7 +940,9 @@
Margin="5">
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs
index cb747e3..f0db238 100644
--- a/UVtools.WPF/Windows/ToolWindow.axaml.cs
+++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs
@@ -631,6 +631,25 @@ namespace UVtools.WPF.Windows
{
AvaloniaXamlLoader.Load(this);
}
+
+ public void FitToSize()
+ {
+ SizeToContent = SizeToContent.Manual;
+ Height = MaxHeight;
+ DispatcherTimer.Run(() =>
+ {
+ if (Math.Max((int)_contentScrollViewer.Extent.Height - (int)_contentScrollViewer.Viewport.Height, 0) == 0)
+ {
+ Height = 10;
+ SizeToContent = SizeToContent.WidthAndHeight;
+ }
+ Position = new PixelPoint(
+ (int)(App.MainWindow.Position.X + App.MainWindow.Width / 2 - Width / 2),
+ App.MainWindow.Position.Y + 20
+ );
+ return false;
+ }, TimeSpan.FromMilliseconds(1));
+ }
#endregion
/*protected override void OnOpened(EventArgs e)