diff options
40 files changed, 1467 insertions, 56 deletions
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 + { + /// <summary> + /// Gets if the detection is enabled + /// </summary> + public bool Enabled { get; set; } = true; + + /// <summary> + /// Get the offset from top to sum to printer max Z height + /// </summary> + public float Offset { get; set; } + + public PrintHeightDetectionConfiguration(bool enabled = true) + { + Enabled = enabled; + } + } + public class LayerIssue : IEquatable<LayerIssue>, IEnumerable<Point> { @@ -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<LayerIssue> 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<LayerIssue>(); @@ -803,6 +805,7 @@ namespace UVtools.Core OverhangDetectionConfiguration overhangConfig = null, ResinTrapDetectionConfiguration resinTrapConfig = null, TouchingBoundDetectionConfiguration touchBoundConfig = null, + PrintHeightDetectionConfiguration printHeightConfig = null, bool emptyLayersConfig = true, List<LayerIssue> 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<LayerIssue>(); @@ -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(); } + /// <summary> + /// Copy this operation base configuration to another operation. + /// Layer range, ROI, Masks + /// </summary> + /// <param name="operation"></param> + 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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.IO; +using System.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<string>("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<bool>("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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +namespace UVtools.Core.Scripting +{ + public abstract class ScriptBaseInput + { + /// <summary> + /// Gets the input label + /// </summary> + public string Label { get; init; } + + /// <summary> + /// Gets the hover tooltip for this input + /// </summary> + public string ToolTip { get; init; } + + /// <summary> + /// Gets the value representative unit name + /// </summary> + public string Unit { get; init; } + } + + public abstract class ScriptBaseInput<T> : ScriptBaseInput + { + /// <summary> + /// Gets or sets the value for this input + /// </summary> + 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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +namespace UVtools.Core.Scripting +{ + public class ScriptCheckBoxInput : ScriptBaseInput<bool> + { + } +} 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 + { + /// <summary> + /// Gets the script name + /// </summary> + public string Name { get; set; } = "Unnamed script"; + + /// <summary> + /// Gets the script description of what it does + /// </summary> + public string Description { get; set; } = "I don't know my purpose, do not run me!"; + + /// <summary> + /// Gets the script author name + /// </summary> + public string Author { get; set; } = "Undefined"; + + /// <summary> + /// Gets the script version + /// </summary> + public Version Version { get; set; } = new(0, 1); + + /// <summary> + /// List of user inputs to show on GUI for configuration of the script + /// </summary> + public List<ScriptBaseInput> 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. <https://fsf.org/> + * 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 + { + /// <summary> + /// Gets the loaded slicer file + /// </summary> + public FileFormat SlicerFile { get; init; } + + /// <summary> + /// Gets the progress operation for loading bar + /// </summary> + public OperationProgress Progress { get; set; } = new("Unknown"); + + /// <summary> + /// Gets the current operation holding the layer range, mask, roi, etc + /// </summary> + public Operation Operation { get; init; } + + /// <summary> + /// Gets the script configuration + /// </summary> + 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. <https://fsf.org/> + * 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<T> : ScriptBaseInput<T> + { + /// <summary> + /// Gets the minimum for this input + /// </summary> + public T Minimum { get; init; } + + /// <summary> + /// Gets the minimum for this input + /// </summary> + public T Maximum { get; init; } + + /// <summary> + /// Gets the increment value for this + /// </summary> + public T Increment { get; init; } + + /// <summary> + /// Gets the number of decimal plates to round the value + /// </summary> + 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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.IO; +using System.Text.RegularExpressions; + +namespace UVtools.Core.Scripting +{ + public static class ScriptParser + { + public static string ParseScriptFromFile(string path) + { + return ParseScriptFromText(File.ReadAllText(path)); + } + + /// <summary> + /// Parse the script and clean forbidden keywords + /// </summary> + /// <param name="text">Text to parse</param> + /// <returns>The parsed text</returns> + 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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +namespace UVtools.Core.Scripting +{ + public class ScriptTextBoxInput : ScriptBaseInput<string> + { + /// <summary> + /// Gets if this input accepts multi lines + /// </summary> + 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 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>2.7.2</Version> + <Version>2.8.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> @@ -46,6 +46,7 @@ <ItemGroup> <PackageReference Include="BinarySerializer" Version="8.6.0" /> <PackageReference Include="Emgu.CV" Version="4.5.1.4349" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.10.0-1.final" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Portable.BouncyCastle" Version="1.8.10" /> <PackageReference Include="System.Memory" Version="4.5.4" /> 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. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using Emgu.CV.CvEnum; +using UVtools.Core.Operations; +using UVtools.Core.Scripting; + +namespace UVtools.ScriptSample +{ + /// <summary> + /// Performs a black inset around objects + /// </summary> + 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 + }; + /// <summary> + /// Set configurations here, this function trigger just after load a script + /// </summary> + 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 + }); + } + + /// <summary> + /// Validate user inputs here, this function trigger when user click on execute + /// </summary> + /// <returns>A error message, empty or null if validation passes.</returns> + public string ScriptValidate() + { + return null; + } + + /// <summary> + /// Execute the script, this function trigger when when user click on execute and validation passes + /// </summary> + /// <returns>True if executes successfully to the end, otherwise false.</returns> + public bool ScriptExecute() + { + List<Operation> 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<ushort> 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 @@ <PropertyGroup> <TargetFramework>net5.0</TargetFramework> + <Company>PTRTECH</Company> + <Description>Scripting samples</Description> + <Authors>Tiago Conceição</Authors> + <PackageLicenseFile>LICENSE</PackageLicenseFile> + <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> + <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> + <RepositoryType>Git</RepositoryType> + <Copyright>Copyright © 2020 PTRTECH</Copyright> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\UVtools.Core\UVtools.Core.csproj" /> </ItemGroup> + <ItemGroup> + <None Include="..\LICENSE"> + <Pack>True</Pack> + <PackagePath></PackagePath> + </None> + </ItemGroup> + </Project> diff --git a/UVtools.WPF/Assets/Icons/GCode-16x16.png b/UVtools.WPF/Assets/Icons/GCode-16x16.png Binary files differdeleted file mode 100644 index 127cecc..0000000 --- a/UVtools.WPF/Assets/Icons/GCode-16x16.png +++ /dev/null diff --git a/UVtools.WPF/Assets/Icons/bug-16x16.png b/UVtools.WPF/Assets/Icons/bug-16x16.png Binary files differnew file mode 100644 index 0000000..0f82fe9 --- /dev/null +++ b/UVtools.WPF/Assets/Icons/bug-16x16.png diff --git a/UVtools.WPF/Assets/Icons/facebook-16x16.png b/UVtools.WPF/Assets/Icons/facebook-16x16.png Binary files differnew file mode 100644 index 0000000..e03391e --- /dev/null +++ b/UVtools.WPF/Assets/Icons/facebook-16x16.png diff --git a/UVtools.WPF/Assets/Icons/file-code-16x16.png b/UVtools.WPF/Assets/Icons/file-code-16x16.png Binary files differnew file mode 100644 index 0000000..1d1ebd7 --- /dev/null +++ b/UVtools.WPF/Assets/Icons/file-code-16x16.png diff --git a/UVtools.WPF/Assets/Icons/lightbulb-16x16.png b/UVtools.WPF/Assets/Icons/lightbulb-16x16.png Binary files differnew file mode 100644 index 0000000..01c3427 --- /dev/null +++ b/UVtools.WPF/Assets/Icons/lightbulb-16x16.png diff --git a/UVtools.WPF/Assets/Icons/question-16x16.png b/UVtools.WPF/Assets/Icons/question-16x16.png Binary files differnew file mode 100644 index 0000000..bb4e680 --- /dev/null +++ b/UVtools.WPF/Assets/Icons/question-16x16.png diff --git a/UVtools.WPF/Assets/Icons/wikipedia-16x16.png b/UVtools.WPF/Assets/Icons/wikipedia-16x16.png Binary files differnew file mode 100644 index 0000000..71ea4ec --- /dev/null +++ b/UVtools.WPF/Assets/Icons/wikipedia-16x16.png 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<FileDialogFilter> ScriptsFileFilter = new() + { + new FileDialogFilter + { + Name = "Script Files", + Extensions = new List<string> + { + "csx", + "cs", + } + } + }; + public static List<FileDialogFilter> ToAvaloniaFileFilter(List<KeyValuePair<string, List<string>>> data) { var result = new List<FileDialogFilter>(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 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="UVtools.WPF.Controls.Tools.ToolScriptingControl"> + <StackPanel Orientation="Vertical"> + <Grid RowDefinitions="Auto" + ColumnDefinitions="Auto,10,500,5,Auto"> + + <TextBlock Grid.Row="0" Grid.Column="0" + Text="Script file:" + VerticalAlignment="Center"/> + + <TextBox Grid.Row="0" Grid.Column="2" + IsReadOnly="True" + Text="{Binding Operation.FilePath}" /> + + <StackPanel Grid.Row="0" Grid.Column="4" + Orientation="Horizontal" Spacing="1"> + <Button + ToolTip.Tip="Loads an script file" + VerticalAlignment="Center" + Command="{Binding LoadScript}"> + <Image Source="/Assets/Icons/file-import-16x16.png" /> + </Button> + + <Button + IsEnabled="{Binding Operation.HaveFile}" + ToolTip.Tip="Reloads the script" + VerticalAlignment="Center" + Command="{Binding ReloadScript}"> + <Image Source="/Assets/Icons/refresh-16x16.png" /> + </Button> + + <Button + IsEnabled="{Binding Operation.HaveFile}" + ToolTip.Tip="Open the script folder" + VerticalAlignment="Center" + Command="{Binding OpenScriptFolder}"> + <Image Source="/Assets/Icons/open-16x16.png" /> + </Button> + + <Button + IsEnabled="{Binding Operation.HaveFile}" + ToolTip.Tip="Open the script file" + VerticalAlignment="Center" + Command="{Binding OpenScriptFile}"> + <Image Source="/Assets/Icons/file-code-16x16.png" /> + </Button> + </StackPanel> + + </Grid> + + <StackPanel + Name="ScriptConfigurationPanel" + IsVisible="{Binding Operation.CanExecute}" + Margin="0,10" Spacing="5"/> + + <Grid + Name="ScriptVariablesGrid" + IsVisible="{Binding Operation.CanExecute}" + ColumnDefinitions="Auto,10,Auto,5,Auto"> + + </Grid> + </StackPanel> +</UserControl> 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<StackPanel>("ScriptConfigurationPanel"); + _scriptVariablesGrid = this.FindControl<Grid>("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<sbyte> 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<byte> 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<short> 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<ushort> 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<int> 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<uint> 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<long> 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<ulong> 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<float> 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<double> 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<decimal> 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 @@ <MenuItem Header="_Website" InputGesture="Ctrl + F1" HotKey="Ctrl + F1" - Command="{Binding OpenWebsite}"> + Command="{Binding OpenHomePage}"> <MenuItem.Icon> <Image Source="\Assets\Icons\internet-explorer-16x16.png"/> </MenuItem.Icon> </MenuItem> + + <MenuItem + Header="Wi_ki & tutorials" + Command="{Binding OpenWebsite}" + CommandParameter="https://github.com/sn4k3/UVtools/wiki"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\wikipedia-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + + <MenuItem + Header="_Facebook group" + Command="{Binding OpenWebsite}" + CommandParameter="https://www.facebook.com/groups/uvtools"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\facebook-16x16.png"/> + </MenuItem.Icon> + </MenuItem> <MenuItem Header="_Donate" @@ -182,16 +200,6 @@ <Separator/> <MenuItem - Header="_Benchmark" - Command="{Binding MenuHelpBenchmarkClicked}"> - <MenuItem.Icon> - <Image Source="\Assets\Icons\microchip-16x16.png"/> - </MenuItem.Icon> - </MenuItem> - - <Separator/> - - <MenuItem Header="_Material manager" HotKey="F10" InputGesture="F10" @@ -210,6 +218,16 @@ </MenuItem> <Separator/> + + <MenuItem + Header="_Benchmark" + Command="{Binding MenuHelpBenchmarkClicked}"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\microchip-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + + <Separator/> <MenuItem Header="_Open settings folder" @@ -219,6 +237,35 @@ </MenuItem.Icon> </MenuItem> + <Separator/> + + <MenuItem + Header="_Report a issue" + Command="{Binding OpenWebsite}" + CommandParameter="https://github.com/sn4k3/UVtools/issues/new?assignees=sn4k3&labels=&template=bug_report.md&title=%5BBUG%5D+"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\bug-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + + <MenuItem + Header="Ask a _question" + Command="{Binding OpenWebsite}" + CommandParameter="https://github.com/sn4k3/UVtools/discussions/categories/q-a"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\question-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + + <MenuItem + Header="Suggest an improvement or new features" + Command="{Binding OpenWebsite}" + CommandParameter="https://github.com/sn4k3/UVtools/discussions/categories/ideas"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\lightbulb-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + </MenuItem> <MenuItem @@ -728,6 +775,9 @@ IsChecked="{Binding Settings.Issues.ComputeTouchingBounds}" Content="Touching bounds"/> <CheckBox + IsChecked="{Binding Settings.Issues.ComputePrintHeight}" + Content="Print height"/> + <CheckBox IsChecked="{Binding Settings.Issues.ComputeEmptyLayers}" Content="Empty layers"/> </ContextMenu> 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 @@ -215,6 +215,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 @@ <PackageLicenseFile>LICENSE</PackageLicenseFile> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <RepositoryType>Git</RepositoryType> - <Version>2.7.2</Version> + <Version>2.8.0</Version> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> 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 @@ <StackPanel Orientation="Vertical"> <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" Text="File dialog"/> - <Grid Margin="10" RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto" ColumnDefinitions="Auto,10,*,Auto,Auto"> + <Grid Margin="10" + RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,429,Auto,Auto"> <TextBlock VerticalAlignment="Center" Grid.Row="0" Grid.Column="0" @@ -103,7 +105,7 @@ <ComboBox Items="{Binding FileOpenDialogFilters}" SelectedIndex="{Binding Settings.General.DefaultOpenFileExtensionIndex}" HorizontalAlignment="Left" - Width="473" + Width="490" MaxDropDownHeight="350" Grid.ColumnSpan="3" Grid.Row="0" Grid.Column="2"/> @@ -196,6 +198,29 @@ <Image Source="/Assets/Icons/delete-16x16.png"/> </Button> + <TextBlock VerticalAlignment="Center" + Grid.Row="10" Grid.Column="0" + Text="File convert default directory:"/> + <TextBox + Name="General.DefaultDirectoryScripts" + Text="{Binding Settings.General.DefaultDirectoryScripts}" + Grid.Row="10" Grid.Column="2" IsReadOnly="True"/> + <Button Grid.Row="10" Grid.Column="3" + VerticalAlignment="Stretch" + HorizontalAlignment="Stretch" + Command="{Binding GeneralOpenFolderField}" + CommandParameter="DefaultDirectoryScripts"> + <Image Source="/Assets/Icons/open-16x16.png"/> + </Button> + <Button + Command="{Binding GeneralClearField}" + CommandParameter="DefaultDirectoryScripts" + VerticalAlignment="Stretch" + HorizontalAlignment="Stretch" + Grid.Row="10" Grid.Column="4"> + <Image Source="/Assets/Icons/delete-16x16.png"/> + </Button> + </Grid> <CheckBox IsChecked="{Binding Settings.General.PromptOverwriteFileSave}" Margin="10,0" Content="On file 'Save' prompt for file overwrite for the first time" /> @@ -738,19 +763,40 @@ IsChecked="{Binding Settings.Issues.ComputeIssuesOnClickTab}" /> - <StackPanel Orientation="Horizontal" Margin="10" Spacing="10"> - <TextBlock Text="Compute:" VerticalAlignment="Center"/> - <CheckBox Content="Islands" - IsChecked="{Binding Settings.Issues.ComputeIslands}"/> - <CheckBox Content="Overhangs" - IsChecked="{Binding Settings.Issues.ComputeOverhangs}"/> - <CheckBox Content="Resin traps" - IsChecked="{Binding Settings.Issues.ComputeResinTraps}"/> - <CheckBox Content="Touching boundary" - IsChecked="{Binding Settings.Issues.ComputeTouchingBounds}"/> - <CheckBox Content="Empty layers" - IsChecked="{Binding Settings.Issues.ComputeEmptyLayers}"/> - </StackPanel> + <Grid + RowDefinitions="Auto" + ColumnDefinitions="Auto,10,Auto" + Margin="10"> + <TextBlock + Grid.Row="0" + Grid.Column="0" + Text="Compute:" VerticalAlignment="Center"/> + <StackPanel + Grid.Row="0" + Grid.Column="2" + Orientation="Vertical" Spacing="10"> + + <StackPanel + Orientation="Horizontal" Spacing="10"> + <CheckBox Content="Islands" + IsChecked="{Binding Settings.Issues.ComputeIslands}"/> + <CheckBox Content="Overhangs" + IsChecked="{Binding Settings.Issues.ComputeOverhangs}"/> + <CheckBox Content="Resin traps" + IsChecked="{Binding Settings.Issues.ComputeResinTraps}"/> + <CheckBox Content="Touching boundary" + IsChecked="{Binding Settings.Issues.ComputeTouchingBounds}"/> + <CheckBox Content="Print height" + IsChecked="{Binding Settings.Issues.ComputePrintHeight}"/> + </StackPanel> + + <StackPanel + Orientation="Horizontal" Spacing="10"> + <CheckBox Content="Empty layers" + IsChecked="{Binding Settings.Issues.ComputeEmptyLayers}"/> + </StackPanel> + </StackPanel> + </Grid> </StackPanel> </Border> @@ -760,10 +806,12 @@ Margin="5"> <StackPanel Orientation="Vertical"> - <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" Text="Islands"/> + <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" + ToolTip.Tip="Unsupported pixels" + Text="Islands"/> <CheckBox Margin="10,10" - Content="Enhance island detection with an combined overhang check" + Content="Enhance island detection with a combined overhang check" IsChecked="{Binding Settings.Issues.IslandEnhancedDetection}" ToolTip.Tip="Combines the island and overhang detections for a better and more realistic detection, also to discard false-positives. (Slower) 
If enabled, and when a island is found, it will check for overhangs on that same island, if no overhang found then the island will be discarded and considered safe, otherwise it will flag as an island issue. @@ -789,7 +837,7 @@ VerticalAlignment="Center" Text="Pixel intensity threshold for island detection" ToolTip.Tip="Pixels below this threshold will be considered black during island detection. -
Pixels equal to or above this theshold will be considred white during island detection. +
Pixels equal to or above this threshold will be considered white during island detection. 
0 to disable." /> </StackPanel> @@ -892,7 +940,9 @@ Margin="5"> <StackPanel Orientation="Vertical"> - <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" Text="Overhangs"/> + <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" + ToolTip.Tip="Unsupported pixels that goes beyond a certain angle/threshold" + Text="Overhangs"/> <CheckBox Margin="10,10" @@ -924,7 +974,9 @@ Margin="5"> <StackPanel Orientation="Vertical"> - <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" Text="Resin traps"/> + <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" + ToolTip.Tip="Trapped resin inside hollow objects" + Text="Resin traps"/> <StackPanel Orientation="Horizontal" Margin="10" Spacing="10"> <NumericUpDown Width="60" @@ -992,7 +1044,9 @@ Margin="5"> <StackPanel Orientation="Vertical"> - <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" Text="Touching boundary"/> + <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" + ToolTip.Tip="White pixels that nearly or touch the build plate edges" + Text="Touching boundary"/> <StackPanel Orientation="Horizontal" Margin="10" Spacing="10"> <NumericUpDown Width="60" @@ -1125,6 +1179,31 @@ </StackPanel> </Border> + <Border + Classes="GroupBox" + Margin="5"> + + <StackPanel Orientation="Vertical"> + <TextBlock Padding="10" Background="LightBlue" FontWeight="Bold" + ToolTip.Tip="Layers that surpass printer maximum height" + Text="Print height"/> + + <StackPanel Orientation="Horizontal" Margin="10" Spacing="10"> + <NumericUpDown Width="60" + Minimum="-50" + Maximum="50" + Increment="0.1" + Value="{Binding Settings.Issues.PrintHeightOffset}"/> + <TextBlock + VerticalAlignment="Center" + Text="Offset height from printer maximum Z height" + ToolTip.Tip="Layers height will be compared with printer maximum possible height + this offset, if a layer surpass this height, it will be marked as a issue. +
Use negative values if you want to give some down margin from top most. +
Note: Some file formats lacks of printer maximum height information, and in this cases the object final height will be used instead as printer maximum, in this cases use a Offset of 0 to not have false detected issues."/> + </StackPanel> + </StackPanel> + </Border> + </StackPanel> </ScrollViewer> </Grid> 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) |