Files
UVtools/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs
Tiago Conceição b4504f83a5 v3.12.3
- (Improvement) Increase `PixelSize`, `PixelWdith` and `PixelHeight` precision from 3 to 4 decimals (#684)
- (Improvement) Suggestion wait time before/after cure: When unable to set time per layer and/or per bottom and normal layers it will show a simpler message with just one time that it can apply
- (Fix) Unable to apply the wait time before cure suggestion in some formats due incorrectly check for the wait time after cure availability (#686)
2023-03-31 21:41:39 +01:00

628 lines
26 KiB
C#

/*
* 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.ComponentModel;
using System.Linq;
using System.Text;
using UVtools.Core.Layers;
using UVtools.Core.Operations;
namespace UVtools.Core.Suggestions;
public sealed class SuggestionWaitTimeBeforeCure : Suggestion
{
#region Enums
public enum SuggestionWaitTimeBeforeCureSetType : byte
{
[Description("Fixed: Use a fixed time")]
Fixed,
[Description("Proportional to layer pixels")]
ProportionalLayerPixels,
[Description("Proportional to layer area")]
ProportionalLayerArea,
}
public enum SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom : byte
{
[Description("Previous mass")]
Previous,
[Description("Average of previous masses")]
Average,
[Description("Maximum of previous masses")]
Maximum,
}
#endregion
#region Members
private SuggestionWaitTimeBeforeCureSetType _setType = SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea;
private decimal _bottomHeight = 1;
private decimal _fixedBottomWaitTimeBeforeCure = 15;
private decimal _fixedWaitTimeBeforeCure = 2;
private byte _waitTimeBeforeCureTransitionLayerCount = 8;
private decimal _proportionalBottomWaitTimeBeforeCure = 30;
private decimal _proportionalWaitTimeBeforeCure = 10;
private uint _proportionalBottomLayerPixels = 5000000;
private uint _proportionalLayerPixels = 5000000;
private uint _proportionalBottomLayerArea = 10000;
private uint _proportionalLayerArea = 10000;
private decimal _proportionalBottomWaitTimeBeforeCureMaximumDifference = 1;
private decimal _proportionalWaitTimeBeforeCureMaximumDifference = 1;
private SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom _proportionalCalculateMassFrom = SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous;
private decimal _proportionalMassRelativeHeight = 0.2m;
private decimal _minimumBottomWaitTimeBeforeCure = 10;
private decimal _minimumWaitTimeBeforeCure = 1;
private decimal _maximumBottomWaitTimeBeforeCure = 30;
private decimal _maximumWaitTimeBeforeCure = 4;
private bool _createEmptyFirstLayer = true;
#endregion
#region Properties
public override bool IsAvailable
{
get
{
if (SlicerFile is null) return false;
return SlicerFile.CanUseAnyWaitTimeBeforeCure || SlicerFile.CanUseAnyLightOffDelay || SlicerFile.CanUseLayerAnyWaitTimeBeforeCure;
}
}
public override bool IsApplied
{
get
{
if (SlicerFile is null) return false;
switch (_applyWhen)
{
case SuggestionApplyWhen.OutsideLimits:
if (SlicerFile.CanUseBottomWaitTimeBeforeCure || SlicerFile.CanUseBottomLightOffDelay)
{
var waitTime = (decimal)SlicerFile.GetBottomWaitTimeBeforeCure();
if (waitTime < _minimumWaitTimeBeforeCure ||
waitTime > _maximumWaitTimeBeforeCure) return false;
}
if (SlicerFile.CanUseWaitTimeBeforeCure || SlicerFile.CanUseLightOffDelay)
{
var waitTime = (decimal)SlicerFile.GetNormalWaitTimeBeforeCure();
if (waitTime < _minimumWaitTimeBeforeCure ||
waitTime > _maximumWaitTimeBeforeCure) return false;
}
if (SlicerFile.CanUseLayerWaitTimeBeforeCure || SlicerFile.CanUseLayerLightOffDelay)
{
foreach (var layer in SlicerFile)
{
if (layer.IsDummy)
{
if (layer.GetWaitTimeBeforeCure() > 0.1) return false;
}
else
{
var waitTime = (decimal)layer.GetWaitTimeBeforeCure();
if (waitTime < _minimumWaitTimeBeforeCure || waitTime > _maximumWaitTimeBeforeCure) return false;
}
}
}
break;
case SuggestionApplyWhen.Different:
if (SlicerFile.CanUseBottomWaitTimeBeforeCure || SlicerFile.CanUseBottomLightOffDelay)
{
if (Math.Abs(SlicerFile.GetBottomWaitTimeBeforeCure() - CalculateWaitTime(true)) > 0.1) return false;
}
if (SlicerFile.CanUseWaitTimeBeforeCure || SlicerFile.CanUseLightOffDelay)
{
if (Math.Abs(SlicerFile.GetNormalWaitTimeBeforeCure() - CalculateWaitTime(false)) > 0.1) return false;
}
if (SlicerFile.CanUseLayerWaitTimeBeforeCure || SlicerFile.CanUseLayerLightOffDelay)
{
foreach (var layer in SlicerFile)
{
if (layer.IsDummy)
{
if (layer.GetWaitTimeBeforeCure() > 0.1) return false;
}
else
{
if (Math.Abs(layer.GetWaitTimeBeforeCure() - CalculateWaitTime(layer)) > 0.1) return false;
}
}
}
break;
default:
throw new ArgumentOutOfRangeException();
}
return true;
}
}
public override string Title => "Wait time before cure";
public override string Description => "Rest some time before cure the layer is crucial to let the resin settle after the lift sequence and allow some time for the arm settle at the correct Z position as the resin will offer some resistance and push the structure.\n" +
"This lead to better quality with more successful prints, less lamination problems, better first layers with more success of stick to the build plate and less elephant foot effect.";
public override string Message
{
get
{
if(SlicerFile.CanUseLayerAnyWaitTimeBeforeCure
|| SlicerFile
is {CanUseBottomWaitTimeBeforeCure: true, CanUseWaitTimeBeforeCure: true}
or {CanUseBottomLightOffDelay: true, CanUseLightOffDelay: true})
{
return IsApplied
? $"{GlobalAppliedMessage}: {SlicerFile.GetBottomWaitTimeBeforeCure()}/{SlicerFile.GetNormalWaitTimeBeforeCure()}s"
: $"{GlobalNotAppliedMessage} of {SlicerFile.GetBottomWaitTimeBeforeCure()}/{SlicerFile.GetNormalWaitTimeBeforeCure()}s " +
$"is out of the recommended {CalculateWaitTime(true)}/{CalculateWaitTime(false)}s";
}
// Single property
return IsApplied
? $"{GlobalAppliedMessage}: {SlicerFile.GetNormalWaitTimeBeforeCure()}s"
: $"{GlobalNotAppliedMessage} of {SlicerFile.GetNormalWaitTimeBeforeCure()}s " +
$"is out of the recommended {CalculateWaitTime(false)}s";
}
}
public override string ToolTip
{
get
{
if (SlicerFile.CanUseLayerAnyWaitTimeBeforeCure
|| SlicerFile
is {CanUseBottomWaitTimeBeforeCure: true, CanUseWaitTimeBeforeCure: true}
or {CanUseBottomLightOffDelay: true, CanUseLightOffDelay: true})
{
return (_setType == SuggestionWaitTimeBeforeCureSetType.Fixed
? $"The recommended wait time must be {_fixedBottomWaitTimeBeforeCure}/{_fixedWaitTimeBeforeCure}s"
: $"The recommended wait time is a ratio of (wait time){_proportionalWaitTimeBeforeCure}s to (exposure time){_proportionalLayerArea}s") +
$" constrained from [Bottoms={_minimumBottomWaitTimeBeforeCure}s to {_maximumBottomWaitTimeBeforeCure}s] and [Normals={_minimumWaitTimeBeforeCure}s to {_maximumWaitTimeBeforeCure}s].\n" +
$"Explanation: {Description}";
}
// Single property
return (_setType == SuggestionWaitTimeBeforeCureSetType.Fixed
? $"The recommended wait time must be {_fixedWaitTimeBeforeCure}s"
: $"The recommended wait time is a ratio of (wait time){_proportionalWaitTimeBeforeCure}s to (exposure time){_proportionalLayerArea}s") +
$" constrained from {_minimumWaitTimeBeforeCure}s to {_maximumWaitTimeBeforeCure}s.\n" +
$"Explanation: {Description}";
}
}
public override string? ConfirmationMessage
{
get
{
if (SlicerFile.CanUseLayerAnyWaitTimeBeforeCure
|| SlicerFile
is {CanUseBottomWaitTimeBeforeCure: true, CanUseWaitTimeBeforeCure: true}
or {CanUseBottomLightOffDelay: true, CanUseLightOffDelay: true})
{
return $"{Title}: {SlicerFile.BottomWaitTimeBeforeCure}/{SlicerFile.WaitTimeBeforeCure}s » {CalculateWaitTime(true)}/{CalculateWaitTime(false)}s";
}
// Single property
return $"{Title}: {SlicerFile.WaitTimeBeforeCure}s » {CalculateWaitTime(false)}s";
}
}
public override string? InformationUrl => "https://blog.honzamrazek.cz/2022/01/prints-not-sticking-to-the-build-plate-layer-separation-rough-surface-on-a-resin-printer-resin-viscosity-the-common-denominator";
public SuggestionWaitTimeBeforeCureSetType SetType
{
get => _setType;
set
{
if(!RaiseAndSetIfChanged(ref _setType, value)) return;
RaisePropertyChanged(nameof(IsSetTypeFixed));
RaisePropertyChanged(nameof(IsSetTypeProportionalLayerPixels));
RaisePropertyChanged(nameof(IsSetTypeProportionalLayerArea));
}
}
public bool IsSetTypeFixed => _setType == SuggestionWaitTimeBeforeCureSetType.Fixed;
public bool IsSetTypeProportionalLayerPixels => _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels;
public bool IsSetTypeProportionalLayerArea => _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea;
public SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom ProportionalCalculateMassFrom
{
get => _proportionalCalculateMassFrom;
set
{
if (!RaiseAndSetIfChanged(ref _proportionalCalculateMassFrom, value)) return;
RaisePropertyChanged(nameof(IsProportionalCalculateMassFromPrevious));
}
}
public decimal ProportionalMassRelativeHeight
{
get => _proportionalMassRelativeHeight;
set => RaiseAndSetIfChanged(ref _proportionalMassRelativeHeight, Math.Max(0, Math.Round(value, Layer.HeightPrecision)));
}
public bool IsProportionalCalculateMassFromPrevious => _proportionalCalculateMassFrom == SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous;
public decimal BottomHeight
{
get => _bottomHeight;
set => RaiseAndSetIfChanged(ref _bottomHeight, Math.Max(0, value));
}
public decimal FixedBottomWaitTimeBeforeCure
{
get => _fixedBottomWaitTimeBeforeCure;
set
{
if(!RaiseAndSetIfChanged(ref _fixedBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2))) return;
RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement));
}
}
public decimal FixedWaitTimeBeforeCure
{
get => _fixedWaitTimeBeforeCure;
set
{
if (!RaiseAndSetIfChanged(ref _fixedWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2))) return;
RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement));
}
}
public byte WaitTimeBeforeCureTransitionLayerCount
{
get => _waitTimeBeforeCureTransitionLayerCount;
set
{
if(!RaiseAndSetIfChanged(ref _waitTimeBeforeCureTransitionLayerCount, value)) return;
RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement));
}
}
public decimal WaitTimeBeforeCureTransitionDecrement => _waitTimeBeforeCureTransitionLayerCount == 0 ? 0 : Math.Round((_fixedBottomWaitTimeBeforeCure - _fixedWaitTimeBeforeCure) / (_waitTimeBeforeCureTransitionLayerCount + 1), 2);
public decimal ProportionalBottomWaitTimeBeforeCure
{
get => _proportionalBottomWaitTimeBeforeCure;
set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2));
}
public decimal ProportionalWaitTimeBeforeCure
{
get => _proportionalWaitTimeBeforeCure;
set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2));
}
public uint ProportionalBottomLayerPixels
{
get => _proportionalBottomLayerPixels;
set => RaiseAndSetIfChanged(ref _proportionalBottomLayerPixels, Math.Max(1, value));
}
public uint ProportionalLayerPixels
{
get => _proportionalLayerPixels;
set => RaiseAndSetIfChanged(ref _proportionalLayerPixels, Math.Max(1, value));
}
public uint ProportionalBottomLayerArea
{
get => _proportionalBottomLayerArea;
set => RaiseAndSetIfChanged(ref _proportionalBottomLayerArea, Math.Max(1, value));
}
public uint ProportionalLayerArea
{
get => _proportionalLayerArea;
set => RaiseAndSetIfChanged(ref _proportionalLayerArea, Math.Max(1, value));
}
public decimal ProportionalBottomWaitTimeBeforeCureMaximumDifference
{
get => _proportionalBottomWaitTimeBeforeCureMaximumDifference;
set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCureMaximumDifference, Math.Round(Math.Max(0, value), 2));
}
public decimal ProportionalWaitTimeBeforeCureMaximumDifference
{
get => _proportionalWaitTimeBeforeCureMaximumDifference;
set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCureMaximumDifference, Math.Round(Math.Max(0, value), 2));
}
public decimal MinimumBottomWaitTimeBeforeCure
{
get => _minimumBottomWaitTimeBeforeCure;
set => RaiseAndSetIfChanged(ref _minimumBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2));
}
public decimal MinimumWaitTimeBeforeCure
{
get => _minimumWaitTimeBeforeCure;
set => RaiseAndSetIfChanged(ref _minimumWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2));
}
public decimal MaximumBottomWaitTimeBeforeCure
{
get => _maximumBottomWaitTimeBeforeCure;
set => RaiseAndSetIfChanged(ref _maximumBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2));
}
public decimal MaximumWaitTimeBeforeCure
{
get => _maximumWaitTimeBeforeCure;
set => RaiseAndSetIfChanged(ref _maximumWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2));
}
public bool CreateEmptyFirstLayer
{
get => _createEmptyFirstLayer;
set => RaiseAndSetIfChanged(ref _createEmptyFirstLayer, value);
}
#endregion
#region Constructor
public SuggestionWaitTimeBeforeCure()
{
_applyWhen = SuggestionApplyWhen.Different;
}
#endregion
#region Overrides
public override string? Validate()
{
var sb = new StringBuilder();
if (_minimumBottomWaitTimeBeforeCure > _maximumBottomWaitTimeBeforeCure)
{
sb.AppendLine("Minimum bottom limit can't be higher than maximum bottom limit");
}
if (_minimumWaitTimeBeforeCure > _maximumWaitTimeBeforeCure)
{
sb.AppendLine("Minimum normal limit can't be higher than maximum normal limit");
}
if (_setType is SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels
or SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea)
{
if (_proportionalWaitTimeBeforeCure <= 0)
{
sb.AppendLine("The proportional wait time must be higher than 0s");
}
}
switch (_setType)
{
case SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels:
{
if (_proportionalBottomLayerPixels <= 0)
{
sb.AppendLine("The bottom proportional pixels must be higher than 0");
}
if (_proportionalLayerPixels <= 0)
{
sb.AppendLine("The proportional pixels must be higher than 0");
}
break;
}
case SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea:
{
if (_proportionalBottomLayerArea <= 0)
{
sb.AppendLine("The bottom proportional layer area must be higher than 0mm²");
}
if (_proportionalLayerArea <= 0)
{
sb.AppendLine("The proportional layer area must be higher than 0mm²");
}
break;
}
}
return sb.ToString();
}
#endregion
#region Methods
protected override bool ExecuteInternally(OperationProgress progress)
{
SlicerFile.SuppressRebuildPropertiesWork(() =>
{
if (SlicerFile.CanUseBottomWaitTimeBeforeCure || SlicerFile.CanUseBottomLightOffDelay)
{
SlicerFile.SetBottomWaitTimeBeforeCureOrLightOffDelay(CalculateWaitTime(true));
}
if (SlicerFile.CanUseWaitTimeBeforeCure || SlicerFile.CanUseLightOffDelay)
{
SlicerFile.SetNormalWaitTimeBeforeCureOrLightOffDelay(CalculateWaitTime(false));
}
});
if (SlicerFile.CanUseLayerWaitTimeBeforeCure || SlicerFile.CanUseLayerLightOffDelay)
{
foreach (var layer in SlicerFile)
{
layer.SetWaitTimeBeforeCureOrLightOffDelay(CalculateWaitTime(layer));
}
}
if (_createEmptyFirstLayer && SlicerFile is {CanUseLayerPositionZ: true, SupportsGCode: false})
{
var firstLayer = SlicerFile.FirstLayer!;
if (!firstLayer.IsDummy) // First layer is not blank as it seems, lets create one
{
firstLayer = firstLayer.Clone();
using var mat = SlicerFile.CreateMatWithDummyPixel();
firstLayer.LayerMat = mat;
firstLayer.ExposureTime = SlicerFile.SupportsGCode ? 0 : 0.01f;
//firstLayer.LiftHeightTotal = SlicerFile.SupportsGCode ? 0 : 0.1f;
SlicerFile.FirstLayer!.LiftHeightTotal = SlicerFile.SupportsGCode ? 0 : 0.1f; // Already on position, try to not lift
firstLayer.SetNoDelays();
SlicerFile.SuppressRebuildPropertiesWork(() =>
{
if (SlicerFile.BottomLayerCount is > 0 and <= 7)
{
// Increase bottom layer count as dummy layer will count as one but constrain to a maximum
SlicerFile.BottomLayerCount++;
}
SlicerFile.Prepend(firstLayer);
});
}
}
return true;
}
public float CalculateWaitTime(bool isBottomLayer, Layer? layer = null)
{
if (layer is not null)
{
// Reassign isBottomLayer given the layer
isBottomLayer = layer.IsBottomLayer || (_bottomHeight > 0 && (decimal)layer.PositionZ <= _bottomHeight);
}
if (_setType == SuggestionWaitTimeBeforeCureSetType.Fixed)
{
if(layer is null || isBottomLayer || _waitTimeBeforeCureTransitionLayerCount == 0) return (float)(isBottomLayer ? _fixedBottomWaitTimeBeforeCure : _fixedWaitTimeBeforeCure);
// Check for transition layer
var firstNormalLayer = SlicerFile.FirstOrDefault(target => (decimal) target.PositionZ > _bottomHeight);
if (firstNormalLayer is not null)
{
if (layer.Index >= firstNormalLayer.Index &&
layer.Index <= firstNormalLayer.Index + _waitTimeBeforeCureTransitionLayerCount)
{
// Is transition layer
return (float)Math.Round(Math.Max(_fixedBottomWaitTimeBeforeCure - WaitTimeBeforeCureTransitionDecrement * (layer.Index - firstNormalLayer.Index + 1), _minimumWaitTimeBeforeCure), 2);
}
}
// Fallback
return (float)(isBottomLayer ? _fixedBottomWaitTimeBeforeCure : _fixedWaitTimeBeforeCure);
}
if (layer is null || _setType == SuggestionWaitTimeBeforeCureSetType.Fixed)
{
return (float)(isBottomLayer ? _fixedBottomWaitTimeBeforeCure : _fixedWaitTimeBeforeCure);
}
if (layer.IsDummy) return 0; // Empty layer or not exposed, don't need wait time
float mass = 0;
if (layer.Index > 0 && _proportionalCalculateMassFrom != SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous && _proportionalMassRelativeHeight > 0)
{
//var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); // Skip all previous empty layer
//if (previousLayer is not null) layer = previousLayer;
uint count = 0;
var previousLayer = layer;
while ((previousLayer = previousLayer!.PreviousLayer) is not null && (layer.PositionZ - previousLayer.PositionZ) <= (float)_proportionalMassRelativeHeight)
{
if(previousLayer.IsDummy) continue; // Skip empty or not exposed layers
count++;
switch (_proportionalCalculateMassFrom)
{
case SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Average:
mass += _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels
? previousLayer.NonZeroPixelCount
: previousLayer.GetArea();
break;
case SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Maximum:
mass = Math.Max(_setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels
? previousLayer.NonZeroPixelCount
: previousLayer.GetArea(), mass);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
if (_proportionalCalculateMassFrom == SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Average && mass > 0 && count > 0)
{
mass /= count;
}
}
if (mass <= 0)
{
var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2) ?? layer.GetNextLayerWithAtLeastPixelCountOf(2); // Skip all empty layers
if (previousLayer is null) // No parent layer to calculate from, set to fixed values
{
return isBottomLayer ? (float)_fixedBottomWaitTimeBeforeCure : (float)_fixedWaitTimeBeforeCure;
}
mass = _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels
? previousLayer.NonZeroPixelCount
: previousLayer.GetArea();
}
float value = _setType switch
{
SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels => (float) (isBottomLayer
? (decimal)mass * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerPixels
: (decimal)mass * _proportionalWaitTimeBeforeCure / _proportionalLayerPixels),
SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea => (float) (isBottomLayer
? (decimal)mass * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerArea
: (decimal)mass * _proportionalWaitTimeBeforeCure / _proportionalLayerArea),
_ => throw new ArgumentOutOfRangeException()
};
if (isBottomLayer)
{
if (_proportionalBottomWaitTimeBeforeCureMaximumDifference > 0)
{
var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2);
if(previousLayer is not null)
{
value = Math.Clamp(value,
Math.Max(0, previousLayer.WaitTimeBeforeCure - (float)_proportionalBottomWaitTimeBeforeCureMaximumDifference),
previousLayer.WaitTimeBeforeCure + (float)_proportionalBottomWaitTimeBeforeCureMaximumDifference);
}
}
}
else
{
if (_proportionalWaitTimeBeforeCureMaximumDifference > 0)
{
var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2);
if (previousLayer is not null)
{
value = Math.Clamp(value,
Math.Max(0, previousLayer.WaitTimeBeforeCure - (float)_proportionalWaitTimeBeforeCureMaximumDifference),
previousLayer.WaitTimeBeforeCure + (float)_proportionalWaitTimeBeforeCureMaximumDifference);
}
}
}
return (float)Math.Clamp(Math.Round((decimal)value, 2),
isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure,
isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure);
}
public float CalculateWaitTime(Layer layer) => layer.IsDummy ? 0 : CalculateWaitTime(false, layer);
#endregion
}