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 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+