From 65ef92d7f01bbdc3dbc94582a269f90ee37ff6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiago=20Concei=C3=A7=C3=A3o?= Date: Fri, 2 Apr 2021 04:26:55 +0100 Subject: 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" --- CHANGELOG.md | 26 ++ UVtools.Core/Layer/LayerIssue.cs | 28 +- UVtools.Core/Layer/LayerManager.cs | 42 +- UVtools.Core/Operations/Operation.cs | 14 + .../Operations/OperationCalibrateExposureFinder.cs | 1 + UVtools.Core/Operations/OperationEditParameters.cs | 1 + .../Operations/OperationIPrintedThisFile.cs | 1 + UVtools.Core/Operations/OperationLayerClone.cs | 1 + UVtools.Core/Operations/OperationLayerReHeight.cs | 1 + UVtools.Core/Operations/OperationLayerRemove.cs | 1 + UVtools.Core/Operations/OperationRepairLayers.cs | 3 +- UVtools.Core/Operations/OperationScripting.cs | 168 +++++++ UVtools.Core/Scripting/ScriptBaseInput.cs | 36 ++ UVtools.Core/Scripting/ScriptCheckBoxInput.cs | 14 + UVtools.Core/Scripting/ScriptConfiguration.cs | 33 ++ UVtools.Core/Scripting/ScriptGlobals.cs | 35 ++ UVtools.Core/Scripting/ScriptNumericalInput.cs | 35 ++ UVtools.Core/Scripting/ScriptParser.cs | 60 +++ UVtools.Core/Scripting/ScriptTextBoxInput.cs | 18 + UVtools.Core/UVtools.Core.csproj | 3 +- .../ScriptAutomateWorkflowSample.cs | 115 +++++ UVtools.ScriptSample/ScriptInsetSample.cs | 2 +- UVtools.ScriptSample/UVtools.ScriptSample.csproj | 15 + UVtools.WPF/Assets/Icons/GCode-16x16.png | Bin 151 -> 0 bytes UVtools.WPF/Assets/Icons/bug-16x16.png | Bin 0 -> 201 bytes UVtools.WPF/Assets/Icons/facebook-16x16.png | Bin 0 -> 127 bytes UVtools.WPF/Assets/Icons/file-code-16x16.png | Bin 0 -> 178 bytes UVtools.WPF/Assets/Icons/lightbulb-16x16.png | Bin 0 -> 227 bytes UVtools.WPF/Assets/Icons/question-16x16.png | Bin 0 -> 195 bytes UVtools.WPF/Assets/Icons/wikipedia-16x16.png | Bin 0 -> 233 bytes UVtools.WPF/Controls/Helpers.cs | 13 + .../Controls/Tools/ToolScriptingControl.axaml | 67 +++ .../Controls/Tools/ToolScriptingControl.axaml.cs | 498 +++++++++++++++++++++ UVtools.WPF/MainWindow.Issues.cs | 23 +- UVtools.WPF/MainWindow.axaml | 72 ++- UVtools.WPF/MainWindow.axaml.cs | 33 +- UVtools.WPF/UVtools.WPF.csproj | 2 +- UVtools.WPF/UserSettings.cs | 22 + UVtools.WPF/Windows/SettingsWindow.axaml | 121 ++++- UVtools.WPF/Windows/ToolWindow.axaml.cs | 19 + 40 files changed, 1467 insertions(+), 56 deletions(-) create mode 100644 UVtools.Core/Operations/OperationScripting.cs create mode 100644 UVtools.Core/Scripting/ScriptBaseInput.cs create mode 100644 UVtools.Core/Scripting/ScriptCheckBoxInput.cs create mode 100644 UVtools.Core/Scripting/ScriptConfiguration.cs create mode 100644 UVtools.Core/Scripting/ScriptGlobals.cs create mode 100644 UVtools.Core/Scripting/ScriptNumericalInput.cs create mode 100644 UVtools.Core/Scripting/ScriptParser.cs create mode 100644 UVtools.Core/Scripting/ScriptTextBoxInput.cs create mode 100644 UVtools.ScriptSample/ScriptAutomateWorkflowSample.cs delete mode 100644 UVtools.WPF/Assets/Icons/GCode-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/bug-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/facebook-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/file-code-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/lightbulb-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/question-16x16.png create mode 100644 UVtools.WPF/Assets/Icons/wikipedia-16x16.png create mode 100644 UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml create mode 100644 UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs 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 @@ + Command="{Binding OpenHomePage}"> + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) -- cgit v1.2.3