mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-08 17:42:31 +02:00
ea4f7e0400
- **UI:** - (Improvement) Layer navigation load time by parallel `Mat` to `Bitmap` conversion - (Improvement) Allow to show exceptions without the stack trace and detailed trigger action by using the `MessageExceiption` (#644) - (Improvement) Allow progress to have and display a detailed log (#644) - (Improvement) Convert format to another with multiple versions will now only show the possible versions for the extension - **Suggestion - Wait time before cure:** - (Improvement) Set the first wait time based on first valid layer mass rather than use the fixed limit - (Improvement) Set zero time to empty and dummy layers - (Improvement) When creating the dummy layer also increment the bottom layer count as the created layer count as one - **PCB Exposure:** - (Add) Excellon Drill Format (drl) to cut off holes (Implementation may lack some advanced features, please confirm the result) (#646) - (Fix) Arc (G03) with negative offsets (I-/J-) was not drawing the shape correctly - (Fix) Implement the rotation for the outline primitive (#645) - **File formats:** - (Improvement) Formats now sanitize the selected version before encode given the file extension, if version is out of range it will force the last known version - (Fix) CBDDLP: Remove a table from the file that might cause layer corruption - (Add) Operations - `AfterCompleteReport` property: Gets or sets an report to show to the user after complete the operation with success - (Improvement) Suggestion - Wait time after cure: Set zero time to empty and dummy layers - (Improvement) Slight improvement on the contour intersection check, yields better performance on resin and suction cup detection - (Improvement) Allow to trigger message boxes from operations and scripts (#644) - (Upgrade) .NET from 6.0.12 to 6.0.13
481 lines
18 KiB
C#
481 lines
18 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.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UVtools.Core.FileFormats;
|
|
using UVtools.Core.Layers;
|
|
|
|
namespace UVtools.Core.Operations;
|
|
|
|
|
|
public class OperationTimelapse : Operation
|
|
{
|
|
#region Enums
|
|
public enum TimelapseRaiseMode
|
|
{
|
|
[Description("Lift height: Use the lift sequence to raise to the set postion (Faster)")]
|
|
LiftHeight,
|
|
|
|
[Description("Virtual layer: Print a blank layer to simulate and raise to the set position (Slower)")]
|
|
VirtualLayer,
|
|
}
|
|
#endregion
|
|
|
|
#region Members
|
|
private decimal _raisePositionZ;
|
|
private bool _outputDummyPixel = true;
|
|
private decimal _raiseEachNthHeight = 1;
|
|
private TimelapseRaiseMode _raiseMode = TimelapseRaiseMode.LiftHeight;
|
|
private decimal _waitTimeAfterLift = 1;
|
|
private decimal _exposureTime = 1;
|
|
private bool _ensureLastLayer = true;
|
|
private bool _useCustomLift;
|
|
private decimal _slowLiftHeight = 3;
|
|
private decimal _liftSpeed;
|
|
private decimal _liftSpeed2;
|
|
private decimal _slowRetractHeight = 3;
|
|
private decimal _retractSpeed;
|
|
private decimal _retractSpeed2;
|
|
|
|
#endregion
|
|
|
|
#region Overrides
|
|
|
|
public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Normal;
|
|
public override string IconClass => "fa-solid fa-camera";
|
|
public override string Title => "Timelapse";
|
|
public override string Description =>
|
|
"Raise the build platform to a set position every odd-even height to be able to take a photo and create a time-lapse video of the print.\n" +
|
|
"You will require external hardware to take the photos, and create the time-lapse video by your own.\n" +
|
|
"NOTE: Only use this tool once. It will delay the total print time significantly.";
|
|
|
|
public override string ConfirmationText =>
|
|
$"raise the platform at every odd-even {_raiseEachNthHeight}mm to Z={_raisePositionZ}mm and generate {NumberOfLifts} additional lifts?";
|
|
|
|
public override string ProgressTitle => "Raising layers";
|
|
|
|
public override string ProgressAction => "Raised layer";
|
|
|
|
public override string? ValidateSpawn()
|
|
{
|
|
if(SlicerFile is {CanUseLayerPositionZ: false, CanUseLayerLiftHeight: false})
|
|
{
|
|
return NotSupportedMessage;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public override string? ValidateInternally()
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
if (!ValidateSpawn(out var message))
|
|
{
|
|
sb.AppendLine(message);
|
|
}
|
|
|
|
if ((_raiseMode == TimelapseRaiseMode.LiftHeight && !SlicerFile.CanUseLayerLiftHeight) ||
|
|
(_raiseMode == TimelapseRaiseMode.VirtualLayer && !SlicerFile.CanUseLayerPositionZ))
|
|
{
|
|
return $"The raise method {_raiseMode} is not compatible with this printer / file format, please choose other method.";
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
var result = $"[Mode: {_raiseMode}] [Z={_raisePositionZ}mm] [Raise each: {_raiseEachNthHeight}mm]";
|
|
if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
|
|
return result;
|
|
}
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public TimelapseRaiseMode RaiseMode
|
|
{
|
|
get => _raiseMode;
|
|
set
|
|
{
|
|
if(!RaiseAndSetIfChanged(ref _raiseMode, value)) return;
|
|
RaisePropertyChanged(nameof(IsLiftHeightMode));
|
|
RaisePropertyChanged(nameof(IsVirtualLayerMode));
|
|
RaisePropertyChanged(nameof(NumberOfLifts));
|
|
}
|
|
}
|
|
|
|
public bool IsLiftHeightMode => _raiseMode is TimelapseRaiseMode.LiftHeight;
|
|
public bool IsVirtualLayerMode => _raiseMode is TimelapseRaiseMode.VirtualLayer;
|
|
|
|
/// <summary>
|
|
/// Sets or gets the Z position to raise to
|
|
/// </summary>
|
|
public decimal RaisePositionZ
|
|
{
|
|
get => _raisePositionZ;
|
|
set => RaiseAndSetIfChanged(ref _raisePositionZ, Layer.RoundHeight(Math.Clamp(value, 10, 10000)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// True to output a dummy pixel on bounding rectangle position to avoid empty layer and blank image, otherwise set to false
|
|
/// </summary>
|
|
public bool OutputDummyPixel
|
|
{
|
|
get => _outputDummyPixel;
|
|
set => RaiseAndSetIfChanged(ref _outputDummyPixel, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the alternating height in millimeters to raise when, it will raise only at each defined millimeters and skip the same next millimeters
|
|
/// </summary>
|
|
public decimal RaiseEachNthHeight
|
|
{
|
|
get => _raiseEachNthHeight;
|
|
set
|
|
{
|
|
if(!RaiseAndSetIfChanged(ref _raiseEachNthHeight, Math.Max(0, Layer.RoundHeight(value)))) return;
|
|
RaisePropertyChanged(nameof(RaiseEachNthLayers));
|
|
RaisePropertyChanged(nameof(NumberOfLifts));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the alternating layer count to raise when, it will raise only at each defined layers and skip the same next layers
|
|
/// </summary>
|
|
public ushort RaiseEachNthLayers
|
|
{
|
|
get
|
|
{
|
|
if (_raiseEachNthHeight == 0) return 1;
|
|
return (ushort)Math.Max(1, _raiseEachNthHeight / (decimal)SlicerFile.LayerHeight);
|
|
}
|
|
set => RaiseEachNthHeight = (decimal)SlicerFile.LayerHeight * value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of additional lifts
|
|
/// </summary>
|
|
public uint NumberOfLifts => (uint)(Math.Min(SlicerFile.PrintHeight, (float)_raisePositionZ) / Math.Max(SlicerFile.LayerHeight, (float)_raiseEachNthHeight))
|
|
+ (_ensureLastLayer && _raiseMode == TimelapseRaiseMode.VirtualLayer ? 1u : 0);
|
|
|
|
public decimal WaitTimeAfterLift
|
|
{
|
|
get => _waitTimeAfterLift;
|
|
set => RaiseAndSetIfChanged(ref _waitTimeAfterLift, Math.Max(0, value));
|
|
}
|
|
|
|
public decimal ExposureTime
|
|
{
|
|
get => _exposureTime;
|
|
set => RaiseAndSetIfChanged(ref _exposureTime, Math.Max(0, value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets if last layer must be ensured in the sequence,
|
|
/// If true, it will generate an obligatory additional layer to cover the last layer
|
|
/// </summary>
|
|
public bool EnsureLastLayer
|
|
{
|
|
get => _ensureLastLayer;
|
|
set
|
|
{
|
|
if(!RaiseAndSetIfChanged(ref _ensureLastLayer, value)) return;
|
|
RaisePropertyChanged(nameof(NumberOfLifts));
|
|
}
|
|
}
|
|
|
|
public bool UseCustomLift
|
|
{
|
|
get => _useCustomLift;
|
|
set => RaiseAndSetIfChanged(ref _useCustomLift, value);
|
|
}
|
|
|
|
public bool CanUseCustomLift => _useCustomLift && SlicerFile.CanUseLayerLiftHeight;
|
|
|
|
public decimal SlowLiftHeight
|
|
{
|
|
get => _slowLiftHeight;
|
|
set => RaiseAndSetIfChanged(ref _slowLiftHeight, Math.Max(0, value));
|
|
}
|
|
|
|
public decimal LiftSpeed
|
|
{
|
|
get => _liftSpeed;
|
|
set => RaiseAndSetIfChanged(ref _liftSpeed, Math.Max(0, value));
|
|
}
|
|
|
|
public decimal LiftSpeed2
|
|
{
|
|
get => _liftSpeed2;
|
|
set => RaiseAndSetIfChanged(ref _liftSpeed2, Math.Max(0, value));
|
|
}
|
|
|
|
public decimal SlowRetractHeight
|
|
{
|
|
get => _slowRetractHeight;
|
|
set => RaiseAndSetIfChanged(ref _slowRetractHeight, Math.Max(0, value));
|
|
}
|
|
|
|
public decimal RetractSpeed
|
|
{
|
|
get => _retractSpeed;
|
|
set => RaiseAndSetIfChanged(ref _retractSpeed, Math.Max(0, value));
|
|
}
|
|
|
|
public decimal RetractSpeed2
|
|
{
|
|
get => _retractSpeed2;
|
|
set => RaiseAndSetIfChanged(ref _retractSpeed2, Math.Max(0, value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public OperationTimelapse() { }
|
|
|
|
public OperationTimelapse(FileFormat slicerFile) : base(slicerFile)
|
|
{
|
|
if (_raisePositionZ <= 0) _raisePositionZ = (decimal)SlicerFile.MachineZ;
|
|
if(_exposureTime <= 0) _exposureTime = SlicerFile.SupportsGCode ? 0 : 0.05M;
|
|
|
|
if (_liftSpeed <= 0) _liftSpeed = (decimal) SlicerFile.LiftSpeed;
|
|
if (_liftSpeed2 <= 0) _liftSpeed2 = (decimal) SlicerFile.MaximumSpeed;
|
|
|
|
if (SlicerFile.CanUseLayerRetractSpeed2) // TSMC
|
|
{
|
|
if (_retractSpeed <= 0) _retractSpeed = (decimal)SlicerFile.MaximumSpeed;
|
|
if (_retractSpeed2 <= 0) _retractSpeed2 = (decimal)SlicerFile.RetractSpeed2;
|
|
}
|
|
else
|
|
{
|
|
if (_retractSpeed <= 0) _retractSpeed = (decimal)SlicerFile.RetractSpeed;
|
|
if (_retractSpeed2 <= 0) _retractSpeed2 = (decimal)SlicerFile.MaximumSpeed;
|
|
}
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Equality
|
|
|
|
protected bool Equals(OperationTimelapse other)
|
|
{
|
|
return _raisePositionZ == other._raisePositionZ && _outputDummyPixel == other._outputDummyPixel && _raiseEachNthHeight == other._raiseEachNthHeight && _raiseMode == other._raiseMode && _waitTimeAfterLift == other._waitTimeAfterLift && _exposureTime == other._exposureTime && _ensureLastLayer == other._ensureLastLayer && _useCustomLift == other._useCustomLift && _slowLiftHeight == other._slowLiftHeight && _liftSpeed == other._liftSpeed && _liftSpeed2 == other._liftSpeed2 && _slowRetractHeight == other._slowRetractHeight && _retractSpeed == other._retractSpeed && _retractSpeed2 == other._retractSpeed2;
|
|
}
|
|
|
|
public override bool Equals(object? obj)
|
|
{
|
|
if (ReferenceEquals(null, obj)) return false;
|
|
if (ReferenceEquals(this, obj)) return true;
|
|
if (obj.GetType() != this.GetType()) return false;
|
|
return Equals((OperationTimelapse) obj);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
var hashCode = new HashCode();
|
|
hashCode.Add(_raisePositionZ);
|
|
hashCode.Add(_outputDummyPixel);
|
|
hashCode.Add(_raiseEachNthHeight);
|
|
hashCode.Add((int) _raiseMode);
|
|
hashCode.Add(_waitTimeAfterLift);
|
|
hashCode.Add(_exposureTime);
|
|
hashCode.Add(_ensureLastLayer);
|
|
hashCode.Add(_useCustomLift);
|
|
hashCode.Add(_slowLiftHeight);
|
|
hashCode.Add(_liftSpeed);
|
|
hashCode.Add(_liftSpeed2);
|
|
hashCode.Add(_slowRetractHeight);
|
|
hashCode.Add(_retractSpeed);
|
|
hashCode.Add(_retractSpeed2);
|
|
return hashCode.ToHashCode();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
public void OptimizeRaisePositionZ()
|
|
{
|
|
RaisePositionZ = (decimal) Math.Min(SlicerFile.MachineZ, SlicerFile.PrintHeight + 5);
|
|
}
|
|
|
|
public void MaxRaisePositionZ()
|
|
{
|
|
RaisePositionZ = (decimal) SlicerFile.MachineZ;
|
|
}
|
|
|
|
protected override bool ExecuteInternally(OperationProgress progress)
|
|
{
|
|
var virtualLayers = new List<uint>();
|
|
float checkpointHeight = SlicerFile[0].PositionZ;
|
|
|
|
for (uint layerIndex = LayerIndexStart; layerIndex <= LayerIndexEnd; layerIndex++)
|
|
{
|
|
progress++;
|
|
var layer = SlicerFile[layerIndex];
|
|
if ((decimal)layer.PositionZ >= _raisePositionZ) break; // pass the target height, do not continue
|
|
if (_raiseMode == TimelapseRaiseMode.VirtualLayer && _ensureLastLayer && layerIndex == LayerIndexEnd)
|
|
{
|
|
virtualLayers.Add(layerIndex+1);
|
|
break;
|
|
}
|
|
if (_raiseEachNthHeight > 0 && (decimal)Layer.RoundHeight(layer.PositionZ - checkpointHeight) < _raiseEachNthHeight) continue;
|
|
checkpointHeight = layer.PositionZ;
|
|
|
|
switch (_raiseMode)
|
|
{
|
|
case TimelapseRaiseMode.LiftHeight:
|
|
if (CanUseCustomLift)
|
|
{
|
|
layer.LiftSpeed = (float)_liftSpeed;
|
|
layer.RetractSpeed = (float)_retractSpeed;
|
|
|
|
if (SlicerFile.CanUseLayerLiftHeight2)
|
|
{
|
|
layer.LiftHeight = (float)_slowLiftHeight;
|
|
}
|
|
|
|
if (SlicerFile.CanUseLayerLiftSpeed2)
|
|
{
|
|
layer.LiftSpeed2 = (float)_liftSpeed2;
|
|
}
|
|
|
|
if (SlicerFile.CanUseLayerRetractHeight2)
|
|
{
|
|
layer.RetractHeight2 = (float)_slowRetractHeight;
|
|
}
|
|
|
|
if (SlicerFile.CanUseLayerRetractSpeed2)
|
|
{
|
|
layer.RetractSpeed2 = (float)_retractSpeed2;
|
|
}
|
|
}
|
|
|
|
if (SlicerFile.CanUseLayerLiftHeight2 && (layer.LiftHeight2 > 0 || CanUseCustomLift && _slowLiftHeight > 0)) // TSMC
|
|
{
|
|
layer.LiftHeight2 = Math.Max(0, (float)_raisePositionZ - layer.PositionZ - layer.LiftHeight);
|
|
}
|
|
else
|
|
{
|
|
layer.LiftHeightTotal = Math.Max(layer.LiftHeightTotal, (float)_raisePositionZ - layer.PositionZ);
|
|
}
|
|
|
|
if (SlicerFile.CanUseLayerWaitTimeAfterLift && _waitTimeAfterLift > 0)
|
|
{
|
|
layer.WaitTimeAfterLift = (float) _waitTimeAfterLift;
|
|
}
|
|
|
|
break;
|
|
case TimelapseRaiseMode.VirtualLayer:
|
|
virtualLayers.Add(layerIndex);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(RaiseMode));
|
|
}
|
|
}
|
|
|
|
if (virtualLayers.Count > 0 && _raiseMode == TimelapseRaiseMode.VirtualLayer)
|
|
{
|
|
var minLiftSpeed = CanUseCustomLift ? (float)Math.Min(_liftSpeed, _liftSpeed2) : SlicerFile.MinimumNormalSpeed;
|
|
var minRetractSpeed = CanUseCustomLift ? (float)Math.Min(_retractSpeed, _retractSpeed2) : SlicerFile.MinimumNormalSpeed;
|
|
var maxLiftSpeed = CanUseCustomLift ? (float)Math.Max(_liftSpeed, _liftSpeed2) : SlicerFile.MaximumSpeed;
|
|
var maxRetractSpeed = CanUseCustomLift ? (float)Math.Max(_retractSpeed, _retractSpeed2) : SlicerFile.MaximumSpeed;
|
|
|
|
using var mat = _outputDummyPixel
|
|
? SlicerFile.CreateMatWithDummyPixel()
|
|
: SlicerFile.CreateMat();
|
|
|
|
var virtualSlowLiftLayer = new Layer(SlicerFile.LayerCount, mat, SlicerFile)
|
|
{
|
|
PositionZ = (float)_raisePositionZ,
|
|
ExposureTime = SlicerFile.SupportsGCode ? 0 : 0.01f,
|
|
LiftHeightTotal = SlicerFile.SupportsGCode ? 0 : 0.1f,
|
|
LiftSpeed = minLiftSpeed,
|
|
LiftSpeed2 = maxLiftSpeed,
|
|
RetractSpeed = maxRetractSpeed,
|
|
RetractSpeed2 = maxRetractSpeed,
|
|
RetractHeight2 = 0
|
|
};
|
|
|
|
virtualSlowLiftLayer.SetNoDelays();
|
|
|
|
var virtualPhotoLayer = virtualSlowLiftLayer.Clone();
|
|
virtualPhotoLayer.ExposureTime = (float)_exposureTime;
|
|
virtualPhotoLayer.LiftSpeed = maxLiftSpeed;
|
|
|
|
virtualSlowLiftLayer.LightPWM = 0; // Disable light power if possible
|
|
|
|
var slowLiftHeight = CanUseCustomLift ? (float)_slowLiftHeight : SlicerFile.LiftHeight;
|
|
|
|
var layers = SlicerFile.ToList();
|
|
uint insertedLayers = 0;
|
|
foreach (var insertIndex in virtualLayers)
|
|
{
|
|
if (insertIndex < LayerIndexEnd)
|
|
{
|
|
// Replace lift with retract
|
|
if (SlicerFile.CanUseLayerRetractHeight2 && SlicerFile[insertIndex].RetractHeight2 > 0)
|
|
{
|
|
SlicerFile[insertIndex].LiftHeightTotal = SlicerFile[insertIndex].RetractHeight2;
|
|
}
|
|
|
|
SlicerFile[insertIndex].RetractHeight2 = 0;
|
|
SlicerFile[insertIndex].LiftSpeed = maxRetractSpeed;
|
|
|
|
if (CanUseCustomLift)
|
|
{
|
|
SlicerFile[insertIndex].LiftHeight = (float) _slowRetractHeight;
|
|
SlicerFile[insertIndex].RetractSpeed = minRetractSpeed;
|
|
|
|
if (SlicerFile.CanUseLayerLiftSpeed2)
|
|
{
|
|
SlicerFile[insertIndex].LiftSpeed2 = maxLiftSpeed;
|
|
}
|
|
|
|
if (SlicerFile.CanUseLayerRetractSpeed2)
|
|
{
|
|
SlicerFile[insertIndex].RetractSpeed2 = maxRetractSpeed;
|
|
}
|
|
}
|
|
}
|
|
|
|
layers.Insert((int)(insertIndex + insertedLayers), virtualPhotoLayer.Clone());
|
|
|
|
if (slowLiftHeight > 0 && insertIndex > 0)
|
|
{
|
|
virtualSlowLiftLayer.PositionZ = SlicerFile[insertIndex - 1].PositionZ + slowLiftHeight;
|
|
if (virtualSlowLiftLayer.PositionZ >= virtualPhotoLayer.PositionZ)
|
|
{
|
|
// Slow lift layer must be lower than photo layer, break this insertion from now on
|
|
slowLiftHeight = 0;
|
|
virtualPhotoLayer.LiftSpeed = minLiftSpeed;
|
|
continue;
|
|
}
|
|
layers.Insert((int)(insertIndex + insertedLayers), virtualSlowLiftLayer.Clone());
|
|
insertedLayers++;
|
|
}
|
|
|
|
insertedLayers++;
|
|
}
|
|
|
|
SlicerFile.SuppressRebuildPropertiesWork(() => SlicerFile.Layers = layers.ToArray());
|
|
}
|
|
|
|
return !progress.Token.IsCancellationRequested;
|
|
}
|
|
|
|
#endregion
|
|
} |