diff --git a/CHANGELOG.md b/CHANGELOG.md index 1797a493..06bbe51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## 16/05/2022 - v3.4.2 + +- **Core:** + - (Add) Getter `FileFormat.DisplayPixelCount` Gets the display total number of pixels (ResolutionX * ResolutionY) + - (Add) Getter `Layer.NonZeroPixelRatio` Gets the ratio between non zero pixels and display number of pixels + - (Add) Getter `Layer.NonZeroPixelPercentage` Gets the percentage of non zero pixels relative to the display number of pixels + - (Add) Getter `Layer.PreviousHeightLayer()` Gets the previous layer with a different height from the current, returns null if no previous layer + - (Add) Getter `Layer.NextHeightLayer()` Gets the next layer with a different height from the current, returns null if no next layer + - (Add) Method `Layer.GetPreviousLayerWithAtLeastPixelCountOf()` Gets the previous layer matching at least a number of pixels, returns null if no previous layer + - (Add) Method `Layer.GetNextLayerWithAtLeastPixelCountOf()` Gets the next layer matching at least a number of pixels, returns null if no next layer + - (Add) Method `Operation.GetRoiOrVolumeBounds()` returns the selected ROI rectangle or model volume bounds rectangle + - (Add) Documentation around `Operation` methods + - (Fix) Open files in partial mode when the resolution is not defined would cause a `NullPointerException` (#474) +- **Suggestion: Wait time before cure** + - (Add) Proportional maximum time change: Sets the maximum allowed time difference relative to the previous layer (#471) + - (Add) Proportional mass get modes: Previous, Average and Maximum relative to a defined height (#471) + - (Change) Proportional set type sets fallback time to the first layer + - (Fix) Proportional set type was taking current layer mass instead of looking to the previous cured layer (#471) +- **Tools:** + - **Edit print parameters:** + - (Change) Incorporate the unit label into the numeric input box + - (Change) Allow TSMC speeds to be 0 as minimum value (#472) + - (Fix) PCB Exposure: The thumbnail has random noise around the image +- **Settings:** + - (Add) Tools: "Always prompt for confirmation before execute the operation" + - (Fix) Changing layer compression method when no file is loaded would cause a error +- **UI:** + - (Add) Holding Shift key while drag and drop a .uvtop file will try to execute the operation without showing the window or prompt + - (Add) Drag and drop a .cs or .csx file into UVtools will load and show the scripting dialog with the file selected +- (Add) Errors that crash application will now show an report window with the crash information and able to fast report them +- (Add) "Version" key and value on registry to tell the current installed version (Windows MSI only) +- (Upgrade) AvaloniaUI from 0.10.13 to 0.10.14 +- (Upgrade) .NET from 6.0.4 to 6.0.5 +- ## 02/05/2022 - v3.4.1 - (Add) Suggestion - Wait time before cure: Allow to set the number of layers to smooth transition from bottom to normal wait time (Defaults to 8) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 516ea107..d889774a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,32 @@ -- (Add) Suggestion - Wait time before cure: Allow to set the number of layers to smooth transition from bottom to normal wait time (Defaults to 8) -- (Fix) Tool - PCB Exposure: Pixels per millimeter was been set to fixed value instead of use printer lcd pitch, causing wrong dimentions on different from 50µm pitch -- (Fix) Tool - PCB Exposure: Unable to run the tool when the display size information isn't available +- **Core:** + - (Add) Getter `FileFormat.DisplayPixelCount` Gets the display total number of pixels (ResolutionX * ResolutionY) + - (Add) Getter `Layer.NonZeroPixelRatio` Gets the ratio between non zero pixels and display number of pixels + - (Add) Getter `Layer.NonZeroPixelPercentage` Gets the percentage of non zero pixels relative to the display number of pixels + - (Add) Getter `Layer.PreviousHeightLayer()` Gets the previous layer with a different height from the current, returns null if no previous layer + - (Add) Getter `Layer.NextHeightLayer()` Gets the next layer with a different height from the current, returns null if no next layer + - (Add) Method `Layer.GetPreviousLayerWithAtLeastPixelCountOf()` Gets the previous layer matching at least a number of pixels, returns null if no previous layer + - (Add) Method `Layer.GetNextLayerWithAtLeastPixelCountOf()` Gets the next layer matching at least a number of pixels, returns null if no next layer + - (Add) Method `Operation.GetRoiOrVolumeBounds()` returns the selected ROI rectangle or model volume bounds rectangle + - (Add) Documentation around `Operation` methods + - (Fix) Open files in partial mode when the resolution is not defined would cause a `NullPointerException` (#474) +- **Suggestion: Wait time before cure** + - (Add) Proportional maximum time change: Sets the maximum allowed time difference relative to the previous layer (#471) + - (Add) Proportional mass get modes: Previous, Average and Maximum relative to a defined height (#471) + - (Change) Proportional set type sets fallback time to the first layer + - (Fix) Proportional set type was taking current layer mass instead of looking to the previous cured layer (#471) +- **Tools:** + - **Edit print parameters:** + - (Change) Incorporate the unit label into the numeric input box + - (Change) Allow TSMC speeds to be 0 as minimum value (#472) + - (Fix) PCB Exposure: The thumbnail has random noise around the image +- **Settings:** + - (Add) Tools: "Always prompt for confirmation before execute the operation" + - (Fix) Changing layer compression method when no file is loaded would cause a error +- **UI:** + - (Add) Holding Shift key while drag and drop a .uvtop file will try to execute the operation without showing the window or prompt + - (Add) Drag and drop a .cs or .csx file into UVtools will load and show the scripting dialog with the file selected +- (Add) Errors that crash application will now show an report window with the crash information and able to fast report them +- (Upgrade) AvaloniaUI from 0.10.13 to 0.10.14 +- (Upgrade) .NET from 6.0.4 to 6.0.5 +- diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index a3f87224..45106a35 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -38,7 +38,7 @@ - + diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 5c506ae9..833fd8de 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -489,7 +489,7 @@ public static Mat CropByBounds(this Mat src, Size margin) using var roi = src.Size == rect.Size ? src.Roi(src.Size) : src.Roi(rect); var numberOfChannels = roi.NumberOfChannels; - var cropped = new Mat(roi.Rows + margin.Height * 2, roi.Cols + margin.Width * 2, roi.Depth, numberOfChannels); + var cropped = InitMat(new Size(roi.Width + margin.Width * 2, roi.Height + margin.Height * 2), numberOfChannels, roi.Depth); using var dest = new Mat(cropped, new Rectangle(margin.Width, margin.Height, roi.Width, roi.Height)); roi.CopyTo(dest); diff --git a/UVtools.Core/FileFormats/CWSFile.cs b/UVtools.Core/FileFormats/CWSFile.cs index 8fb5f6af..b32d673c 100644 --- a/UVtools.Core/FileFormats/CWSFile.cs +++ b/UVtools.Core/FileFormats/CWSFile.cs @@ -18,7 +18,6 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Xml.Serialization; using UVtools.Core.Converters; using UVtools.Core.Extensions; diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 69ec05a6..b62099d6 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -172,7 +172,7 @@ public class PrintParameterModifier #region Instances public static PrintParameterModifier PositionZ { get; } = new ("Position Z", "Absolute Z position", "mm", 0, 100000, 0.01, Layer.HeightPrecision); public static PrintParameterModifier BottomLayerCount { get; } = new ("Bottom layer count", "Number of bottom/burn-in layers", "layers", 0, ushort.MaxValue, 1, 0); - public static PrintParameterModifier TransitionLayerCount { get; } = new ("Transition layer count", "Number of transition layers", "layers",0, ushort.MaxValue, 1, 0); + public static PrintParameterModifier TransitionLayerCount { get; } = new ("Transition layer count", "Number of fade/transition layers", "layers",0, ushort.MaxValue, 1, 0); public static PrintParameterModifier BottomLightOffDelay { get; } = new("Bottom light-off seconds", "Total motor movement time + rest time to wait before cure a new bottom layer", "s"); public static PrintParameterModifier LightOffDelay { get; } = new("Light-off seconds", "Total motor movement time + rest time to wait before cure a new layer", "s"); @@ -180,8 +180,8 @@ public class PrintParameterModifier public static PrintParameterModifier BottomWaitTimeBeforeCure { get; } = new ("Bottom wait before cure", "Time to wait/rest before cure a new bottom layer\nChitubox: Rest after retract\nLychee: Wait before print", "s"); public static PrintParameterModifier WaitTimeBeforeCure { get; } = new ("Wait before cure", "Time to wait/rest before cure a new layer\nChitubox: Rest after retract\nLychee: Wait before print", "s"); - public static PrintParameterModifier BottomExposureTime { get; } = new ("Bottom exposure time", "Bottom layers cure time", "s", 0.1M); - public static PrintParameterModifier ExposureTime { get; } = new ("Exposure time", "Layers cure time", "s", 0.1M); + public static PrintParameterModifier BottomExposureTime { get; } = new ("Bottom exposure time", "Bottom layers exposure time", "s", 0.1M); + public static PrintParameterModifier ExposureTime { get; } = new ("Exposure time", "Normal layers exposure time", "s", 0.1M); public static PrintParameterModifier BottomWaitTimeAfterCure { get; } = new("Bottom wait after cure", "Time to wait/rest after cure a new bottom layer\nChitubox: Rest before lift\nLychee: Wait after print", "s"); public static PrintParameterModifier WaitTimeAfterCure { get; } = new("Wait after cure", "Time to wait/rest after cure a new bottom layer\nChitubox: Rest before lift\nLychee: Wait after print", "s"); @@ -189,14 +189,14 @@ public class PrintParameterModifier public static PrintParameterModifier BottomLiftHeight { get; } = new ("Bottom lift height", "Bottom lift/peel height between layers", "mm"); public static PrintParameterModifier LiftHeight { get; } = new ("Lift height", @"Lift/peel height between layers", "mm"); - public static PrintParameterModifier BottomLiftSpeed { get; } = new ("Bottom lift speed", null, "mm/min", 10, 5000, 5); - public static PrintParameterModifier LiftSpeed { get; } = new ("Lift speed", null, "mm/min", 10, 5000, 5); + public static PrintParameterModifier BottomLiftSpeed { get; } = new ("Bottom lift speed", "Lift speed of bottom layers", "mm/min", 10, 5000, 5); + public static PrintParameterModifier LiftSpeed { get; } = new ("Lift speed", "Lift speed of normal layers", "mm/min", 10, 5000, 5); public static PrintParameterModifier BottomLiftHeight2 { get; } = new("2) Bottom lift height", "Bottom second lift/peel height between layers", "mm"); public static PrintParameterModifier LiftHeight2 { get; } = new("2) Lift height", @"Second lift/peel height between layers", "mm"); - public static PrintParameterModifier BottomLiftSpeed2 { get; } = new("2) Bottom lift speed", null, "mm/min", 10, 5000, 5); - public static PrintParameterModifier LiftSpeed2 { get; } = new("2) Lift speed", null, "mm/min", 10, 5000, 5); + public static PrintParameterModifier BottomLiftSpeed2 { get; } = new("2) Bottom lift speed", "Lift speed of bottom layers for the second lift sequence (TSMC)", "mm/min", 0, 5000, 5); + public static PrintParameterModifier LiftSpeed2 { get; } = new("2) Lift speed", "Lift speed of normal layers for the second lift sequence (TSMC)", "mm/min", 0, 5000, 5); public static PrintParameterModifier BottomWaitTimeAfterLift { get; } = new("Bottom wait after lift", "Time to wait/rest after a lift/peel sequence at bottom layers\nChitubox: Rest after lift\nLychee: Wait after lift", "s"); public static PrintParameterModifier WaitTimeAfterLift { get; } = new("Wait after lift", "Time to wait/rest after a lift/peel sequence at layers\nChitubox: Rest after lift\nLychee: Wait after lift", "s"); @@ -204,10 +204,10 @@ public class PrintParameterModifier public static PrintParameterModifier BottomRetractSpeed { get; } = new ("Bottom retract speed", "Bottom down speed from lift height to next layer cure position", "mm/min", 10, 5000, 5); public static PrintParameterModifier RetractSpeed { get; } = new ("Retract speed", "Down speed from lift height to next layer cure position", "mm/min", 10, 5000, 5); - public static PrintParameterModifier BottomRetractHeight2 { get; } = new("2) Bottom retract height", null, "mm"); - public static PrintParameterModifier RetractHeight2 { get; } = new("2) Retract height", null, "mm"); - public static PrintParameterModifier BottomRetractSpeed2 { get; } = new("2) Bottom retract speed", null, "mm/min", 10, 5000, 5); - public static PrintParameterModifier RetractSpeed2 { get; } = new("2) Retract speed", null, "mm/min", 10, 5000, 5); + public static PrintParameterModifier BottomRetractHeight2 { get; } = new("2) Bottom retract height", "Slow retract height of bottom layers (TSMC)", "mm"); + public static PrintParameterModifier RetractHeight2 { get; } = new("2) Retract height", "Slow retract height of normal layers (TSMC)", "mm"); + public static PrintParameterModifier BottomRetractSpeed2 { get; } = new("2) Bottom retract speed", "Slow retract speed of bottom layers (TSMC)", "mm/min", 0, 5000, 5); + public static PrintParameterModifier RetractSpeed2 { get; } = new("2) Retract speed", "Slow retract speed of normal layers (TSMC)", "mm/min", 0, 5000, 5); public static PrintParameterModifier BottomLightPWM { get; } = new ("Bottom light PWM", "UV LED power for bottom layers", "☀", 1, byte.MaxValue, 5, 0); public static PrintParameterModifier LightPWM { get; } = new ("Light PWM", "UV LED power for layers", "☀", 1, byte.MaxValue, 5, 0); @@ -1428,7 +1428,7 @@ public bool IsModified } /// - /// Gets the bounding rectangle of the object + /// Gets the bounding rectangle of the model /// public Rectangle BoundingRectangle { @@ -1492,6 +1492,11 @@ public Size Resolution /// public abstract uint ResolutionY { get; set; } + /// + /// Gets the display total number of pixels ( * ) + /// + public uint DisplayPixelCount => ResolutionX * ResolutionY; + /// /// Gets the size of display in millimeters /// @@ -5320,7 +5325,7 @@ public bool Sanitize() throw new InvalidDataException($"Layer {layerIndex - 1} ({this[layerIndex - 1].PositionZ}mm) have a higher Z position than the successor layer {layerIndex} ({this[layerIndex].PositionZ}mm).\n"); } - if (ResolutionX == 0 || ResolutionY == 0) + if ((ResolutionX == 0 || ResolutionY == 0) && DecodeType == FileDecodeType.Full) { var layer = FirstLayer; if (layer is not null) diff --git a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs index 43ab3a4e..8ad7e920 100644 --- a/UVtools.Core/FileFormats/PhotonWorkshopFile.cs +++ b/UVtools.Core/FileFormats/PhotonWorkshopFile.cs @@ -623,7 +623,7 @@ private unsafe Mat DecodePWS() if (pixel > imageLength) { image.Dispose(); - throw new FileLoadException("Error image ran off the end"); + throw new FileLoadException("Image ran off the end."); } } } @@ -763,6 +763,12 @@ private Mat DecodePW0() //color &= 0xff; + if (pixelPos + repeat > imageLength) + { + mat.Dispose(); + throw new FileLoadException($"Image ran off the end: {pixelPos}+ {repeat} = {pixelPos + repeat}, expecting: {imageLength}"); + } + // We only need to set the non-zero pixels mat.FillSpan(ref pixelPos, repeat, color); @@ -772,18 +778,12 @@ private Mat DecodePW0() //i++; break; } - - if (pixelPos > imageLength) - { - mat.Dispose(); - throw new FileLoadException($"Error image ran off the end: {pixelPos - repeat}({repeat}) of {imageLength}"); - } } if (pixelPos > 0 && pixelPos != imageLength) { mat.Dispose(); - throw new FileLoadException($"Error image ended short: {pixelPos} of {imageLength}"); + throw new FileLoadException($"Image ended short: {pixelPos}, expecting: {imageLength}"); } return mat; diff --git a/UVtools.Core/Layers/Layer.cs b/UVtools.Core/Layers/Layer.cs index e0b56836..6493a532 100644 --- a/UVtools.Core/Layers/Layer.cs +++ b/UVtools.Core/Layers/Layer.cs @@ -98,12 +98,40 @@ public uint NonZeroPixelCount internal set { if (!RaiseAndSetIfChanged(ref _nonZeroPixelCount, value)) return; + RaisePropertyChanged(nameof(NonZeroPixelRatio)); + RaisePropertyChanged(nameof(NonZeroPixelPercentage)); RaisePropertyChanged(nameof(Area)); RaisePropertyChanged(nameof(Volume)); MaterialMilliliters = -1; // Recalculate } } + /// + /// Gets the ratio between non zero pixels and display number of pixels + /// + public double NonZeroPixelRatio + { + get + { + var displayPixelCount = SlicerFile.DisplayPixelCount; + if (displayPixelCount == 0) return double.NaN; + return (double)_nonZeroPixelCount / displayPixelCount; + } + } + + /// + /// Gets the percentage of non zero pixels relative to the display number of pixels + /// + public double NonZeroPixelPercentage + { + get + { + var pixelRatio = NonZeroPixelRatio; + if (double.IsNaN(pixelRatio)) return double.NaN; + return pixelRatio * 100.0; + } + } + /// /// Gets if this layer is empty/all black pixels /// @@ -214,7 +242,37 @@ public Layer? PreviousLayer if (IsFirstLayer || _index > SlicerFile.Count) return null; return SlicerFile[_index - 1]; } + } + + /// + /// Gets the previous layer with a different height from the current, returns null if no previous layer + /// + public Layer? PreviousHeightLayer + { + get + { + if (IsFirstLayer || _index > SlicerFile.Count) return null; + for (int i = (int)_index - 1; i >= 0; i--) + { + if (SlicerFile[i].PositionZ < _positionZ) return SlicerFile[i]; + } + return null; + } + } + + /// + /// Gets the previous layer matching at least pixels, returns null if no previous layer + /// + public Layer? GetPreviousLayerWithAtLeastPixelCountOf(uint numberOfPixels) + { + if (IsFirstLayer || _index > SlicerFile.Count) return null; + for (int i = (int)_index - 1; i >= 0; i--) + { + if (SlicerFile[i].NonZeroPixelCount >= numberOfPixels) return SlicerFile[i]; + } + + return null; } /// @@ -229,6 +287,37 @@ public Layer? NextLayer } } + /// + /// Gets the next layer with a different height from the current, returns null if no next layer + /// + public Layer? NextHeightLayer + { + get + { + if (_index >= SlicerFile.LastLayerIndex) return null; + for (var i = _index + 1; i < SlicerFile.LayerCount; i++) + { + if (SlicerFile[i].PositionZ > _positionZ) return SlicerFile[i]; + } + + return null; + } + } + + /// + /// Gets the next layer matching at least pixels, returns null if no next layer + /// + public Layer? GetNextLayerWithAtLeastPixelCountOf(uint numberOfPixels) + { + if (_index >= SlicerFile.LastLayerIndex) return null; + for (var i = _index + 1; i < SlicerFile.LayerCount; i++) + { + if (SlicerFile[i].NonZeroPixelCount >= numberOfPixels) return SlicerFile[i]; + } + + return null; + } + /// /// Gets the layer index /// diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index 0bafa089..d1b50b42 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -66,6 +66,9 @@ public OperationImportFrom ImportedFrom set => RaiseAndSetIfChanged(ref _importedFrom, value); } + /// + /// Gets or sets the parent + /// [XmlIgnore] public FileFormat SlicerFile { @@ -78,6 +81,9 @@ public FileFormat SlicerFile } } + /// + /// Gets the bounding rectangle of the model, preserved from any change during and after execution + /// [XmlIgnore] public Rectangle OriginalBoundingRectangle { @@ -85,6 +91,9 @@ public Rectangle OriginalBoundingRectangle private set => RaiseAndSetIfChanged(ref _originalBoundingRectangle, value); } + /// + /// Gets or sets any object which is not used internally + /// [XmlIgnore] public object? Tag { get; set; } @@ -93,6 +102,9 @@ public Rectangle OriginalBoundingRectangle /// public string Id => GetType().Name.Remove(0, ClassNameLength); + /// + /// Gets the starting layer selection + /// public virtual LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.All; /// @@ -104,6 +116,9 @@ public LayerRangeSelection LayerRangeSelection set => RaiseAndSetIfChanged(ref _layerRangeSelection, value); } + /// + /// Gets a string representing the layer range, used with profiles + /// public virtual string LayerRangeString { get @@ -228,9 +243,19 @@ public virtual uint LayerIndexEnd } } + /// + /// Gets if any bottom layer is included in the selected layer range + /// public bool LayerRangeHaveBottoms => LayerIndexStart < (SlicerFile.FirstNormalLayer?.Index ?? 0); + + /// + /// Gets if any normal layer is included in the selected layer range + /// public bool LayerRangeHaveNormals => LayerIndexEnd >= (SlicerFile.FirstNormalLayer?.Index ?? 0); + /// + /// Gets the number of selected layers + /// public uint LayerRangeCount => (uint)Math.Max(0, (int)LayerIndexEnd - LayerIndexStart + 1); /// @@ -242,6 +267,9 @@ public string? ProfileName set => RaiseAndSetIfChanged(ref _profileName, value); } + /// + /// Gets if this profile is the default to load + /// public bool ProfileIsDefault { get => _profileIsDefault; @@ -262,6 +290,9 @@ public Rectangle ROI } } + /// + /// Gets if there is an ROI associated + /// public bool HaveROI => !ROI.IsEmpty; /// @@ -279,8 +310,14 @@ public Point[][]? MaskPoints } } + /// + /// Gets if there is masks associated + /// public bool HaveMask => _maskPoints is not null && _maskPoints.Length > 0; + /// + /// Gets if there is roi or masks associated + /// public bool HaveROIorMask => HaveROI || HaveMask; /// @@ -310,6 +347,9 @@ protected Operation(FileFormat slicerFile) : this() #region Methods + /// + /// Gets if the operation can spawn + /// public bool CanSpawn => string.IsNullOrWhiteSpace(ValidateSpawn()); /// @@ -326,6 +366,10 @@ public bool ValidateSpawn(out string? message) return string.IsNullOrWhiteSpace(message); } + /// + /// Validates the operation, return null or empty if validates + /// + /// public virtual string? ValidateInternally() { if (!ValidateSpawn(out var message)) @@ -345,11 +389,18 @@ public bool ValidateSpawn(out string? message) return ValidateInternally(); } + /// + /// Gets if the operation is able to execute + /// + /// public bool CanValidate() { return string.IsNullOrWhiteSpace(Validate()); } + /// + /// Selects all layers from first to last layer + /// public void SelectAllLayers() { LayerIndexStart = 0; @@ -357,12 +408,19 @@ public void SelectAllLayers() LayerRangeSelection = LayerRangeSelection.All; } + /// + /// Selects one layer + /// + /// Layer index to select public void SelectCurrentLayer(uint layerIndex) { LayerIndexStart = LayerIndexEnd = layerIndex; LayerRangeSelection = LayerRangeSelection.Current; } + /// + /// Selects all bottom layers + /// public void SelectBottomLayers() { LayerIndexStart = 0; @@ -370,6 +428,9 @@ public void SelectBottomLayers() LayerRangeSelection = LayerRangeSelection.Bottom; } + /// + /// Selects all normal layers + /// public void SelectNormalLayers() { LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; @@ -377,30 +438,49 @@ public void SelectNormalLayers() LayerRangeSelection = LayerRangeSelection.Normal; } + /// + /// Select the first layer (0) + /// public void SelectFirstLayer() { LayerIndexStart = LayerIndexEnd = 0; LayerRangeSelection = LayerRangeSelection.First; } + /// + /// Select the last layer + /// public void SelectLastLayer() { LayerIndexStart = LayerIndexEnd = SlicerFile.LastLayerIndex; LayerRangeSelection = LayerRangeSelection.Last; } + /// + /// Selects from first to a layer index + /// + /// To layer index to select public void SelectFirstToCurrentLayer(uint currentLayerIndex) { LayerIndexStart = 0; LayerIndexEnd = Math.Min(currentLayerIndex, SlicerFile.LastLayerIndex); } + /// + /// Selects from a layer index to the last layer + /// + /// From layer index to select public void SelectCurrentToLastLayer(uint currentLayerIndex) { LayerIndexStart = Math.Min(currentLayerIndex, SlicerFile.LastLayerIndex); LayerIndexEnd = SlicerFile.LastLayerIndex; } + /// + /// Selects layer given a range type + /// + /// + /// public void SelectLayers(LayerRangeSelection range) { switch (range) @@ -436,67 +516,141 @@ public void SelectLayers(LayerRangeSelection range) /// public virtual void InitWithSlicerFile() { } + /// + /// Clears the ROI and set to empty + /// public void ClearROI() { ROI = Rectangle.Empty; } + /// + /// Clear and + /// public void ClearROIandMasks() { ClearROI(); ClearMasks(); } + /// + /// Set only if not set already + /// + /// ROI to set public void SetROIIfEmpty(Rectangle roi) { if (HaveROI) return; ROI = roi; } + /// + /// Gets the size, but if empty returns the file resolution size instead + /// + /// public Size GetRoiSizeOrDefault() => GetRoiSizeOrDefault(SlicerFile.Resolution); - public Size GetRoiSizeOrDefault(Mat? defaultMat) => defaultMat is null ? GetRoiSizeOrDefault() : GetRoiSizeOrDefault(defaultMat.Size); + + /// + /// Gets the size, but if empty returns size instead + /// + /// + /// + public Size GetRoiSizeOrDefault(Mat? src) => src is null ? GetRoiSizeOrDefault() : GetRoiSizeOrDefault(src.Size); + + /// + /// Gets the size, but if empty returns the size from instead + /// + /// + /// public Size GetRoiSizeOrDefault(Rectangle fallbackRectangle) => GetRoiSizeOrDefault(fallbackRectangle.Size); + + /// + /// Gets the size, but if empty returns the instead + /// + /// + /// public Size GetRoiSizeOrDefault(Size fallbackSize) { return HaveROI ? _roi.Size : fallbackSize; } + /// + /// Gets the size, but if empty returns the model volume bounds size instead + /// + /// public Size GetRoiSizeOrVolumeSize() => GetRoiSizeOrVolumeSize(_originalBoundingRectangle.Size); + /// + /// Gets the size, but if empty returns the instead + /// + /// + /// public Size GetRoiSizeOrVolumeSize(Size fallbackSize) { return HaveROI ? _roi.Size : fallbackSize; } - - public Mat GetRoiOrDefault(Mat defaultMat) + /// + /// Gets a cropped shared from by the , but if empty return the instead + /// + /// + /// + public Mat GetRoiOrDefault(Mat src) { - return HaveROI && defaultMat.Size != _roi.Size ? defaultMat.Roi(_roi) : defaultMat; + return HaveROI && src.Size != _roi.Size ? src.Roi(_roi) : src; } - public Mat GetRoiOrDefault(Mat defaultMat, Rectangle fallbackRoi) + /// + /// Gets a cropped shared from by the , but if empty crop by + /// + /// + /// + /// + public Mat GetRoiOrDefault(Mat src, Rectangle fallbackRoi) { - if (HaveROI && defaultMat.Size != _roi.Size) return defaultMat.Roi(_roi); - if (fallbackRoi.IsEmpty) return defaultMat; - return defaultMat.Size != fallbackRoi.Size ? defaultMat.Roi(fallbackRoi) : defaultMat; + if (HaveROI && src.Size != _roi.Size) return src.Roi(_roi); + if (fallbackRoi.IsEmpty) return src; + return src.Size != fallbackRoi.Size ? src.Roi(fallbackRoi) : src; } + /// + /// Gets a cropped shared from by the , but if empty crop by + /// + /// + /// public Mat GetRoiOrVolumeBounds(Mat defaultMat) { return GetRoiOrDefault(defaultMat, _originalBoundingRectangle); } + /// + /// Gets the , but if empty returns + /// + /// + public Rectangle GetRoiOrVolumeBounds() => HaveROI ? _roi : _originalBoundingRectangle; + + /// + /// Clears all masks + /// public void ClearMasks() { MaskPoints = null; } + /// + /// Sets masks only if they are empty + /// + /// public void SetMasksIfEmpty(Point[][] points) { if (HaveMask) return; MaskPoints = points; } + /// + /// Returns a mask given + /// + /// + /// public Mat? GetMask(Mat mat) => GetMask(_maskPoints, mat); public Mat? GetMask(Point[][]? points, Mat mat) @@ -507,6 +661,12 @@ public void SetMasksIfEmpty(Point[][] points) return GetRoiOrDefault(mask); } + /// + /// Apply a mask to a mat + /// + /// Original untouched mat + /// Mat to modify and apply the mask + /// Mask public void ApplyMask(Mat original, Mat result, Mat? mask) { if (mask is null) return; @@ -538,12 +698,23 @@ public void ApplyMask(Mat original, Mat result) ApplyMask(original, result, mask); } - + /// + /// Execute the operation internally, to be override by class + /// + /// + /// + /// protected virtual bool ExecuteInternally(OperationProgress progress) { throw new NotImplementedException(); } + /// + /// Execute the operation + /// + /// + /// + /// public bool Execute(OperationProgress? progress = null) { if (_slicerFile is null) throw new InvalidOperationException($"{Title} can't execute due the lacking of a file parent."); @@ -566,6 +737,13 @@ public bool Execute(OperationProgress? progress = null) public Task ExecuteAsync(OperationProgress? progress = null) => Task.Run(() => Execute(progress), progress?.Token ?? default); + /// + /// Execute the operation on a given + /// + /// + /// + /// + /// public virtual bool Execute(Mat mat, params object[]? arguments) { throw new NotImplementedException(); @@ -597,12 +775,21 @@ public void CopyConfigurationTo(Operation operation) operation.MaskPoints = MaskPoints; } + /// + /// Serialize class to XML file + /// + /// + /// public void Serialize(string path, bool indent = false) { if(indent) XmlExtensions.SerializeToFile(this, path, XmlExtensions.SettingsIndent); else XmlExtensions.SerializeToFile(this, path); } + /// + /// Clone object + /// + /// public virtual Operation Clone() { var operation = MemberwiseClone() as Operation; @@ -623,6 +810,11 @@ public override string ToString() #region Static Methods + /// + /// Deserialize from a XML file + /// + /// XML file path + /// public static Operation? Deserialize(string path) { if (!File.Exists(path)) return null; @@ -641,6 +833,12 @@ public override string ToString() return Deserialize(path, type); } + /// + /// Deserialize from a XML file + /// + /// XML file path + /// + /// public static Operation Deserialize(string path, Type type) { var serializer = new XmlSerializer(type); @@ -650,6 +848,12 @@ public static Operation Deserialize(string path, Type type) return operation; } + /// + /// Deserialize from a XML file + /// + /// XML file path + /// + /// public static Operation Deserialize(string path, Operation operation) => Deserialize(path, operation.GetType()); #endregion diff --git a/UVtools.Core/Operations/OperationPCBExposure.cs b/UVtools.Core/Operations/OperationPCBExposure.cs index d8c721da..31772639 100644 --- a/UVtools.Core/Operations/OperationPCBExposure.cs +++ b/UVtools.Core/Operations/OperationPCBExposure.cs @@ -195,9 +195,7 @@ public Mat GetMat() protected override bool ExecuteInternally(OperationProgress progress) { using var mat = GetMat(); - var layer = new Layer(mat, SlicerFile); - layer.SetNoDelays(); - + SlicerFile.SuppressRebuildPropertiesWork(() => { SlicerFile.LayerHeight = (float) _layerHeight; @@ -207,7 +205,7 @@ protected override bool ExecuteInternally(OperationProgress progress) SlicerFile.LiftHeightTotal = 0; SlicerFile.SetNoDelays(); - SlicerFile.Layers = new[] { layer }; + SlicerFile.Layers = new[] { new Layer(mat, SlicerFile) }; }, true); diff --git a/UVtools.Core/Operations/OperationScripting.cs b/UVtools.Core/Operations/OperationScripting.cs index ce6835f2..0ec7e6e4 100644 --- a/UVtools.Core/Operations/OperationScripting.cs +++ b/UVtools.Core/Operations/OperationScripting.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using System; +using System.Diagnostics; using System.IO; using System.Xml.Serialization; using UVtools.Core.FileFormats; @@ -176,7 +177,7 @@ public void ReloadScriptFromText(string? text = null) _scriptState = CSharpScript.RunAsync(_scriptText, ScriptOptions.Default.AddReferences(typeof(About).Assembly).WithAllowUnsafe(true), ScriptGlobals).Result; - + var result = _scriptState.ContinueWithAsync("ScriptInit();").Result; RaisePropertyChanged(nameof(CanExecute)); diff --git a/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs b/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs index fa818de3..5c846948 100644 --- a/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs +++ b/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs @@ -18,7 +18,7 @@ namespace UVtools.Core.Suggestions; public sealed class SuggestionWaitTimeBeforeCure : Suggestion { #region Enums - public enum SuggestionWaitTimeBeforeCureSetType + public enum SuggestionWaitTimeBeforeCureSetType : byte { [Description("Fixed: Use a fixed time")] Fixed, @@ -27,10 +27,21 @@ public enum SuggestionWaitTimeBeforeCureSetType [Description("Proportional to layer area")] ProportionalLayerArea, } + + public enum SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom : byte + { + [Description("Previous mass")] + Previous, + [Description("Average of previous masses")] + Average, + [Description("Maximum of previous masses")] + Maximum, + } #endregion #region Members private SuggestionWaitTimeBeforeCureSetType _setType = SuggestionWaitTimeBeforeCureSetType.Fixed; + private decimal _bottomHeight = 1; private decimal _fixedBottomWaitTimeBeforeCure = 20; private decimal _fixedWaitTimeBeforeCure = 2; @@ -41,12 +52,16 @@ public enum SuggestionWaitTimeBeforeCureSetType private uint _proportionalLayerPixels = 1000000; private uint _proportionalBottomLayerArea = 1000; private uint _proportionalLayerArea = 1000; + private decimal _proportionalBottomWaitTimeBeforeCureMaximumDifference = 1; + private decimal _proportionalWaitTimeBeforeCureMaximumDifference = 1; + private SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom _proportionalCalculateMassFrom = SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous; + private decimal _proportionalMassRelativeHeight = 0.2m; private decimal _minimumBottomWaitTimeBeforeCure = 5; private decimal _minimumWaitTimeBeforeCure = 1; private decimal _maximumBottomWaitTimeBeforeCure = 120; private decimal _maximumWaitTimeBeforeCure = 12; private bool _createEmptyFirstLayer = true; - + #endregion #region Properties @@ -154,11 +169,29 @@ public SuggestionWaitTimeBeforeCureSetType SetType public bool IsSetTypeFixed => _setType == SuggestionWaitTimeBeforeCureSetType.Fixed; public bool IsSetTypeProportionalLayerPixels => _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels; public bool IsSetTypeProportionalLayerArea => _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea; - + + public SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom ProportionalCalculateMassFrom + { + get => _proportionalCalculateMassFrom; + set + { + if (!RaiseAndSetIfChanged(ref _proportionalCalculateMassFrom, value)) return; + RaisePropertyChanged(nameof(IsProportionalCalculateMassFromPrevious)); + } + } + + public decimal ProportionalMassRelativeHeight + { + get => _proportionalMassRelativeHeight; + set => RaiseAndSetIfChanged(ref _proportionalMassRelativeHeight, Math.Max(0, Math.Round(value, Layer.HeightPrecision))); + } + + public bool IsProportionalCalculateMassFromPrevious => _proportionalCalculateMassFrom == SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous; + public decimal BottomHeight { get => _bottomHeight; - set => RaiseAndSetIfChanged(ref _bottomHeight, value); + set => RaiseAndSetIfChanged(ref _bottomHeight, Math.Max(0, value)); } public decimal FixedBottomWaitTimeBeforeCure @@ -166,7 +199,7 @@ public decimal FixedBottomWaitTimeBeforeCure get => _fixedBottomWaitTimeBeforeCure; set { - if(!RaiseAndSetIfChanged(ref _fixedBottomWaitTimeBeforeCure, Math.Round(value, 2))) return; + if(!RaiseAndSetIfChanged(ref _fixedBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2))) return; RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement)); } } @@ -176,7 +209,7 @@ public decimal FixedWaitTimeBeforeCure get => _fixedWaitTimeBeforeCure; set { - if (!RaiseAndSetIfChanged(ref _fixedWaitTimeBeforeCure, Math.Round(value, 2))) return; + if (!RaiseAndSetIfChanged(ref _fixedWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2))) return; RaisePropertyChanged(nameof(WaitTimeBeforeCureTransitionDecrement)); } } @@ -196,13 +229,13 @@ public byte WaitTimeBeforeCureTransitionLayerCount public decimal ProportionalBottomWaitTimeBeforeCure { get => _proportionalBottomWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal ProportionalWaitTimeBeforeCure { get => _proportionalWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } @@ -230,28 +263,40 @@ public uint ProportionalLayerArea set => RaiseAndSetIfChanged(ref _proportionalLayerArea, Math.Max(1, value)); } + public decimal ProportionalBottomWaitTimeBeforeCureMaximumDifference + { + get => _proportionalBottomWaitTimeBeforeCureMaximumDifference; + set => RaiseAndSetIfChanged(ref _proportionalBottomWaitTimeBeforeCureMaximumDifference, Math.Round(Math.Max(0, value), 2)); + } + + public decimal ProportionalWaitTimeBeforeCureMaximumDifference + { + get => _proportionalWaitTimeBeforeCureMaximumDifference; + set => RaiseAndSetIfChanged(ref _proportionalWaitTimeBeforeCureMaximumDifference, Math.Round(Math.Max(0, value), 2)); + } + public decimal MinimumBottomWaitTimeBeforeCure { get => _minimumBottomWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _minimumBottomWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _minimumBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal MinimumWaitTimeBeforeCure { get => _minimumWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _minimumWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _minimumWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal MaximumBottomWaitTimeBeforeCure { get => _maximumBottomWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _maximumBottomWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _maximumBottomWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public decimal MaximumWaitTimeBeforeCure { get => _maximumWaitTimeBeforeCure; - set => RaiseAndSetIfChanged(ref _maximumWaitTimeBeforeCure, Math.Round(value, 2)); + set => RaiseAndSetIfChanged(ref _maximumWaitTimeBeforeCure, Math.Round(Math.Max(0, value), 2)); } public bool CreateEmptyFirstLayer @@ -410,23 +455,97 @@ public float CalculateWaitTime(bool isBottomLayer, Layer? layer = null) } if (layer.NonZeroPixelCount <= 1) return 0; // Empty layer, don't need wait time - - return _setType switch + + float mass = 0; + if (layer.Index > 0 && _proportionalCalculateMassFrom != SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Previous && _proportionalMassRelativeHeight > 0) + { + //var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); // Skip all previous empty layer + //if (previousLayer is not null) layer = previousLayer; + uint count = 0; + + Layer? previousLayer = layer; + + while ((previousLayer = previousLayer!.PreviousLayer) is not null && (layer.PositionZ - previousLayer.PositionZ) <= (float)_proportionalMassRelativeHeight) + { + if(previousLayer.NonZeroPixelCount < 2) continue; // Skip empty layers + + count++; + switch (_proportionalCalculateMassFrom) + { + case SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Average: + mass += _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels + ? previousLayer.NonZeroPixelCount + : previousLayer.GetArea(); + break; + case SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Maximum: + mass = Math.Max(_setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels + ? previousLayer.NonZeroPixelCount + : previousLayer.GetArea(), mass); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (_proportionalCalculateMassFrom == SuggestionWaitTimeBeforeCureProportionalCalculateMassFrom.Average && mass > 0 && count > 0) + { + mass /= count; + } + } + + if (mass <= 0) + { + var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); // Skip all previous empty layer + if (previousLayer is null) + { + return isBottomLayer ? (float)_fixedBottomWaitTimeBeforeCure : (float)_fixedWaitTimeBeforeCure; + } + mass = _setType == SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels + ? previousLayer.NonZeroPixelCount + : previousLayer.GetArea(); + } + + float value = _setType switch { - SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels => (float)Math.Round( - (isBottomLayer - ? layer.NonZeroPixelCount * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerPixels - : layer.NonZeroPixelCount * _proportionalWaitTimeBeforeCure / _proportionalLayerPixels).Clamp( - isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure, - isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure), 2), - SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea => (float) Math.Round( - (isBottomLayer - ? (decimal)layer.GetArea() * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerArea - : (decimal)layer.GetArea() * _proportionalWaitTimeBeforeCure / _proportionalLayerArea).Clamp( - isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure, - isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure), 2), + SuggestionWaitTimeBeforeCureSetType.ProportionalLayerPixels => (float) (isBottomLayer + ? (decimal)mass * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerPixels + : (decimal)mass * _proportionalWaitTimeBeforeCure / _proportionalLayerPixels), + SuggestionWaitTimeBeforeCureSetType.ProportionalLayerArea => (float) (isBottomLayer + ? (decimal)mass * _proportionalBottomWaitTimeBeforeCure / _proportionalBottomLayerArea + : (decimal)mass * _proportionalWaitTimeBeforeCure / _proportionalLayerArea), _ => throw new ArgumentOutOfRangeException() }; + + if (isBottomLayer) + { + if (_proportionalBottomWaitTimeBeforeCureMaximumDifference > 0) + { + var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); + if(previousLayer is not null) + { + value = value.Clamp( + Math.Max(0, previousLayer.WaitTimeBeforeCure - (float)_proportionalBottomWaitTimeBeforeCureMaximumDifference), + previousLayer.WaitTimeBeforeCure + (float)_proportionalBottomWaitTimeBeforeCureMaximumDifference); + } + } + } + else + { + if (_proportionalWaitTimeBeforeCureMaximumDifference > 0) + { + var previousLayer = layer.GetPreviousLayerWithAtLeastPixelCountOf(2); + if (previousLayer is not null) + { + value = value.Clamp( + Math.Max(0, previousLayer.WaitTimeBeforeCure - (float)_proportionalWaitTimeBeforeCureMaximumDifference), + previousLayer.WaitTimeBeforeCure + (float)_proportionalWaitTimeBeforeCureMaximumDifference); + } + } + } + + return (float)Math.Round((decimal)value, 2).Clamp( + isBottomLayer ? _minimumBottomWaitTimeBeforeCure : _minimumWaitTimeBeforeCure, + isBottomLayer ? _maximumBottomWaitTimeBeforeCure : _maximumWaitTimeBeforeCure); } public float CalculateWaitTime(Layer layer) => CalculateWaitTime(false, layer); diff --git a/UVtools.Core/SystemOS/SystemAware.cs b/UVtools.Core/SystemOS/SystemAware.cs index a7d75735..2b892176 100644 --- a/UVtools.Core/SystemOS/SystemAware.cs +++ b/UVtools.Core/SystemOS/SystemAware.cs @@ -255,6 +255,16 @@ public static void StartProcess(string name, string? arguments = null, bool wait } } + public static void StartThisApplication(string? arguments = null) + { + var executable = Environment.ProcessPath; + + if (File.Exists(executable)) // Direct execute + { + StartProcess(executable, arguments); + } + } + public static string GetExecutableName(string executable) { return OperatingSystem.IsWindows() ? $"{executable}.exe" : executable; diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index f9417e32..7c95a6ad 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ https://github.com/sn4k3/UVtools https://github.com/sn4k3/UVtools MSLA/DLP, file analysis, calibration, repair, conversion and manipulation - 3.4.1 + 3.4.2 Copyright © 2020 PTRTECH UVtools.png AnyCPU;x64 @@ -66,11 +66,11 @@ - + - + diff --git a/UVtools.Installer/Code/Product.wxs b/UVtools.Installer/Code/Product.wxs index d392cc84..b14c5f7c 100644 --- a/UVtools.Installer/Code/Product.wxs +++ b/UVtools.Installer/Code/Product.wxs @@ -35,6 +35,7 @@ + diff --git a/UVtools.InstallerMM/UVtools.InstallerMM.wxs b/UVtools.InstallerMM/UVtools.InstallerMM.wxs index e28de766..5e61ad8a 100644 --- a/UVtools.InstallerMM/UVtools.InstallerMM.wxs +++ b/UVtools.InstallerMM/UVtools.InstallerMM.wxs @@ -2,7 +2,7 @@ - + @@ -1554,8 +1554,8 @@ - - + + diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs index 163df81e..0558025b 100644 --- a/UVtools.WPF/App.axaml.cs +++ b/UVtools.WPF/App.axaml.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Web; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -25,7 +26,6 @@ using UVtools.Core.FileFormats; using UVtools.Core.Managers; using UVtools.Core.SystemOS; -using UVtools.WPF.Extensions; using UVtools.WPF.Structures; using UVtools.WPF.Windows; using Bitmap = Avalonia.Media.Imaging.Bitmap; @@ -180,10 +180,6 @@ public override void OnFrameworkInitializationCompleted() UserSettings.Load(); UserSettings.SetVersion(); - MaterialManager.Load(); - OperationProfiles.Load(); - SuggestionManager.Load(); - /*ThemeSelector = ThemeSelector.Create(Path.Combine(ApplicationPath, "Assets", "Themes")); ThemeSelector.LoadSelectedTheme(Path.Combine(UserSettings.SettingsFolder, "selected.theme")); if (ThemeSelector.SelectedTheme.Name == "UVtoolsDark" || ThemeSelector.SelectedTheme.Name == "Light") @@ -208,9 +204,92 @@ public override void OnFrameworkInitializationCompleted() ApplyTheme(); } - try + if (Program.IsCrashReport) { - if (CvInvoke.Init()) + //Program.Args = new[] {"--crash-report", "test2", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }; + if (string.IsNullOrWhiteSpace(Program.Args[1])) return; + if(string.IsNullOrWhiteSpace(Program.Args[2])) return; + var category = Program.Args[1]; + var message = $"{Program.Args[2]}\nCategory: {category}"; + + var bugReportMessageMk = $"# Report\n```\n{message}\n```"; + + try + { + var append = $"\n\n# System\n{AboutWindow.GetEssentialInformationStatic()}"; + message += append; + bugReportMessageMk += append; + } + catch + { + // ignored + } + + var append2 = $"\n\nMachine date time: {DateTime.Now}\n UTC date time: {DateTime.UtcNow}"; + message += append2; + bugReportMessageMk += $"{append2}\n\n# Additional information and Workflow\nComplete with additional information and the workflow that caused this crash."; + + try + { + Current?.Clipboard?.SetTextAsync(bugReportMessageMk); + } + catch + { + // ignored + } + + + var reportButton = MessageWindow.CreateButton("Report", "fas fa-bug"); + reportButton.Click += (sender, e) => + { + Current?.Clipboard?.SetTextAsync(bugReportMessageMk); + using var reader = new StringReader(message); + SystemAware.OpenBrowser($"https://github.com/sn4k3/UVtools/issues/new?assignees=sn4k3&labels=&template=bug_report.md&title={HttpUtility.UrlEncode($"[Crash] {reader.ReadLine()}")}&body={HttpUtility.UrlEncode("")}"); + e.Handled = true; + }; + + var helpButton = MessageWindow.CreateButton("Help", "fas fa-question"); + helpButton.Click += (sender, e) => + { + Current?.Clipboard?.SetTextAsync(bugReportMessageMk); + SystemAware.OpenBrowser("https://github.com/sn4k3/UVtools/discussions/categories/q-a"); + e.Handled = true; + }; + + var restartButton = MessageWindow.CreateButton("Restart", "fas fa-redo-alt"); + restartButton.Click += (sender, e) => + { + SystemAware.StartThisApplication(); + }; + + desktop.MainWindow = new MessageWindow($"{About.SoftwareWithVersion} - Crash report", + "far fa-frown", + $"{About.Software} crashed due an unexpected {category.ToLowerInvariant()} error.\nYou can report this error if you find necessary.\nFind more details below:\n", + message, + new[] + { + reportButton, + helpButton, + restartButton, + MessageWindow.CreateCloseButton("fas fa-sign-out-alt") + }); + } + else + { + try + { + if (!CvInvoke.Init()) + { + desktop.MainWindow = new CantRunWindow(); + } + } + catch (Exception e) + { + desktop.MainWindow = new CantRunWindow(); + Console.WriteLine(e.ToString()); + } + + if (desktop.MainWindow is null) { if (Design.IsDesignMode) { @@ -226,56 +305,15 @@ public override void OnFrameworkInitializationCompleted() }; } - MainWindow = new MainWindow(); - if (UserSettings.Instance.General.StartMaximized) - { - MainWindow.WindowState = WindowState.Maximized; - } - else - { - if (UserSettings.Instance.General.RestoreWindowLastSize) - { - MainWindow.Width = UserSettings.Instance.General.LastWindowBounds.Width; - MainWindow.Height = UserSettings.Instance.General.LastWindowBounds.Height; - } + MaterialManager.Load(); + OperationProfiles.Load(); + SuggestionManager.Load(); - if (UserSettings.Instance.General.RestoreWindowLastPosition) - { - MainWindow.Position = new PixelPoint(UserSettings.Instance.General.LastWindowBounds.Location.X, UserSettings.Instance.General.LastWindowBounds.Location.Y); - } - } + MainWindow = new MainWindow(); desktop.MainWindow = MainWindow; } - else - { - desktop.MainWindow = new CantRunWindow(); - } - } - catch (Exception e) - { - desktop.MainWindow = new CantRunWindow(); - Console.WriteLine(e.ToString()); } - - /*try - { - if(!CvInvoke.Init()) - await MainWindow.MessageBoxError("UVtools can not init OpenCV library\n" + - "Please build or install this dependencies in order to run UVtools\n" + - "Check manual or page at 'Requirements' section for help", - "UVtools can not run"); - } - catch (Exception e) - { - await MainWindow.MessageBoxError("UVtools can not run due lack of dependencies from cvextern/OpenCV\n" + - "Please build or install this dependencies in order to run UVtools\n" + - "Check manual or page at 'Requirements' section for help\n\n" + - "Additional information:\n" + - $"{e}", "UVtools can not run"); - return; - }*/ - //desktop.Exit += (sender, e) => ThemeSelector.SaveSelectedTheme(Path.Combine(UserSettings.SettingsFolder, "selected.theme")); } diff --git a/UVtools.WPF/ConsoleArguments.cs b/UVtools.WPF/ConsoleArguments.cs index 788b104f..552b5005 100644 --- a/UVtools.WPF/ConsoleArguments.cs +++ b/UVtools.WPF/ConsoleArguments.cs @@ -25,7 +25,7 @@ public static class ConsoleArguments /// True if is a valid argument, otherwise false public static bool ParseArgs(string[] args) { - if(args is null || args.Length == 0) return false; + if(args.Length == 0) return false; if (args[0] is "--cmd" && args.Length > 1) { diff --git a/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml b/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml index 596e1e4a..47d0c077 100644 --- a/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml +++ b/UVtools.WPF/Controls/Suggestions/SuggestionWaitTimeBeforeCureControl.axaml @@ -2,9 +2,9 @@ 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" + mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="600" x:Class="UVtools.WPF.Controls.Suggestions.SuggestionWaitTimeBeforeCureControl"> - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - @@ -77,12 +77,6 @@ - typeof(Window); public DialogResults DialogResult { get; set; } = DialogResults.Unknown; @@ -78,6 +89,21 @@ public enum DialogResults public double WindowMaxHeight => this.GetScreenWorkingArea().Height - UserSettings.Instance.General.WindowsVerticalMargin; + public Size WindowMaxSize + { + get + { + var size = this.GetScreenWorkingArea(); + return new Size(size.Width - UserSettings.Instance.General.WindowsHorizontalMargin, + this.GetScreenWorkingArea().Height - UserSettings.Instance.General.WindowsVerticalMargin); + } + } + + public WindowConstrainsMaxSizeType WindowConstrainMaxSize { get; set; } = WindowConstrainsMaxSizeType.UserSettings; + + public double WindowsWidthMaxSizeRatio { get; set; } = 1; + public double WindowsHeightMaxSizeRatio { get; set; } = 1; + public UserSettings Settings => UserSettings.Instance; public virtual FileFormat SlicerFile @@ -97,10 +123,42 @@ public WindowEx() protected override void OnOpened(EventArgs e) { base.OnOpened(e); - if (!CanResize && WindowState == WindowState.Normal) + AutoConstainsWindowMaxSize(); + } + + /*protected override Size MeasureOverride(Size availableSize) + { + var result = base.MeasureOverride(availableSize); + if (SizeToContent == SizeToContent.Manual) return result; + + if (MaxWidth > 0 && MaxWidth < result.Width) + { + result = result.WithWidth(MaxWidth); + } + if (MaxHeight > 0 && MaxHeight < result.Height) + { + result = result.WithHeight(MaxHeight); + } + + return result; + }*/ + + public void AutoConstainsWindowMaxSize() + { + if (!CanResize && WindowState == WindowState.Normal && WindowConstrainMaxSize != WindowConstrainsMaxSizeType.None) { - MaxWidth = WindowMaxWidth; - MaxHeight = WindowMaxHeight; + switch (WindowConstrainMaxSize) + { + case WindowConstrainsMaxSizeType.UserSettings: + MaxWidth = WindowMaxWidth; + MaxHeight = WindowMaxHeight; + break; + case WindowConstrainsMaxSizeType.Ratio: + var size = this.GetScreenWorkingArea(); + if (WindowsWidthMaxSizeRatio is > 0 and < 1) MaxWidth = size.Width * WindowsWidthMaxSizeRatio; + if (WindowsHeightMaxSizeRatio is > 0 and < 1) MaxHeight = size.Height * WindowsHeightMaxSizeRatio; + break; + } } } diff --git a/UVtools.WPF/ErrorLog.cs b/UVtools.WPF/ErrorLog.cs index 0955aede..1dd6775d 100644 --- a/UVtools.WPF/ErrorLog.cs +++ b/UVtools.WPF/ErrorLog.cs @@ -20,6 +20,7 @@ public static void AppendLine(string errorType, string text) } catch (Exception exception) { + Console.WriteLine(exception); Debug.WriteLine(exception); } } diff --git a/UVtools.WPF/Extensions/WindowExtensions.cs b/UVtools.WPF/Extensions/WindowExtensions.cs index 76cbc30c..8a672205 100644 --- a/UVtools.WPF/Extensions/WindowExtensions.cs +++ b/UVtools.WPF/Extensions/WindowExtensions.cs @@ -95,7 +95,7 @@ public static void ResetDataContext(this Window window) public static Screen GetCurrentScreen(this Window window) { return //window.Screens.ScreenFromVisual(window) ?? - window.Screens.ScreenFromVisual(App.MainWindow) ?? + window.Screens.ScreenFromVisual(App.MainWindow ?? window) ?? window.Screens.Primary ?? window.Screens.All[0]; } diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index ff0db6f0..37e697e2 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -491,8 +491,7 @@ public string LayerPixelCountStr get { if (!LayerCache.IsCached) return "Pixels: 0"; - var pixelPercent = Math.Round(LayerCache.Layer.NonZeroPixelCount * 100.0 / (SlicerFile.ResolutionX * SlicerFile.ResolutionY), 2); - var text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)"; + var text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({LayerCache.Layer.NonZeroPixelPercentage:F2}%)"; var volume = LayerCache.Layer.Volume; if (volume > 0) { diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 1c24f955..aa65ff64 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -22,6 +22,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Org.BouncyCastle.Operators; using UVtools.AvaloniaControls; using UVtools.Core; using UVtools.Core.Extensions; @@ -250,6 +251,30 @@ public MenuItem[] MenuFileConvertItems public MainWindow() { + if (Settings.General.StartMaximized) + { + WindowState = WindowState.Maximized; + } + else + { + if (Settings.General.RestoreWindowLastPosition) + { + Position = new PixelPoint(Settings.General.LastWindowBounds.Location.X, Settings.General.LastWindowBounds.Location.Y); + } + + if (Settings.General.RestoreWindowLastSize) + { + Width = Settings.General.LastWindowBounds.Width; + Height = Settings.General.LastWindowBounds.Height; + } + + var windowSize = this.GetScreenWorkingArea(); + if (Width >= windowSize.Width || Height >= windowSize.Height) + { + WindowState = WindowState.Maximized; + } + } + InitializeComponent(); //App.ThemeSelector?.EnableThemes(this); @@ -703,7 +728,6 @@ await this.MessageBoxError($"Unable to eject the drive {removableDrive.Name}\n\n protected override void OnOpened(EventArgs e) { base.OnOpened(e); - var windowSize = this.GetScreenWorkingArea(); var clientSizeObs = this.GetObservable(ClientSizeProperty); clientSizeObs.Subscribe(size => @@ -726,11 +750,6 @@ protected override void OnOpened(EventArgs e) ProcessFiles(args.Data.GetFileNames()?.ToArray()); }); - if (!Settings.General.StartMaximized && (ClientSize.Width >= windowSize.Width || ClientSize.Height >= windowSize.Height)) - { - WindowState = WindowState.Maximized; - } - AddLog($"{About.Software} start", Program.ProgramStartupTime.Elapsed.TotalSeconds); if (Settings.General.CheckForUpdatesOnStartup) @@ -1067,7 +1086,7 @@ public async void MenuFileSettingsClicked() App.ApplyTheme(); } - if (oldLayerCompressionCodec != Settings.General.LayerCompressionCodec) + if (oldLayerCompressionCodec != Settings.General.LayerCompressionCodec && IsFileLoaded) { IsGUIEnabled = false; ShowProgressWindow($"Changing layers compression codec from {oldLayerCompressionCodec.ToString().ToUpper()} to {Settings.General.LayerCompressionCodec.ToString().ToUpper()}"); @@ -1210,7 +1229,7 @@ await this.MessageBoxWithHeaderQuestion( private void UpdateTitle() { var title = $"{About.Software} "; - + if (IsFileLoaded) { title += $"File: {SlicerFile.Filename} ({Math.Round(LastStopWatch.ElapsedMilliseconds / 1000m, 2)}s) "; @@ -1254,17 +1273,39 @@ public async void ProcessFiles(string[] files, bool openNewWindow = false, FileF try { var operation = Operation.Deserialize(files[i]); - await ShowRunOperation(operation); + operation.SlicerFile = SlicerFile; + if ((_globalModifiers & KeyModifiers.Shift) != 0) await RunOperation(operation); + else await ShowRunOperation(operation); } catch (Exception e) { Debug.WriteLine(e); - throw; } continue; } + if (files[i].EndsWith(".cs") || files[i].EndsWith(".csx")) + { + if (!IsFileLoaded) continue; + try + { + var operation = new OperationScripting(SlicerFile); + operation.ReloadScriptFromFile(files[i]); + if (operation.CanExecute) + { + if ((_globalModifiers & KeyModifiers.Shift) != 0) await RunOperation(operation); + else await ShowRunOperation(operation); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + continue; + } + if (i == 0 && !openNewWindow && (_globalModifiers & KeyModifiers.Shift) == 0) { @@ -1293,7 +1334,7 @@ async void ProcessFile(string fileName, FileFormat.FileDecodeType fileDecodeType var fileNameOnly = Path.GetFileName(fileName); SlicerFile = FileFormat.FindByExtensionOrFilePath(fileName, true); if (SlicerFile is null) return; - + IsGUIEnabled = false; ShowProgressWindow($"Opening: {fileNameOnly}"); @@ -2024,6 +2065,8 @@ await this.MessageBoxError($"The file was open in partial mode and the tool \"{c public async Task RunOperation(Operation baseOperation) { if (baseOperation is null) return false; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + baseOperation.SlicerFile ??= SlicerFile; switch (baseOperation) { diff --git a/UVtools.WPF/Program.cs b/UVtools.WPF/Program.cs index 2e71d422..b72d4398 100644 --- a/UVtools.WPF/Program.cs +++ b/UVtools.WPF/Program.cs @@ -1,17 +1,12 @@ using System; using System.Diagnostics; -using System.Drawing; using System.Globalization; -using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using Avalonia; -using Emgu.CV; -using Emgu.CV.Structure; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.MaterialDesign; -using UVtools.Core.Extensions; -using UVtools.Core.Gerber; -using UVtools.WPF.Extensions; +using UVtools.Core.SystemOS; namespace UVtools.WPF; @@ -19,7 +14,8 @@ namespace UVtools.WPF; public static class Program { - public static string[] Args = null!; + public static string[] Args = Array.Empty(); + public static bool IsCrashReport; public static Stopwatch ProgramStartupTime = null!; // Initialization code. Don't use any Avalonia, third-party APIs or any @@ -43,6 +39,11 @@ public static void Main(string[] args) return; } + if (Args.Length > 2 && Args[0] == "--crash-report") + { + IsCrashReport = true; + } + /*Slicer slicer = new(Size.Empty, SizeF.Empty, "D:\\Cube100x100x100.stl"); var slices = slicer.SliceModel(0.05f); @@ -60,33 +61,43 @@ public static void Main(string[] args) //var machinesText = Machine.GenerateMachinePresetsFromPrusaSlicer(); // Add the event handler for handling non-UI thread exceptions to the event. - AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => HandleUnhandledException("Non-UI", (Exception)e.ExceptionObject); + TaskScheduler.UnobservedTaskException += (sender, e) => HandleUnhandledException("Task", e.Exception); //AppDomain.CurrentDomain.FirstChanceException += CurrentDomainOnFirstChanceException; - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + try + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + catch (Exception e) + { + HandleUnhandledException("Application", e); + } + // Closing } - private static void CurrentDomainOnFirstChanceException(object? sender, FirstChanceExceptionEventArgs e) + private static void HandleUnhandledException(string category, Exception ex) { - ErrorLog.AppendLine("First chance exception", e.Exception.ToString()); - } - - - private static async void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e) - { - var ex = (Exception)e.ExceptionObject; - ErrorLog.AppendLine("Fatal Non-UI Error", ex.ToString()); + ErrorLog.AppendLine($"Fatal {category} Error", ex.ToString()); - try - { - var errorMsg = $"An application error occurred. Please contact the administrator with the following information:\n\n{ex}"; - await App.MainWindow.MessageBoxError(errorMsg, "Fatal Non-UI Error"); - } - catch (Exception exception) + if (!IsCrashReport) { - Debug.WriteLine(exception); + try + { + SystemAware.StartThisApplication($"--crash-report \"{category}\" \"{ex}\""); + //var errorMsg = $"An application error occurred. Please contact the administrator with the following information:\n\n{ex}"; + //await App.MainWindow.MessageBoxError(errorMsg, "Fatal Non-UI Error"); + } + catch (Exception exception) + { + Debug.WriteLine(exception); + Console.WriteLine(exception); + } } + + Environment.Exit(-1); } // Avalonia configuration, don't remove; also used by visual designer. diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 32d4b43d..3be333b2 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ LICENSE https://github.com/sn4k3/UVtools Git - 3.4.1 + 3.4.2 AnyCPU;x64 UVtools.png README.md @@ -39,15 +39,15 @@ 1701;1702; - - - - + + + + - + diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs index 37adc531..69c45d63 100644 --- a/UVtools.WPF/UserSettings.cs +++ b/UVtools.WPF/UserSettings.cs @@ -12,6 +12,7 @@ using System.Diagnostics; using System.Drawing; using System.IO; +using System.Reactive.Disposables; using System.Xml.Serialization; using Avalonia.Media; using JetBrains.Annotations; @@ -1394,6 +1395,7 @@ public LayerRepairUserSettings Clone() public sealed class ToolsUserSettings : BindableBase { private bool _expandDescriptions = true; + private bool _promptForConfirmation = true; private bool _restoreLastUsedSettings; private bool _lastUsedSettingsKeepOnCloseFile = true; private bool _lastUsedSettingsPriorityOverDefaultProfile = true; @@ -1404,6 +1406,12 @@ public bool ExpandDescriptions set => RaiseAndSetIfChanged(ref _expandDescriptions, value); } + public bool PromptForConfirmation + { + get => _promptForConfirmation; + set => RaiseAndSetIfChanged(ref _promptForConfirmation, value); + } + public bool RestoreLastUsedSettings { get => _restoreLastUsedSettings; diff --git a/UVtools.WPF/Windows/AboutWindow.axaml.cs b/UVtools.WPF/Windows/AboutWindow.axaml.cs index 9fa07263..420f8f06 100644 --- a/UVtools.WPF/Windows/AboutWindow.axaml.cs +++ b/UVtools.WPF/Windows/AboutWindow.axaml.cs @@ -132,6 +132,24 @@ private void InitializeComponent() public void OpenLicense() => SystemAware.OpenBrowser(About.LicenseUrl); + public static string GetEssentialInformationStatic() + { + var message = new StringBuilder(); + message.AppendLine($"{About.SoftwareWithVersionArch}"); + message.AppendLine($"Operative system: {OSDescription}"); + message.AppendLine($"Processor: {ProcessorName}"); + message.AppendLine($"Processor cores: {ProcessorCount}"); + message.AppendLine($"Memory RAM: {MemoryRAMDescription}"); + message.AppendLine($"Runtime: {RuntimeDescription}"); + message.AppendLine($"Framework: {FrameworkDescription}"); + message.AppendLine($"AvaloniaUI: {AvaloniaUIDescription}"); + message.AppendLine($"OpenCV: {OpenCVVersion}"); + message.AppendLine(); + message.AppendLine($"Path: {App.ApplicationPath}"); + message.AppendLine($"Executable: {App.AppExecutable}"); + return message.ToString(); + } + private string GetEssentialInformation() { var message = new StringBuilder(); diff --git a/UVtools.WPF/Windows/CantRunWindow.axaml b/UVtools.WPF/Windows/CantRunWindow.axaml index 18e53075..870251c4 100644 --- a/UVtools.WPF/Windows/CantRunWindow.axaml +++ b/UVtools.WPF/Windows/CantRunWindow.axaml @@ -1,4 +1,4 @@ - - + diff --git a/UVtools.WPF/Windows/CantRunWindow.axaml.cs b/UVtools.WPF/Windows/CantRunWindow.axaml.cs index d3cb4db9..c3612ca6 100644 --- a/UVtools.WPF/Windows/CantRunWindow.axaml.cs +++ b/UVtools.WPF/Windows/CantRunWindow.axaml.cs @@ -6,21 +6,18 @@ * of this license document, but changing it is not allowed. */ using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using UVtools.Core.SystemOS; +using UVtools.WPF.Controls; namespace UVtools.WPF.Windows { - public partial class CantRunWindow : Window + public partial class CantRunWindow : WindowEx { public CantRunWindow() { InitializeComponent(); DataContext = this; -#if DEBUG - this.AttachDevTools(); -#endif } private void InitializeComponent() diff --git a/UVtools.WPF/Windows/MessageWindow.axaml b/UVtools.WPF/Windows/MessageWindow.axaml new file mode 100644 index 00000000..3215c962 --- /dev/null +++ b/UVtools.WPF/Windows/MessageWindow.axaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Windows/MessageWindow.axaml.cs b/UVtools.WPF/Windows/MessageWindow.axaml.cs new file mode 100644 index 00000000..407fd809 --- /dev/null +++ b/UVtools.WPF/Windows/MessageWindow.axaml.cs @@ -0,0 +1,141 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using UVtools.WPF.Controls; + +namespace UVtools.WPF.Windows +{ + public partial class MessageWindow : WindowEx + { + private string? _headerIcon; + private ushort _headerIconSize = 64; + private string _headerTitle; + private bool _aboutButtonIsVisible = true; + private string _message; + + private readonly StackPanel _buttonsRightPanel; + + public string? HeaderIcon + { + get => _headerIcon; + set + { + if (!RaiseAndSetIfChanged(ref _headerIcon, value)) return; + RaisePropertyChanged(nameof(HeaderIsVisible)); + } + } + + public ushort HeaderIconSize + { + get => _headerIconSize; + set => RaiseAndSetIfChanged(ref _headerIconSize, value); + } + + public string? HeaderTitle + { + get => _headerTitle; + set + { + if(!RaiseAndSetIfChanged(ref _headerTitle, value)) return; + RaisePropertyChanged(nameof(HeaderIsVisible)); + } + } + + public bool HeaderIsVisible => !string.IsNullOrWhiteSpace(HeaderIcon) || !string.IsNullOrWhiteSpace(HeaderTitle); + + public string Message + { + get => _message; + set => RaiseAndSetIfChanged(ref _message, value); + } + + public bool AboutButtonIsVisible + { + get => _aboutButtonIsVisible; + set => RaiseAndSetIfChanged(ref _aboutButtonIsVisible, value); + } + + public MessageWindow() + { + InitializeComponent(); + + CanResize = Settings.General.WindowsCanResize; + if (WindowStartupLocation == WindowStartupLocation.CenterOwner && Owner is null) + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + + AutoConstainsWindowMaxSize(); + + _buttonsRightPanel = this.FindControl("ButtonsRightPanel"); + DataContext = this; + } + + public MessageWindow(string title, string? headerIcon, string? headerTitle, string message, ButtonWithIcon[]? buttons = null) : this() + { + Title = title; + HeaderIcon = headerIcon; + HeaderTitle = headerTitle; + Message = message; + + if (buttons is not null && buttons.Length > 0) + { + _buttonsRightPanel.Children.Clear(); + _buttonsRightPanel.Children.AddRange(buttons); + + foreach (var button in buttons) + { + button.Click += (sender, e) => + { + if (e.Handled) return; + Close(button); + e.Handled = true; + }; + } + } + } + + public MessageWindow(string title, string message, ButtonWithIcon[]? buttons = null) : this(title, null, null, message, buttons) { } + + /*protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + }*/ + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public async void OpenAboutWindow() + { + await new AboutWindow().ShowDialog(this); + } + + public static ButtonWithIcon CreateButton(string? text, string? icon, int padding = 10) => + new() + { + Icon = icon, + Text = text, + VerticalAlignment = VerticalAlignment.Center, + Padding = new Thickness(padding) + }; + + public static ButtonWithIcon CreateButton(string? text, int padding = 10) => CreateButton(text, null, padding); + + public static ButtonWithIcon CreateCancelButton(string? icon = null, int padding = 10) + { + var btn = CreateButton("Cancel", icon, padding); + btn.IsCancel = true; + return btn; + } + + public static ButtonWithIcon CreateCloseButton(string? icon = null, int padding = 10) + { + var btn = CreateButton("Close", icon, padding); + btn.IsCancel = true; + return btn; + } + } +} diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml b/UVtools.WPF/Windows/SettingsWindow.axaml index 88c8e5da..cda1778d 100644 --- a/UVtools.WPF/Windows/SettingsWindow.axaml +++ b/UVtools.WPF/Windows/SettingsWindow.axaml @@ -1886,6 +1886,9 @@ ToolTip.Tip="Disable this if you want to gain some vertical space for windows, you can still toggle the descriptions and read them if required." Content="Expand and show descriptions by default"/> + + diff --git a/UVtools.WPF/Windows/ToolWindow.axaml.cs b/UVtools.WPF/Windows/ToolWindow.axaml.cs index fea7a289..0dc25426 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml.cs +++ b/UVtools.WPF/Windows/ToolWindow.axaml.cs @@ -815,10 +815,9 @@ public async void Process() ToolControl.BaseOperation.SetMasksIfEmpty(Masks); if (!await ToolControl.ValidateForm()) return; - if (!string.IsNullOrEmpty(ToolControl.BaseOperation.ConfirmationText)) + if (Settings.Tools.PromptForConfirmation && !string.IsNullOrEmpty(ToolControl.BaseOperation.ConfirmationText)) { - var result = - await this.MessageBoxQuestion( + var result = await this.MessageBoxQuestion( $"Are you sure you want to {ToolControl.BaseOperation.ConfirmationText}"); if (result != ButtonResult.Yes) return; }