diff --git a/CHANGELOG.md b/CHANGELOG.md index f621776..8e8f5c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 27/01/2022 - v2.27.7 + +- **Pixel Arithmetic:** + - (Add) Corrode: Number of passes + - (Change) Corrode: Noise area default from 3px² to 1px² + - (Change) Fuzzy skin preset: Wall thickness from 6px to 4px + - (Change) Fuzzy skin preset: Max noise offset: 64 +- (Add) Core: More helper functions: Area, Volume, LastBottomLayer, BottomLayersHeight, SetNoDelays, SetWaitTimeBeforeCureOrLightOffDelay +- (Change) Display the current layer volume instead of area +- (Fix) Having a first empty layer will miscalculate the model rectangle bounds +- (Fix) Tool - Calculator - Model tilt: Change formula to use arctan instead of tanh +- (Upgrade) OpenCV from 4.5.4 to 4.5.5 +- (Upgrade) AvaloniaUI from 0.10.11 to 0.10.12 (#378) + ## 07/01/2022 - v2.27.6 - **PrusaSlicer:** @@ -11,7 +25,7 @@ ## 05/01/2022 - v2.27.5 - **Pixel Arithmetic:** - - (Add) Corode: Noise pixel area, defaulting to 3px2 + - (Add) Corode: Noise pixel area, defaulting to 3px² - (Change) Corode: Cryptonumeric random to normal random to speed up calculation - (Change) Fuzzy skin preset: Set a ignore threshold area of 5000px2 - (Improvement) Masking performance and auto crop the layer to speed up the processing when using an "Apply to" other than "All" diff --git a/README.md b/README.md index 5c80b1c..0f0382a 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ But also, i need victims for test subject. Proceed at your own risk! - LGS30 (Longer Orange 30) - LGS120 (Longer Orange 120) - LGS4K (Longer Orange 4K & mono) +- Flashforge SVGX - VDA.ZIP (Voxeldance Additive) - VDT (Voxeldance Tango) - UVJ (Zip file format for manual manipulation) diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index 68508f1..7ed8446 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -34,7 +34,7 @@ - + diff --git a/UVtools.Core/Extensions/RectangleExtensions.cs b/UVtools.Core/Extensions/RectangleExtensions.cs index 877b200..b122051 100644 --- a/UVtools.Core/Extensions/RectangleExtensions.cs +++ b/UVtools.Core/Extensions/RectangleExtensions.cs @@ -14,8 +14,7 @@ namespace UVtools.Core.Extensions { public static Point Center(this Rectangle src) { - return new Point(src.X + src.Width / 2, - src.Y + src.Height / 2); + return new Point(src.Right / 2, src.Bottom / 2); } } diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 4bd1b01..e04dda0 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -991,6 +991,11 @@ namespace UVtools.Core.FileFormats /// public Layer FirstLayer => LayerManager.FirstLayer; + /// + /// Gets the last bottom layer + /// + public Layer LastBottomLayer => LayerManager.LastOrDefault(layer => layer.IsBottomLayer); + /// /// Gets the first layer normal layer /// @@ -1283,6 +1288,15 @@ namespace UVtools.Core.FileFormats } } + /// + /// Gets or sets the total height for the bottom layers in millimeters + /// + public float BottomLayersHeight + { + get => LastBottomLayer?.PositionZ ?? 0; + set => BottomLayerCount = (ushort)Math.Ceiling(value / LayerHeight); + } + #region Universal Properties /// @@ -3346,6 +3360,18 @@ namespace UVtools.Core.FileFormats return changed; } + public void SetNoDelays() + { + BottomLightOffDelay = 0; + LightOffDelay = 0; + BottomWaitTimeBeforeCure = 0; + WaitTimeBeforeCure = 0; + BottomWaitTimeAfterCure = 0; + WaitTimeAfterCure = 0; + BottomWaitTimeAfterLift = 0; + WaitTimeAfterLift = 0; + } + public float CalculateBottomLightOffDelay(float extraTime = 0) => CalculateLightOffDelay(true, extraTime); public bool SetBottomLightOffDelay(float extraTime = 0) => SetLightOffDelay(true, extraTime); @@ -3391,6 +3417,61 @@ namespace UVtools.Core.FileFormats return false; } + /// + /// Attempt to set wait time before cure if supported, otherwise fallback to light-off delay + /// + /// The time to set + /// When true and time is zero, it will calculate light-off delay without extra time, otherwise false to set light-off delay to 0 when time is 0 + public void SetWaitTimeBeforeCureOrLightOffDelay(bool isBottomLayer, float time = 0, bool zeroLightOffDelayCalculateBase = false) + { + if (isBottomLayer) + { + SetBottomWaitTimeBeforeCureOrLightOffDelay(time, zeroLightOffDelayCalculateBase); + } + else + { + SetNormalWaitTimeBeforeCureOrLightOffDelay(time, zeroLightOffDelayCalculateBase); + } + } + + public void SetBottomWaitTimeBeforeCureOrLightOffDelay(float time = 0, bool zeroLightOffDelayCalculateBase = false) + { + if (CanUseBottomWaitTimeBeforeCure) + { + BottomLightOffDelay = 0; + BottomWaitTimeBeforeCure = time; + } + else if (CanUseBottomLightOffDelay) + { + if (time == 0 && !zeroLightOffDelayCalculateBase) + { + BottomLightOffDelay = 0; + return; + } + + SetBottomLightOffDelay(time); + } + } + + public void SetNormalWaitTimeBeforeCureOrLightOffDelay(float time = 0, bool zeroLightOffDelayCalculateBase = false) + { + if (CanUseWaitTimeBeforeCure) + { + LightOffDelay = 0; + WaitTimeBeforeCure = time; + } + else if (CanUseLightOffDelay) + { + if (time == 0 && !zeroLightOffDelayCalculateBase) + { + LightOffDelay = 0; + return; + } + + SetNormalLightOffDelay(time); + } + } + /// /// Rebuilds GCode based on current settings /// diff --git a/UVtools.Core/Layers/Layer.cs b/UVtools.Core/Layers/Layer.cs index d355b0e..ab57fb5 100644 --- a/UVtools.Core/Layers/Layer.cs +++ b/UVtools.Core/Layers/Layer.cs @@ -75,7 +75,8 @@ namespace UVtools.Core.Layers internal set { if (!RaiseAndSetIfChanged(ref _nonZeroPixelCount, value)) return; - RaisePropertyChanged(nameof(ExposureMillimeters)); + RaisePropertyChanged(nameof(Area)); + RaisePropertyChanged(nameof(Volume)); MaterialMilliliters = -1; // Recalculate } } @@ -85,14 +86,17 @@ namespace UVtools.Core.Layers /// public bool IsEmpty => _nonZeroPixelCount == 0; - public float ExposureMillimeters - { - get - { - if (SlicerFile is null) return 0; - return (float) Math.Round(SlicerFile.PixelSizeMax * _nonZeroPixelCount, 2); - } - } + /// + /// Gets the layer area (XY) + /// Pixel size * number of pixels + /// + public float Area => GetArea(3); + + /// + /// Gets the layer volume (XYZ) + /// Pixel size * number of pixels * layer height + /// + public float Volume => GetVolume(3); /// /// Gets the bounding rectangle for the image area @@ -465,7 +469,7 @@ namespace UVtools.Core.Layers return layerHeight; } - return SlicerFile.LayerHeight; + return SlicerFile?.LayerHeight ?? 0; } } @@ -481,7 +485,7 @@ namespace UVtools.Core.Layers //var globalMilliliters = SlicerFile.MaterialMilliliters - _materialMilliliters; if (value < 0) { - value = (float) Math.Round(SlicerFile.PixelArea * LayerHeight * NonZeroPixelCount / 1000f, 4); + value = (float) Math.Round(GetVolume() / 1000f, 4); } if(!RaiseAndSetIfChanged(ref _materialMilliliters, value)) return; @@ -787,6 +791,31 @@ namespace UVtools.Core.Layers #region Methods + /// + /// Gets the layer area (XY) + /// Pixel size * number of pixels + /// + public float GetArea() => (SlicerFile?.PixelArea ?? 0) * _nonZeroPixelCount; + + + /// + /// Gets the layer area (XY) + /// Pixel size * number of pixels + /// + public float GetArea(byte roundToDigits) => (float)Math.Round(GetArea(), roundToDigits); + + /// + /// Gets the layer volume (XYZ) + /// Pixel size * number of pixels * layer height + /// + public float GetVolume() => GetArea() * LayerHeight; + + /// + /// Gets the layer volume (XYZ) + /// Pixel size * number of pixels * layer height + /// + public float GetVolume(byte roundToDigits) => (float)Math.Round(GetArea() * LayerHeight, roundToDigits); + public float CalculateMotorMovementTime(float extraTime = 0) { return OperationCalculator.LightOffDelayC.CalculateSeconds(this, extraTime); diff --git a/UVtools.Core/Layers/LayerManager.cs b/UVtools.Core/Layers/LayerManager.cs index 020e4a0..207a8f7 100644 --- a/UVtools.Core/Layers/LayerManager.cs +++ b/UVtools.Core/Layers/LayerManager.cs @@ -689,8 +689,24 @@ namespace UVtools.Core var firstLayer = FirstLayer; if (!_boundingRectangle.IsEmpty || LayerCount == 0 || firstLayer is null || !firstLayer.HaveImage) return _boundingRectangle; progress ??= new OperationProgress(OperationProgress.StatusOptimizingBounds, LayerCount - 1); - _boundingRectangle = firstLayer.BoundingRectangle; - if (_boundingRectangle.IsEmpty) // Safe checking + _boundingRectangle = Rectangle.Empty; + uint firstValidLayerBounds = 0; + + void FindFirstBoundingRectangle() + { + for (uint layerIndex = 0; layerIndex < Count; layerIndex++) + { + firstValidLayerBounds = layerIndex; + if (this[layerIndex] is null || this[layerIndex].BoundingRectangle == Rectangle.Empty) continue; + _boundingRectangle = this[layerIndex].BoundingRectangle; + break; + } + } + + FindFirstBoundingRectangle(); + //_boundingRectangle = firstLayer.BoundingRectangle; + + if (_boundingRectangle.IsEmpty) // Safe checking, all layers haven't a bounding rectangle { progress.Reset(OperationProgress.StatusOptimizingBounds, LayerCount-1); Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, layerIndex => @@ -699,25 +715,29 @@ namespace UVtools.Core this[layerIndex].GetBoundingRectangle(); - if (progress is null) return; progress.LockAndIncrement(); }); - _boundingRectangle = firstLayer.BoundingRectangle; if (progress.Token.IsCancellationRequested) { _boundingRectangle = Rectangle.Empty; progress.Token.ThrowIfCancellationRequested(); } + + FindFirstBoundingRectangle(); } - progress.Reset(OperationProgress.StatusCalculatingBounds, LayerCount-1); - for (int i = 1; i < LayerCount; i++) + if (firstValidLayerBounds+1 < LayerCount) { - if(this[i] is null || this[i].BoundingRectangle.IsEmpty) continue; - _boundingRectangle = Rectangle.Union(_boundingRectangle, this[i].BoundingRectangle); - progress++; + progress.Reset(OperationProgress.StatusCalculatingBounds, LayerCount - firstValidLayerBounds - 1); + for (var i = firstValidLayerBounds+1; i < LayerCount; i++) + { + if (this[i] is null || this[i].BoundingRectangle.IsEmpty) continue; + _boundingRectangle = Rectangle.Union(_boundingRectangle, this[i].BoundingRectangle); + progress++; + } } + RaisePropertyChanged(nameof(BoundingRectangle)); return _boundingRectangle; } @@ -770,6 +790,16 @@ namespace UVtools.Core return _layers?.Where((layer, index) => !layer.IsEmpty && index >= layerStartIndex && index <= layerEndIndex).MaxBy(layer => layer.NonZeroPixelCount).FirstOrDefault(); } + public IEnumerable GetLayersFromHeightRange(float startPositionZ, float endPositionZ) + { + return this.Where(layer => layer.PositionZ >= startPositionZ && layer.PositionZ <= endPositionZ); + } + + public IEnumerable GetLayersFromHeightRange(float endPositionZ) + { + return this.Where(layer => layer.PositionZ <= endPositionZ); + } + public static void MutateGetVarsIterationChamfer(uint startLayerIndex, uint endLayerIndex, int iterationsStart, int iterationsEnd, ref bool isFade, out float iterationSteps, out int maxIteration) { iterationSteps = 0; diff --git a/UVtools.Core/Operations/OperationCalculator.cs b/UVtools.Core/Operations/OperationCalculator.cs index 779272e..8c63c06 100644 --- a/UVtools.Core/Operations/OperationCalculator.cs +++ b/UVtools.Core/Operations/OperationCalculator.cs @@ -354,7 +354,7 @@ namespace UVtools.Core.Operations private decimal _layerHeight = 0.05m; public override string Description => "Calculates the optimal model tilt angle for printing and to minimize the visual layer effect."; - public override string Formula => "Angleº = tanh(Layer height / XYResolution) * (180 / PI)"; + public override string Formula => "Angleº = arctan(Layer height / XYResolution) * (180 / PI)"; public OptimalModelTilt(Size resolution, SizeF display, decimal layerHeight = 0.05m) { @@ -437,7 +437,7 @@ namespace UVtools.Core.Operations public decimal XYResolutionUm => Math.Round(XYResolution * 1000, 2); public decimal TiltAngleDegrees => - XYResolution > 0 ? (decimal) Math.Round(Math.Tanh((double) (_layerHeight / XYResolution)) * (180 / Math.PI), 3) : 0; + XYResolution > 0 ? (decimal) Math.Round(Math.Atan((double) (_layerHeight / XYResolution)) * (180 / Math.PI), 3) : 0; } } diff --git a/UVtools.Core/Operations/OperationPixelArithmetic.cs b/UVtools.Core/Operations/OperationPixelArithmetic.cs index a351d01..d89befb 100644 --- a/UVtools.Core/Operations/OperationPixelArithmetic.cs +++ b/UVtools.Core/Operations/OperationPixelArithmetic.cs @@ -75,7 +75,8 @@ namespace UVtools.Core.Operations private short _noiseMinOffset = -128; private short _noiseMaxOffset = 128; private byte _noiseThreshold; - private ushort _noisePixelArea = 3; + private ushort _noisePixelArea = 1; + private byte _noisePasses = 1; #endregion @@ -366,7 +367,13 @@ namespace UVtools.Core.Operations public ushort NoisePixelArea { get => _noisePixelArea; - set => RaiseAndSetIfChanged(ref _noisePixelArea, value); + set => RaiseAndSetIfChanged(ref _noisePixelArea, Math.Max((byte)1, value)); + } + + public byte NoisePasses + { + get => _noisePasses; + set => RaiseAndSetIfChanged(ref _noisePasses, Math.Max((byte)1, value)); } public byte Value @@ -781,7 +788,13 @@ namespace UVtools.Core.Operations } if (zoneBrightness <= _noiseThreshold) continue; - byte brightness = (byte)Math.Clamp(random.Next(_noiseMinOffset, _noiseMaxOffset + 1) + zoneBrightness, byte.MinValue, byte.MaxValue); + byte brightness = zoneBrightness; + + for (ushort i = 0; i < _noisePasses; i++) + { + brightness = (byte)Math.Clamp(random.Next(_noiseMinOffset, _noiseMaxOffset + 1) + brightness, byte.MinValue, byte.MaxValue); + } + //byte brightness = (byte)Math.Clamp(RandomNumberGenerator.GetInt32(_noiseMinOffset, _noiseMaxOffset + 1) + zoneBrightness, byte.MinValue, byte.MaxValue); for (var y1 = y; y1 < y + _noisePixelArea && y1 < bounds.Bottom; y1++) { @@ -904,8 +917,8 @@ namespace UVtools.Core.Operations Operator = PixelArithmeticOperators.Corrode; ApplyMethod = PixelArithmeticApplyMethod.ModelSurfaceAndInset; NoiseMinOffset = -200; - NoiseMaxOffset = 127; - WallThickness = 6; + NoiseMaxOffset = 64; + WallThickness = 4; IgnoreAreaOperator = PixelArithmeticIgnoreAreaOperator.SmallerThan; IgnoreAreaThreshold = 5000; } @@ -1257,7 +1270,7 @@ namespace UVtools.Core.Operations protected bool Equals(OperationPixelArithmetic other) { - return _operator == other._operator && _applyMethod == other._applyMethod && _wallThicknessStart == other._wallThicknessStart && _wallThicknessEnd == other._wallThicknessEnd && _wallChamfer == other._wallChamfer && _ignoreAreaOperator == other._ignoreAreaOperator && _ignoreAreaThreshold == other._ignoreAreaThreshold && _value == other._value && _usePattern == other._usePattern && _thresholdType == other._thresholdType && _thresholdMaxValue == other._thresholdMaxValue && _patternAlternatePerLayersNumber == other._patternAlternatePerLayersNumber && _patternInvert == other._patternInvert && _patternText == other._patternText && _patternTextAlternate == other._patternTextAlternate && _patternGenMinBrightness == other._patternGenMinBrightness && _patternGenBrightness == other._patternGenBrightness && _patternGenInfillThickness == other._patternGenInfillThickness && _patternGenInfillSpacing == other._patternGenInfillSpacing && _noiseMinOffset == other._noiseMinOffset && _noiseMaxOffset == other._noiseMaxOffset && _noiseThreshold == other._noiseThreshold && _noisePixelArea == other._noisePixelArea; + return _operator == other._operator && _applyMethod == other._applyMethod && _wallThicknessStart == other._wallThicknessStart && _wallThicknessEnd == other._wallThicknessEnd && _wallChamfer == other._wallChamfer && _ignoreAreaOperator == other._ignoreAreaOperator && _ignoreAreaThreshold == other._ignoreAreaThreshold && _value == other._value && _usePattern == other._usePattern && _thresholdType == other._thresholdType && _thresholdMaxValue == other._thresholdMaxValue && _patternAlternatePerLayersNumber == other._patternAlternatePerLayersNumber && _patternInvert == other._patternInvert && _patternText == other._patternText && _patternTextAlternate == other._patternTextAlternate && _patternGenMinBrightness == other._patternGenMinBrightness && _patternGenBrightness == other._patternGenBrightness && _patternGenInfillThickness == other._patternGenInfillThickness && _patternGenInfillSpacing == other._patternGenInfillSpacing && _noiseMinOffset == other._noiseMinOffset && _noiseMaxOffset == other._noiseMaxOffset && _noiseThreshold == other._noiseThreshold && _noisePixelArea == other._noisePixelArea && _noisePasses == other._noisePasses; } public override bool Equals(object obj) @@ -1294,6 +1307,7 @@ namespace UVtools.Core.Operations hashCode.Add(_noiseMaxOffset); hashCode.Add(_noiseThreshold); hashCode.Add(_noisePixelArea); + hashCode.Add(_noisePasses); return hashCode.ToHashCode(); } diff --git a/UVtools.Core/Suggestions/Suggestion.cs b/UVtools.Core/Suggestions/Suggestion.cs new file mode 100644 index 0000000..e9623bd --- /dev/null +++ b/UVtools.Core/Suggestions/Suggestion.cs @@ -0,0 +1,159 @@ +/* + * 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.Xml.Serialization; +using UVtools.Core.FileFormats; +using UVtools.Core.Objects; + +namespace UVtools.Core.Suggestions +{ + public abstract class Suggestion : BindableBase + { + #region Members + + private bool _enabled = true; + private bool _autoApply; + private FileFormat _slicerFile; + + #endregion + + #region Properties + + /// + /// Gets or sets if this suggestion is enabled + /// + public bool Enabled + { + get => _enabled; + set => RaiseAndSetIfChanged(ref _enabled, value); + } + + /// + /// Gets or sets if this suggestion can be auto applied once file load + /// + public bool AutoApply + { + get => _autoApply; + set => RaiseAndSetIfChanged(ref _autoApply, value); + } + + /// + /// Gets or sets the + /// + [XmlIgnore] + public FileFormat SlicerFile + { + get => _slicerFile; + set + { + if (ReferenceEquals(_slicerFile, value)) return; + _slicerFile = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(IsAvailable)); + RaisePropertyChanged(nameof(IsApplied)); + } + } + + /// + /// Gets if this suggestion is informative only and contain no actions to execute + /// + public virtual bool IsInformativeOnly => false; + + /// + /// Gets if this suggestion is available given the + /// + public virtual bool IsAvailable => true; + + /// + /// Gets if this suggestion is already applied given the + /// + public abstract bool IsApplied { get; } + + /// + /// Gets the title for this suggestion + /// + public abstract string Title { get; } + + /// + /// Gets the message for this suggestion + /// + public abstract string Message { get; } + + /// + /// Gets the tooltip message + /// + public virtual string ToolTip => null; + + public virtual string InformationUrl => null; + + /// + /// Gets the confirmation message before apply the suggestion + /// + public virtual string ConfirmationMessage => null; + + + public string GlobalAppliedMessage => $"✓ {Title}"; + public string GlobalNotAppliedMessage => $"âš  {Title}"; + + #endregion + + #region Methods + + public void RefreshNotifyAll() + { + RaisePropertyChanged(nameof(IsAvailable)); + RaisePropertyChanged(nameof(IsApplied)); + RefreshNotifyMessage(); + } + + public void RefreshNotifyMessage() + { + RaisePropertyChanged(nameof(Message)); + RaisePropertyChanged(nameof(ToolTip)); + } + + /// + /// Executes and applies the suggestion + /// + /// + /// + protected virtual bool ExecuteInternally() + { + throw new NotImplementedException(); + } + + /// + /// Executes and applies the suggestion + /// + /// + /// + public bool Execute() + { + if (_slicerFile is null) throw new InvalidOperationException($"The suggestion '{Title}' can't execute due the lacking of a file parent."); + if (!Enabled || !IsAvailable || IsApplied || IsInformativeOnly) return false; + + var result = ExecuteInternally(); + + RaisePropertyChanged(nameof(IsApplied)); + RefreshNotifyMessage(); + + return result; + } + + /// + /// Executes only if this suggestion is marked with as true + /// + /// + public bool ExecuteIfAutoApply() + { + return _autoApply && Execute(); + } + #endregion + } +} diff --git a/UVtools.Core/Suggestions/SuggestionBottomLayerCount.cs b/UVtools.Core/Suggestions/SuggestionBottomLayerCount.cs new file mode 100644 index 0000000..5c5ddf7 --- /dev/null +++ b/UVtools.Core/Suggestions/SuggestionBottomLayerCount.cs @@ -0,0 +1,96 @@ +/* + * 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.Suggestions +{ + public sealed class SuggestionBottomLayerCount : Suggestion + { + #region Members + + private decimal _targetBottomHeight = 0.25m; + private decimal _minimumBottomHeight = 0.07m; + private decimal _maximumBottomHeight = 0.4m; + private byte _minimumBottomLayerCount = 3; + private byte _maximumBottomLayerCount = 7; + + #endregion + + #region Properties + + public override bool IsAvailable => SlicerFile?.CanUseBottomLayerCount ?? false; + + public override bool IsApplied + { + get + { + if (SlicerFile is null) return false; + var bottomHeight = (decimal)SlicerFile.BottomLayersHeight; + return bottomHeight >= _minimumBottomHeight && bottomHeight <= _maximumBottomHeight + && SlicerFile.BottomLayerCount >= _minimumBottomLayerCount && SlicerFile.BottomLayerCount <= _maximumBottomLayerCount; + } + } + + public override string Title => "Bottom layer count"; + public override string Message => IsApplied + ? $"{GlobalAppliedMessage}: {SlicerFile.BottomLayerCount} / {SlicerFile.BottomLayersHeight}mm" + : $"{GlobalNotAppliedMessage} ({SlicerFile.BottomLayerCount}) is out of the recommended {BottomLayerCountValue} layers"; + + public override string ToolTip => $"The recommended total height for the bottom layers must be between {_minimumBottomHeight}mm and {_maximumBottomHeight}mm constrained from {_minimumBottomLayerCount} to {_maximumBottomLayerCount} layers.\n" + + "Explanation: Bottom layers should be kept to a minimum, usually from 2 or 3, it function is to provide a good adhesion to the build plate, and that happens on the first layer, using a high count have disadvantages."; + + public override string InformationUrl => "https://ameralabs.com/blog/default-3d-printing-raft-settings"; + + public override string ConfirmationMessage => $"{Title}: {SlicerFile.BottomLayerCount} » {BottomLayerCountValue}"; + + public decimal TargetBottomHeight + { + get => _targetBottomHeight; + set => RaiseAndSetIfChanged(ref _targetBottomHeight, value); + } + + public decimal MinimumBottomHeight + { + get => _minimumBottomHeight; + set => RaiseAndSetIfChanged(ref _minimumBottomHeight, value); + } + + public decimal MaximumBottomHeight + { + get => _maximumBottomHeight; + set => RaiseAndSetIfChanged(ref _maximumBottomHeight, value); + } + + public byte MinimumBottomLayerCount + { + get => _minimumBottomLayerCount; + set => RaiseAndSetIfChanged(ref _minimumBottomLayerCount, value); + } + + public byte MaximumBottomLayerCount + { + get => _maximumBottomLayerCount; + set => RaiseAndSetIfChanged(ref _maximumBottomLayerCount, value); + } + + public ushort BottomLayerCountValue => Math.Clamp((ushort)Math.Ceiling((float)_targetBottomHeight / SlicerFile.LayerHeight), _minimumBottomLayerCount, _maximumBottomLayerCount); + + #endregion + + #region Methods + + protected override bool ExecuteInternally() + { + SlicerFile.BottomLayerCount = BottomLayerCountValue; + return true; + } + + #endregion + } +} diff --git a/UVtools.Core/Suggestions/SuggestionLayerHeight.cs b/UVtools.Core/Suggestions/SuggestionLayerHeight.cs new file mode 100644 index 0000000..2603292 --- /dev/null +++ b/UVtools.Core/Suggestions/SuggestionLayerHeight.cs @@ -0,0 +1,84 @@ +/* + * 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.Extensions; +using Layer = UVtools.Core.Layers.Layer; + +namespace UVtools.Core.Suggestions +{ + public sealed class SuggestionLayerHeight : Suggestion + { + #region Members + + private decimal _minimumLayerHeight = 0.03m; + private decimal _maximumLayerHeight = 0.10m; + private byte _maximumLayerHeightDecimalDigits = 2; + + #endregion + + #region Properties + + public override bool IsApplied + { + get + { + if (SlicerFile is null) return false; + if ((decimal)SlicerFile.LayerHeight < _minimumLayerHeight || (decimal)SlicerFile.LayerHeight > _maximumLayerHeight) return false; + if (SlicerFile.LayerHeight.DecimalDigits() > _maximumLayerHeightDecimalDigits) return false; + + foreach (var layer in SlicerFile) + { + if ((decimal)layer.LayerHeight < _minimumLayerHeight || (decimal)layer.LayerHeight > _maximumLayerHeight) return false; + if (layer.LayerHeight.DecimalDigits() > _maximumLayerHeightDecimalDigits) return false; + } + + return true; + } + } + + public override bool IsInformativeOnly => true; + + public override string Title => "Layer height"; + public override string Message => IsApplied + ? $"{GlobalAppliedMessage}: {SlicerFile.LayerHeight}mm" + : $"{GlobalNotAppliedMessage} is out of the recommended {_minimumLayerHeight}mm » {_maximumLayerHeight}mm, up to {_maximumLayerHeightDecimalDigits} decimal digit(s)"; + + public override string ToolTip => $"The recommended layer height is between {_minimumLayerHeight}mm and {_maximumLayerHeight}mm up to {_maximumLayerHeightDecimalDigits} digit(s) precision.\n" + + "Explanation: Using the right layer height is important to get successful prints:\n" + + "Thin layers may cause problems on adhesion, delamination, will print much slower and have no real visual benefits.\n" + + "Thick layers may not fully cure no matter the exposure time you use, causing delamination and other hazards. Read your resin datasheet to know the limits.\n" + + "Using layer height with too many decimal digits may produce a wrong positioning due stepper step loss and/or Z axis quality.\n" + + "Solution: Re-slice the model with proper layer height."; + + public override string ConfirmationMessage => $"{Title}: Re-slice the model with proper layer height"; + + public decimal MinimumLayerHeight + { + get => _minimumLayerHeight; + set => RaiseAndSetIfChanged(ref _minimumLayerHeight, value.Clamp(Layer.MinimumHeight, Layer.MaximumHeight)); + } + + public decimal MaximumLayerHeight + { + get => _maximumLayerHeight; + set => RaiseAndSetIfChanged(ref _maximumLayerHeight, value.Clamp(Layer.MinimumHeight, Layer.MaximumHeight)); + } + + public byte MaximumLayerHeightDecimalPlates + { + get => _maximumLayerHeightDecimalDigits; + set => RaiseAndSetIfChanged(ref _maximumLayerHeightDecimalDigits, value.Clamp(2, 4)); + } + + #endregion + + #region Methods + + #endregion + } +} diff --git a/UVtools.Core/Suggestions/SuggestionWaitTimeAfterCure.cs b/UVtools.Core/Suggestions/SuggestionWaitTimeAfterCure.cs new file mode 100644 index 0000000..f5b0da5 --- /dev/null +++ b/UVtools.Core/Suggestions/SuggestionWaitTimeAfterCure.cs @@ -0,0 +1,182 @@ +/* + * 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 UVtools.Core.Extensions; + +namespace UVtools.Core.Suggestions +{ + public sealed class SuggestionWaitTimeAfterCure : Suggestion + { + #region Enums + public enum SuggestionWaitTimeAfterCureApplyType + { + Fixed, + ExposureProportional + } + #endregion + + #region Members + private SuggestionWaitTimeAfterCureApplyType _applyType = SuggestionWaitTimeAfterCureApplyType.Fixed; + private decimal _fixedBottomWaitTimeAfterCure = 7; + private decimal _fixedWaitTimeAfterCure = 1; + private decimal _proportionalExposureTime = 2; + private decimal _proportionalWaitTimeAfterCure = 1; + private decimal _minimumBottomWaitTimeAfterCure = 3; + private decimal _minimumWaitTimeAfterCure = 1; + private decimal _maximumBottomWaitTimeAfterCure = 20; + private decimal _maximumWaitTimeAfterCure = 12; + #endregion + + #region Properties + + public override bool IsAvailable => SlicerFile?.CanUseAnyWaitTimeAfterCure ?? false; + + public override bool IsApplied + { + get + { + if (SlicerFile is null) return false; + if (SlicerFile.CanUseBottomWaitTimeAfterCure) + { + if ((decimal) SlicerFile.BottomWaitTimeAfterCure < _minimumWaitTimeAfterCure || + (decimal) SlicerFile.BottomWaitTimeAfterCure > _maximumWaitTimeAfterCure || + Math.Abs(SlicerFile.BottomWaitTimeAfterCure - CalculateWaitTime(true, (decimal)SlicerFile.BottomExposureTime)) > 0.1) return false; + } + if (SlicerFile.CanUseWaitTimeAfterCure) + { + if ((decimal)SlicerFile.WaitTimeAfterCure < _minimumWaitTimeAfterCure || + (decimal)SlicerFile.WaitTimeAfterCure > _maximumWaitTimeAfterCure || + Math.Abs(SlicerFile.WaitTimeAfterCure - CalculateWaitTime(false, (decimal)SlicerFile.ExposureTime)) > 0.1) return false; + } + + if (SlicerFile.CanUseLayerWaitTimeAfterCure) + { + foreach (var layer in SlicerFile) + { + if ((decimal)layer.WaitTimeAfterCure < _minimumWaitTimeAfterCure || + (decimal)layer.WaitTimeAfterCure > _maximumWaitTimeAfterCure || + Math.Abs(layer.WaitTimeAfterCure - CalculateWaitTime(layer.IsBottomLayer, (decimal)layer.ExposureTime)) > 0.1) return false; + } + } + return true; + } + } + + public override string Title => "Wait time after cure"; + public override string Message => IsApplied + ? $"{GlobalAppliedMessage}: {SlicerFile.BottomWaitTimeAfterCure}/{SlicerFile.WaitTimeAfterCure}s" + : $"{GlobalNotAppliedMessage} of {SlicerFile.BottomWaitTimeAfterCure}/{SlicerFile.WaitTimeAfterCure}s " + + $"is out of the recommended {CalculateWaitTime(true, (decimal) SlicerFile.BottomWaitTimeAfterCure)}/{CalculateWaitTime(false, (decimal)SlicerFile.WaitTimeAfterCure)}s"; + + //public override string ToolTip => $"The recommended total height for the bottom layers must be between {_minimumBottomHeight}mm and {_maximumBottomHeight}mm constrained from {_minimumBottomLayerCount} to {_maximumBottomLayerCount} layers.\n" + + // "Explanation: Bottom layers should be kept to a minimum, usually from 2 or 3, it function is to provide a good adhesion to the build plate, and that happens on the first layer, using a high count have disadvantages."; + + public override string InformationUrl => "https://ameralabs.com/blog/default-3d-printing-raft-settings"; + + public override string ConfirmationMessage => $"{Title}: {SlicerFile.BottomWaitTimeAfterCure}/{SlicerFile.WaitTimeAfterCure}s » {CalculateWaitTime(true, (decimal)SlicerFile.BottomWaitTimeAfterCure)}/{CalculateWaitTime(false, (decimal)SlicerFile.WaitTimeAfterCure)}s"; + + private SuggestionWaitTimeAfterCureApplyType ApplyType + { + get => _applyType; + set => RaiseAndSetIfChanged(ref _applyType, value); + } + + public decimal FixedBottomWaitTimeAfterCure + { + get => _fixedBottomWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _fixedBottomWaitTimeAfterCure, Math.Round(value, 2)); + } + + public decimal FixedWaitTimeAfterCure + { + get => _fixedWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _fixedWaitTimeAfterCure, Math.Round(value, 2)); + } + + public decimal ProportionalExposureTime + { + get => _proportionalExposureTime; + set => RaiseAndSetIfChanged(ref _proportionalExposureTime, Math.Round(value, 2)); + } + + public decimal ProportionalWaitTimeAfterCure + { + get => _proportionalWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _proportionalWaitTimeAfterCure, Math.Round(value, 2)); + } + + public decimal MinimumBottomWaitTimeAfterCure + { + get => _minimumBottomWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _minimumBottomWaitTimeAfterCure, Math.Round(value, 2)); + } + + public decimal MinimumWaitTimeAfterCure + { + get => _minimumWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _minimumWaitTimeAfterCure, Math.Round(value, 2)); + } + + public decimal MaximumBottomWaitTimeAfterCure + { + get => _maximumBottomWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _maximumBottomWaitTimeAfterCure, Math.Round(value, 2)); + } + + public decimal MaximumWaitTimeAfterCure + { + get => _maximumWaitTimeAfterCure; + set => RaiseAndSetIfChanged(ref _maximumWaitTimeAfterCure, Math.Round(value, 2)); + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally() + { + if (SlicerFile.CanUseBottomWaitTimeAfterCure) + { + SlicerFile.BottomWaitTimeAfterCure = CalculateWaitTime(true, (decimal)SlicerFile.BottomExposureTime); + } + if (SlicerFile.CanUseWaitTimeAfterCure) + { + SlicerFile.WaitTimeAfterCure = CalculateWaitTime(false, (decimal)SlicerFile.ExposureTime); + } + + if (SlicerFile.CanUseLayerWaitTimeAfterCure) + { + foreach (var layer in SlicerFile) + { + layer.WaitTimeAfterCure = CalculateWaitTime(layer.IsBottomLayer, (decimal) layer.ExposureTime); + } + } + + return true; + } + + + public float CalculateWaitTime(bool isBottomLayer, decimal exposureTime) + { + return _applyType switch + { + SuggestionWaitTimeAfterCureApplyType.Fixed => (float) (isBottomLayer + ? _fixedBottomWaitTimeAfterCure + : _fixedWaitTimeAfterCure), + SuggestionWaitTimeAfterCureApplyType.ExposureProportional => (float) Math.Round( + (exposureTime * _proportionalWaitTimeAfterCure / _proportionalExposureTime).Clamp( + isBottomLayer ? _minimumBottomWaitTimeAfterCure : _minimumWaitTimeAfterCure, + isBottomLayer ? _maximumBottomWaitTimeAfterCure : _maximumWaitTimeAfterCure), 2), + _ => throw new ArgumentOutOfRangeException() + }; + } + + #endregion + } +} diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index e8fce27..d197d21 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.27.6 + 2.27.7 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 @@ -60,7 +60,7 @@ - + diff --git a/UVtools.Platforms/linux-arm64/libcvextern.so b/UVtools.Platforms/linux-arm64/libcvextern.so index 1a5062d..d57c955 100644 Binary files a/UVtools.Platforms/linux-arm64/libcvextern.so and b/UVtools.Platforms/linux-arm64/libcvextern.so differ diff --git a/UVtools.Platforms/osx-x64/libcvextern.dylib b/UVtools.Platforms/osx-x64/libcvextern.dylib index 7b8afcf..eb8bdfc 100644 Binary files a/UVtools.Platforms/osx-x64/libcvextern.dylib and b/UVtools.Platforms/osx-x64/libcvextern.dylib differ diff --git a/UVtools.Platforms/rhel-x64/libcvextern.so b/UVtools.Platforms/rhel-x64/libcvextern.so index 57e78cc..eb198ab 100644 Binary files a/UVtools.Platforms/rhel-x64/libcvextern.so and b/UVtools.Platforms/rhel-x64/libcvextern.so differ diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs index 5ab32f6..c302212 100644 --- a/UVtools.WPF/App.axaml.cs +++ b/UVtools.WPF/App.axaml.cs @@ -142,7 +142,7 @@ namespace UVtools.WPF uri = new Uri($"avares://{assemblyName}{url}"); } - var res = AvaloniaLocator.Current.GetService().Open(uri); + var res = AvaloniaLocator.Current.GetService()?.Open(uri); return res; } diff --git a/UVtools.WPF/Assets/Icons/info-circle-16x16.png b/UVtools.WPF/Assets/Icons/info-circle-16x16.png new file mode 100644 index 0000000..c1fe64d Binary files /dev/null and b/UVtools.WPF/Assets/Icons/info-circle-16x16.png differ diff --git a/UVtools.WPF/Assets/Icons/shield-virus-32x32.png b/UVtools.WPF/Assets/Icons/shield-virus-32x32.png new file mode 100644 index 0000000..e984a64 Binary files /dev/null and b/UVtools.WPF/Assets/Icons/shield-virus-32x32.png differ diff --git a/UVtools.WPF/Assets/Styles/StylesLight.xaml b/UVtools.WPF/Assets/Styles/StylesLight.xaml index d030ff4..2a966ef 100644 --- a/UVtools.WPF/Assets/Styles/StylesLight.xaml +++ b/UVtools.WPF/Assets/Styles/StylesLight.xaml @@ -16,5 +16,5 @@ - + \ No newline at end of file diff --git a/UVtools.WPF/Controls/Tools/ToolPixelArithmeticControl.axaml b/UVtools.WPF/Controls/Tools/ToolPixelArithmeticControl.axaml index 193a74b..cf81890 100644 --- a/UVtools.WPF/Controls/Tools/ToolPixelArithmeticControl.axaml +++ b/UVtools.WPF/Controls/Tools/ToolPixelArithmeticControl.axaml @@ -156,6 +156,17 @@ Maximum="65535" Width="80" Value="{Binding Operation.NoisePixelArea}"/> + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs index 3ce50ad..e86c387 100644 --- a/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolScriptingControl.axaml.cs @@ -43,6 +43,7 @@ namespace UVtools.WPF.Controls.Tools case ToolWindow.Callbacks.Loaded: if(ParentWindow is not null) ParentWindow.ButtonOkEnabled = Operation.CanExecute; ReloadGUI(); + ReloadScript(); Operation.PropertyChanged += (sender, e) => { if (e.PropertyName == nameof(Operation.CanExecute)) diff --git a/UVtools.WPF/Controls/WindowEx.cs b/UVtools.WPF/Controls/WindowEx.cs index 7901b47..8b529e6 100644 --- a/UVtools.WPF/Controls/WindowEx.cs +++ b/UVtools.WPF/Controls/WindowEx.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index 278cdba..3618687 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -489,11 +489,11 @@ namespace UVtools.WPF { if (!LayerCache.IsCached) return "Pixels: 0"; var pixelPercent = Math.Round(LayerCache.Layer.NonZeroPixelCount * 100.0 / (SlicerFile.ResolutionX * SlicerFile.ResolutionY), 2); - string text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)"; - var exposedMillimeters = LayerCache.Layer.ExposureMillimeters; - if (exposedMillimeters > 0) + var text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)"; + var volume = LayerCache.Layer.Volume; + if (volume > 0) { - text += $"\nMillimeters: {exposedMillimeters}"; + text += $"\nVolume: {volume:F2}mm³"; } return text; } diff --git a/UVtools.WPF/MainWindow.Suggestions.cs b/UVtools.WPF/MainWindow.Suggestions.cs new file mode 100644 index 0000000..db7afc7 --- /dev/null +++ b/UVtools.WPF/MainWindow.Suggestions.cs @@ -0,0 +1,98 @@ +/* + * 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.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using Avalonia.Controls; +using MessageBox.Avalonia.Enums; +using UVtools.Core.Suggestions; +using UVtools.WPF.Extensions; + +namespace UVtools.WPF +{ + public partial class MainWindow + { + #region Members + + private ListBox _suggestionsAvailableListBox; + + #endregion + + #region Properties + public Suggestion[] Suggestions { get; } = + { + //new SuggestionBottomLayerCount(), + //new SuggestionWaitTimeAfterCure(), + //new SuggestionLayerHeight() + }; + + public RangeObservableCollection SuggestionsAvailable { get; } = new(); + public RangeObservableCollection SuggestionsApplied { get; } = new(); + + #endregion + + #region Methods + public void InitSuggestions() + { + _suggestionsAvailableListBox = this.FindControl("SuggestionsAvailableListBox"); + } + + public void PopulateSuggestions(bool tryToAutoApply = true) + { + var suggestionsAvailable = new List(); + var suggestionsApplied = new List(); + foreach (var suggestion in Suggestions) + { + suggestion.SlicerFile = SlicerFile; + if(!suggestion.Enabled || !suggestion.IsAvailable) continue; + if(tryToAutoApply) suggestion.ExecuteIfAutoApply(); + + if(suggestion.IsApplied) suggestionsApplied.Add(suggestion); + else suggestionsAvailable.Add(suggestion); + } + + SuggestionsAvailable.ReplaceCollection(suggestionsAvailable); + SuggestionsApplied.ReplaceCollection(suggestionsApplied); + } + + public async void ApplySuggestionsClicked() + { + if (!IsFileLoaded || _suggestionsAvailableListBox.SelectedItems.Count == 0) return; + var suggestions = _suggestionsAvailableListBox.SelectedItems.Cast().Where(suggestion => !suggestion.IsInformativeOnly).ToArray(); + if (suggestions.Length == 0) return; + var sb = new StringBuilder($"Are you sure you want to apply the following {suggestions.Length} suggestions?:\n\n"); + + foreach (var suggestion in suggestions) + { + sb.AppendLine(suggestion.ConfirmationMessage); + } + if (await this.MessageBoxQuestion(sb.ToString(), "Apply suggestions?") != ButtonResult.Yes) return; + + foreach (var suggestion in suggestions) + { + suggestion.Execute(); + } + + PopulateSuggestions(false); + } + + public async void ApplySuggestionClicked(Suggestion suggestion) + { + if (!IsFileLoaded || suggestion is null || suggestion.IsInformativeOnly) return; + + if (await this.MessageBoxQuestion($"Are you sure you want to apply the following suggestion?:\n\n{suggestion.ConfirmationMessage}", "Apply the suggestion?") != ButtonResult.Yes) return; + + suggestion.Execute(); + PopulateSuggestions(false); + } + + #endregion + } +} diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml index e30fd9c..d1c757e 100644 --- a/UVtools.WPF/MainWindow.axaml +++ b/UVtools.WPF/MainWindow.axaml @@ -883,6 +883,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LICENSE https://github.com/sn4k3/UVtools Git - 2.27.6 + 2.27.7 AnyCPU;x64 UVtools.png README.md @@ -38,12 +38,12 @@ 1701;1702; - + - - - - + + + +