diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-09-04 00:21:41 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-09-04 00:21:41 +0300 |
commit | 6a2a52ebcb3bcd98476d13f24d562383fc8756b4 (patch) | |
tree | 2cac7b6c41b1e8f597da980cd5ebf91e33751021 | |
parent | 774fe9d5d096aa2922f2219ddfeeb18c925530de (diff) |
v2.21.0v2.21.0
- **UI:**
- **Menu:**
- (Add) File - Open recent: Open any recent open file from a list
Shift + Click: Open file in a new window
Shift + Ctrl + Click: Remove file from recent list
Ctrl + Click: Purge non-existing files
- (Add) File - Send to: Copy the file directly to a removable drive (Windows only)
- **(Add) Layer navigation buttons:**
- SB: Navigate to the smallest bottom layer in mass
- LB: Navigate to the largest bottom layer in mass
- SN: Navigate to the smallest normal layer in mass
- LN: Navigate to the largest normal layer in mass
- (Add) Layer outline - Distance detection: Calculates the distance to the closest zero pixel for each pixel
- **Tools:**
- **Dynamic Lifts:**
- (Improvement) Select normal layers by default
- (Improvement) Hide light-off delay fields when the file format don't support them
- (Fix) Light-off delay fields was not hidding when set a mode that dont require the extra time fields
- **Exposure time finder:**
- (Fix) Fix the 'light-off delay' field not being show on files that support wait time before cure
- (Change) Field name 'Light-off delay' to 'Wait time before cure'
- (Add) Fade exposure time: The double exposure method clones the selected layer range and print the same layer twice with different exposure times and strategies
- (Add) Double exposure: The double exposure method clones the selected layer range and print the same layer twice with different exposure times and strategies
- (Add) Clone layers: Option to keep the same z position for the cloned layers instead of rebuild model height
- (Improvement) The layer range selector for normal and bottom layers now selects the correct range based on IsBottom property rather than layer index
- (Fix) The layer range selector was setting a very high last layer index when bottom layer count is 0
- (Fix) Pixel arithmetic: Threshold types "Otsu" and "Triangle" are flags to combine with other types, it will auto append the "Binnary" type
- (Add) Support for Encrypted CTB (read-only)
35 files changed, 1803 insertions, 139 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d34147..c921ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 03/09/2021 - v2.21.0 + +- **UI:** + - **Menu:** + - (Add) File - Open recent: Open any recent open file from a list + Shift + Click: Open file in a new window + Shift + Ctrl + Click: Remove file from recent list + Ctrl + Click: Purge non-existing files + - (Add) File - Send to: Copy the file directly to a removable drive (Windows only) + - **(Add) Layer navigation buttons:** + - SB: Navigate to the smallest bottom layer in mass + - LB: Navigate to the largest bottom layer in mass + - SN: Navigate to the smallest normal layer in mass + - LN: Navigate to the largest normal layer in mass + - (Add) Layer outline - Distance detection: Calculates the distance to the closest zero pixel for each pixel +- **Tools:** + - **Dynamic Lifts:** + - (Improvement) Select normal layers by default + - (Improvement) Hide light-off delay fields when the file format don't support them + - (Fix) Light-off delay fields was not hidding when set a mode that dont require the extra time fields + - **Exposure time finder:** + - (Fix) Fix the 'light-off delay' field not being show on files that support wait time before cure + - (Change) Field name 'Light-off delay' to 'Wait time before cure' + - (Add) Fade exposure time: The double exposure method clones the selected layer range and print the same layer twice with different exposure times and strategies + - (Add) Double exposure: The double exposure method clones the selected layer range and print the same layer twice with different exposure times and strategies + - (Add) Clone layers: Option to keep the same z position for the cloned layers instead of rebuild model height + - (Improvement) The layer range selector for normal and bottom layers now selects the correct range based on IsBottom property rather than layer index + - (Fix) The layer range selector was setting a very high last layer index when bottom layer count is 0 + - (Fix) Pixel arithmetic: Threshold types "Otsu" and "Triangle" are flags to combine with other types, it will auto append the "Binnary" type +- (Add) Support for Encrypted CTB (read-only) + ## 31/08/2021 - v2.20.5 - (Add) Setting - Max degree of parallelism: Sets the maximum number of concurrent tasks/threads/operations enabled to run by parallel method calls. diff --git a/UVtools.Core/FileFormats/CTBEncryptedFile.cs b/UVtools.Core/FileFormats/CTBEncryptedFile.cs index e37bf8c..39321da 100644 --- a/UVtools.Core/FileFormats/CTBEncryptedFile.cs +++ b/UVtools.Core/FileFormats/CTBEncryptedFile.cs @@ -35,11 +35,14 @@ namespace UVtools.Core.FileFormats public const byte HASH_LENGTH = 32; public const uint LAYER_XOR_KEY = 0xEFBEADDE; + public const string Secret0 = "XxUBHR0JHSE6DU8YCVMxORpIG0wSOTobGE8KGjkzVBwOGhZ6MxoMAAgWeiQRDB0JEiE/GwFPAx11JgEdHwMAMHYbAU8YGzwlVAoBDwEsJgAKC0wVPDoRTwkDATg3AEFlOxZ1NwYKTw0UND8aHBtMHToiVB8KHh48IgAKC0wGJjMGTwsNBzR2EQEMHgolIh0AAUBTOTkXBBxAUzY5GhwbHhI8OAdDTx4WJiIGBgwYGjo4B0NPARw7OQQAAwUJNCIdAAFMEjsyVAEAAl4mMxocCkwDOjodDAYJAHUiHA4bTAMnMwIKARgAdTkABwoeAHUwBgACTBAnMxUbCkwSOzJUAwoNF3gwGx0YDQExdgcAAxkHPDkaHE8NATojGgtPGBY2PhoAAwMULHh+KRoAH3UlAR8fAwEhPxoITxgbPCVUCQYAFnUwGx0CDQd1IRsaAwhTNzNUHBsJA3g0FQwETBU6JFRcK0wHMDUcAQAAHDIvVA4BCFMhPhFPDAMeOCMaBhsVUzogER0OAB97dicbBgAfeXYDCk8NHzk5A08bA1MnMxULTxgbMHYSBgMJUzM5Bk8dCQU8MwNDTx4WNjkCCh1MFzQiFU8OAhd1MhEbCg8HdSYGAA0AFjglVBsATB40PRFPFgMGdTdUDQYYUzg5BgpPDxwjMwYKC0wVJzkZTwIFACE3HwocQnkFOhEOHAlTODcfCk8VHCAkVBwHBRUhdhIdAAFTIT4dHE8cAToyAQwbH1M0OBBPBwkfJXYABwpMQBF2AAoMBB06OhsIFkwUOnYSAB0bEicyVA4BCFM6JhEBTmY="; public const string Secret1 = "hQ36XB6yTk+zO02ysyiowt8yC1buK+nbLWyfY40EXoU="; public const string Secret2 = "Wld+ampndVJecmVjYH5cWQ=="; + - public static readonly byte[] Bigfoot = new byte[32]; - public static readonly byte[] CookieMonster = new byte[16]; + public static readonly string Preamble = CryptExtensions.XORCipherString(System.Convert.FromBase64String(Secret0), About.Software); + public static byte[] Bigfoot = CryptExtensions.XORCipher(System.Convert.FromBase64String(Secret1), About.Software); + public static byte[] CookieMonster = CryptExtensions.XORCipher(System.Convert.FromBase64String(Secret2), About.Software); #endregion @@ -576,7 +579,11 @@ namespace UVtools.Core.FileFormats public override FileFormatType FileType => FileFormatType.Binary; public override FileExtension[] FileExtensions { get; } = { - new(typeof(CTBEncryptedFile), "ctb", "Chitubox CTB (Encrypted)"), + new(typeof(CTBEncryptedFile), "ctb", "Chitubox CTB (Encrypted)", true +#if !DEBUG + , false +#endif + ), new(typeof(CTBEncryptedFile), "encrypted.ctb", "Chitubox CTB (Encrypted)", false, false), }; @@ -1042,12 +1049,12 @@ namespace UVtools.Core.FileFormats { Previews = new Preview[ThumbnailsCount]; - if (Bigfoot is not null && Bigfoot[0] == 0 && File.Exists("MAGIC.ectb")) + /*if (Bigfoot is not null && Bigfoot[0] == 0 && File.Exists("MAGIC.ectb")) { using var fs = new FileStream("MAGIC.ectb", FileMode.Open); fs.ReadBytes(Bigfoot); fs.ReadBytes(CookieMonster); - } + }*/ } #endregion @@ -1253,7 +1260,7 @@ namespace UVtools.Core.FileFormats { throw new FileLoadException( "Unable to load this file due to an Chitubox bug and the impossibility to auto correct some of these layers.\n" + - "Please increase the portion of the plate in use and reslice the file."); + "Please increase the portion of the plate in use and re-slice the file."); } } //inputFile.ReadBytes(HashLength); @@ -1261,6 +1268,10 @@ namespace UVtools.Core.FileFormats protected override void EncodeInternally(string fileFullPath, OperationProgress progress) { +#if !DEBUG + throw new NotSupportedException(Preamble); +#endif + using var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write); //uint currentOffset = 0; @@ -1293,7 +1304,9 @@ namespace UVtools.Core.FileFormats }; var previewBytes = preview.Encode(image); - +#if !DEBUG + Bigfoot = new byte[32]; CookieMonster = new byte[16]; +#endif if (previewBytes.Length == 0) continue; if (i == 0) @@ -1345,6 +1358,9 @@ namespace UVtools.Core.FileFormats progress.LockAndIncrement(); }); +#if !DEBUG + Bigfoot = new byte[32]; CookieMonster = new byte[16]; +#endif progress.Reset(OperationProgress.StatusWritingFile, LayerCount); for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 6163492..4dd3ea8 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -278,9 +278,7 @@ namespace UVtools.Core.FileFormats new SL1File(), // Prusa SL1 new ChituboxZipFile(), // Zip new ChituboxFile(), // cbddlp, cbt, photon -#if DEBUG new CTBEncryptedFile(), // encrypted ctb -#endif new PhotonSFile(), // photons new PHZFile(), // phz new FDGFile(), // fdg @@ -2497,6 +2495,13 @@ namespace UVtools.Core.FileFormats progress ??= new OperationProgress(); progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); +#if !DEBUG + if (this is CTBEncryptedFile) + { + throw new NotSupportedException(CTBEncryptedFile.Preamble); + } +#endif + _layerManager.Sanitize(); FileFullPath = fileFullPath; diff --git a/UVtools.Core/Layer/Layer.cs b/UVtools.Core/Layer/Layer.cs index 77bd7ec..f2cec59 100644 --- a/UVtools.Core/Layer/Layer.cs +++ b/UVtools.Core/Layer/Layer.cs @@ -740,6 +740,29 @@ namespace UVtools.Core } /// <summary> + /// Attempt to set wait time before cure if supported, otherwise fallback to light-off delay + /// </summary> + /// <param name="time">The time to set</param> + /// <param name="zeroLightOffDelayCalculateBase">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</param> + public void SetWaitTimeBeforeCureOrLightOffDelay(float time = 0, bool zeroLightOffDelayCalculateBase = false) + { + if (SlicerFile.CanUseLayerWaitTimeBeforeCure) + { + LightOffDelay = 0; + WaitTimeBeforeCure = time; + } + else + { + if (time == 0 && !zeroLightOffDelayCalculateBase) + { + LightOffDelay = 0; + return; + } + SetLightOffDelay(time); + } + } + + /// <summary> /// Zero all 'wait times / delays' for this layer /// </summary> public void SetNoDelays() diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index 9f52000..6560de9 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -106,6 +106,11 @@ namespace UVtools.Core.Operations } /// <summary> + /// Gets if the LayerIndexEnd selector is enabled + /// </summary> + public virtual bool LayerIndexEndEnabled => true; + + /// <summary> /// Gets if this operation should set layer range to the actual layer index on layer preview /// </summary> public virtual bool PassActualLayerIndex => false; @@ -173,8 +178,13 @@ namespace UVtools.Core.Operations get => _layerIndexStart; set { - if(!RaiseAndSetIfChanged(ref _layerIndexStart, value)) return; + if (SlicerFile is not null) + { + value = Math.Min(value, SlicerFile.LastLayerIndex); + } + if (!RaiseAndSetIfChanged(ref _layerIndexStart, value)) return; RaisePropertyChanged(nameof(LayerRangeCount)); + RaisePropertyChanged(nameof(LayerRangeHaveBottoms)); } } @@ -186,11 +196,19 @@ namespace UVtools.Core.Operations get => _layerIndexEnd; set { + if (SlicerFile is not null) + { + value = Math.Min(value, SlicerFile.LastLayerIndex); + } if(!RaiseAndSetIfChanged(ref _layerIndexEnd, value)) return; RaisePropertyChanged(nameof(LayerRangeCount)); + RaisePropertyChanged(nameof(LayerRangeHaveNormals)); } } + public bool LayerRangeHaveBottoms => LayerIndexStart < (SlicerFile.FirstNormalLayer?.Index ?? 0); + public bool LayerRangeHaveNormals => LayerIndexEnd >= (SlicerFile.FirstNormalLayer?.Index ?? 0); + public uint LayerRangeCount => LayerIndexEnd - LayerIndexStart + 1; /// <summary> @@ -318,13 +336,13 @@ namespace UVtools.Core.Operations public void SelectBottomLayers() { LayerIndexStart = 0; - LayerIndexEnd = SlicerFile.BottomLayerCount - 1u; + LayerIndexEnd = Math.Max(1, SlicerFile.FirstNormalLayer?.Index ?? 1) - 1u; LayerRangeSelection = Enumerations.LayerRangeSelection.Bottom; } public void SelectNormalLayers() { - LayerIndexStart = SlicerFile.BottomLayerCount; + LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; LayerIndexEnd = SlicerFile.LastLayerIndex; LayerRangeSelection = Enumerations.LayerRangeSelection.Normal; } diff --git a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs index b400c6a..59f2450 100644 --- a/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs +++ b/UVtools.Core/Operations/OperationCalibrateElephantFoot.cs @@ -425,7 +425,7 @@ namespace UVtools.Core.Operations /// <returns></returns> public Mat[] GetLayers() { - Mat[] layers = new Mat[3]; + var layers = new Mat[3]; var anchor = new Point(-1, -1); var kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), anchor); diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index b69a64d..d199840 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -168,8 +168,8 @@ namespace UVtools.Core.Operations private bool _differentSettingsForSamePositionedLayers; private bool _samePositionedLayersLiftHeightEnabled = true; private decimal _samePositionedLayersLiftHeight; - private bool _samePositionedLayersLightOffDelayEnabled = true; - private decimal _samePositionedLayersLightOffDelay; + private bool _samePositionedLayersWaitTimeBeforeCureEnabled = true; + private decimal _samePositionedLayersWaitTimeBeforeCure; private bool _multipleExposures; private CalibrateExposureFinderExposureGenTypes _exposureGenType = CalibrateExposureFinderExposureGenTypes.Linear; private bool _exposureGenIgnoreBaseExposure; @@ -907,16 +907,16 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _samePositionedLayersLiftHeight, Math.Round(value, 2)); } - public bool SamePositionedLayersLightOffDelayEnabled + public bool SamePositionedLayersWaitTimeBeforeCureEnabled { - get => _samePositionedLayersLightOffDelayEnabled; - set => RaiseAndSetIfChanged(ref _samePositionedLayersLightOffDelayEnabled, value); + get => _samePositionedLayersWaitTimeBeforeCureEnabled; + set => RaiseAndSetIfChanged(ref _samePositionedLayersWaitTimeBeforeCureEnabled, value); } - public decimal SamePositionedLayersLightOffDelay + public decimal SamePositionedLayersWaitTimeBeforeCure { - get => _samePositionedLayersLightOffDelay; - set => RaiseAndSetIfChanged(ref _samePositionedLayersLightOffDelay, Math.Round(value, 2)); + get => _samePositionedLayersWaitTimeBeforeCure; + set => RaiseAndSetIfChanged(ref _samePositionedLayersWaitTimeBeforeCure, Math.Round(value, 2)); } public bool MultipleExposures @@ -1140,12 +1140,12 @@ namespace UVtools.Core.Operations if (SlicerFile.SupportsGCode) { _samePositionedLayersLiftHeight = 0; - _samePositionedLayersLightOffDelay = 2; + _samePositionedLayersWaitTimeBeforeCure = 2; } else { _samePositionedLayersLiftHeight = 0.1m; - _samePositionedLayersLightOffDelay = 0; + _samePositionedLayersWaitTimeBeforeCure = 0; } } @@ -1176,7 +1176,7 @@ namespace UVtools.Core.Operations if (_multipleExposuresBaseLayersCustomExposure <= 0) _multipleExposuresBaseLayersCustomExposure = (decimal)SlicerFile.ExposureTime; - if (!SlicerFile.HaveLayerParameterModifier(FileFormat.PrintParameterModifier.ExposureTime)) + if (!SlicerFile.CanUseLayerExposureTime) { _multipleLayerHeight = false; _multipleExposures = false; @@ -1197,7 +1197,7 @@ namespace UVtools.Core.Operations private bool Equals(OperationCalibrateExposureFinder other) { - return _displayWidth == other._displayWidth && _displayHeight == other._displayHeight && _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _chamferLayers == other._chamferLayers && _erodeBottomIterations == other._erodeBottomIterations && _partMargin == other._partMargin && _enableAntiAliasing == other._enableAntiAliasing && _mirrorOutput == other._mirrorOutput && _baseHeight == other._baseHeight && _featuresHeight == other._featuresHeight && _featuresMargin == other._featuresMargin && _staircaseThicknessPx == other._staircaseThicknessPx && _holesEnabled == other._holesEnabled && _holeShape == other._holeShape && _unitOfMeasure == other._unitOfMeasure && _holeDiametersPx == other._holeDiametersPx && _holeDiametersMm == other._holeDiametersMm && _barsEnabled == other._barsEnabled && _barSpacing == other._barSpacing && _barLength == other._barLength && _barVerticalSplitter == other._barVerticalSplitter && _barFenceThickness == other._barFenceThickness && _barFenceOffset == other._barFenceOffset && _barThicknessesPx == other._barThicknessesPx && _barThicknessesMm == other._barThicknessesMm && _textEnabled == other._textEnabled && _textFont == other._textFont && _textScale.Equals(other._textScale) && _textThickness == other._textThickness && _text == other._text && _multipleBrightness == other._multipleBrightness && _multipleBrightnessExcludeFrom == other._multipleBrightnessExcludeFrom && _multipleBrightnessValues == other._multipleBrightnessValues && _multipleBrightnessGenExposureTime == other._multipleBrightnessGenExposureTime && _multipleBrightnessGenEmulatedAALevel == other._multipleBrightnessGenEmulatedAALevel && _multipleBrightnessGenExposureFractions == other._multipleBrightnessGenExposureFractions && _multipleLayerHeight == other._multipleLayerHeight && _multipleLayerHeightMaximum == other._multipleLayerHeightMaximum && _multipleLayerHeightStep == other._multipleLayerHeightStep && _multipleExposuresBaseLayersPrintMode == other._multipleExposuresBaseLayersPrintMode && _multipleExposuresBaseLayersCustomExposure == other._multipleExposuresBaseLayersCustomExposure && _differentSettingsForSamePositionedLayers == other._differentSettingsForSamePositionedLayers && _samePositionedLayersLiftHeightEnabled == other._samePositionedLayersLiftHeightEnabled && _samePositionedLayersLiftHeight == other._samePositionedLayersLiftHeight && _samePositionedLayersLightOffDelayEnabled == other._samePositionedLayersLightOffDelayEnabled && _samePositionedLayersLightOffDelay == other._samePositionedLayersLightOffDelay && _multipleExposures == other._multipleExposures && _exposureGenType == other._exposureGenType && _exposureGenIgnoreBaseExposure == other._exposureGenIgnoreBaseExposure && _exposureGenBottomStep == other._exposureGenBottomStep && _exposureGenNormalStep == other._exposureGenNormalStep && _exposureGenTests == other._exposureGenTests && _exposureGenManualLayerHeight == other._exposureGenManualLayerHeight && _exposureGenManualBottom == other._exposureGenManualBottom && _exposureGenManualNormal == other._exposureGenManualNormal && Equals(_exposureTable, other._exposureTable) && _bullsEyeEnabled == other._bullsEyeEnabled && _bullsEyeConfigurationPx == other._bullsEyeConfigurationPx && _bullsEyeConfigurationMm == other._bullsEyeConfigurationMm && _bullsEyeInvertQuadrants == other._bullsEyeInvertQuadrants && _counterTrianglesEnabled == other._counterTrianglesEnabled && _counterTrianglesTipOffset == other._counterTrianglesTipOffset && _counterTrianglesFence == other._counterTrianglesFence && _patternModel == other._patternModel && _bullsEyeFenceThickness == other._bullsEyeFenceThickness && _bullsEyeFenceOffset == other._bullsEyeFenceOffset && _patternModelGlueBottomLayers == other._patternModelGlueBottomLayers; + return _displayWidth == other._displayWidth && _displayHeight == other._displayHeight && _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _chamferLayers == other._chamferLayers && _erodeBottomIterations == other._erodeBottomIterations && _partMargin == other._partMargin && _enableAntiAliasing == other._enableAntiAliasing && _mirrorOutput == other._mirrorOutput && _baseHeight == other._baseHeight && _featuresHeight == other._featuresHeight && _featuresMargin == other._featuresMargin && _staircaseThicknessPx == other._staircaseThicknessPx && _holesEnabled == other._holesEnabled && _holeShape == other._holeShape && _unitOfMeasure == other._unitOfMeasure && _holeDiametersPx == other._holeDiametersPx && _holeDiametersMm == other._holeDiametersMm && _barsEnabled == other._barsEnabled && _barSpacing == other._barSpacing && _barLength == other._barLength && _barVerticalSplitter == other._barVerticalSplitter && _barFenceThickness == other._barFenceThickness && _barFenceOffset == other._barFenceOffset && _barThicknessesPx == other._barThicknessesPx && _barThicknessesMm == other._barThicknessesMm && _textEnabled == other._textEnabled && _textFont == other._textFont && _textScale.Equals(other._textScale) && _textThickness == other._textThickness && _text == other._text && _multipleBrightness == other._multipleBrightness && _multipleBrightnessExcludeFrom == other._multipleBrightnessExcludeFrom && _multipleBrightnessValues == other._multipleBrightnessValues && _multipleBrightnessGenExposureTime == other._multipleBrightnessGenExposureTime && _multipleBrightnessGenEmulatedAALevel == other._multipleBrightnessGenEmulatedAALevel && _multipleBrightnessGenExposureFractions == other._multipleBrightnessGenExposureFractions && _multipleLayerHeight == other._multipleLayerHeight && _multipleLayerHeightMaximum == other._multipleLayerHeightMaximum && _multipleLayerHeightStep == other._multipleLayerHeightStep && _multipleExposuresBaseLayersPrintMode == other._multipleExposuresBaseLayersPrintMode && _multipleExposuresBaseLayersCustomExposure == other._multipleExposuresBaseLayersCustomExposure && _differentSettingsForSamePositionedLayers == other._differentSettingsForSamePositionedLayers && _samePositionedLayersLiftHeightEnabled == other._samePositionedLayersLiftHeightEnabled && _samePositionedLayersLiftHeight == other._samePositionedLayersLiftHeight && _samePositionedLayersWaitTimeBeforeCureEnabled == other._samePositionedLayersWaitTimeBeforeCureEnabled && _samePositionedLayersWaitTimeBeforeCure == other._samePositionedLayersWaitTimeBeforeCure && _multipleExposures == other._multipleExposures && _exposureGenType == other._exposureGenType && _exposureGenIgnoreBaseExposure == other._exposureGenIgnoreBaseExposure && _exposureGenBottomStep == other._exposureGenBottomStep && _exposureGenNormalStep == other._exposureGenNormalStep && _exposureGenTests == other._exposureGenTests && _exposureGenManualLayerHeight == other._exposureGenManualLayerHeight && _exposureGenManualBottom == other._exposureGenManualBottom && _exposureGenManualNormal == other._exposureGenManualNormal && Equals(_exposureTable, other._exposureTable) && _bullsEyeEnabled == other._bullsEyeEnabled && _bullsEyeConfigurationPx == other._bullsEyeConfigurationPx && _bullsEyeConfigurationMm == other._bullsEyeConfigurationMm && _bullsEyeInvertQuadrants == other._bullsEyeInvertQuadrants && _counterTrianglesEnabled == other._counterTrianglesEnabled && _counterTrianglesTipOffset == other._counterTrianglesTipOffset && _counterTrianglesFence == other._counterTrianglesFence && _patternModel == other._patternModel && _bullsEyeFenceThickness == other._bullsEyeFenceThickness && _bullsEyeFenceOffset == other._bullsEyeFenceOffset && _patternModelGlueBottomLayers == other._patternModelGlueBottomLayers; } public override bool Equals(object obj) @@ -1257,8 +1257,8 @@ namespace UVtools.Core.Operations hashCode.Add(_differentSettingsForSamePositionedLayers); hashCode.Add(_samePositionedLayersLiftHeightEnabled); hashCode.Add(_samePositionedLayersLiftHeight); - hashCode.Add(_samePositionedLayersLightOffDelayEnabled); - hashCode.Add(_samePositionedLayersLightOffDelay); + hashCode.Add(_samePositionedLayersWaitTimeBeforeCureEnabled); + hashCode.Add(_samePositionedLayersWaitTimeBeforeCure); hashCode.Add(_multipleExposures); hashCode.Add((int) _exposureGenType); hashCode.Add(_exposureGenIgnoreBaseExposure); @@ -2260,7 +2260,7 @@ namespace UVtools.Core.Operations foreach (var layer in layers) { if(_samePositionedLayersLiftHeightEnabled) layer.LiftHeightTotal = (float) _samePositionedLayersLiftHeight; - if(_samePositionedLayersLightOffDelayEnabled) layer.LightOffDelay = (float) _samePositionedLayersLightOffDelay; + if(_samePositionedLayersWaitTimeBeforeCureEnabled) layer.SetWaitTimeBeforeCureOrLightOffDelay((float) _samePositionedLayersWaitTimeBeforeCure); } } diff --git a/UVtools.Core/Operations/OperationDoubleExposure.cs b/UVtools.Core/Operations/OperationDoubleExposure.cs new file mode 100644 index 0000000..5de3e0e --- /dev/null +++ b/UVtools.Core/Operations/OperationDoubleExposure.cs @@ -0,0 +1,389 @@ +/* + * 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.Drawing; +using System.Text; +using System.Threading.Tasks; +using Emgu.CV; +using Emgu.CV.CvEnum; +using UVtools.Core.FileFormats; + +namespace UVtools.Core.Operations +{ + [Serializable] + public class OperationDoubleExposure : Operation + { + #region Members + private decimal _firstBottomExposure; + private decimal _firstNormalExposure; + private decimal _secondBottomExposure; + private decimal _secondNormalExposure; + private byte _firstBottomErodeIterations = 4; + private byte _secondBottomErodeIterations; + private byte _firstNormalErodeIterations = 1; + private byte _secondNormalErodeIterations; + private bool _secondLayerDifference = true; + private byte _secondLayerDifferenceOverlapErodeIterations = 10; + private bool _differentSettingsForSecondLayer; + private bool _secondLayerLiftHeightEnabled = true; + private decimal _secondLayerLiftHeight; + private bool _secondLayerWaitTimeBeforeCureEnabled = true; + private decimal _secondLayerWaitTimeBeforeCure; + + #endregion + + #region Overrides + public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Bottom; + public override string Title => "Double exposure"; + public override string Description => + "The double exposure method clones the selected layer range and print the same layer twice with different exposure times and strategies.\n" + + "Can be used to eliminate the elephant foot effect or to harden a layer in two steps.\n" + + "After this, do not apply any modification which reconstruct the z positions of the layers.\n" + + "Note: To eliminate the elephant foot effect, the use of wall dimming method recommended."; + + public override string ConfirmationText => + $"double exposure model layers {LayerIndexStart} through {LayerIndexEnd}"; + + public override string ProgressTitle => + $"Double exposure from layers {LayerIndexStart} to {LayerIndexEnd}"; + + public override string ProgressAction => "Cloned layers"; + + public override string ValidateSpawn() + { + if (!SlicerFile.CanUseLayerLiftHeight || !SlicerFile.CanUseLayerExposureTime) + { + return NotSupportedMessage; + } + + return null; + } + + public override string ValidateInternally() + { + var sb = new StringBuilder(); + + //if (LayerRangeHaveBottoms && _firstBottomExposure == _secondBottomExposure && _firstBottomErodeIterations == _secondBottomErodeIterations) + // sb.AppendLine("The settings for bottoms layers will produce exactly to equal layers"); + + + float lastPositionZ = SlicerFile[LayerIndexStart].PositionZ; + for (uint layerIndex = LayerIndexStart + 1; layerIndex <= LayerIndexEnd; layerIndex++) + { + if (lastPositionZ == SlicerFile[layerIndex].PositionZ) + { + sb.AppendLine($"The selected layer range already have modified layers with same z position, starting at layer {layerIndex}. Not safe to continue."); + break; + } + lastPositionZ = SlicerFile[layerIndex].PositionZ; + } + + + return sb.ToString(); + } + + public override string ToString() + { + var result = $"[1º exp: {_firstBottomExposure}/{_firstNormalExposure}s erode: {_firstBottomErodeIterations}/{_firstNormalErodeIterations}px] " + + $"[2º exp: {_secondBottomExposure}/{_secondNormalExposure}s erode: {_secondBottomErodeIterations}/{_secondNormalErodeIterations}px] " + + $"[Diff: {_secondLayerDifference} Overlap: {_secondLayerDifferenceOverlapErodeIterations}px]" + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + #endregion + + #region Properties + + public decimal FirstBottomExposure + { + get => _firstBottomExposure; + set => RaiseAndSetIfChanged(ref _firstBottomExposure, Math.Round(value, 2)); + } + + public decimal FirstNormalExposure + { + get => _firstNormalExposure; + set => RaiseAndSetIfChanged(ref _firstNormalExposure, Math.Round(value, 2)); + } + + public decimal SecondBottomExposure + { + get => _secondBottomExposure; + set => RaiseAndSetIfChanged(ref _secondBottomExposure, Math.Round(value, 2)); + } + + public decimal SecondNormalExposure + { + get => _secondNormalExposure; + set => RaiseAndSetIfChanged(ref _secondNormalExposure, Math.Round(value, 2)); + } + + public byte FirstBottomErodeIterations + { + get => _firstBottomErodeIterations; + set => RaiseAndSetIfChanged(ref _firstBottomErodeIterations, value); + } + + public byte SecondBottomErodeIterations + { + get => _secondBottomErodeIterations; + set => RaiseAndSetIfChanged(ref _secondBottomErodeIterations, value); + } + + public byte FirstNormalErodeIterations + { + get => _firstNormalErodeIterations; + set => RaiseAndSetIfChanged(ref _firstNormalErodeIterations, value); + } + + public byte SecondNormalErodeIterations + { + get => _secondNormalErodeIterations; + set => RaiseAndSetIfChanged(ref _secondNormalErodeIterations, value); + } + + public bool SecondLayerDifference + { + get => _secondLayerDifference; + set => RaiseAndSetIfChanged(ref _secondLayerDifference, value); + } + + public byte SecondLayerDifferenceOverlapErodeIterations + { + get => _secondLayerDifferenceOverlapErodeIterations; + set => RaiseAndSetIfChanged(ref _secondLayerDifferenceOverlapErodeIterations, value); + } + + public bool DifferentSettingsForSecondLayer + { + get => _differentSettingsForSecondLayer; + set => RaiseAndSetIfChanged(ref _differentSettingsForSecondLayer, value); + } + + public bool SecondLayerLiftHeightEnabled + { + get => _secondLayerLiftHeightEnabled; + set => RaiseAndSetIfChanged(ref _secondLayerLiftHeightEnabled, value); + } + + public decimal SecondLayerLiftHeight + { + get => _secondLayerLiftHeight; + set => RaiseAndSetIfChanged(ref _secondLayerLiftHeight, value); + } + + public bool SecondLayerWaitTimeBeforeCureEnabled + { + get => _secondLayerWaitTimeBeforeCureEnabled; + set => RaiseAndSetIfChanged(ref _secondLayerWaitTimeBeforeCureEnabled, value); + } + + public decimal SecondLayerWaitTimeBeforeCure + { + get => _secondLayerWaitTimeBeforeCure; + set => RaiseAndSetIfChanged(ref _secondLayerWaitTimeBeforeCure, value); + } + + #endregion + + #region Constructor + + public OperationDoubleExposure() { } + + public OperationDoubleExposure(FileFormat slicerFile) : base(slicerFile) + { + if (SlicerFile.SupportPerLayerSettings) + { + _differentSettingsForSecondLayer = true; + if (SlicerFile.SupportsGCode) + { + _secondLayerLiftHeight = 0; + _secondLayerWaitTimeBeforeCure = 2; + } + else + { + _secondLayerLiftHeight = 0.1m; + _secondLayerWaitTimeBeforeCure = 0; + } + } + } + + public override void InitWithSlicerFile() + { + base.InitWithSlicerFile(); + if (_firstBottomExposure <= 0) _firstBottomExposure = (decimal)SlicerFile.BottomExposureTime; + if (_firstNormalExposure <= 0) _firstNormalExposure = (decimal)SlicerFile.ExposureTime; + if (_secondBottomExposure <= 0) _secondBottomExposure = (decimal)SlicerFile.ExposureTime; + if (_secondNormalExposure <= 0) _secondNormalExposure = (decimal)SlicerFile.ExposureTime; + } + + #endregion + + #region Equality + + protected bool Equals(OperationDoubleExposure other) + { + return _firstBottomExposure == other._firstBottomExposure && _firstNormalExposure == other._firstNormalExposure && _secondBottomExposure == other._secondBottomExposure && _secondNormalExposure == other._secondNormalExposure && _firstBottomErodeIterations == other._firstBottomErodeIterations && _secondBottomErodeIterations == other._secondBottomErodeIterations && _firstNormalErodeIterations == other._firstNormalErodeIterations && _secondNormalErodeIterations == other._secondNormalErodeIterations && _secondLayerDifference == other._secondLayerDifference && _secondLayerDifferenceOverlapErodeIterations == other._secondLayerDifferenceOverlapErodeIterations && _differentSettingsForSecondLayer == other._differentSettingsForSecondLayer && _secondLayerLiftHeightEnabled == other._secondLayerLiftHeightEnabled && _secondLayerLiftHeight == other._secondLayerLiftHeight && _secondLayerWaitTimeBeforeCureEnabled == other._secondLayerWaitTimeBeforeCureEnabled && _secondLayerWaitTimeBeforeCure == other._secondLayerWaitTimeBeforeCure; + } + + 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((OperationDoubleExposure)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(_firstBottomExposure); + hashCode.Add(_firstNormalExposure); + hashCode.Add(_secondBottomExposure); + hashCode.Add(_secondNormalExposure); + hashCode.Add(_firstBottomErodeIterations); + hashCode.Add(_secondBottomErodeIterations); + hashCode.Add(_firstNormalErodeIterations); + hashCode.Add(_secondNormalErodeIterations); + hashCode.Add(_secondLayerDifference); + hashCode.Add(_secondLayerDifferenceOverlapErodeIterations); + hashCode.Add(_differentSettingsForSecondLayer); + hashCode.Add(_secondLayerLiftHeightEnabled); + hashCode.Add(_secondLayerLiftHeight); + hashCode.Add(_secondLayerWaitTimeBeforeCureEnabled); + hashCode.Add(_secondLayerWaitTimeBeforeCure); + return hashCode.ToHashCode(); + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + var anchor = new Point(-1, -1); + var kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), anchor); + + var layers = new Layer[SlicerFile.LayerCount+LayerRangeCount]; + + // Untouched + for (uint i = 0; i < LayerIndexStart; i++) + { + layers[i] = SlicerFile[i]; + } + + Parallel.For(LayerIndexStart, LayerIndexEnd + 1, CoreSettings.ParallelOptions, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + + var firstLayer = SlicerFile[layerIndex]; + var secondLayer = firstLayer.Clone(); + var isBottomLayer = firstLayer.IsBottomLayer; + + firstLayer.ExposureTime = (float)( isBottomLayer ? _firstBottomExposure : _firstNormalExposure); + secondLayer.ExposureTime = (float)(isBottomLayer ? _secondBottomExposure : _secondNormalExposure); + + if (_differentSettingsForSecondLayer) + { + if (_secondLayerLiftHeightEnabled) secondLayer.LiftHeightTotal = (float)_secondLayerLiftHeight; + if (_secondLayerWaitTimeBeforeCureEnabled) secondLayer.SetWaitTimeBeforeCureOrLightOffDelay((float)_secondLayerWaitTimeBeforeCure); + } + + byte firstErodeIterations = isBottomLayer ? _firstBottomErodeIterations : _firstNormalErodeIterations; + byte secondErodeIterations = isBottomLayer ? _secondBottomErodeIterations : _secondNormalErodeIterations; + + using (var mat = firstLayer.LayerMat) + { + //using Mat matOriginal = _secondExposureLayerDifference ? mat.Clone() : null; + if (firstErodeIterations > 0 && firstErodeIterations == secondErodeIterations) + { + CvInvoke.Erode(mat, mat, kernel, anchor, firstErodeIterations, BorderType.Reflect101, default); + firstLayer.LayerMat = mat; + firstLayer.CopyImageTo(secondLayer); + + if (_secondLayerDifference && _secondLayerDifferenceOverlapErodeIterations > 0) + { + using var matErode = new Mat(); + CvInvoke.Erode(mat, matErode, kernel, anchor, _secondLayerDifferenceOverlapErodeIterations, BorderType.Reflect101, default); + //CvInvoke.Threshold(matErode, matErode, 127, 255, ThresholdType.Binary); + CvInvoke.Subtract(mat, matErode, mat); + secondLayer.LayerMat = mat; + } + else + { + firstLayer.CopyImageTo(secondLayer); + } + } + else + { + Mat firstMat = null; + Mat secondMat = null; + if (firstErodeIterations > 0) + { + firstMat = new Mat(); + CvInvoke.Erode(mat, firstMat, kernel, anchor, firstErodeIterations, BorderType.Reflect101, default); + firstLayer.LayerMat = firstMat; + } + + if (secondErodeIterations > 0) + { + secondMat = new Mat(); + CvInvoke.Erode(mat, secondMat, kernel, anchor, secondErodeIterations, BorderType.Reflect101, default); + } + + if(firstMat is not null && _secondLayerDifference) + { + if (firstErodeIterations + _secondLayerDifferenceOverlapErodeIterations != secondErodeIterations) + { + if (_secondLayerDifferenceOverlapErodeIterations > 0 && + firstErodeIterations + _secondLayerDifferenceOverlapErodeIterations != secondErodeIterations) + { + CvInvoke.Erode(firstMat, firstMat, kernel, anchor, _secondLayerDifferenceOverlapErodeIterations, BorderType.Reflect101, default); + //CvInvoke.Threshold(firstMat, firstMat, 127, 255, ThresholdType.Binary); + } + + CvInvoke.AbsDiff(firstMat, secondMat ?? mat, mat); + secondLayer.LayerMat = mat; + } + } + else if (secondMat is not null) + { + secondLayer.LayerMat = secondMat; + } + + firstMat?.Dispose(); + secondMat?.Dispose(); + } + } + + uint index = LayerIndexStart + (uint)(layerIndex - LayerIndexStart) * 2; + + layers[index] = firstLayer; + layers[index + 1] = secondLayer; + + progress.LockAndIncrement(); + }); + + // Untouched + for (uint i = LayerIndexEnd+1; i < SlicerFile.LayerCount; i++) + { + layers[i + LayerRangeCount] = SlicerFile[i]; + } + + SlicerFile.SuppressRebuildPropertiesWork(() => + { + SlicerFile.LayerManager.Layers = layers; + }); + + return !progress.Token.IsCancellationRequested; + } + + #endregion + } +} diff --git a/UVtools.Core/Operations/OperationDynamicLayerHeight.cs b/UVtools.Core/Operations/OperationDynamicLayerHeight.cs index 321a815..2fe3e42 100644 --- a/UVtools.Core/Operations/OperationDynamicLayerHeight.cs +++ b/UVtools.Core/Operations/OperationDynamicLayerHeight.cs @@ -148,7 +148,7 @@ namespace UVtools.Core.Operations for (uint layerIndex = 1; layerIndex < SlicerFile.LayerCount; layerIndex++) { - if ((decimal)Math.Round(SlicerFile[layerIndex].PositionZ - SlicerFile[layerIndex - 1].PositionZ, Layer.HeightPrecision) == + if ((decimal)Layer.RoundHeight(SlicerFile[layerIndex].PositionZ - SlicerFile[layerIndex - 1].PositionZ) == (decimal)SlicerFile.LayerHeight) continue; return $"This file contain layer(s) with modified positions, starting at layer {layerIndex}.\n" + $"This tool requires sequential layers with equal height.\n" + diff --git a/UVtools.Core/Operations/OperationDynamicLifts.cs b/UVtools.Core/Operations/OperationDynamicLifts.cs index ea82e3d..ff533a4 100644 --- a/UVtools.Core/Operations/OperationDynamicLifts.cs +++ b/UVtools.Core/Operations/OperationDynamicLifts.cs @@ -51,6 +51,8 @@ namespace UVtools.Core.Operations #region Overrides + public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Normal; + public override string Title => "Dynamic lifts"; public override string Description => @@ -69,8 +71,7 @@ namespace UVtools.Core.Operations public override string ValidateSpawn() { - if (!SlicerFile.HaveLayerParameterModifier(FileFormat.PrintParameterModifier.LiftHeight) || - !SlicerFile.HaveLayerParameterModifier(FileFormat.PrintParameterModifier.LiftSpeed)) + if (!SlicerFile.CanUseLayerLiftHeight || !SlicerFile.CanUseLayerLiftSpeed) { return NotSupportedMessage; } @@ -124,23 +125,10 @@ namespace UVtools.Core.Operations return result; } - protected override void OnPropertyChanged(PropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - if (e.PropertyName is nameof(LayerRangeCount)) - { - RaisePropertyChanged(nameof(IsBottomLayersEnabled)); - RaisePropertyChanged(nameof(IsNormalLayersEnabled)); - } - } - #endregion #region Properties - public bool IsBottomLayersEnabled => LayerIndexStart < SlicerFile.BottomLayerCount; - public bool IsNormalLayersEnabled => LayerIndexEnd >= SlicerFile.BottomLayerCount; - public float MinBottomLiftHeight { get => _minBottomLiftHeight; @@ -263,9 +251,6 @@ namespace UVtools.Core.Operations if(_minLiftSpeed <= 0) _minLiftSpeed = SlicerFile.LiftSpeed; if (_maxLiftSpeed <= 0 || _maxLiftSpeed < _minLiftSpeed) _maxLiftSpeed = _minLiftSpeed; - - RaisePropertyChanged(nameof(IsBottomLayersEnabled)); - RaisePropertyChanged(nameof(IsNormalLayersEnabled)); } #endregion diff --git a/UVtools.Core/Operations/OperationFadeExposureTime.cs b/UVtools.Core/Operations/OperationFadeExposureTime.cs new file mode 100644 index 0000000..6a9ffd9 --- /dev/null +++ b/UVtools.Core/Operations/OperationFadeExposureTime.cs @@ -0,0 +1,190 @@ +/* + * 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.Text; +using UVtools.Core.FileFormats; + +namespace UVtools.Core.Operations +{ + [Serializable] + public class OperationFadeExposureTime : Operation + { + #region Members + + private uint _layerCount = 10; + private decimal _fromExposureTime; + private decimal _toExposureTime; + + #endregion + + #region Overrides + public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.Normal; + public override bool LayerIndexEndEnabled => false; + public override string Title => "Fade exposure time"; + + public override string Description => + "Fade the exposure time in increments from a start to a end value on the selected layer range."; + + public override string ConfirmationText => + $"fade exposure time model layers {LayerIndexStart} through {LayerIndexEnd} with increments of {IncrementValue}s"; + + public override string ProgressTitle => + $"Fading exposure time from layers {LayerIndexStart} to {LayerIndexEnd} with increments of {IncrementValue}s"; + + public override string ProgressAction => "Faded layers"; + + public override string ValidateSpawn() + { + if (!SlicerFile.CanUseLayerExposureTime) + { + return NotSupportedMessage; + } + + return null; + } + + public override string ValidateInternally() + { + var sb = new StringBuilder(); + + if (_layerCount == 0) sb.AppendLine("The layer count must be higher than 0."); + if(_fromExposureTime == _toExposureTime) sb.AppendLine("The starting exposure time can't be the same as the ending exposure time."); + + return sb.ToString(); + } + + public override string ToString() + { + var result = $"[Layers: {LayerRangeCount} From: {_fromExposureTime}s To: {_toExposureTime}s @ {IncrementValue}s] " + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LayerIndexStart)) + { + LayerCount = _layerCount; // Sanitize + LayerIndexEnd = LayerIndexStart + _layerCount - 1 ; // Sync + RaisePropertyChanged(nameof(MaximumLayerCount)); + RaisePropertyChanged(nameof(IncrementValue)); + } + /*else if (e.PropertyName == nameof(LayerIndexEnd)) + { + LayerCount = LayerRangeCount; + RaisePropertyChanged(nameof(IncrementValue)); + }*/ + + base.OnPropertyChanged(e); + } + + #endregion + + #region Properties + + public uint LayerCount + { + get => _layerCount; + set + { + if (!RaiseAndSetIfChanged(ref _layerCount, Math.Min(value, SlicerFile.LayerCount - LayerIndexStart))) return; + LayerIndexEnd = LayerIndexStart + _layerCount - 1; + RaisePropertyChanged(nameof(MaximumLayerCount)); + RaisePropertyChanged(nameof(IncrementValue)); + } + } + + public uint MaximumLayerCount => Math.Max(LayerCount, SlicerFile.LayerCount - LayerIndexStart); + + public decimal FromExposureTime + { + get => _fromExposureTime; + set + { + if(!RaiseAndSetIfChanged(ref _fromExposureTime, Math.Round(value, 2))) return; + RaisePropertyChanged(nameof(IncrementValue)); + } + } + + public decimal ToExposureTime + { + get => _toExposureTime; + set + { + if(!RaiseAndSetIfChanged(ref _toExposureTime, Math.Round(value, 2))) return; + RaisePropertyChanged(nameof(IncrementValue)); + } + } + + public decimal IncrementValue => Math.Round(IncrementValueRaw, 2); + public decimal IncrementValueRaw => (_toExposureTime - _fromExposureTime) / (LayerRangeCount + 1); + + #endregion + + #region Constructor + + public OperationFadeExposureTime() { } + + public OperationFadeExposureTime(FileFormat slicerFile) : base(slicerFile) { } + + public override void InitWithSlicerFile() + { + base.InitWithSlicerFile(); + if (_fromExposureTime <= 0) _fromExposureTime = (decimal)SlicerFile.BottomExposureTime; + if (_toExposureTime <= 0) _toExposureTime = (decimal)SlicerFile.ExposureTime; + + LayerIndexEnd = LayerIndexStart + _layerCount - 1; // Sync + } + + #endregion + + #region Equality + + protected bool Equals(OperationFadeExposureTime other) + { + return _fromExposureTime == other._fromExposureTime && _toExposureTime == other._toExposureTime && _layerCount == other._layerCount; + } + + 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((OperationFadeExposureTime)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(_fromExposureTime, _toExposureTime, _layerCount); + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + LayerIndexEnd = LayerIndexStart + _layerCount - 1; // Sanitize + + var increment = IncrementValueRaw; + var exposure = _fromExposureTime; + for (uint layerIndex = LayerIndexStart; layerIndex <= LayerIndexEnd; layerIndex++) + { + progress.Token.ThrowIfCancellationRequested(); + exposure += increment; + SlicerFile[layerIndex].ExposureTime = (float)exposure; + } + + return !progress.Token.IsCancellationRequested; + } + + #endregion + } +} diff --git a/UVtools.Core/Operations/OperationLayerClone.cs b/UVtools.Core/Operations/OperationLayerClone.cs index 3b6fd8a..7c873d9 100644 --- a/UVtools.Core/Operations/OperationLayerClone.cs +++ b/UVtools.Core/Operations/OperationLayerClone.cs @@ -22,6 +22,8 @@ namespace UVtools.Core.Operations { #region Members private uint _clones = 1; + private bool _keepSamePositionZ; + #endregion #region Overrides @@ -69,14 +71,29 @@ namespace UVtools.Core.Operations #region Properties /// <summary> + /// Gets or sets if cloned layers will keep same position z or get the height rebuilt + /// </summary> + public bool KeepSamePositionZ + { + get => _keepSamePositionZ; + set => RaiseAndSetIfChanged(ref _keepSamePositionZ, value); + } + + /// <summary> /// Gets or sets the number of clones /// </summary> public uint Clones { get => _clones; - set => RaiseAndSetIfChanged(ref _clones, value); + set + { + if(!RaiseAndSetIfChanged(ref _clones, value)) return; + RaisePropertyChanged(nameof(ExtraLayers)); + } } + public uint ExtraLayers => (uint)Math.Max(0, ((int)LayerIndexEnd - LayerIndexStart + 1) * _clones); + #endregion #region Constructor @@ -107,7 +124,11 @@ namespace UVtools.Core.Operations } } - SlicerFile.LayerManager.Layers = newLayers; + SlicerFile.SuppressRebuildPropertiesWork(() => + { + SlicerFile.LayerManager.Layers = newLayers; + }, !_keepSamePositionZ); + /*var oldLayers = SlicerFile.LayerManager.Layers; diff --git a/UVtools.Core/Operations/OperationPixelArithmetic.cs b/UVtools.Core/Operations/OperationPixelArithmetic.cs index 12fab9f..426acc6 100644 --- a/UVtools.Core/Operations/OperationPixelArithmetic.cs +++ b/UVtools.Core/Operations/OperationPixelArithmetic.cs @@ -626,7 +626,9 @@ namespace UVtools.Core.Operations CvInvoke.BitwiseXor(target, tempMat, target, applyMask); break; case PixelArithmeticOperators.Threshold: - CvInvoke.Threshold(target, target, _value, _thresholdMaxValue, _thresholdType); + var tempThreshold = _thresholdType; + if (_thresholdType is ThresholdType.Otsu or ThresholdType.Triangle) tempThreshold |= ThresholdType.Binary; + CvInvoke.Threshold(target, target, _value, _thresholdMaxValue, tempThreshold); if (_applyMethod != PixelArithmeticApplyMethod.All) ApplyMask(originalRoi, target, applyMask); break; case PixelArithmeticOperators.AbsDiff: diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 5de153d..fc4b636 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.20.5</Version> + <Version>2.21.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> diff --git a/UVtools.WPF/Assets/Icons/equals-16x16.png b/UVtools.WPF/Assets/Icons/equals-16x16.png Binary files differnew file mode 100644 index 0000000..8b7528a --- /dev/null +++ b/UVtools.WPF/Assets/Icons/equals-16x16.png diff --git a/UVtools.WPF/Assets/Icons/history-16x16.png b/UVtools.WPF/Assets/Icons/history-16x16.png Binary files differnew file mode 100644 index 0000000..17fe4c3 --- /dev/null +++ b/UVtools.WPF/Assets/Icons/history-16x16.png diff --git a/UVtools.WPF/Assets/Icons/share-square-16x16.png b/UVtools.WPF/Assets/Icons/share-square-16x16.png Binary files differnew file mode 100644 index 0000000..73b9e2f --- /dev/null +++ b/UVtools.WPF/Assets/Icons/share-square-16x16.png diff --git a/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml b/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml index dda24b6..21e0d2f 100644 --- a/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml +++ b/UVtools.WPF/Controls/Calibrators/CalibrateExposureFinderControl.axaml @@ -1149,22 +1149,40 @@ <CheckBox Grid.Row="2" Grid.Column="0" ToolTip.Tip="Use a low value to speed up layers with same Z position, a delay is not really required here. 
Set no delay (0s) is not recommended for gcode printers, as most need some time to render the image before move to the next command, 2s is recommended as a safe-guard." - Content="Light-off delay:" - IsVisible="{Binding SlicerFile.CanUseLayerLightOffDelay}" - IsChecked="{Binding Operation.SamePositionedLayersLightOffDelayEnabled}" - VerticalAlignment="Center"/> + Content="Wait time before cure:" + IsChecked="{Binding Operation.SamePositionedLayersWaitTimeBeforeCureEnabled}" + VerticalAlignment="Center"> + <CheckBox.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.Or}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="SlicerFile.CanUseLayerWaitTimeBeforeCure"/> + </MultiBinding> + </CheckBox.IsVisible> + </CheckBox> <NumericUpDown Grid.Row="2" Grid.Column="2" Increment="0.5" Minimum="0" Maximum="1000" FormatString="F2" - IsVisible="{Binding SlicerFile.CanUseLayerLightOffDelay}" - IsEnabled="{Binding Operation.SamePositionedLayersLightOffDelayEnabled}" - Value="{Binding Operation.SamePositionedLayersLightOffDelay}"/> + IsEnabled="{Binding Operation.SamePositionedLayersWaitTimeBeforeCureEnabled}" + Value="{Binding Operation.SamePositionedLayersWaitTimeBeforeCure}"> + <NumericUpDown.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.Or}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="SlicerFile.CanUseLayerWaitTimeBeforeCure"/> + </MultiBinding> + </NumericUpDown.IsVisible> + </NumericUpDown> <TextBlock Grid.Row="2" Grid.Column="4" Text="s" - IsVisible="{Binding SlicerFile.CanUseLayerLightOffDelay}" - VerticalAlignment="Center"/> + VerticalAlignment="Center"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.Or}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="SlicerFile.CanUseLayerWaitTimeBeforeCure"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> </Grid> diff --git a/UVtools.WPF/Controls/Tools/ToolDoubleExposureControl.axaml b/UVtools.WPF/Controls/Tools/ToolDoubleExposureControl.axaml new file mode 100644 index 0000000..56dd8ef --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolDoubleExposureControl.axaml @@ -0,0 +1,240 @@ +<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.ToolDoubleExposureControl"> + <StackPanel Spacing="10"> + <Grid RowDefinitions="Auto,10,Auto,10,Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,Auto,5,Auto,40,Auto,5,Auto"> + <TextBlock Grid.Row="0" Grid.Column="2" + VerticalAlignment="Center" + HorizontalAlignment="Center" + FontWeight="Bold" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + Text="Bottom layers"/> + <TextBlock Grid.Row="0" Grid.Column="6" + VerticalAlignment="Center" + HorizontalAlignment="Center" + FontWeight="Bold" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Text="Normal layers"/> + + <TextBlock Grid.Row="2" Grid.Column="0" + VerticalAlignment="Center" + Text="1º exposure:"/> + + <NumericUpDown + Grid.Row="2" Grid.Column="2" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + VerticalAlignment="Center" + Minimum="0.01" + Maximum="1000" + Increment="0.5" + Value="{Binding Operation.FirstBottomExposure}"/> + <TextBlock Grid.Row="2" Grid.Column="4" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + Text="s"/> + + <NumericUpDown + Grid.Row="2" Grid.Column="6" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + VerticalAlignment="Center" + Minimum="0.01" + Maximum="1000" + Increment="0.5" + Value="{Binding Operation.FirstNormalExposure}"/> + <TextBlock Grid.Row="2" Grid.Column="8" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Text="s"/> + + <TextBlock Grid.Row="4" Grid.Column="0" + VerticalAlignment="Center" + Text="1º erode iterations:"/> + + <NumericUpDown + Grid.Row="4" Grid.Column="2" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + VerticalAlignment="Center" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.FirstBottomErodeIterations}"/> + <TextBlock Grid.Row="4" Grid.Column="4" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + Text="px"/> + + <NumericUpDown + Grid.Row="4" Grid.Column="6" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + VerticalAlignment="Center" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.FirstNormalErodeIterations}"/> + <TextBlock Grid.Row="4" Grid.Column="8" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Text="px"/> + + <TextBlock Grid.Row="6" Grid.Column="0" + VerticalAlignment="Center" + Text="2º exposure:"/> + + <NumericUpDown + Grid.Row="6" Grid.Column="2" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + VerticalAlignment="Center" + Minimum="0.01" + Maximum="1000" + Increment="0.5" + Value="{Binding Operation.SecondBottomExposure}"/> + <TextBlock Grid.Row="6" Grid.Column="4" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + Text="s"/> + + <NumericUpDown + Grid.Row="6" Grid.Column="6" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Minimum="0.01" + Maximum="1000" + Increment="0.5" + Value="{Binding Operation.SecondNormalExposure}"/> + <TextBlock Grid.Row="6" Grid.Column="8" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Text="s"/> + + <TextBlock Grid.Row="8" Grid.Column="0" + VerticalAlignment="Center" + Text="2º erode iterations:"/> + + <NumericUpDown + Grid.Row="8" Grid.Column="2" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + VerticalAlignment="Center" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.SecondBottomErodeIterations}"/> + <TextBlock Grid.Row="8" Grid.Column="4" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" + Text="px"/> + + <NumericUpDown + Grid.Row="8" Grid.Column="6" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.SecondNormalErodeIterations}"/> + <TextBlock Grid.Row="8" Grid.Column="8" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" + Text="px"/> + </Grid> + + <ToggleSwitch + OffContent="Exposure the whole image for the second layer" + OnContent="Exposure the difference between first and second layer for the second layer" + IsChecked="{Binding Operation.SecondLayerDifference}"/> + + <Grid RowDefinitions="Auto" + ColumnDefinitions="Auto,10,Auto,5,Auto"> + <TextBlock Grid.Row="0" Grid.Column="0" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.SecondLayerDifference}" + ToolTip.Tip="When the 'Exposure the difference between first and second layer for the second layer' is active, +this setting will further erode the layer producing a overlap of n pixel perimeters over the previous layer" + Text="Difference overlap margin:"/> + <NumericUpDown + Grid.Row="0" Grid.Column="2" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.SecondLayerDifference}" + Minimum="0" + Maximum="255" + Increment="1" + Value="{Binding Operation.SecondLayerDifferenceOverlapErodeIterations}"/> + <TextBlock Grid.Row="0" Grid.Column="4" + VerticalAlignment="Center" + IsEnabled="{Binding Operation.SecondLayerDifference}" + Text="px"/> + </Grid> + + + <CheckBox + ToolTip.Tip="Change some defined settings for the second layers" + Content="Use different settings for the second layer:" + IsChecked="{Binding Operation.DifferentSettingsForSecondLayer}"/> + + <Grid RowDefinitions="Auto,10,Auto" ColumnDefinitions="Auto,10,Auto,5,Auto" IsEnabled="{Binding Operation.DifferentSettingsForSecondLayer}"> + <CheckBox Grid.Row="0" Grid.Column="0" + ToolTip.Tip="Use a low value to speed up layers with same Z position, lift is not really required here. +
Set no lift height (0mm) will not work on most of the printers, so far, only gcode printers are known/able to use no lifts. +
However set 0mm on a not compatible printer will cause no harm, value will be contained inside a min-max inside firmware." + Content="Lift height:" + IsVisible="{Binding SlicerFile.CanUseLayerLiftHeight}" + IsChecked="{Binding Operation.SecondLayerLiftHeightEnabled}" + VerticalAlignment="Center"/> + <NumericUpDown Grid.Row="0" Grid.Column="2" + Increment="0.5" + Minimum="0" + Maximum="1000" + FormatString="F2" + IsVisible="{Binding SlicerFile.CanUseLayerLiftHeight}" + IsEnabled="{Binding Operation.SecondLayerLiftHeightEnabled}" + Value="{Binding Operation.SecondLayerLiftHeight}"/> + <TextBlock Grid.Row="0" Grid.Column="4" + Text="mm" + IsVisible="{Binding SlicerFile.CanUseLayerLiftHeight}" + VerticalAlignment="Center"/> + + <CheckBox Grid.Row="2" Grid.Column="0" + ToolTip.Tip="Use a low value to speed up layers with same Z position, a delay is not really required here. +
Set no delay (0s) is not recommended for gcode printers, as most need some time to render the image before move to the next command, 2s is recommended as a safe-guard." + Content="Wait time before cure:" + IsChecked="{Binding Operation.SecondLayerWaitTimeBeforeCureEnabled}" + VerticalAlignment="Center"> + <CheckBox.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.Or}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="SlicerFile.CanUseLayerWaitTimeBeforeCure"/> + </MultiBinding> + </CheckBox.IsVisible> + </CheckBox> + <NumericUpDown Grid.Row="2" Grid.Column="2" + Increment="0.5" + Minimum="0" + Maximum="1000" + FormatString="F2" + IsEnabled="{Binding Operation.SecondLayerWaitTimeBeforeCureEnabled}" + Value="{Binding Operation.SecondLayerWaitTimeBeforeCure}"> + <NumericUpDown.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.Or}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="SlicerFile.CanUseLayerWaitTimeBeforeCure"/> + </MultiBinding> + </NumericUpDown.IsVisible> + </NumericUpDown> + <TextBlock Grid.Row="2" Grid.Column="4" + Text="s" + VerticalAlignment="Center"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.Or}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="SlicerFile.CanUseLayerWaitTimeBeforeCure"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> + + </Grid> + + </StackPanel> +</UserControl> diff --git a/UVtools.WPF/Controls/Tools/ToolDoubleExposureControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolDoubleExposureControl.axaml.cs new file mode 100644 index 0000000..db2b324 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolDoubleExposureControl.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia.Markup.Xaml; +using UVtools.Core.Operations; + +namespace UVtools.WPF.Controls.Tools +{ + public partial class ToolDoubleExposureControl : ToolControl + { + public OperationDoubleExposure Operation => BaseOperation as OperationDoubleExposure; + + public ToolDoubleExposureControl() + { + BaseOperation = new OperationDoubleExposure(SlicerFile); + if (!ValidateSpawn()) return; + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml b/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml index 91a4219..8a53f62 100644 --- a/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml +++ b/UVtools.WPF/Controls/Tools/ToolDynamicLiftsControl.axaml @@ -81,12 +81,12 @@ Text="View"/> <TextBlock Grid.Row="2" Grid.Column="0" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Text="Bottom lift height:"/> <NumericUpDown Grid.Row="2" Grid.Column="2" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Minimum="1" Maximum="100" @@ -95,13 +95,13 @@ Value="{Binding Operation.MinBottomLiftHeight}"/> <TextBlock Grid.Row="2" Grid.Column="3" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="/"/> <NumericUpDown Grid.Row="2" Grid.Column="4" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Minimum="1" Maximum="100" @@ -110,12 +110,12 @@ Value="{Binding Operation.MaxBottomLiftHeight}"/> <TextBlock Grid.Row="2" Grid.Column="6" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Text="mm"/> <Button Grid.Row="2" Grid.Column="8" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" @@ -125,12 +125,12 @@ Content="Smallest"/> <TextBlock Grid.Row="4" Grid.Column="0" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Text="Bottom lift speed:"/> <NumericUpDown Grid.Row="4" Grid.Column="2" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Minimum="5" Maximum="1000" @@ -139,13 +139,13 @@ Value="{Binding Operation.MinBottomLiftSpeed}"/> <TextBlock Grid.Row="4" Grid.Column="3" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="/"/> <NumericUpDown Grid.Row="4" Grid.Column="4" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Minimum="5" Maximum="1000" @@ -154,12 +154,12 @@ Value="{Binding Operation.MaxBottomLiftSpeed}"/> <TextBlock Grid.Row="4" Grid.Column="6" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Center" Text="mm/min"/> <Button Grid.Row="4" Grid.Column="8" - IsEnabled="{Binding Operation.IsBottomLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveBottoms}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" @@ -170,12 +170,12 @@ <TextBlock Grid.Row="6" Grid.Column="0" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Text="Lift height:"/> <NumericUpDown Grid.Row="6" Grid.Column="2" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Minimum="1" Maximum="100" @@ -184,13 +184,13 @@ Value="{Binding Operation.MinLiftHeight}"/> <TextBlock Grid.Row="6" Grid.Column="3" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="/"/> <NumericUpDown Grid.Row="6" Grid.Column="4" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Minimum="1" Maximum="100" @@ -199,12 +199,12 @@ Value="{Binding Operation.MaxLiftHeight}"/> <TextBlock Grid.Row="6" Grid.Column="6" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Text="mm"/> <Button Grid.Row="6" Grid.Column="8" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" @@ -214,12 +214,12 @@ Content="Smallest"/> <TextBlock Grid.Row="8" Grid.Column="0" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Text="Lift speed:"/> <NumericUpDown Grid.Row="8" Grid.Column="2" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Minimum="5" Maximum="1000" @@ -228,13 +228,13 @@ Value="{Binding Operation.MinLiftSpeed}"/> <TextBlock Grid.Row="8" Grid.Column="3" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="/"/> <NumericUpDown Grid.Row="8" Grid.Column="4" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Minimum="5" Maximum="1000" @@ -243,12 +243,12 @@ Value="{Binding Operation.MaxLiftSpeed}"/> <TextBlock Grid.Row="8" Grid.Column="6" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Center" Text="mm/min"/> <Button Grid.Row="8" Grid.Column="8" - IsEnabled="{Binding Operation.IsNormalLayersEnabled}" + IsEnabled="{Binding Operation.LayerRangeHaveNormals}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" @@ -258,70 +258,114 @@ Content="Largest"/> <TextBlock Grid.Row="10" Grid.Column="0" + IsVisible="{Binding SlicerFile.CanUseLayerLightOffDelay}" VerticalAlignment="Center" Text="Light-off mode:"/> <ComboBox Grid.Row="10" Grid.Column="2" Grid.ColumnSpan="3" + IsVisible="{Binding SlicerFile.CanUseLayerLightOffDelay}" HorizontalAlignment="Stretch" Items="{Binding Operation.LightOffDelaySetMode, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}" SelectedItem="{Binding Operation.LightOffDelaySetMode, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/> <TextBlock Grid.Row="12" Grid.Column="2" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold" - Text="Bottom extra time"/> + Text="Bottom extra time"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> <TextBlock Grid.Row="12" Grid.Column="4" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold" - Text="Normal extra time"/> + Text="Normal extra time"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> <TextBlock Grid.Row="14" Grid.Column="0" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" - Text="Light-off delay:"/> + Text="Light-off delay:"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> <NumericUpDown Grid.Row="14" Grid.Column="2" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" Minimum="0" Maximum="100" Increment="1" FormatString="F2" - Value="{Binding Operation.LightOffDelayBottomExtraTime}"/> + Value="{Binding Operation.LightOffDelayBottomExtraTime}"> + <NumericUpDown.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </NumericUpDown.IsVisible> + </NumericUpDown> <TextBlock Grid.Row="14" Grid.Column="3" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold" - Text="/"/> + Text="/"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> <NumericUpDown Grid.Row="14" Grid.Column="4" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" Minimum="0" Maximum="100" Increment="1" FormatString="F2" - Value="{Binding Operation.LightOffDelayExtraTime}"/> + Value="{Binding Operation.LightOffDelayExtraTime}"> + <NumericUpDown.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </NumericUpDown.IsVisible> + </NumericUpDown> <TextBlock Grid.Row="14" Grid.Column="6" - IsEnabled="{Binding !Operation.LightOffSetMode}" - IsVisible="{Binding !Operation.LightOffSetMode}" + IsEnabled="{Binding !Operation.LightOffDelaySetMode}" VerticalAlignment="Center" - Text="s"/> + Text="s"> + <TextBlock.IsVisible> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="SlicerFile.CanUseLayerLightOffDelay"/> + <Binding Path="!Operation.LightOffDelaySetMode"/> + </MultiBinding> + </TextBlock.IsVisible> + </TextBlock> </Grid> </StackPanel> diff --git a/UVtools.WPF/Controls/Tools/ToolFadeExposureTimeControl.axaml b/UVtools.WPF/Controls/Tools/ToolFadeExposureTimeControl.axaml new file mode 100644 index 0000000..53939bd --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolFadeExposureTimeControl.axaml @@ -0,0 +1,56 @@ +<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.ToolFadeExposureTimeControl"> + <Grid RowDefinitions="Auto,10,Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,Auto,5,Auto,5,Auto,5,Auto"> + + <TextBlock Grid.Row="0" Grid.Column="0" + VerticalAlignment="Center" + Text="Layer count:"/> + <NumericUpDown + Grid.Row="0" Grid.Column="2" + Minimum="1" + Maximum="{Binding Operation.MaximumLayerCount}" + Increment="1" + Value="{Binding Operation.LayerCount}"/> + + <TextBlock Grid.Row="2" Grid.Column="0" + VerticalAlignment="Center" + Text="Exposure time:"/> + <NumericUpDown + Grid.Row="2" Grid.Column="2" + Minimum="0.1" + Maximum="1000" + Increment="0.5" + Value="{Binding Operation.FromExposureTime}"/> + <TextBlock Grid.Row="2" Grid.Column="4" + VerticalAlignment="Center" + Text="->"/> + <NumericUpDown + Grid.Row="2" Grid.Column="6" + Minimum="0.1" + Maximum="1000" + Increment="0.5" + Value="{Binding Operation.ToExposureTime}"/> + <TextBlock Grid.Row="2" Grid.Column="8" + VerticalAlignment="Center" + Text="s"/> + + <TextBlock Grid.Row="4" Grid.Column="0" + VerticalAlignment="Center" + Text="Time increment:"/> + <NumericUpDown + Grid.Row="4" Grid.Column="2" + IsReadOnly="True" + ShowButtonSpinner="False" + AllowSpin="False" + Value="{Binding Operation.IncrementValue}"/> + <TextBlock Grid.Row="4" Grid.Column="4" Grid.ColumnSpan="5" + VerticalAlignment="Center" + Text="s / per layer"/> + + </Grid> +</UserControl> diff --git a/UVtools.WPF/Controls/Tools/ToolFadeExposureTimeControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolFadeExposureTimeControl.axaml.cs new file mode 100644 index 0000000..a75fe28 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolFadeExposureTimeControl.axaml.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Markup.Xaml; +using UVtools.Core.Operations; +using UVtools.WPF.Windows; + +namespace UVtools.WPF.Controls.Tools +{ + public partial class ToolFadeExposureTimeControl : ToolControl + { + public OperationFadeExposureTime Operation => BaseOperation as OperationFadeExposureTime; + + public ToolFadeExposureTimeControl() + { + BaseOperation = new OperationFadeExposureTime(SlicerFile); + if (!ValidateSpawn()) return; + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public override void Callback(ToolWindow.Callbacks callback) + { + switch (callback) + { + case ToolWindow.Callbacks.Init: + case ToolWindow.Callbacks.Loaded: + ParentWindow.LayerIndexEnd = Operation.LayerIndexStart + Operation.LayerCount - 1; + Operation.PropertyChanged += (sender, e) => + { + if (e.PropertyName != nameof(Operation.LayerCount)) return; + ParentWindow.LayerIndexEnd = Operation.LayerIndexStart + Operation.LayerCount - 1; + }; + ParentWindow.PropertyChanged += (sender, e) => + { + if (e.PropertyName is nameof(ParentWindow.LayerIndexStart) or nameof(ParentWindow.LayerIndexEnd)) + { + ParentWindow.LayerIndexEnd = Operation.LayerIndexStart + Operation.LayerCount - 1; + } + }; + break; + } + + } + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml index 98c3084..38f3b06 100644 --- a/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml +++ b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml @@ -5,7 +5,12 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UVtools.WPF.Controls.Tools.ToolLayerCloneControl"> <StackPanel Spacing="10"> - <StackPanel Spacing="10" Orientation="Horizontal"> + <ToggleSwitch + OffContent="Rebuild whole model height with the new layers" + OnContent="Keep the same z position for the cloned layers" + IsChecked="{Binding Operation.KeepSamePositionZ}" + /> + <StackPanel Spacing="10" Orientation="Horizontal"> <TextBlock VerticalAlignment="Center" Text="Clones:"/> diff --git a/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs index 382c5c8..93a9e62 100644 --- a/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs @@ -10,13 +10,12 @@ namespace UVtools.WPF.Controls.Tools { public OperationLayerClone Operation => BaseOperation as OperationLayerClone; - public uint ExtraLayers => (uint)Math.Max(0, ((int)Operation.LayerIndexEnd - Operation.LayerIndexStart + 1) * Operation.Clones); - + public string InfoLayersStr { get { - uint extraLayers = ExtraLayers; + uint extraLayers = Operation.ExtraLayers; return $"Layers: {App.SlicerFile.LayerCount} → {SlicerFile.LayerCount + extraLayers} (+ {extraLayers})"; } } @@ -25,7 +24,7 @@ namespace UVtools.WPF.Controls.Tools { get { - float extraHeight = Layer.RoundHeight(ExtraLayers * SlicerFile.LayerHeight); + float extraHeight = Operation.KeepSamePositionZ ? 0 : Layer.RoundHeight(Operation.ExtraLayers * SlicerFile.LayerHeight); return $"Height: {App.SlicerFile.PrintHeight}mm → {Layer.RoundHeight(SlicerFile.PrintHeight + extraHeight)}mm (+ {extraHeight}mm)"; } } diff --git a/UVtools.WPF/Extensions/WindowExtensions.cs b/UVtools.WPF/Extensions/WindowExtensions.cs index b72784d..c6bb5c0 100644 --- a/UVtools.WPF/Extensions/WindowExtensions.cs +++ b/UVtools.WPF/Extensions/WindowExtensions.cs @@ -32,7 +32,7 @@ namespace UVtools.WPF.Extensions Style = style, WindowIcon = new WindowIcon(App.GetAsset("/Assets/Icons/UVtools.ico")), WindowStartupLocation = location, - CanResize = false, + CanResize = UserSettings.Instance.General.WindowsCanResize, MaxWidth = window.GetScreenWorkingArea().Width - UserSettings.Instance.General.WindowsHorizontalMargin, MaxHeight = window.GetScreenWorkingArea().Height - UserSettings.Instance.General.WindowsVerticalMargin, SizeToContent = SizeToContent.WidthAndHeight, diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index 4e94c57..fd53e1d 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -76,6 +76,7 @@ namespace UVtools.WPF private bool _showLayerOutlineLayerBoundary; private bool _showLayerOutlineHollowAreas; private bool _showLayerOutlineEdgeDetection; + private bool _showLayerOutlineDistanceDetection; private bool _showLayerOutlineSkeletonize; @@ -389,6 +390,16 @@ namespace UVtools.WPF } } + public bool ShowLayerOutlineDistanceDetection + { + get => _showLayerOutlineDistanceDetection; + set + { + if (!RaiseAndSetIfChanged(ref _showLayerOutlineDistanceDetection, value)) return; + ShowLayer(); + } + } + public bool ShowLayerOutlineSkeletonize { get => _showLayerOutlineSkeletonize; @@ -661,32 +672,47 @@ namespace UVtools.WPF public void GoFirstLayer() { - if (SlicerFile is null) return; + if (!IsFileLoaded) return; if (!CanGoDown) return; ActualLayer = 0; } public void GoPreviousLayer() { - if (SlicerFile is null) return; + if (!IsFileLoaded) return; if (!CanGoDown) return; ActualLayer--; } public void GoNextLayer() { - if (SlicerFile is null) return; + if (!IsFileLoaded) return; if (!CanGoUp) return; ActualLayer++; } public void GoLastLayer() { - if (SlicerFile is null) return; + if (!IsFileLoaded) return; if (!CanGoUp) return; ActualLayer = SliderMaximumValue; } + public void GoMassLayer(string which) + { + if (!IsFileLoaded) return; + var layer = which switch + { + "SB" => SlicerFile.LayerManager.SmallestBottomLayer, + "LB" => SlicerFile.LayerManager.LargestBottomLayer, + "SN" => SlicerFile.LayerManager.SmallestNormalLayer, + "LN" => SlicerFile.LayerManager.LargestNormalLayer, + _ => null + }; + if (layer is null) return; + ActualLayer = layer.Index; + } + public void RefreshLayerImage() { LayerImageBox.Image = LayerCache.ImageBgr.ToBitmap(); @@ -723,6 +749,14 @@ namespace UVtools.WPF CvInvoke.Canny(LayerCache.Image, canny, 80, 40, 3, true); CvInvoke.CvtColor(canny, LayerCache.ImageBgr, ColorConversion.Gray2Bgr); } + else if (_showLayerOutlineDistanceDetection) + { + using var distance = new Mat(); + CvInvoke.DistanceTransform(LayerCache.Image, distance, null, DistType.C, 3); + //distance.ConvertTo(distance, DepthType.Cv8U); + CvInvoke.Normalize(distance, distance, byte.MinValue, byte.MaxValue, NormType.MinMax, DepthType.Cv8U); + CvInvoke.CvtColor(distance, LayerCache.ImageBgr, ColorConversion.Gray2Bgr); + } else if (_showLayerOutlineSkeletonize) { using var skeletonize = LayerCache.Image.Skeletonize(); diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml index 3db3bc7..6c98575 100644 --- a/UVtools.WPF/MainWindow.axaml +++ b/UVtools.WPF/MainWindow.axaml @@ -30,6 +30,19 @@ <Image Source="\Assets\Icons\file-import-16x16.png"/> </MenuItem.Icon> </MenuItem> + <MenuItem + Name="MainMenu.File.OpenRecent" + Header="Open recent" + ToolTip.ShowDelay="2000" + ToolTip.Tip="On a file: +
Shift + Click: Open file in a new window +
Shift + Ctrl + Click: Remove file from list +
Ctrl + Click: Purge non-existing files" + Items="{Binding MenuFileOpenRecentItems}"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\file-import-16x16.png"/> + </MenuItem.Icon> + </MenuItem> <MenuItem Name="MainMenu.File.Reload" Header="_Reload" @@ -61,6 +74,15 @@ <Image Source="\Assets\Icons\save-as-16x16.png"/> </MenuItem.Icon> </MenuItem> + <MenuItem + Name="MainMenu.File.SendTo" + Header="Send to" + IsEnabled="{Binding IsFileLoaded}" + Items="{Binding MenuFileSendToItems}"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\share-square-16x16.png"/> + </MenuItem.Icon> + </MenuItem> <MenuItem Name="MainMenu.File.Close" Header="_Close" @@ -1664,7 +1686,7 @@ IsEnabled="{Binding IsFileLoaded}" DockPanel.Dock="Right" ColumnDefinitions="160" - RowDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="5"> + RowDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto,Auto" Margin="5"> <TextBlock Text="{Binding MaximumLayerString}" Name="Layer.Navigation.Up" @@ -1786,6 +1808,37 @@ <Image Width="16" Height="16" Source="/Assets/Icons/arrow-top-16x16.png"/> </Button> </Grid> + + <StackPanel Grid.Row="6" + Margin="0,1,0,0" + HorizontalAlignment="Center" + VerticalAlignment="Center" + Orientation="Horizontal" Spacing="1"> + <Button + ToolTip.Tip="Navigate to the smallest bottom layer in mass" + Command="{Binding GoMassLayer}" + CommandParameter="SB" + Content="SB"/> + + <Button + ToolTip.Tip="Navigate to the largest bottom layer in mass" + Command="{Binding GoMassLayer}" + CommandParameter="LB" + Content="LB"/> + + <Button + ToolTip.Tip="Navigate to the smallest normal layer in mass" + Command="{Binding GoMassLayer}" + CommandParameter="SN" + Content="SN"/> + + <Button + ToolTip.Tip="Navigate to the largest normal layer in mass" + Command="{Binding GoMassLayer}" + CommandParameter="LN" + Content="LN"/> + </StackPanel> + </Grid> @@ -1910,11 +1963,35 @@ Content="Hollow areas"/> <CheckBox IsChecked="{Binding ShowLayerOutlineEdgeDetection}" - Content="Edge detection"/> + Content="Edge detection"> + <CheckBox.IsEnabled> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="!ShowLayerOutlineDistanceDetection"/> + <Binding Path="!ShowLayerOutlineSkeletonize"/> + </MultiBinding> + </CheckBox.IsEnabled> + </CheckBox> + <CheckBox + IsChecked="{Binding ShowLayerOutlineDistanceDetection}" + ToolTip.Tip="Calculates the distance to the closest zero pixel for each pixel" + Content="Distance detection"> + <CheckBox.IsEnabled> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="!ShowLayerOutlineEdgeDetection"/> + <Binding Path="!ShowLayerOutlineSkeletonize"/> + </MultiBinding> + </CheckBox.IsEnabled> + </CheckBox> <CheckBox IsChecked="{Binding ShowLayerOutlineSkeletonize}" - IsEnabled="{Binding !ShowLayerOutlineEdgeDetection}" - Content="Skeletonize"/> + Content="Skeletonize"> + <CheckBox.IsEnabled> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="!ShowLayerOutlineEdgeDetection"/> + <Binding Path="!ShowLayerOutlineDistanceDetection"/> + </MultiBinding> + </CheckBox.IsEnabled> + </CheckBox> </ContextMenu> </Button.ContextMenu> <StackPanel Orientation="Horizontal"> diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index d9ac0c4..72ef210 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -208,10 +208,18 @@ namespace UVtools.WPF }, new() { - Tag = new OperationDynamicLayerHeight(), + Tag = new OperationFadeExposureTime(), Icon = new Avalonia.Controls.Image { - Source = new Bitmap(App.GetAsset("/Assets/Icons/dynamic-layers-16x16.png")) + Source = new Bitmap(App.GetAsset("/Assets/Icons/history-16x16.png")) + } + }, + new() + { + Tag = new OperationDoubleExposure(), + Icon = new Avalonia.Controls.Image + { + Source = new Bitmap(App.GetAsset("/Assets/Icons/equals-16x16.png")) } }, new() @@ -224,6 +232,14 @@ namespace UVtools.WPF }, new() { + Tag = new OperationDynamicLayerHeight(), + Icon = new Avalonia.Controls.Image + { + Source = new Bitmap(App.GetAsset("/Assets/Icons/dynamic-layers-16x16.png")) + } + }, + new() + { Tag = new OperationLayerReHeight(), Icon = new Avalonia.Controls.Image { @@ -407,6 +423,9 @@ namespace UVtools.WPF private bool _isGUIEnabled = true; private uint _savesCount; private bool _canSave; + private MenuItem _menuFileSendTo; + private MenuItem[] _menuFileOpenRecentItems; + private MenuItem[] _menuFileSendToItems; private MenuItem[] _menuFileConvertItems; private PointerEventArgs _globalPointerEventArgs; @@ -501,6 +520,18 @@ namespace UVtools.WPF set => RaiseAndSetIfChanged(ref _canSave, value); } + public MenuItem[] MenuFileOpenRecentItems + { + get => _menuFileOpenRecentItems; + set => RaiseAndSetIfChanged(ref _menuFileOpenRecentItems, value); + } + + public MenuItem[] MenuFileSendToItems + { + get => _menuFileSendToItems; + set => RaiseAndSetIfChanged(ref _menuFileSendToItems, value); + } + public MenuItem[] MenuFileConvertItems { get => _menuFileConvertItems; @@ -522,7 +553,8 @@ namespace UVtools.WPF InitClipboardLayers(); InitLayerPreview(); - + RefreshRecentFiles(true); + TabInformation = this.FindControl<TabItem>("TabInformation"); TabGCode = this.FindControl<TabItem>("TabGCode"); TabIssues = this.FindControl<TabItem>("TabIssues"); @@ -566,6 +598,94 @@ namespace UVtools.WPF { ProcessFiles(e.Data.GetFileNames().ToArray()); }); + + _menuFileSendTo = this.FindControl<MenuItem>("MainMenu.File.SendTo"); + this.FindControl<MenuItem>("MainMenu.File").SubmenuOpened += (sender, e) => + { + if (!IsFileLoaded) return; + + var menuItems = new List<MenuItem>(); + + var drives = DriveInfo.GetDrives(); + + foreach (var drive in drives) + { + if(drive.DriveType != DriveType.Removable || !drive.IsReady) continue; // Not our target, skip + if (SlicerFile.FileFullPath.StartsWith(drive.Name)) continue; // File already on this device, skip + + var header = drive.Name; + if (!string.IsNullOrWhiteSpace(drive.VolumeLabel)) + { + header += $" {drive.VolumeLabel}"; + } + + header += $" ({SizeExtensions.SizeSuffix(drive.AvailableFreeSpace)}) [{drive.DriveFormat}]"; + + var menuItem = new MenuItem + { + Header = header, + Tag = drive, + }; + menuItem.Click += FileSendToItemClick; + + menuItems.Add(menuItem); + } + + MenuFileSendToItems = menuItems.ToArray(); + _menuFileSendTo.IsVisible = _menuFileSendTo.IsEnabled = menuItems.Count > 0; + }; + } + + private async void FileSendToItemClick(object? sender, RoutedEventArgs e) + { + if (sender is not MenuItem menuItem) return; + if (menuItem.Tag is not DriveInfo drive) return; + + if (!drive.IsReady) + { + await this.MessageBoxError($"The device {drive.Name} is not ready/available at this time.", "Unable to copy the file"); + return; + } + + if (CanSave) + { + switch (await this.MessageBoxQuestion("There are unsaved changes. Do you want to save the current file before copy it over?\n\n" + + "Yes: Save the current file and copy it over.\n" + + "No: Copy the file without current modifications.\n" + + "Cancel: Abort the operation.", "Send to - Unsaved changes", ButtonEnum.YesNoCancel)) + { + case ButtonResult.Yes: + await SaveFile(true); + break; + case ButtonResult.No: + break; + default: + return; + } + } + + ShowProgressWindow($"Copying: {SlicerFile.Filename} to {drive.Name}", false); + Progress.ItemName = "Copying"; + await Task.Factory.StartNew(() => + { + try + { + File.Copy(SlicerFile.FileFullPath, $"{drive.Name}{SlicerFile.Filename}", true); + return true; + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + Dispatcher.UIThread.InvokeAsync(async () => + await this.MessageBoxError(exception.ToString(), "Unable to copy the file")); + } + + return false; + }); + + IsGUIEnabled = true; } protected override void OnOpened(EventArgs e) @@ -1083,6 +1203,8 @@ namespace UVtools.WPF return; } + AddRecentFile(fileName); + if (SlicerFile.LayerCount == 0) { await this.MessageBoxError("It seems this file has no layers. Possible causes could be:\n" + @@ -1144,6 +1266,7 @@ namespace UVtools.WPF if (task && convertedFile is not null) { SlicerFile = convertedFile; + AddRecentFile(SlicerFile.FileFullPath); } } } @@ -1325,6 +1448,12 @@ namespace UVtools.WPF } SlicerFile.PropertyChanged += SlicerFileOnPropertyChanged; +#if !DEBUG + if (SlicerFile is CTBEncryptedFile) + { + await this.MessageBoxInfo(CTBEncryptedFile.Preamble, "Information"); + } +#endif } private void SlicerFileOnPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -1448,7 +1577,7 @@ namespace UVtools.WPF } - + public async Task<bool> SaveFile(bool ignoreOverwriteWarning) => await SaveFile(null, ignoreOverwriteWarning); public async Task<bool> SaveFile(string filepath = null, bool ignoreOverwriteWarning = false) { @@ -1470,7 +1599,7 @@ namespace UVtools.WPF IsGUIEnabled = false; ShowProgressWindow($"Saving {Path.GetFileName(filepath)}"); - + var task = await Task.Factory.StartNew( () => { try @@ -1575,7 +1704,7 @@ namespace UVtools.WPF } - #region Operations +#region Operations public async Task<Operation> ShowRunOperation(Type type, Operation loadOperation = null) { var operation = await ShowOperation(type, loadOperation); @@ -1735,10 +1864,102 @@ namespace UVtools.WPF return result; } - #endregion + private void RefreshRecentFiles(bool reloadFiles = false) + { + if(reloadFiles) RecentFiles.Load(); + + var items = new List<MenuItem>(); + + foreach (var file in RecentFiles.Instance) + { + var item = new MenuItem + { + Header = Path.GetFileName(file), + Tag = file, + IsEnabled = SlicerFile?.FileFullPath != file + }; + ToolTip.SetTip(item, file); + ToolTip.SetPlacement(item, PlacementMode.Right); + ToolTip.SetShowDelay(item, 100); + items.Add(item); + + item.Click += MenuFileOpenRecentItemOnClick; + } + + MenuFileOpenRecentItems = items.ToArray(); + } + + private void AddRecentFile(string file) + { + if (file == Path.Combine(App.ApplicationPath, About.DemoFile)) return; + RecentFiles.Load(); + RecentFiles.Instance.Insert(0, file); + RecentFiles.Save(); + RefreshRecentFiles(); + } + + private async void MenuFileOpenRecentItemOnClick(object? sender, RoutedEventArgs e) + { + if (sender is not MenuItem { Tag: string file }) return; + if (IsFileLoaded && SlicerFile.FileFullPath == file) return; + + if (_globalModifiers == KeyModifiers.Control) + { + if (await this.MessageBoxQuestion("Are you sure you want to purge the non-existing files from the recent list?", + "Purge the non-existing files?") == ButtonResult.Yes) + { + /*if (_globalModifiers == KeyModifiers.Shift) + { + RecentFiles.ClearFiles(true); + RefreshRecentFiles(); + return; + }*/ + if (RecentFiles.PurgenNonExistingFiles(true) > 0) RefreshRecentFiles(); + } + + return; + } + + if ((_globalModifiers & KeyModifiers.Control) != 0 && + (_globalModifiers & KeyModifiers.Shift) != 0) + { + if (await this.MessageBoxQuestion($"Are you sure you want to remove the selected file from the recent list?\n{file}", + "Remove the file from recent list?") == ButtonResult.Yes) + { + RecentFiles.Load(); + RecentFiles.Instance.Remove(file); + RecentFiles.Save(); + + RefreshRecentFiles(); + } + + return; + } + + if (!File.Exists(file)) + { + if (await this.MessageBoxQuestion($"The file: {file} does not exists anymore.\n" + + "Do you want to remove this file from recent list?", + "The file does not exists") == ButtonResult.Yes) + { + RecentFiles.Load(); + RecentFiles.Instance.Remove(file); + RecentFiles.Save(); + + RefreshRecentFiles(); + } + + return; + } + + if (_globalModifiers == KeyModifiers.Shift) App.NewInstance(file); + else ProcessFile(file); + } + +#endregion - #endregion +#endregion } } diff --git a/UVtools.WPF/Structures/OperationProfiles.cs b/UVtools.WPF/Structures/OperationProfiles.cs index 479e196..982fd16 100644 --- a/UVtools.WPF/Structures/OperationProfiles.cs +++ b/UVtools.WPF/Structures/OperationProfiles.cs @@ -37,6 +37,8 @@ namespace UVtools.WPF.Structures [XmlElement(typeof(OperationPixelDimming))] [XmlElement(typeof(OperationInfill))] [XmlElement(typeof(OperationBlur))] + [XmlElement(typeof(OperationFadeExposureTime))] + [XmlElement(typeof(OperationDoubleExposure))] [XmlElement(typeof(OperationDynamicLayerHeight))] [XmlElement(typeof(OperationDynamicLifts))] [XmlElement(typeof(OperationRaiseOnPrintFinish))] diff --git a/UVtools.WPF/Structures/RecentFiles.cs b/UVtools.WPF/Structures/RecentFiles.cs new file mode 100644 index 0000000..35d32dd --- /dev/null +++ b/UVtools.WPF/Structures/RecentFiles.cs @@ -0,0 +1,201 @@ +/* + * 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace UVtools.WPF.Structures +{ + public class RecentFiles : IList<string> + { + #region Properties + /// <summary> + /// Default filepath for store + /// </summary> + private static string FilePath => Path.Combine(UserSettings.SettingsFolder, "recentfiles.dat"); + + private readonly List<string> _files = new(); + + public byte MaxEntries { get; set; } = 40; + + #endregion + + #region Singleton + + private static Lazy<RecentFiles> _instanceHolder = + new(() => new RecentFiles()); + + /// <summary> + /// Instance of <see cref="UserSettings"/> (singleton) + /// </summary> + public static RecentFiles Instance => _instanceHolder.Value; + + //public static List<Operation> Operations => _instance.Operations; + #endregion + + #region Constructor + + private RecentFiles() + { } + + #endregion + + #region Static Methods + /// <summary> + /// Clear all files + /// </summary> + /// <param name="save">True to save settings on file, otherwise false</param> + public static void ClearFiles(bool save = true) + { + Instance.Clear(); + if (save) Save(); + } + + /// <summary> + /// Load settings from file + /// </summary> + public static void Load() + { + if (!File.Exists(FilePath)) + { + return; + } + + Instance.Clear(); + + try + { + using var tr = new StreamReader(FilePath); + + string path; + while ((path = tr.ReadLine()) is not null) + { + if(string.IsNullOrWhiteSpace(path)) continue; + + try + { + Path.GetFullPath(path); + } + catch (Exception e) + { + continue; + } + + Instance.Add(path); + } + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + } + } + + /// <summary> + /// Save settings to file + /// </summary> + public static void Save() + { + try + { + using var tw = new StreamWriter(FilePath); + + foreach (var file in Instance) + { + tw.WriteLine(file); + } + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + } + } + + public static int PurgenNonExistingFiles(bool save = true) + { + Load(); + var count = Instance.RemoveAll(s => !File.Exists(s)); + if(save && count > 0) Save(); + return count; + } + + #endregion + + #region List Implementation + public IEnumerator<string> GetEnumerator() + { + return _files.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_files).GetEnumerator(); + } + + public void Add(string item) + { + _files.RemoveAll(s => s == item); + if (Count >= MaxEntries) return; + _files.Add(item); + } + + public void Clear() + { + _files.Clear(); + } + + public bool Contains(string item) + { + return _files.Contains(item); + } + + public void CopyTo(string[] array, int arrayIndex) + { + _files.CopyTo(array, arrayIndex); + } + + public bool Remove(string item) + { + return _files.Remove(item); + } + + public int Count => _files.Count; + + public bool IsReadOnly => ((ICollection<string>)_files).IsReadOnly; + + public int IndexOf(string item) + { + return _files.IndexOf(item); + } + + public void Insert(int index, string item) + { + _files.RemoveAll(s => s == item); + _files.Insert(index, item); + while (Count > MaxEntries) + { + RemoveAt(Count-1); + } + } + + public void RemoveAt(int index) + { + _files.RemoveAt(index); + } + + public int RemoveAll(Predicate<string> match) => _files.RemoveAll(match); + + public string this[int index] + { + get => _files[index]; + set => _files[index] = value; + } + #endregion + } +} diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 3f1d96f..fb90318 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.20.5</Version> + <Version>2.21.0</Version> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> diff --git a/UVtools.WPF/Windows/ToolWindow.axaml b/UVtools.WPF/Windows/ToolWindow.axaml index e8959f5..299b4a8 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml +++ b/UVtools.WPF/Windows/ToolWindow.axaml @@ -53,6 +53,7 @@ VerticalAlignment="Center" HorizontalAlignment="Right" IsChecked="{Binding LayerRangeSync}" + IsVisible="{Binding LayerIndexEndEnabled}" ToolTip.Tip="Synchronize and lock the layer range for single layer navigation" Content="Synchronize"/> </Grid> @@ -93,9 +94,14 @@ VerticalAlignment="Center" Minimum="0" Maximum="{Binding MaximumLayerIndex}" - IsEnabled="{Binding !LayerRangeSync}" - Value="{Binding LayerIndexEnd}" - /> + Value="{Binding LayerIndexEnd}"> + <NumericUpDown.IsEnabled> + <MultiBinding Converter="{x:Static BoolConverters.And}"> + <Binding Path="LayerIndexEndEnabled"/> + <Binding Path="!LayerRangeSync"/> + </MultiBinding> + </NumericUpDown.IsEnabled> + </NumericUpDown> <Button Name="LayerSelectPresetButton" Grid.Row="0" diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs index cd7a6fe..c43aa92 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml.cs +++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs @@ -38,6 +38,7 @@ namespace UVtools.WPF.Windows private bool _layerRangeSync; private uint _layerIndexStart; private uint _layerIndexEnd; + private bool _layerIndexEndEnabled = true; private bool _isROIVisible; private bool _isMasksVisible; @@ -110,13 +111,14 @@ namespace UVtools.WPF.Windows get => _layerIndexStart; set { + value = Math.Min(value, SlicerFile.LastLayerIndex); + if (ToolControl?.BaseOperation is not null) { ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; ToolControl.BaseOperation.LayerIndexStart = value; } - value = value.Clamp(0, SlicerFile.LastLayerIndex); if (!RaiseAndSetIfChanged(ref _layerIndexStart, value)) return; RaisePropertyChanged(nameof(LayerStartMM)); RaisePropertyChanged(nameof(LayerRangeCountStr)); @@ -137,13 +139,14 @@ namespace UVtools.WPF.Windows get => _layerIndexEnd; set { + value = Math.Min(value, SlicerFile.LastLayerIndex); + if (ToolControl?.BaseOperation is not null) { ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.None; ToolControl.BaseOperation.LayerIndexEnd = value; } - value = value.Clamp(0, SlicerFile.LastLayerIndex); if (!RaiseAndSetIfChanged(ref _layerIndexEnd, value)) return; RaisePropertyChanged(nameof(LayerEndMM)); RaisePropertyChanged(nameof(LayerRangeCountStr)); @@ -151,7 +154,14 @@ namespace UVtools.WPF.Windows } public float LayerEndMM => SlicerFile[_layerIndexEnd].PositionZ; - + + public bool LayerIndexEndEnabled + { + get => _layerIndexEndEnabled; + set => RaiseAndSetIfChanged(ref _layerIndexEndEnabled, value); + } + + public string LayerRangeCountStr { get @@ -198,14 +208,14 @@ namespace UVtools.WPF.Windows public void SelectBottomLayers() { LayerIndexStart = 0; - LayerIndexEnd = SlicerFile.BottomLayerCount-1u; + LayerIndexEnd = Math.Max(1, SlicerFile.FirstNormalLayer?.Index ?? 1) - 1u; if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Bottom; } public void SelectNormalLayers() { - LayerIndexStart = SlicerFile.BottomLayerCount; + LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; LayerIndexEnd = MaximumLayerIndex; if (ToolControl is not null) ToolControl.BaseOperation.LayerRangeSelection = Enumerations.LayerRangeSelection.Normal; @@ -571,13 +581,14 @@ namespace UVtools.WPF.Windows } } - public ToolWindow(string description = null, bool layerRangeVisible = true) : this() + public ToolWindow(string description = null, bool layerRangeVisible = true, bool layerEndIndexEnabled = true) : this() { _description = description; _layerRangeVisible = layerRangeVisible; + _layerIndexEndEnabled = layerEndIndexEnabled; } - public ToolWindow(ToolControl toolControl) : this(toolControl.BaseOperation.Description, toolControl.BaseOperation.StartLayerRangeSelection != Enumerations.LayerRangeSelection.None) + public ToolWindow(ToolControl toolControl) : this(toolControl.BaseOperation.Description, toolControl.BaseOperation.StartLayerRangeSelection != Enumerations.LayerRangeSelection.None, toolControl.BaseOperation.LayerIndexEndEnabled) { ToolControl = toolControl; toolControl.ParentWindow = this; diff --git a/build/CreateRelease.WPF.ps1 b/build/CreateRelease.WPF.ps1 index 4985e72..f1fc18f 100644 --- a/build/CreateRelease.WPF.ps1 +++ b/build/CreateRelease.WPF.ps1 @@ -34,7 +34,7 @@ Set-Location $PSScriptRoot\.. #################################### $enableMSI = $true #$buildOnly = 'win-x64' -#$buildOnly = 'linux-x64' +#$buildOnly = 'osx-x64' $enableNugetPublish = $true # Profilling $stopWatch = New-Object -TypeName System.Diagnostics.Stopwatch |