diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b172d77..9a9d146a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 12/12/2023 - v4.0.5 + +- (Add) Tool - Phased Exposure: Replace "Double Exposure" with "Phased Exposure" tool, same effect but allow to define more steps (#778) +- (Fix) Infill: Honeycomb Infill does not completely infill model (#789) +- (Improvement) File -> Open recent: Disable all inexistent files (Can no longer be clicked) +- (Upgrade) .NET from 6.0.24 to 6.0.25 +- (Upgrade) AvaloniaUI from 11.0.5 to 11.0.6 +- ## 09/11/2023 - v4.0.4 - **FileFormat Anycubic:** diff --git a/CREDITS.md b/CREDITS.md index bbf8e57a..7019994b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -79,4 +79,6 @@ - Robert Redden - Nick Spirov - Sylvain Chartrand -- Michael Pullen \ No newline at end of file +- Michael Pullen +- Albeck Károly +- Mark Johnston \ No newline at end of file diff --git a/README.md b/README.md index 53997af4..c7cb683c 100644 --- a/README.md +++ b/README.md @@ -252,13 +252,13 @@ The following commands are the old way and commands under the UI executable, the - Click on: Turn Windows features on or off - Check the "Media Extensions" and click Ok -1. 4GB RAM or higher +1. 8GB RAM or higher + 512MB per CPU core 2. 64 bit System 3. 1920 x 1080 @ 100% scale as minimum resolution ## Linux -1. 4GB RAM or higher +1. 8GB RAM or higher + 512MB per CPU core 2. 64 bit System 3. 1920 x 1080 @ 100% scale as minimum resolution @@ -290,7 +290,7 @@ If you downloaded the **.AppImage** package variant you must set run permissions ## Mac 1. macOS 10.15 Catalina or higher -1. 4GB RAM or higher +1. 8GB RAM or higher + 512MB per CPU core 3. **For Mac M1/M2 (ARM):** 1. Install via the [auto installer](https://github.com/sn4k3/UVtools#to-auto-install-on-macos-homebrew) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 6296823a..a09964a9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,13 +1,7 @@ -- **FileFormat Anycubic:** - - (Fix) Preview marker size and `PropertyFields` for given file version - - (Fix) Do not cache and reuse layer images if equal on file write, as that is causing "file damage" message (#782, #787) -- **Tools:** - - **Mask:** - - (Improvement) Apply the mask with Multiply instead of BitwiseAnd (#747) - - **Layer and pixel arithmetic**: - - (Improvement) Use the byte scale value (1/255) for multiply operations - - **Infill:** - - (Add) Floor/Ceil thickness (#785) - - (Fix) Remove the debug blocking window when selecting the honeycomb pattern -- (Upgrade) .NET from 6.0.23 to 6.0.24 +- (Add) Tool - Phased Exposure: Replace "Double Exposure" with "Phased Exposure" tool, same effect but allow to define more steps (#778) +- (Fix) Infill: Honeycomb Infill does not completely infill model (#789) +- (Improvement) File -> Open recent: Disable all inexistent files (Can no longer be clicked) +- (Upgrade) .NET from 6.0.24 to 6.0.25 +- (Upgrade) AvaloniaUI from 11.0.5 to 11.0.6 +- diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index ece6905e..10ec67c3 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -24,7 +24,7 @@ - + diff --git a/UVtools.Cmd/Symbols/PrintMachines.cs b/UVtools.Cmd/Symbols/PrintMachines.cs index e0fd4608..f674b170 100644 --- a/UVtools.Cmd/Symbols/PrintMachines.cs +++ b/UVtools.Cmd/Symbols/PrintMachines.cs @@ -37,7 +37,7 @@ internal static Command CreateCommand() if (xmlFormat) { - Console.WriteLine(XmlExtensions.SerializeObject(Machine.Machines, XmlExtensions.SettingsIndent)); + Console.WriteLine(XmlExtensions.SerializeToString(Machine.Machines, XmlExtensions.SettingsIndent)); return; } diff --git a/UVtools.Core/EmguCV/MatRoi.cs b/UVtools.Core/EmguCV/MatRoi.cs index 46755011..43a95879 100644 --- a/UVtools.Core/EmguCV/MatRoi.cs +++ b/UVtools.Core/EmguCV/MatRoi.cs @@ -40,6 +40,10 @@ public MatRoi(Mat sourceMat, Rectangle roi) #endregion + public MatRoi Clone() + { + return new MatRoi(SourceMat.Clone(), Roi); + } public void Dispose() { diff --git a/UVtools.Core/Extensions/ClassExtensions.cs b/UVtools.Core/Extensions/ClassExtensions.cs index 222c76ff..83c57ebb 100644 --- a/UVtools.Core/Extensions/ClassExtensions.cs +++ b/UVtools.Core/Extensions/ClassExtensions.cs @@ -6,21 +6,56 @@ * of this license document, but changing it is not allowed. */ +using System.IO; +using System.Text; using System.Text.Json; +using System.Xml; +using System.Xml.Serialization; namespace UVtools.Core.Extensions; public static class ClassExtensions { - public static T? CloneByJsonSerialization(this T classToClone) where T : class + private static readonly JsonSerializerOptions JsonCloneSerializeSettings = new() { - var clone = JsonSerializer.SerializeToUtf8Bytes(classToClone); - return JsonSerializer.Deserialize(clone); + //DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false, + Converters = + { + new System.Text.Json.Serialization.JsonStringEnumConverter() + }, + IncludeFields = true, + }; + + private static readonly XmlWriterSettings XmlCloneSerializeSettings = new() + { + // If set to true XmlWriter would close MemoryStream automatically and using would then do double dispose + // Code analysis does not understand that. That's why there is a suppress message. + CloseOutput = false, + Encoding = Encoding.UTF8, + OmitXmlDeclaration = false, + Indent = false, + }; + + public static T CloneByJsonSerialization(this T classToClone) where T : class + { + var clone = JsonSerializer.SerializeToUtf8Bytes(classToClone, JsonCloneSerializeSettings); + return JsonSerializer.Deserialize(clone, JsonCloneSerializeSettings)!; } public static T CloneByXmlSerialization(this T classToClone) where T : class { - var clone = XmlExtensions.SerializeObject(classToClone); - return XmlExtensions.DeserializeFromText(clone); + //var clone = XmlExtensions.SerializeObject(classToClone, Encoding.UTF8, false); + //return XmlExtensions.DeserializeFromText(clone); + + using var stream = new MemoryStream(); + using var xmlWriter = XmlWriter.Create(stream, XmlCloneSerializeSettings); + xmlWriter.WriteStartDocument(false); // that bool parameter is called "standalone" + var xmlSerializer = new XmlSerializer(classToClone.GetType()); + //XmlSerializerNamespaces? ns = true ? new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }) : null; + xmlSerializer.Serialize(xmlWriter, classToClone); + + stream.Seek(0, SeekOrigin.Begin); + return (T)xmlSerializer.Deserialize(stream)!; } } \ No newline at end of file diff --git a/UVtools.Core/Extensions/ListExtensions.cs b/UVtools.Core/Extensions/ListExtensions.cs new file mode 100644 index 00000000..84494b73 --- /dev/null +++ b/UVtools.Core/Extensions/ListExtensions.cs @@ -0,0 +1,20 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +using System.Collections.Generic; +using System; +using System.Linq; + +namespace UVtools.Core.Extensions; + +public static class ListExtensions +{ + public static List Clone(this List listToClone) where T : class, ICloneable + { + return listToClone.Select(item => (T)item.Clone()).ToList(); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/TypeExtensions.cs b/UVtools.Core/Extensions/TypeExtensions.cs index 1a8b3312..01d952ce 100644 --- a/UVtools.Core/Extensions/TypeExtensions.cs +++ b/UVtools.Core/Extensions/TypeExtensions.cs @@ -44,4 +44,11 @@ public static IEnumerable GetTypesInNamespace(Assembly assembly, string na return assembly.GetTypes() .Where(t => string.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)); } + + public static bool IsPrimitive(this Type type) + { + if (type == typeof(string) + || type == typeof(decimal)) return true; + return type is { IsValueType: true, IsPrimitive: true }; + } } \ No newline at end of file diff --git a/UVtools.Core/Extensions/XmlExtensions.cs b/UVtools.Core/Extensions/XmlExtensions.cs index 5caf55d1..feb49b98 100644 --- a/UVtools.Core/Extensions/XmlExtensions.cs +++ b/UVtools.Core/Extensions/XmlExtensions.cs @@ -23,12 +23,7 @@ public static class XmlExtensions public static void Serialize(object toSerialize, Stream stream, bool noNameSpace = false) { var xmlSerializer = new XmlSerializer(toSerialize.GetType()); - XmlSerializerNamespaces? ns = null; - if (noNameSpace) - { - ns = new(); - ns.Add("", ""); - } + XmlSerializerNamespaces? ns = noNameSpace ? new XmlSerializerNamespaces(new []{XmlQualifiedName.Empty}) : null; xmlSerializer.Serialize(stream, toSerialize, ns); } @@ -40,12 +35,7 @@ public static void Serialize(object toSerialize, Stream stream, XmlWriterSetting xw.WriteStartDocument(standalone); // that bool parameter is called "standalone" var s = new XmlSerializer(toSerialize.GetType()); - XmlSerializerNamespaces? ns = null; - if (noNameSpace) - { - ns = new(); - ns.Add("", ""); - } + XmlSerializerNamespaces? ns = noNameSpace ? new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }) : null; s.Serialize(xw, toSerialize, ns); } @@ -63,6 +53,39 @@ public static void Serialize(object toSerialize, Stream stream, Encoding encodin Serialize(toSerialize, stream, settings, noNameSpace, standalone); } + public static void Serialize(object toSerialize, TextWriter stream, bool noNameSpace = false) + { + var xmlSerializer = new XmlSerializer(toSerialize.GetType()); + XmlSerializerNamespaces? ns = noNameSpace ? new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }) : null; + xmlSerializer.Serialize(stream, toSerialize, ns); + } + + public static void Serialize(object toSerialize, TextWriter stream, XmlWriterSettings settings, bool noNameSpace = false, bool standalone = false) + { + settings.CloseOutput = false; + + using var xw = XmlWriter.Create(stream, settings); + xw.WriteStartDocument(standalone); // that bool parameter is called "standalone" + + var s = new XmlSerializer(toSerialize.GetType()); + XmlSerializerNamespaces? ns = noNameSpace ? new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }) : null; + s.Serialize(xw, toSerialize, ns); + } + + public static void Serialize(object toSerialize, TextWriter stream, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool noNameSpace = false, bool standalone = false) + { + var settings = new XmlWriterSettings + { + // If set to true XmlWriter would close MemoryStream automatically and using would then do double dispose + // Code analysis does not understand that. That's why there is a suppress message. + CloseOutput = false, + Encoding = encoding, + OmitXmlDeclaration = omitXmlDeclaration, + Indent = indent, + }; + Serialize(toSerialize, stream, settings, noNameSpace, standalone); + } + public static void SerializeToFile(object toSerialize, string path, bool noNameSpace = false) { using var stream = new FileStream(path, FileMode.Create); @@ -81,32 +104,25 @@ public static void SerializeToFile(object toSerialize, string path, Encoding enc Serialize(toSerialize, stream, encoding, indent, omitXmlDeclaration, noNameSpace, standalone); } - - public static string SerializeObject(object toSerialize, bool noNameSpace = false) + public static string SerializeToString(object toSerialize, bool noNameSpace = false) { - using var stream = new MemoryStream(); - Serialize(toSerialize, stream, noNameSpace); - stream.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); + using var stringWriter = new StringWriter(); + Serialize(toSerialize, stringWriter, noNameSpace); + return stringWriter.ToString(); } - public static string SerializeObject(object toSerialize, XmlWriterSettings settings, bool noNameSpace = false, bool standalone = false) + public static string SerializeToString(object toSerialize, XmlWriterSettings settings, bool noNameSpace = false, bool standalone = false) { - using var stream = new MemoryStream(); - Serialize(toSerialize, stream, settings, noNameSpace, standalone); - stream.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); + using var stringWriter = new StringWriter(); + Serialize(toSerialize, stringWriter, settings, noNameSpace, standalone); + return stringWriter.ToString(); } - public static string SerializeObject(object toSerialize, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool noNameSpace = false, bool standalone = false) + public static string SerializeToString(object toSerialize, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool noNameSpace = false, bool standalone = false) { - using var stream = new MemoryStream(); - Serialize(toSerialize, stream, encoding, indent, omitXmlDeclaration, noNameSpace, standalone); - stream.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); + using var stringWriter = new StringWriter(); + Serialize(toSerialize, stringWriter, encoding, indent, omitXmlDeclaration, noNameSpace, standalone); + return stringWriter.ToString(); } public static T DeserializeFromStream(Stream stream) @@ -121,7 +137,7 @@ public static T DeserializeFromFile(string filePath) return DeserializeFromStream(stream); } - public static T DeserializeFromText(string text) + public static T DeserializeFromString(string text) { var serializer = new XmlSerializer(typeof(T)); using TextReader reader = new StringReader(text); diff --git a/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs b/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs index dc2e5d20..9bae8427 100644 --- a/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs +++ b/UVtools.Core/FileFormats/FlashForgeSVGXFile.cs @@ -58,7 +58,7 @@ public string SerializeToString() NewLineChars = "\n" }; - var svg = XmlExtensions.SerializeObject(this, settings); + var svg = XmlExtensions.SerializeToString(this, settings); return svg.Replace(" ", string.Empty) .Replace("", "\n") + '\n'; @@ -602,7 +602,7 @@ protected override void DecodeInternally(OperationProgress progress) inputFile.Seek(HeaderSettings.SVGDocumentAddress, SeekOrigin.Begin); string svgDocument = Encoding.UTF8.GetString(inputFile.ReadToEnd()); - SVGDocument = XmlExtensions.DeserializeFromText(svgDocument); + SVGDocument = XmlExtensions.DeserializeFromString(svgDocument); Debug.WriteLine(SVGDocument); diff --git a/UVtools.Core/Operations/OperationDoubleExposure.cs b/UVtools.Core/Operations/OperationDoubleExposure.cs index 6f08ca14..62c76f3e 100644 --- a/UVtools.Core/Operations/OperationDoubleExposure.cs +++ b/UVtools.Core/Operations/OperationDoubleExposure.cs @@ -51,6 +51,9 @@ public class OperationDoubleExposure : Operation "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 is recommended."; + public override bool CanROI => false; + public override bool CanMask => false; + public override string ConfirmationText => $"double exposure model layers {LayerIndexStart} through {LayerIndexEnd}"; diff --git a/UVtools.Core/Operations/OperationInfill.cs b/UVtools.Core/Operations/OperationInfill.cs index 876b4db5..b9c7ed11 100644 --- a/UVtools.Core/Operations/OperationInfill.cs +++ b/UVtools.Core/Operations/OperationInfill.cs @@ -181,14 +181,24 @@ protected override bool ExecuteInternally(OperationProgress progress) if (_infillType == InfillAlgorithm.Honeycomb) { mask = GetHoneycombMask(GetRoiSizeOrVolumeSize()); - //CvInvoke.Imshow("Honeycomb", mask); - //CvInvoke.WaitKey(); + } else if (_infillType == InfillAlgorithm.Concentric) { mask = GetConcentricMask(GetRoiSizeOrVolumeSize()); } +#if DEBUG + /* + if (mask is not null) + { + using var previewMat = new Mat(); + CvInvoke.Resize(mask, previewMat, new Size(mask.Width / 4, mask.Height / 4)); + CvInvoke.Imshow("Honeycomb", previewMat); + CvInvoke.WaitKey(); + }*/ +#endif + var clonedLayers = SlicerFile.CloneLayers(); Parallel.For(LayerIndexStart, LayerIndexEnd + 1, CoreSettings.GetParallelOptions(progress), layerIndex => @@ -549,10 +559,10 @@ public Mat GetHoneycombMask(Size targetSize) var halfInfillSpacingD = _infillSpacing / 2.0; var halfInfillSpacing = (int)Math.Round(halfInfillSpacingD); var halfThickenss = _infillThickness / 2; - int width = (int)Math.Round(4 * (halfInfillSpacing / Math.Sqrt(3))); + int width = (int)Math.Round(4 * (halfInfillSpacingD / Math.Sqrt(3))); var infillColor = new MCvScalar(_infillBrightness); - int cols = (int)Math.Ceiling((float)targetSize.Width / _infillSpacing) + 2; + int cols = (int)Math.Ceiling(targetSize.Width / (width * 0.75f)); int rows = (int)Math.Ceiling((float)targetSize.Height / _infillSpacing); for (int col = 0; col <= cols; col++) diff --git a/UVtools.Core/Operations/OperationPCBExposure.cs b/UVtools.Core/Operations/OperationPCBExposure.cs index d7647f08..efafe9a7 100644 --- a/UVtools.Core/Operations/OperationPCBExposure.cs +++ b/UVtools.Core/Operations/OperationPCBExposure.cs @@ -148,6 +148,18 @@ public override string ToString() if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; } + + public int Count => _files.Count; + + public PCBExposureFile this[int index] => _files[index]; + + public override Operation Clone() + { + var clone = (OperationPCBExposure)base.Clone(); + clone._files = _files.CloneByXmlSerialization(); + return clone; + } + #endregion #region Constructor @@ -459,5 +471,4 @@ protected override bool ExecuteInternally(OperationProgress progress) #endregion - } \ No newline at end of file diff --git a/UVtools.Core/Operations/OperationPhasedExposure.cs b/UVtools.Core/Operations/OperationPhasedExposure.cs new file mode 100644 index 00000000..e43f65f7 --- /dev/null +++ b/UVtools.Core/Operations/OperationPhasedExposure.cs @@ -0,0 +1,426 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using Emgu.CV; +using Emgu.CV.CvEnum; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; +using UVtools.Core.Layers; +using UVtools.Core.Objects; + +namespace UVtools.Core.Operations; + + +public class OperationPhasedExposure : Operation, IEquatable +{ + #region SubClasses + + public sealed class PhasedExposure : BindableBase + { + private decimal _bottomExposureTime; + private ushort _bottomIterations; + private decimal _exposureTime; + private ushort _iterations; + + public decimal BottomExposureTime + { + get => _bottomExposureTime; + set => RaiseAndSetIfChanged(ref _bottomExposureTime, Math.Round(Math.Clamp(value, 0, 1000), 2)); + } + + public ushort BottomIterations + { + get => _bottomIterations; + set => RaiseAndSetIfChanged(ref _bottomIterations, value); + } + + public decimal ExposureTime + { + get => _exposureTime; + set => RaiseAndSetIfChanged(ref _exposureTime, Math.Round(Math.Clamp(value, 0, 1000), 2)); + } + + public ushort Iterations + { + get => _iterations; + set => RaiseAndSetIfChanged(ref _iterations, value); + } + + public PhasedExposure() + { + } + + public override string ToString() + { + return $"{nameof(BottomExposureTime)}: {BottomExposureTime}s, {nameof(BottomIterations)}: {BottomIterations}px, {nameof(ExposureTime)}: {ExposureTime}s, {nameof(Iterations)}: {Iterations}px"; + } + + public PhasedExposure(decimal bottomExposureTime, ushort bottomIterations, decimal exposureTime, ushort iterations) + { + _bottomExposureTime = bottomExposureTime; + _bottomIterations = bottomIterations; + _exposureTime = exposureTime; + _iterations = iterations; + } + + + + public PhasedExposure Clone() + { + return (PhasedExposure)MemberwiseClone(); + } + } + + #endregion + + #region Members + + private RangeObservableCollection _phasedExposures = new(); + private bool _exposureDifferenceOnly = true; + private ushort _exposureDifferenceOnlyOverlapIterations = 10; + private bool _differentSettingsForSequentialLayers; + private bool _sequentialLiftHeightEnabled = true; + private decimal _sequentialLiftHeight; + private bool _sequentialWaitTimeBeforeCureEnabled = true; + private decimal _sequentialWaitTimeBeforeCure; + + #endregion + + #region Overrides + public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.Bottom; + public override string IconClass => "fa-solid fa-bars-staggered"; + public override string Title => "Phased exposure"; + public override string Description => + "The phased exposure method clones the selected layer range and print the same layer with different exposure times and strategies.\n" + + "Can be used to eliminate the elephant foot effect or to harden a layer in multiple 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 is recommended."; + + public override bool CanROI => false; + public override bool CanMask => false; + + public override string ConfirmationText => + $"phased exposure model layers {LayerIndexStart} through {LayerIndexEnd}"; + + public override string ProgressTitle => + $"Phased exposure from layers {LayerIndexStart} to {LayerIndexEnd}"; + + public override string ProgressAction => "Cloned layers"; + + public override string? ValidateSpawn() + { + if (!SlicerFile.CanUseLayerPositionZ || !SlicerFile.CanUseLayerLiftHeight || !SlicerFile.CanUseLayerExposureTime) + { + return NotSupportedMessage; + } + + return null; + } + + public override string? ValidateInternally() + { + var sb = new StringBuilder(); + + if (Count < 2) + { + sb.AppendLine("This tool requires at least two phased exposures in order to run."); + } + + 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; + } + + for (var i = 0; i < _phasedExposures.Count-1; i++) + { + if (_phasedExposures[i].BottomExposureTime == _phasedExposures[i + 1].ExposureTime && + _phasedExposures[i].BottomIterations == _phasedExposures[i + 1].BottomIterations && + _phasedExposures[i].ExposureTime == _phasedExposures[i + 1].ExposureTime && + _phasedExposures[i].Iterations == _phasedExposures[i + 1].Iterations) + { + sb.AppendLine($"Duplicated exposure sequence #{i+1} == #{i+2}. Group of entries can't be duplicated."); + } + } + + + return sb.ToString(); + } + + public IEnumerator GetEnumerator() + { + return _phasedExposures.GetEnumerator(); + } + + public override string ToString() + { + var result = $"[Phases: {Count}] " + + $"[Diff: {_exposureDifferenceOnly} Overlap: {_exposureDifferenceOnlyOverlapIterations}px]" + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + + public int Count => _phasedExposures.Count; + + public PhasedExposure this[int index] => _phasedExposures[index]; + + public override Operation Clone() + { + var clone = (OperationPhasedExposure)base.Clone(); + clone.PhasedExposures = PhasedExposures.CloneByXmlSerialization(); + return clone; + } + + #endregion + + #region Properties + + public RangeObservableCollection PhasedExposures + { + get => _phasedExposures; + set => RaiseAndSetIfChanged(ref _phasedExposures, value); + } + + public bool ExposureDifferenceOnly + { + get => _exposureDifferenceOnly; + set => RaiseAndSetIfChanged(ref _exposureDifferenceOnly, value); + } + + public ushort ExposureDifferenceOnlyOverlapIterations + { + get => _exposureDifferenceOnlyOverlapIterations; + set => RaiseAndSetIfChanged(ref _exposureDifferenceOnlyOverlapIterations, value); + } + + public bool DifferentSettingsForSequentialLayers + { + get => _differentSettingsForSequentialLayers; + set => RaiseAndSetIfChanged(ref _differentSettingsForSequentialLayers, value); + } + + public bool SequentialLiftHeightEnabled + { + get => _sequentialLiftHeightEnabled; + set => RaiseAndSetIfChanged(ref _sequentialLiftHeightEnabled, value); + } + + public decimal SequentialLiftHeight + { + get => _sequentialLiftHeight; + set => RaiseAndSetIfChanged(ref _sequentialLiftHeight, value); + } + + public bool SequentialWaitTimeBeforeCureEnabled + { + get => _sequentialWaitTimeBeforeCureEnabled; + set => RaiseAndSetIfChanged(ref _sequentialWaitTimeBeforeCureEnabled, value); + } + + public decimal SequentialWaitTimeBeforeCure + { + get => _sequentialWaitTimeBeforeCure; + set => RaiseAndSetIfChanged(ref _sequentialWaitTimeBeforeCure, value); + } + + + public KernelConfiguration Kernel { get; set; } = new(); + + #endregion + + #region Constructor + + public OperationPhasedExposure() { } + + public OperationPhasedExposure(FileFormat slicerFile) : base(slicerFile) + { + if (SlicerFile.SupportPerLayerSettings) + { + _differentSettingsForSequentialLayers = true; + if (SlicerFile.SupportsGCode) + { + _sequentialLiftHeight = 0; + _sequentialWaitTimeBeforeCure = 2; + } + else + { + _sequentialLiftHeight = 0.1m; + _sequentialWaitTimeBeforeCure = 0; + } + } + } + + public override void InitWithSlicerFile() + { + base.InitWithSlicerFile(); + if (Count == 0) + { + _phasedExposures.AddRange(new [] + { + new PhasedExposure((decimal)SlicerFile.BottomExposureTime, 10, (decimal)SlicerFile.ExposureTime, 4), + new PhasedExposure((decimal)SlicerFile.ExposureTime, 0, (decimal)SlicerFile.ExposureTime, 0), + }); + } + } + + #endregion + + #region Equality + + public bool Equals(OperationPhasedExposure? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return _phasedExposures.Equals(other._phasedExposures) && _exposureDifferenceOnly == other._exposureDifferenceOnly && _exposureDifferenceOnlyOverlapIterations == other._exposureDifferenceOnlyOverlapIterations && _differentSettingsForSequentialLayers == other._differentSettingsForSequentialLayers && _sequentialLiftHeightEnabled == other._sequentialLiftHeightEnabled && _sequentialLiftHeight == other._sequentialLiftHeight && _sequentialWaitTimeBeforeCureEnabled == other._sequentialWaitTimeBeforeCureEnabled && _sequentialWaitTimeBeforeCure == other._sequentialWaitTimeBeforeCure; + } + + 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((OperationPhasedExposure)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(_phasedExposures, _exposureDifferenceOnly, _exposureDifferenceOnlyOverlapIterations, _differentSettingsForSequentialLayers, _sequentialLiftHeightEnabled, _sequentialLiftHeight, _sequentialWaitTimeBeforeCureEnabled, _sequentialWaitTimeBeforeCure); + } + + public static bool operator ==(OperationPhasedExposure? left, OperationPhasedExposure? right) + { + return Equals(left, right); + } + + public static bool operator !=(OperationPhasedExposure? left, OperationPhasedExposure? right) + { + return !Equals(left, right); + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + var layers = new Layer?[SlicerFile.LayerCount + LayerRangeCount * (_phasedExposures.Count - 1)]; + + // Untouched + for (uint i = 0; i < LayerIndexStart; i++) + { + layers[i] = SlicerFile[i]; + } + + int bottomLayers = SlicerFile.BottomLayerCount; + + Parallel.For(LayerIndexStart, LayerIndexEnd + 1, CoreSettings.GetParallelOptions(progress), layerIndex => + { + progress.PauseIfRequested(); + var layer = SlicerFile[layerIndex]; + + if (layer.IsEmpty) + { + progress.LockAndIncrement(); + return; + } + + var isBottomLayer = layer.IsBottomLayer; + + uint newLayerIndex = (uint)(LayerIndexStart + (layerIndex - LayerIndexStart) * _phasedExposures.Count); + + if (isBottomLayer) Interlocked.Increment(ref bottomLayers); + + using var matRoi = layer.LayerMatBoundingRectangle; + uint affectedLayers = 0; + + var lastPhasedExposure = _phasedExposures[0]; + foreach (var phasedExposure in _phasedExposures) + { + int iterations = isBottomLayer ? phasedExposure.BottomIterations : phasedExposure.Iterations; + bool setNewMat = false; + using var newMatRoi = matRoi.Clone(); + + if (iterations > 0) + { + int tempIterations = iterations; + var kernel = Kernel.GetKernel(ref tempIterations); + CvInvoke.Erode(newMatRoi.RoiMat, newMatRoi.RoiMat, kernel, EmguExtensions.AnchorCenter, tempIterations, BorderType.Reflect101, default); + if (!CvInvoke.HasNonZero(newMatRoi.RoiMat)) continue; // Produce all black layer, ignoring + setNewMat = true; + } + + if (affectedLayers > 0 && _exposureDifferenceOnly) + { + var overlapIterations = _exposureDifferenceOnlyOverlapIterations + (isBottomLayer ? lastPhasedExposure.BottomIterations : lastPhasedExposure.Iterations); + if (overlapIterations > iterations) + { + using var overlapMat = new Mat(); + int tempIterations = overlapIterations; + var kernel = Kernel.GetKernel(ref tempIterations); + CvInvoke.Erode(matRoi.RoiMat, overlapMat, kernel, EmguExtensions.AnchorCenter, tempIterations, BorderType.Reflect101, default); + if (CvInvoke.HasNonZero(overlapMat)) + { + CvInvoke.Subtract(newMatRoi.RoiMat, overlapMat, newMatRoi.RoiMat); + setNewMat = CvInvoke.HasNonZero(newMatRoi.RoiMat); + } + } + } + + var newLayer = layer.Clone(); + newLayer.ExposureTime = (float)(isBottomLayer ? phasedExposure.BottomExposureTime : phasedExposure.ExposureTime); + + if (_differentSettingsForSequentialLayers && affectedLayers > 0) + { + if (_sequentialLiftHeightEnabled) newLayer.LiftHeightTotal = (float)_sequentialLiftHeight; + if (_sequentialWaitTimeBeforeCureEnabled) newLayer.SetWaitTimeBeforeCureOrLightOffDelay((float)_sequentialWaitTimeBeforeCure); + } + + layers[newLayerIndex] = newLayer; + if (setNewMat) layers[newLayerIndex]!.LayerMat = newMatRoi.SourceMat; + + affectedLayers++; + newLayerIndex++; + lastPhasedExposure = phasedExposure; + } + + // Prevent layer loss due erode settings on low pixel layer, keep the original instead + if (affectedLayers == 0) layers[newLayerIndex] = layer; + + + progress.LockAndIncrement(); + }); + + // Untouched + for (uint i = LayerIndexEnd+1; i < SlicerFile.LayerCount; i++) + { + layers[i + LayerRangeCount] = SlicerFile[i]; + } + + SlicerFile.SuppressRebuildPropertiesWork(() => + { + SlicerFile.BottomLayerCount = (ushort)bottomLayers; + SlicerFile.Layers = layers.Where(layer => layer is not null).ToArray()!; + }); + + return !progress.Token.IsCancellationRequested; + } + + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 72267deb..bffba741 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -2,7 +2,7 @@ AnyCPU;x64 - 4.0.4 + 4.0.5 True ..\documentation\$(AssemblyName).xml @@ -25,18 +25,18 @@ - - - - + + + + - + - + diff --git a/UVtools.Installer/UVtools.Installer.wixproj b/UVtools.Installer/UVtools.Installer.wixproj index b906614d..3a822289 100644 --- a/UVtools.Installer/UVtools.Installer.wixproj +++ b/UVtools.Installer/UVtools.Installer.wixproj @@ -61,11 +61,11 @@ - - - - - + + + + + diff --git a/UVtools.UI/Controls/Tools/ToolPCBExposureControl.axaml.cs b/UVtools.UI/Controls/Tools/ToolPCBExposureControl.axaml.cs index 0be0b782..4c449b32 100644 --- a/UVtools.UI/Controls/Tools/ToolPCBExposureControl.axaml.cs +++ b/UVtools.UI/Controls/Tools/ToolPCBExposureControl.axaml.cs @@ -76,7 +76,7 @@ public ToolPCBExposureControl() public override void Callback(ToolWindow.Callbacks callback) { - if (App.SlicerFile is null) return; + if (SlicerFile is null) return; switch (callback) { case ToolWindow.Callbacks.Init: diff --git a/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml b/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml new file mode 100644 index 00000000..05389466 --- /dev/null +++ b/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs b/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs new file mode 100644 index 00000000..e76691ce --- /dev/null +++ b/UVtools.UI/Controls/Tools/ToolPhasedExposureControl.axaml.cs @@ -0,0 +1,115 @@ +using System.Linq; +using Avalonia.Controls; +using UVtools.Core.Operations; +using UVtools.UI.Windows; + +namespace UVtools.UI.Controls.Tools +{ + public partial class ToolPhasedExposureControl : ToolControl + { + public OperationPhasedExposure Operation => (BaseOperation as OperationPhasedExposure)!; + public ToolPhasedExposureControl() + { + BaseOperation = new OperationPhasedExposure(SlicerFile!); + if (!ValidateSpawn()) return; + InitializeComponent(); + } + + public override void Callback(ToolWindow.Callbacks callback) + { + if (SlicerFile is null) return; + switch (callback) + { + case ToolWindow.Callbacks.Init: + case ToolWindow.Callbacks.AfterLoadProfile: + if (ParentWindow is not null) ParentWindow.ButtonOkEnabled = Operation.Count >= 2; + Operation.PhasedExposures.CollectionChanged += (sender, e) => ParentWindow!.ButtonOkEnabled = Operation.Count >= 2; + break; + } + } + + private void PhasedExposuresGrid_OnLoadingRow(object? sender, DataGridRowEventArgs e) + { + e.Row.Header = e.Row.GetIndex() + 1; + } + + public void AddExposure() + { + Operation.PhasedExposures.Add(new OperationPhasedExposure.PhasedExposure((decimal)SlicerFile!.ExposureTime, 0, (decimal)SlicerFile.ExposureTime, 0)); + } + + public void RemoveExposure() + { + Operation.PhasedExposures.RemoveRange(PhasedExposuresGrid.SelectedItems.OfType()); + } + + public void MoveExposureTop() + { + if (PhasedExposuresGrid.SelectedIndex <= 0) return; + var selectedFile = (OperationPhasedExposure.PhasedExposure)PhasedExposuresGrid.SelectedItem; + + var list = Operation.PhasedExposures.ToList(); + list.RemoveAt(PhasedExposuresGrid.SelectedIndex); + list.Insert(0, selectedFile); + Operation.PhasedExposures.ReplaceCollection(list); + + PhasedExposuresGrid.SelectedIndex = 0; + PhasedExposuresGrid.ScrollIntoView(selectedFile, PhasedExposuresGrid.Columns[0]); + } + + public void MoveExposureUp() + { + if (PhasedExposuresGrid.SelectedIndex <= 0) return; + var selectedFile = (OperationPhasedExposure.PhasedExposure)PhasedExposuresGrid.SelectedItem; + var newIndex = PhasedExposuresGrid.SelectedIndex - 1; + + + var list = Operation.PhasedExposures.ToList(); + list.RemoveAt(PhasedExposuresGrid.SelectedIndex); + list.Insert(newIndex, selectedFile); + Operation.PhasedExposures.ReplaceCollection(list); + + PhasedExposuresGrid.SelectedIndex = newIndex; + PhasedExposuresGrid.ScrollIntoView(selectedFile, PhasedExposuresGrid.Columns[0]); + } + + public void MoveExposureDown() + { + if (PhasedExposuresGrid.SelectedIndex == -1 || PhasedExposuresGrid.SelectedIndex == Operation.Count - 1) return; + var selectedFile = (OperationPhasedExposure.PhasedExposure)PhasedExposuresGrid.SelectedItem; + var newIndex = PhasedExposuresGrid.SelectedIndex + 1; + + var list = Operation.PhasedExposures.ToList(); + list.RemoveAt(PhasedExposuresGrid.SelectedIndex); + list.Insert(newIndex, selectedFile); + Operation.PhasedExposures.ReplaceCollection(list); + + PhasedExposuresGrid.SelectedIndex = newIndex; + PhasedExposuresGrid.ScrollIntoView(selectedFile, PhasedExposuresGrid.Columns[0]); + } + + public void MoveExposureBottom() + { + var lastIndex = Operation.Count - 1; + if (PhasedExposuresGrid.SelectedIndex == -1 || PhasedExposuresGrid.SelectedIndex == lastIndex) return; + var selectedFile = (OperationPhasedExposure.PhasedExposure)PhasedExposuresGrid.SelectedItem; + + var list = Operation.PhasedExposures.ToList(); + list.RemoveAt(PhasedExposuresGrid.SelectedIndex); + list.Add(selectedFile); + Operation.PhasedExposures.ReplaceCollection(list); + + PhasedExposuresGrid.SelectedIndex = lastIndex; + PhasedExposuresGrid.ScrollIntoView(selectedFile, PhasedExposuresGrid.Columns[0]); + } + + public void ResetExposures() + { + Operation.PhasedExposures.ReplaceCollection(new [] + { + new OperationPhasedExposure.PhasedExposure((decimal)SlicerFile!.BottomExposureTime, 10, (decimal)SlicerFile.ExposureTime, 4), + new OperationPhasedExposure.PhasedExposure((decimal)SlicerFile.ExposureTime, 0, (decimal)SlicerFile.ExposureTime, 0) + }); + } + } +} diff --git a/UVtools.UI/MainWindow.axaml.cs b/UVtools.UI/MainWindow.axaml.cs index 1e447540..18bd60c2 100644 --- a/UVtools.UI/MainWindow.axaml.cs +++ b/UVtools.UI/MainWindow.axaml.cs @@ -71,17 +71,18 @@ public partial class MainWindow : WindowEx new() { Tag = new OperationMorph()}, new() { Tag = new OperationRaftRelief()}, new() { Tag = new OperationRedrawModel()}, - /*new() { Tag = new OperationThreshold()},*/ + //new() { Tag = new OperationThreshold()}, new() { Tag = new OperationLayerArithmetic()}, new() { Tag = new OperationPixelArithmetic()}, new() { Tag = new OperationMask()}, - /*new() { Tag = new OperationPixelDimming()},*/ + //new() { Tag = new OperationPixelDimming()}, new() { Tag = new OperationLightBleedCompensation()}, new() { Tag = new OperationInfill()}, new() { Tag = new OperationBlur()}, new() { Tag = new OperationPattern()}, new() { Tag = new OperationFadeExposureTime()}, - new() { Tag = new OperationDoubleExposure()}, + //new() { Tag = new OperationDoubleExposure()}, + new() { Tag = new OperationPhasedExposure()}, new() { Tag = new OperationDynamicLifts()}, new() { Tag = new OperationDynamicLayerHeight()}, new() { Tag = new OperationLayerReHeight()}, @@ -2265,7 +2266,7 @@ private void RefreshRecentFiles(bool reloadFiles = false) { Header = Path.GetFileName(file).ReplaceFirst("_", "__"), Tag = file, - IsEnabled = !IsFileLoaded || SlicerFile!.FileFullPath != file + IsEnabled = (!IsFileLoaded || SlicerFile!.FileFullPath != file) && File.Exists(file) }; ToolTip.SetTip(item, file); ToolTip.SetPlacement(item, PlacementMode.Right); @@ -2274,7 +2275,7 @@ private void RefreshRecentFiles(bool reloadFiles = false) item.Click += MenuFileOpenRecentItemOnClick; } - + MenuFileOpenRecentItems = items; } diff --git a/UVtools.UI/UVtools.UI.csproj b/UVtools.UI/UVtools.UI.csproj index a3903702..77b850cd 100644 --- a/UVtools.UI/UVtools.UI.csproj +++ b/UVtools.UI/UVtools.UI.csproj @@ -2,7 +2,7 @@ WinExe UVtools - 4.0.4 + 4.0.5 AnyCPU;x64 true @@ -15,17 +15,17 @@ - - - - - - - + + + + + + + - - - + + +