diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-05-17 05:34:31 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-05-17 05:34:31 +0300 |
commit | 51c82318cf27b8e0d3425aa2ea1b093fc5b7d23f (patch) | |
tree | 1059ece01344eec934792d72bf8b0ed9aad4a06d | |
parent | edd9984a31a90791edf3bcf594855d08507428b5 (diff) |
v2.12.0v2.12.0
- **Layer arithmetic:**
- (Add) Allow to use ':' to define a layer range to set, eg, 0:20 to select from 0 to 20 layers
- (Improvement) Modifications with set ROI and/or Mask(s) are only applied to target layer on that same regions
- (Improvement) Disallow set one layer to the same layer without any modification
- (Improvement) Clear and sanitize non-existing layers indexes
- (Improvement) Disable the layer range selector from dialog
- (Fix) Prevent error when using non-existing layers indexes
- (Fix) Allow use only a mask for operations
- (Fix) Implement the progress bar
- **File formats:**
- (Add) VDA.ZIP (Voxeldance Additive)
- (Improvement) Add a check to global `LightPWM` if 0 it will force to 255
- (Improvement) Add a check to layer `LightPWM` if 0 it will force to 255
- (Add) Allow to save the selected region (ROI) to a image file
- (Update) .NET 5.0.5 to 5.0.6
- (Fix) Getting the transposed rectangle in fliped images are offseting the position by -1
- (Fix) Tools: Hide ROI Region text when empty/not selected
28 files changed, 1241 insertions, 420 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b7b1f..4cb3b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 17/05/2021 - v2.12.0 + +- **Layer arithmetic:** + - (Add) Allow to use ':' to define a layer range to set, eg, 0:20 to select from 0 to 20 layers + - (Improvement) Modifications with set ROI and/or Mask(s) are only applied to target layer on that same regions + - (Improvement) Disallow set one layer to the same layer without any modification + - (Improvement) Clear and sanitize non-existing layers indexes + - (Improvement) Disable the layer range selector from dialog + - (Fix) Prevent error when using non-existing layers indexes + - (Fix) Allow use only a mask for operations + - (Fix) Implement the progress bar +- **File formats:** + - (Add) VDA.ZIP (Voxeldance Additive) + - (Improvement) Add a check to global `LightPWM` if 0 it will force to 255 + - (Improvement) Add a check to layer `LightPWM` if 0 it will force to 255 +- (Add) Allow to save the selected region (ROI) to a image file +- (Update) .NET 5.0.5 to 5.0.6 +- (Fix) Getting the transposed rectangle in fliped images are offseting the position by -1 +- (Fix) Tools: Hide ROI Region text when empty/not selected + ## 13/05/2021 - v2.11.2 - (Improvement) Applied some refactorings on code @@ -76,6 +76,7 @@ But also, i need victims for test subject. Proceed at your own risk! * CXDLP (Creality Box) * LGS (Longer Orange 10) * LGS30 (Longer Orange 30) +* VDA.ZIP (Voxeldance Additive) * VDT (Voxeldance Tango) * UVJ (Zip file format for manual manipulation) * Image files (png, jpg, jpeg, gif, bmp) diff --git a/UVtools.Core/About.cs b/UVtools.Core/About.cs index b6c740d..35373a1 100644 --- a/UVtools.Core/About.cs +++ b/UVtools.Core/About.cs @@ -6,7 +6,9 @@ * of this license document, but changing it is not allowed. */ +using System; using System.IO; +using System.Reflection; namespace UVtools.Core { @@ -19,5 +21,8 @@ namespace UVtools.Core public static string Donate = "https://paypal.me/SkillTournament"; public static string DemoFile = "UVtools_demo_file.sl1"; + + public static Version Version => Assembly.GetExecutingAssembly().GetName().Version; + public static string VersionStr => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); } } diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 0f0ade7..c667d84 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -216,6 +216,7 @@ namespace UVtools.Core.FileFormats new GR1File(), // GR1 Workshop new CXDLPFile(), // Creality Box new LGSFile(), // LGS, LGS30 + new VDAFile(), // VDA new VDTFile(), // VDT new UVJFile(), // UVJ new ImageFile(), // images @@ -367,12 +368,12 @@ namespace UVtools.Core.FileFormats /// <summary> /// Gets the available <see cref="FileFormat.PrintParameterModifier"/> /// </summary> - public abstract PrintParameterModifier[] PrintParameterModifiers { get; } + public virtual PrintParameterModifier[] PrintParameterModifiers => null; /// <summary> /// Gets the available <see cref="FileFormat.PrintParameterModifier"/> per layer /// </summary> - public virtual PrintParameterModifier[] PrintParameterPerLayerModifiers { get; } = null; + public virtual PrintParameterModifier[] PrintParameterPerLayerModifiers => null; /// <summary> /// Checks if a <see cref="PrintParameterModifier"/> exists on print parameters @@ -476,7 +477,7 @@ namespace UVtools.Core.FileFormats /// <summary> /// Gets the original thumbnail sizes /// </summary> - public abstract Size[] ThumbnailsOriginalSize { get; } + public virtual Size[] ThumbnailsOriginalSize => null; /// <summary> /// Gets the thumbnails for this <see cref="FileFormat"/> @@ -587,12 +588,12 @@ namespace UVtools.Core.FileFormats /// <summary> /// Gets or sets the display width in millimeters /// </summary> - public abstract float DisplayWidth { get; set; } + public virtual float DisplayWidth { get; set; } /// <summary> /// Gets or sets the display height in millimeters /// </summary> - public abstract float DisplayHeight { get; set; } + public virtual float DisplayHeight { get; set; } /// <summary> /// Gets or sets if images need to be mirrored on lcd to print on the correct orientation @@ -904,6 +905,8 @@ namespace UVtools.Core.FileFormats set => RaiseAndSet(ref _lightPwm, value); } + public bool CanUseBottomLayerCount => HavePrintParameterModifier(PrintParameterModifier.BottomLayerCount); + public bool CanUseBottomExposureTime => HavePrintParameterModifier(PrintParameterModifier.BottomExposureSeconds); public bool CanUseExposureTime => HavePrintParameterModifier(PrintParameterModifier.ExposureSeconds); public bool CanUseAnyExposureTime => CanUseBottomExposureTime || CanUseExposureTime; @@ -1528,26 +1531,7 @@ namespace UVtools.Core.FileFormats "Lower and fix your layer height on slicer to avoid precision errors.", fileFullPath); } - bool reSaveFile = false; - - if(ResolutionX == 0 || ResolutionY == 0) - { - var layer = FirstLayer; - if (layer is not null) - { - using var mat = layer.LayerMat; - - if (mat.Size.HaveZero()) - { - throw new FileLoadException($"File resolution ({Resolution}) is invalid and can't be auto fixed due invalid layers with same problem ({mat.Size}).", fileFullPath); - } - - Resolution = mat.Size; - reSaveFile = true; - } - } - - reSaveFile |= _layerManager.Sanitize(); + bool reSaveFile = _layerManager.Sanitize(); if (reSaveFile) { diff --git a/UVtools.Core/FileFormats/VDAFile.cs b/UVtools.Core/FileFormats/VDAFile.cs new file mode 100644 index 0000000..ab6ab3a --- /dev/null +++ b/UVtools.Core/FileFormats/VDAFile.cs @@ -0,0 +1,432 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Xml.Serialization; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Util; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.OpenSsl; +using UVtools.Core.Extensions; +using UVtools.Core.GCode; +using UVtools.Core.Operations; + +namespace UVtools.Core.FileFormats +{ + [Serializable] + [XmlRoot(ElementName = "root")] + public class VDARoot + { + [Serializable] + public class VDAFileInfo + { + [Serializable] + public class VDAVersion + { + public ushort Major { get; set; } = 1; + public ushort Minor { get; set; } = 2; + + } + + [Serializable] + public class VDAWritten + { + [Serializable] + [XmlRoot(ElementName = "By")] + public class VDABy + { + [XmlAttribute] + public string ApplicationName { get; set; } = About.Software; + + [XmlAttribute] + public string ApplicationVersion { get; set; } = About.VersionStr; + + public override string ToString() + { + return $"{ApplicationName} v{ApplicationVersion}"; + } + + public void Reset() + { + ApplicationName = About.Software; + ApplicationVersion = About.VersionStr; + } + } + + public VDABy By { get; set; } = new(); + + public string When { get; set; } = DateTime.Now.ToString("u"); + + public void Reset() + { + When = DateTime.Now.ToString("u"); + By.Reset(); + } + } + + + public VDAVersion Version { get; set; } = new(); + + public VDAWritten Written { get; set; } = new(); + } + + [Serializable] + public class VDASlices + { + public ushort Count { get; set; } = 1; + + [XmlElement("thickness")] + public float LayerHeight { get; set; } + + [XmlElement("startHeight")] + public float StartHeight { get; set; } + + [XmlElement("endHeight")] + public float EndHeight { get; set; } + + [XmlElement("layersCount")] + public uint LayerCount { get; set; } + } + + [Serializable] + public class VDAMachines + { + public string FileType { get; set; } = "ZIP File"; + public string Resolution { get; set; } = "1920*1080P"; + public string PixelXSize { get; set; } = "50um"; + public string PixelYSize { get; set; } = "50um"; + + [XmlElement("Anti-Aliasing")] + public byte AntiAliasing { get; set; } = 1; + + public float XLength { get; set; } + public float YWidth { get; set; } + public float ZHeight { get; set; } + } + + public class VDALayer + { + [XmlElement("Index")] + public uint Index { get; set; } + + [XmlElement("zvalue")] + public float ZPosition { get; set; } + + [XmlElement("filename")] + public string Filename { get; set; } + + public VDALayer() + { + } + + public VDALayer(uint index, float zPosition, string filename) + { + Index = index; + ZPosition = zPosition; + Filename = filename; + } + } + + + public VDAFileInfo FileInfo { get; set; } = new(); + public VDASlices Slices { get; set; } = new(); + public VDAMachines Machines { get; set; } = new(); + public List<VDALayer> Layers { get; set; } = new(); + } + + public class VDAFile : FileFormat + { + #region Constants + + #endregion + + #region Properties + public VDARoot ManifestFile { get; set; } = new (); + + public override FileFormatType FileType => FileFormatType.Archive; + + public override FileExtension[] FileExtensions { get; } = { + new("vda.zip", "Voxeldance Additive Zip") + }; + + public override uint ResolutionX + { + get + { + var resolution = ManifestFile.Machines.Resolution.Split('*', StringSplitOptions.TrimEntries); + if (resolution.Length < 2) return 0; + uint.TryParse(resolution[0], out var xRes); + return xRes; + } + set + { + ManifestFile.Machines.Resolution = $"{value}*{ResolutionY}P"; + RaisePropertyChanged(); + } + } + + public override uint ResolutionY + { + get + { + var resolution = ManifestFile.Machines.Resolution.Split('*', StringSplitOptions.TrimEntries); + if (resolution.Length < 2) return 0; + resolution[1] = resolution[1].TrimEnd('P'); + uint.TryParse(resolution[1], out var yRes); + return yRes; + } + set + { + ManifestFile.Machines.Resolution = $"{ResolutionX}*{value}P"; + RaisePropertyChanged(); + } + } + + public override float DisplayWidth + { + get + { + if (ManifestFile.Machines.XLength > 0) return ManifestFile.Machines.XLength; + + var umStr= ManifestFile.Machines.PixelXSize.Replace("um", string.Empty, StringComparison.OrdinalIgnoreCase); + + if (ushort.TryParse(umStr, out var um) && um > 0) + { + return (float) Math.Round(ResolutionX * um / 1000f, 2); + } + + return ManifestFile.Machines.XLength; + } + set + { + ManifestFile.Machines.XLength = (float) Math.Round(value, 2); + ManifestFile.Machines.PixelXSize = $"{Math.Round(value / ResolutionX * 1000, 2)}um"; + RaisePropertyChanged(); + } + } + + public override float DisplayHeight + { + get + { + if (ManifestFile.Machines.YWidth > 0) return ManifestFile.Machines.YWidth; + + var umStr = ManifestFile.Machines.PixelYSize.Replace("um", string.Empty, StringComparison.OrdinalIgnoreCase); + + if (ushort.TryParse(umStr, out var um) && um > 0) + { + return (float)Math.Round(ResolutionY * um / 1000f, 2); + } + + return ManifestFile.Machines.YWidth; + } + set + { + ManifestFile.Machines.YWidth = (float)Math.Round(value, 2); + ManifestFile.Machines.PixelYSize = $"{Math.Round(value / ResolutionY * 1000, 2)}um"; + RaisePropertyChanged(); + } + } + + public override float MachineZ + { + get => ManifestFile.Machines.ZHeight > 0 ? ManifestFile.Machines.ZHeight : base.MachineZ; + set + { + ManifestFile.Machines.ZHeight = value; + RaisePropertyChanged(); + } + } + + public override byte AntiAliasing + { + get => ManifestFile.Machines.AntiAliasing; + set + { + ManifestFile.Machines.AntiAliasing = value.Clamp(1, 16); + RaisePropertyChanged(); + } + } + + public override float LayerHeight + { + get => ManifestFile.Slices.LayerHeight; + set + { + ManifestFile.Slices.LayerHeight = Layer.RoundHeight(value); + RaisePropertyChanged(); + } + } + + public override uint LayerCount + { + get => base.LayerCount; + set => base.LayerCount = ManifestFile.Slices.LayerCount = base.LayerCount; + } + + + public override object[] Configs => new object[] { + ManifestFile.FileInfo.Version, + ManifestFile.FileInfo.Written, + ManifestFile.Machines, + ManifestFile.Slices }; + + #endregion + + #region Constructor + public VDAFile() + { } + #endregion + + #region Methods + + protected override void EncodeInternally(string fileFullPath, OperationProgress progress) + { + using var outputFile = ZipFile.Open(fileFullPath, ZipArchiveMode.Create); + var manifestFilename = Path.GetFileName(fileFullPath). + Replace($".{FileExtensions[0].Extension}{TemporaryFileAppend}", ".xml"). + Replace($".{FileExtensions[0].Extension}", ".xml"); + + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + progress.Token.ThrowIfCancellationRequested(); + var layer = this[layerIndex]; + var filename = $"{layerIndex + 1}".PadLeft(4, '0') + ".png"; + outputFile.PutFileContent(filename, layer.CompressedBytes, ZipArchiveMode.Create); + progress++; + } + + UpdateManifest(); + + XmlSerializer serializer = new(ManifestFile.GetType()); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + var entry = outputFile.CreateEntry(manifestFilename); + using var stream = entry.Open(); + serializer.Serialize(stream, ManifestFile, ns); + } + + protected override void DecodeInternally(string fileFullPath, OperationProgress progress) + { + using (var inputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Read)) + { + var entry = inputFile.Entries.FirstOrDefault(zipEntry => zipEntry.Name.EndsWith(".xml")); + if (entry is null) + { + Clear(); + throw new FileLoadException($".xml manifest not found", fileFullPath); + } + + try + { + var serializer = new XmlSerializer(ManifestFile.GetType()); + using var stream = entry.Open(); + ManifestFile = (VDARoot)serializer.Deserialize(stream); + } + catch (Exception e) + { + Clear(); + throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", fileFullPath); + } + + + LayerManager.Init(ManifestFile.Slices.LayerCount); + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); + + + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + if (progress.Token.IsCancellationRequested) break; + var filename = $"{layerIndex + 1}".PadLeft(4, '0')+".png"; + entry = inputFile.GetEntry(filename); + if (entry is null) + { + Clear(); + throw new FileLoadException($"Layer {filename} not found", fileFullPath); + } + + using var stream = entry.Open(); + this[layerIndex] = new Layer(layerIndex, stream, LayerManager); + + progress++; + } + } + + LayerManager.GetBoundingRectangle(progress); + } + + public override void SaveAs(string filePath = null, OperationProgress progress = null) + { + if (RequireFullEncode) + { + if (!string.IsNullOrEmpty(filePath)) + { + FileFullPath = filePath; + } + Encode(FileFullPath, progress); + return; + } + + if (!string.IsNullOrEmpty(filePath)) + { + File.Copy(FileFullPath, filePath, true); + FileFullPath = filePath; + } + + using var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Update); + bool deleted; + + do + { + deleted = false; + foreach (var zipEntry in outputFile.Entries) + { + if (!zipEntry.Name.EndsWith(".xml")) continue; + zipEntry.Delete(); + deleted = true; + break; + } + } while (deleted); + + var manifestFilename = Path.GetFileName(FileFullPath). + Replace($".{FileExtensions[0].Extension}{TemporaryFileAppend}", ".xml"). + Replace($".{FileExtensions[0].Extension}", ".xml"); + + UpdateManifest(); + + XmlSerializer serializer = new(ManifestFile.GetType()); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + var entry = outputFile.CreateEntry(manifestFilename); + using var stream = entry.Open(); + serializer.Serialize(stream, ManifestFile, ns); + } + + public void UpdateManifest() + { + ManifestFile.FileInfo.Written.Reset(); + ManifestFile.Slices.StartHeight = FirstLayer.PositionZ; + ManifestFile.Slices.EndHeight = LastLayer.PositionZ; + ManifestFile.Layers.Clear(); + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + var layer = this[layerIndex]; + ManifestFile.Layers.Add(new VDARoot.VDALayer(layerIndex, layer.PositionZ, layer.FormatFileName(4, false))); + } + } + #endregion + } +} diff --git a/UVtools.Core/FileFormats/ZCodeFile.cs b/UVtools.Core/FileFormats/ZCodeFile.cs index 412adb5..698e87f 100644 --- a/UVtools.Core/FileFormats/ZCodeFile.cs +++ b/UVtools.Core/FileFormats/ZCodeFile.cs @@ -35,7 +35,7 @@ namespace UVtools.Core.FileFormats public class ZcodePrintDevice { [XmlAttribute("z")] - public ushort MachineZ { get; set; } = 220; + public float MachineZ { get; set; } = 220; [XmlAttribute("height")] public uint ResolutionY { get; set; } = 2400; @@ -257,7 +257,7 @@ namespace UVtools.Core.FileFormats get => ManifestFile.Device.MachineZ > 0 ? ManifestFile.Device.MachineZ : base.MachineZ; set { - ManifestFile.Device.MachineZ = (ushort) value; + ManifestFile.Device.MachineZ = value; RaisePropertyChanged(); } } diff --git a/UVtools.Core/GCode/GCodeBuilder.cs b/UVtools.Core/GCode/GCodeBuilder.cs index 18d5c51..97b4667 100644 --- a/UVtools.Core/GCode/GCodeBuilder.cs +++ b/UVtools.Core/GCode/GCodeBuilder.cs @@ -514,10 +514,10 @@ namespace UVtools.Core.GCode AppendLineIfCanComment(BeginLayerComments, layerIndex, layer.PositionZ); - if (layer.CanExpose) - { - AppendShowImageM6054(GetShowImageString(layerIndex)); - } + //if (layer.CanExpose) + //{ Dont check this for compability + AppendShowImageM6054(GetShowImageString(layerIndex)); + //} if (liftHeight > 0 && liftZPosAbs > layer.PositionZ) { @@ -627,7 +627,7 @@ namespace UVtools.Core.GCode float positionZ = 0; for (uint layerIndex = 0; layerIndex < slicerFile.LayerCount; layerIndex++) { - var layer = slicerFile[layerIndex]; + var layer = slicerFile[layerIndex]; if(layer is null) continue; var startStr = CommandShowImageM6054.ToStringWithoutComments(GetShowImageString(layerIndex)); var endStr = CommandShowImageM6054.ToStringWithoutComments(GetShowImageString(layerIndex+1)); diff --git a/UVtools.Core/Layer/Layer.cs b/UVtools.Core/Layer/Layer.cs index f9f9695..c1e2fc9 100644 --- a/UVtools.Core/Layer/Layer.cs +++ b/UVtools.Core/Layer/Layer.cs @@ -202,7 +202,8 @@ namespace UVtools.Core get => _lightPwm; set { - if (value <= 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLightPWM, SlicerFile.LightPWM); + if (value == 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLightPWM, SlicerFile.LightPWM); + if (value == 0) value = FileFormat.DefaultLightPWM; RaiseAndSetIfChanged(ref _lightPwm, value); } } @@ -511,11 +512,23 @@ namespace UVtools.Core LightOffDelay = CalculateLightOffDelay(extraTime); } - public string FormatFileName(string name) + public string FormatFileName(string prepend, byte padDigits, bool layerIndexZeroStarted = true) { - return $"{name}{Index.ToString().PadLeft(ParentLayerManager.LayerDigits, '0')}.png"; + var index = Index; + if (!layerIndexZeroStarted) + { + index++; + } + return $"{prepend}{index.ToString().PadLeft(padDigits, '0')}.png"; } + public string FormatFileName(string prepend = "", bool layerIndexZeroStarted = true) + => FormatFileName(prepend, ParentLayerManager.LayerDigits, layerIndexZeroStarted); + + public string FormatFileName(byte padDigits, bool layerIndexZeroStarted = true) + => FormatFileName(string.Empty, padDigits, layerIndexZeroStarted); + + public Rectangle GetBoundingRectangle(Mat mat = null, bool reCalculate = false) { if (_nonZeroPixelCount > 0 && !reCalculate) @@ -625,8 +638,8 @@ namespace UVtools.Core // These arrays are used to // get row and column numbers // of 8 neighbors of a given cell - List<LayerIssue> result = new List<LayerIssue>(); - List<Point> pixels = new List<Point>(); + List<LayerIssue> result = new(); + List<Point> pixels = new(); @@ -700,7 +713,7 @@ namespace UVtools.Core int y2; - Queue<Point> queue = new Queue<Point>(); + Queue<Point> queue = new(); queue.Enqueue(new Point(x, y)); // Mark this cell as visited visited[x, y] = true; diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs index df3a950..e7b9c7d 100644 --- a/UVtools.Core/Layer/LayerManager.cs +++ b/UVtools.Core/Layer/LayerManager.cs @@ -441,6 +441,23 @@ namespace UVtools.Core if (this[layerIndex - 1].PositionZ > this[layerIndex].PositionZ) 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 (SlicerFile.ResolutionX == 0 || SlicerFile.ResolutionY == 0) + { + var layer = FirstLayer; + if (layer is not null) + { + using var mat = layer.LayerMat; + + if (mat.Size.HaveZero()) + { + throw new FileLoadException($"File resolution ({SlicerFile.Resolution}) is invalid and can't be auto fixed due invalid layers with same problem ({mat.Size}).", SlicerFile.FileFullPath); + } + + SlicerFile.Resolution = mat.Size; + appliedCorrections = true; + } + } + // Fix 0mm positions at layer 0 if (this[0].PositionZ == 0) { @@ -452,6 +469,18 @@ namespace UVtools.Core appliedCorrections = true; } + // Fix LightPWM of 0 + if (SlicerFile.LightPWM == 0) + { + SlicerFile.LightPWM = FileFormat.DefaultLightPWM; + appliedCorrections = true; + } + if (SlicerFile.BottomLightPWM == 0) + { + SlicerFile.BottomLightPWM = FileFormat.DefaultBottomLightPWM; + appliedCorrections = true; + } + return appliedCorrections; } @@ -779,7 +808,7 @@ namespace UVtools.Core if (touchBoundConfig.Enabled) { // TouchingBounds Checker - List<Point> pixels = new List<Point>(); + List<Point> pixels = new(); bool touchTop = layer.BoundingRectangle.Top <= touchBoundConfig.MarginTop; bool touchBottom = layer.BoundingRectangle.Bottom >= image.Height - touchBoundConfig.MarginBottom; bool touchLeft = layer.BoundingRectangle.Left <= touchBoundConfig.MarginLeft; @@ -1113,7 +1142,7 @@ namespace UVtools.Core for (int i = 1; i < numLabels; i++) { - Rectangle rect = new Rectangle( + Rectangle rect = new( (int) ccStats.GetValue(i, (int) ConnectedComponentsTypes.Left), (int) ccStats.GetValue(i, (int) ConnectedComponentsTypes.Top), (int) ccStats.GetValue(i, (int) ConnectedComponentsTypes.Width), @@ -1128,7 +1157,7 @@ namespace UVtools.Core previousSpan = previousImage.GetPixelSpan<byte>(); } - List<Point> points = new List<Point>(); + List<Point> points = new(); uint pixelsSupportingIsland = 0; for (int y = rect.Y; y < rect.Bottom; y++) @@ -1573,7 +1602,7 @@ namespace UVtools.Core progress ??= new OperationProgress(); progress.Reset("Drawings", (uint) drawings.Count); - ConcurrentDictionary<uint, Mat> modifiedLayers = new ConcurrentDictionary<uint, Mat>(); + ConcurrentDictionary<uint, Mat> modifiedLayers = new(); for (var i = 0; i < drawings.Count; i++) { var operation = drawings[i]; @@ -1648,7 +1677,7 @@ namespace UVtools.Core int yStart = Math.Max(0, operation.Location.Y - operationSupport.TipDiameter / 2); int xStart = Math.Max(0, operation.Location.X - operationSupport.TipDiameter / 2); - using (Mat matCircleRoi = new Mat(mat, new Rectangle(xStart, yStart, operationSupport.TipDiameter, operationSupport.TipDiameter))) + using (Mat matCircleRoi = new(mat, new Rectangle(xStart, yStart, operationSupport.TipDiameter, operationSupport.TipDiameter))) { using (Mat matCircleMask = matCircleRoi.CloneBlank()) { @@ -1683,9 +1712,9 @@ namespace UVtools.Core int yStart = Math.Max(0, operation.Location.Y - radius); int xStart = Math.Max(0, operation.Location.X - radius); - using (Mat matCircleRoi = new Mat(mat, new Rectangle(xStart, yStart, operationDrainHole.Diameter, operationDrainHole.Diameter))) + using (Mat matCircleRoi = new(mat, new Rectangle(xStart, yStart, operationDrainHole.Diameter, operationDrainHole.Diameter))) { - using (Mat matCircleRoiInv = new Mat()) + using (Mat matCircleRoiInv = new()) { CvInvoke.Threshold(matCircleRoi, matCircleRoiInv, 100, 255, ThresholdType.BinaryInv); using (Mat matCircleMask = matCircleRoi.CloneBlank()) diff --git a/UVtools.Core/Objects/BindableBase.cs b/UVtools.Core/Objects/BindableBase.cs index 68a7611..9e63a1f 100644 --- a/UVtools.Core/Objects/BindableBase.cs +++ b/UVtools.Core/Objects/BindableBase.cs @@ -21,12 +21,11 @@ namespace UVtools.Core.Objects /// Multicast event for property change notifications. /// </summary> private PropertyChangedEventHandler _propertyChanged; - private List<string> events = new List<string>(); public event PropertyChangedEventHandler PropertyChanged { - add { _propertyChanged += value; events.Add("added"); } - remove { _propertyChanged -= value; events.Add("removed"); } + add => _propertyChanged += value; + remove => _propertyChanged -= value; } /// <summary> diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index b7f7955..5cb7cad 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -235,6 +235,8 @@ namespace UVtools.Core.Operations public bool HaveMask => _maskPoints is not null && _maskPoints.Length > 0; + public bool HaveROIorMask => HaveROI || HaveMask; + /// <summary> /// Gets if this operation have been executed once /// </summary> diff --git a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs index 9109e29..9fb8d6b 100644 --- a/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs +++ b/UVtools.Core/Operations/OperationCalibrateExposureFinder.cs @@ -128,7 +128,7 @@ namespace UVtools.Core.Operations private bool _bullsEyeInvertQuadrants = true; private bool _counterTrianglesEnabled = true; - private sbyte _counterTrianglesTipOffset = 1; + private sbyte _counterTrianglesTipOffset = 3; private bool _counterTrianglesFence = false; private bool _patternModel; diff --git a/UVtools.Core/Operations/OperationLayerArithmetic.cs b/UVtools.Core/Operations/OperationLayerArithmetic.cs index 2ea9bc8..721f8c9 100644 --- a/UVtools.Core/Operations/OperationLayerArithmetic.cs +++ b/UVtools.Core/Operations/OperationLayerArithmetic.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; @@ -53,6 +54,8 @@ namespace UVtools.Core.Operations #endregion #region Overrides + + public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; public override string Title => "Layer arithmetic"; public override string Description => "Perform arithmetic operations over the layers\n" + @@ -63,7 +66,7 @@ namespace UVtools.Core.Operations "Syntax: <set_to_layer_indexes> = <layer_index> <operator> <layer_index>\n" + "When: \"<set_to_layer_indexes> =\" is omitted, the result will assign to the first layer on the sentence.\n\n" + "Example 1: 10+11\n" + - "Example 2: 10,11,12 = 11+12-10*5\n" + + "Example 2: 10,11,12 = 11+12-10*5 Same as: 10:12 = 11+12-10*5\n" + "On example 1 the layer 10 will be set with the result of layer 10 plus layer 11.\n" + "On example 2 the layers 10,11,12 will be set with the result of layer 11 plus 12 minus 10 all multiplied by layer 5.\n\n" + "Note: Calculation are made sequential, math order rules wont apply here."; @@ -87,13 +90,15 @@ namespace UVtools.Core.Operations sb.AppendLine("No layers to assign."); else if (Operations.Count == 0) sb.AppendLine("No operations to perform."); + else if (!IsValid) + sb.AppendLine("The operation will have no impact and will not be performed."); return sb.ToString(); } public override string ToString() { - var result = $"{_sentence}" + LayerRangeString; + var result = $"{_sentence}"; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; } @@ -106,12 +111,13 @@ namespace UVtools.Core.Operations set => RaiseAndSetIfChanged(ref _sentence, value); } [XmlIgnore] - public List<ArithmeticOperation> Operations { get; } = new(); + public List<ArithmeticOperation> Operations { get; private set; } = new(); [XmlIgnore] - public List<uint> SetLayers { get; } = new List<uint>(); + public List<uint> SetLayers { get; private set; } = new(); - public bool IsValid => SetLayers.Count > 0 & Operations.Count > 0; + public bool IsValid => SetLayers.Count > 0 && Operations.Count > 0 && + !(SetLayers.Count == 1 && Operations.Count == 1 && SetLayers[0] == Operations[0].LayerIndex); #endregion #region Constructor @@ -135,15 +141,31 @@ namespace UVtools.Core.Operations if (splitSentence.Length >= 2) { operations = splitSentence[1]; - var setLayers = splitSentence[0].Replace(" ", string.Empty).Split(','); + var setLayers = splitSentence[0].Replace(" ", string.Empty).Split(',', StringSplitOptions.TrimEntries); foreach (var layer in setLayers) { + var rangeSplit = layer.Split(':', StringSplitOptions.TrimEntries); + if (rangeSplit.Length > 1) + { + uint.TryParse(rangeSplit[0].Trim(), out var startLayer); + if (!uint.TryParse(rangeSplit[1].Trim(), out var endLayer)) endLayer = SlicerFile.LastLayerIndex; + for (var index = startLayer; index <= endLayer; index++) + { + if (SetLayers.Contains(index)) continue; + SetLayers.Add(index); + } + continue; + } + if (!uint.TryParse(layer.Trim(), out var layerIndex)) continue; if (SetLayers.Contains(layerIndex)) continue; SetLayers.Add(layerIndex); } } + SetLayers = SetLayers.Where(layerIndex => layerIndex <= SlicerFile.LastLayerIndex).ToList(); + SetLayers.Sort(); + operations = operations.Replace(" ", string.Empty); if (string.IsNullOrWhiteSpace(operations)) return false; @@ -208,8 +230,10 @@ namespace UVtools.Core.Operations } } - if (Operations.Count == 0) return false; - if (SetLayers.Count == 0) + Operations = Operations.Where(op => op.LayerIndex <= SlicerFile.LastLayerIndex).ToList(); + + //if (Operations.Count == 0) return false; + if (SetLayers.Count == 0 && Operations.Count > 0) { SetLayers.Add(Operations[0].LayerIndex); } @@ -223,12 +247,15 @@ namespace UVtools.Core.Operations using var result = SlicerFile[Operations[0].LayerIndex].LayerMat; using var resultRoi = GetRoiOrDefault(result); + using var imageMask = GetMask(resultRoi); + + progress.ItemCount = (uint) Operations.Count; for (int i = 1; i < Operations.Count; i++) { progress.Token.ThrowIfCancellationRequested(); using var image = SlicerFile[Operations[i].LayerIndex].LayerMat; var imageRoi = GetRoiOrDefault(image); - using var imageMask = GetMask(image); + switch (Operations[i - 1].Operator) { case LayerArithmeticOperators.Add: @@ -256,20 +283,26 @@ namespace UVtools.Core.Operations CvInvoke.AbsDiff(resultRoi, imageRoi, resultRoi); break; } + + progress++; } + progress.Reset("Applied layers", (uint) SetLayers.Count); Parallel.ForEach(SetLayers, layerIndex => { if (progress.Token.IsCancellationRequested) return; - if (Operations.Count == 1 && HaveROI) + progress.LockAndIncrement(); + if (Operations.Count == 1 || HaveROIorMask) { - var mat = SlicerFile[layerIndex].LayerMat; + using var mat = SlicerFile[layerIndex].LayerMat; var matRoi = GetRoiOrDefault(mat); - using var imageMask = GetMask(mat); resultRoi.CopyTo(matRoi, imageMask); SlicerFile[layerIndex].LayerMat = mat; return; } + + //ApplyMask(mat, resultRoi, imageMask); + SlicerFile[layerIndex].LayerMat = result; }); diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index d3eac6a..46b05c3 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>2.11.2</Version> + <Version>2.12.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> @@ -49,7 +49,7 @@ <PackageReference Include="AnimatedGif" Version="1.0.5" /> <PackageReference Include="BinarySerializer" Version="8.6.0" /> <PackageReference Include="Emgu.CV" Version="4.5.1.4349" /> - <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.10.0-2.final" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.10.0-3.final" /> <PackageReference Include="morelinq" Version="3.3.2" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Portable.BouncyCastle" Version="1.8.10" /> diff --git a/UVtools.WPF/Controls/AdvancedImageBox.axaml b/UVtools.WPF/Controls/AdvancedImageBox.axaml index 2c288f6..6866ddf 100644 --- a/UVtools.WPF/Controls/AdvancedImageBox.axaml +++ b/UVtools.WPF/Controls/AdvancedImageBox.axaml @@ -8,10 +8,9 @@ RowDefinitions="*,Auto" ColumnDefinitions="*,Auto"> - <ContentControl - Name="ViewPort" - Background="Transparent" - /> + <ContentPresenter Grid.Row="0" Grid.Column="0" + Name="ViewPort" + Background="Transparent"/> <ScrollBar Name="VerticalScrollBar" @@ -20,8 +19,7 @@ ViewportSize="{Binding #ViewPort.Bounds.Height}" Minimum="0" Maximum="0" - Visibility="Auto" - /> + Visibility="Auto"/> <ScrollBar Name="HorizontalScrollBar" @@ -30,13 +28,12 @@ ViewportSize="{Binding #ViewPort.Bounds.Width}" Minimum="0" Maximum="0" - Visibility="Auto" - /> + Visibility="Auto"/> <Border Grid.Row="1" Grid.Column="1" - Background="WhiteSmoke" - /> + Background="WhiteSmoke"/> + </Grid> </UserControl> diff --git a/UVtools.WPF/Controls/AdvancedImageBox.axaml.cs b/UVtools.WPF/Controls/AdvancedImageBox.axaml.cs index 02e8f87..94910fc 100644 --- a/UVtools.WPF/Controls/AdvancedImageBox.axaml.cs +++ b/UVtools.WPF/Controls/AdvancedImageBox.axaml.cs @@ -2,15 +2,17 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Drawing; using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using UVtools.Core.Extensions; using UVtools.WPF.Extensions; using Bitmap = Avalonia.Media.Imaging.Bitmap; @@ -26,7 +28,7 @@ namespace UVtools.WPF.Controls { public ScrollBar HorizontalScrollBar { get; } public ScrollBar VerticalScrollBar { get; } - public ContentControl ViewPortControl { get; } + public ContentPresenter ViewPort { get; } public Vector Offset { @@ -40,19 +42,18 @@ namespace UVtools.WPF.Controls } } - public Size Viewport => ViewPortControl.Bounds.Size; + public Size ViewPortSize => ViewPort.Bounds.Size; #region Bindable Base /// <summary> /// Multicast event for property change notifications. /// </summary> private PropertyChangedEventHandler _propertyChanged; - private readonly List<string> events = new (); public new event PropertyChangedEventHandler PropertyChanged { - add { _propertyChanged += value; events.Add("added"); } - remove { _propertyChanged -= value; events.Add("removed"); } + add => _propertyChanged += value; + remove => _propertyChanged -= value; } protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { @@ -123,16 +124,10 @@ namespace UVtools.WPF.Controls /// <summary> /// Returns the default zoom levels /// </summary> - public static ZoomLevelCollection Default - { - get - { - return new ZoomLevelCollection(new[] - { - 7, 10, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600, 3200 - }); - } - } + public static ZoomLevelCollection Default => + new(new[] { + 7, 10, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600, 3200 + }); #endregion @@ -233,9 +228,9 @@ namespace UVtools.WPF.Controls /// <param name="arrayIndex">A 64-bit integer that represents the index in the <see cref="Array"/> at which storing begins.</param> public void CopyTo(int[] array, int arrayIndex) { - for (int i = 0; i < this.Count; i++) + for (int i = 0; i < Count; i++) { - array[arrayIndex + i] = this.List.Values[i]; + array[arrayIndex + i] = List.Values[i]; } } @@ -245,7 +240,7 @@ namespace UVtools.WPF.Controls /// <param name="zoomLevel">The zoom level.</param> public int FindNearest(int zoomLevel) { - int nearestValue = this.List.Values[0]; + int nearestValue = List.Values[0]; int nearestDifference = Math.Abs(nearestValue - zoomLevel); for (int i = 1; i < Count; i++) { @@ -297,8 +292,8 @@ namespace UVtools.WPF.Controls /// <returns>The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom.</returns> public int NextZoom(int zoomLevel) { - var index = IndexOf(this.FindNearest(zoomLevel)); - if (index < this.Count - 1) + var index = IndexOf(FindNearest(zoomLevel)); + if (index < Count - 1) { index++; } @@ -381,7 +376,7 @@ namespace UVtools.WPF.Controls public enum SizeModes : byte { /// <summary> - /// The image is disiplayed according to current zoom and scroll properties. + /// The image is displayed according to current zoom and scroll properties. /// </summary> Normal, @@ -393,7 +388,7 @@ namespace UVtools.WPF.Controls /// <summary> /// The image is stretched to fill as much of the client area of the control as possible, whilst retaining the same aspect ratio for the width and height. /// </summary> - //Fit + Fit } [Flags] @@ -452,68 +447,95 @@ namespace UVtools.WPF.Controls #endregion - #region Constants - public static readonly int MinZoom = 10; - public static readonly int MaxZoom = 3500; - #endregion + public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>( + nameof(CanRender), + o => o.CanRender); + /// <summary> + /// Gets or sets if control can render the image + /// </summary> public bool CanRender { get => _canRender; set { - if (!RaiseAndSetIfChanged(ref _canRender, value)) return; + if (!SetAndRaise(CanRenderProperty, ref _canRender, value)) return; if (_canRender) TriggerRender(); } } + public static readonly StyledProperty<byte> GridCellSizeProperty = + AvaloniaProperty.Register<AdvancedImageBox, byte>(nameof(GridCellSize), 15); + /// <summary> - /// Gets or sets the basic cell size + /// Gets or sets the grid cell size /// </summary> public byte GridCellSize { - get => _gridCellSize; - set => RaiseAndSetIfChanged(ref _gridCellSize, value); + get => GetValue(GridCellSizeProperty); + set => SetValue(GridCellSizeProperty, value); } + public static readonly StyledProperty<ISolidColorBrush> GridColorProperty = + AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(nameof(GridColor), Brushes.Gainsboro); + /// <summary> /// Gets or sets the color used to create the checkerboard style background /// </summary> public ISolidColorBrush GridColor { - get => _gridColor; - set => RaiseAndSetIfChanged(ref _gridColor, value); + get => GetValue(GridColorProperty); + set => SetValue(GridColorProperty, value); } + public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty = + AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(nameof(GridColorAlternate), Brushes.White); + /// <summary> /// Gets or sets the color used to create the checkerboard style background /// </summary> public ISolidColorBrush GridColorAlternate { - get => _gridColorAlternate; - set => RaiseAndSetIfChanged(ref _gridColorAlternate, value); + get => GetValue(GridColorAlternateProperty); + set => SetValue(GridColorAlternateProperty, value); } + public static readonly StyledProperty<Bitmap> ImageProperty = + AvaloniaProperty.Register<AdvancedImageBox, Bitmap>(nameof(Image)); + /// <summary> /// Gets or sets the image to be displayed /// </summary> public Bitmap Image { - get => _image; + get => GetValue(ImageProperty); set { - if (!RaiseAndSetIfChanged(ref _image, value)) return; + SetValue(ImageProperty, value); - if (Image is null) + if (value is null) { SelectNone(); } UpdateViewPort(); TriggerRender(); + + RaisePropertyChanged(nameof(IsImageLoaded)); } } + public WriteableBitmap ImageAsWriteableBitmap => (WriteableBitmap) Image; + + public bool IsImageLoaded => Image is not null; + + public static readonly DirectProperty<AdvancedImageBox, Bitmap> TrackerImageProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, Bitmap>( + nameof(TrackerImage), + o => o.TrackerImage, + (o, v) => o.TrackerImage = v); + /// <summary> /// Gets or sets an image to follow the mouse pointer /// </summary> @@ -522,29 +544,33 @@ namespace UVtools.WPF.Controls get => _trackerImage; set { - if (!RaiseAndSetIfChanged(ref _trackerImage, value)) return; - RaisePropertyChanged(nameof(HaveTrackerImage)); + if (!SetAndRaise(TrackerImageProperty, ref _trackerImage, value)) return; TriggerRender(); + RaisePropertyChanged(nameof(HaveTrackerImage)); } } - public bool HaveTrackerImage => !(_trackerImage is null); + public bool HaveTrackerImage => _trackerImage is not null; + + public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(TrackerImageAutoZoom), true); /// <summary> /// Gets or sets if the tracker image will be scaled to the current zoom /// </summary> public bool TrackerImageAutoZoom { - get => _trackerImageAutoZoom; - set => RaiseAndSetIfChanged(ref _trackerImageAutoZoom, value); + get => GetValue(TrackerImageAutoZoomProperty); + set => SetValue(TrackerImageAutoZoomProperty, value); } - + public bool IsHorizontalBarVisible { get { if (Image is null) return false; - return ScaledImageWidth > Viewport.Width; + if (SizeMode != SizeModes.Normal) return false; + return ScaledImageWidth > ViewPortSize.Width; } } @@ -553,34 +579,51 @@ namespace UVtools.WPF.Controls get { if (Image is null) return false; - return ScaledImageHeight > Viewport.Height; + if (SizeMode != SizeModes.Normal) return false; + return ScaledImageHeight > ViewPortSize.Height; } } - public static readonly DirectProperty<AdvancedImageBox, bool> ShowGridProperty = - AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(nameof(ShowGrid), - c => c.ShowGrid, (c, v) => c.ShowGrid = v); + public static readonly StyledProperty<bool> ShowGridProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(ShowGrid), true); + /// <summary> - /// Gets or sets if the checkerboard background should be displayed + /// Gets or sets the grid visibility when reach high zoom levels /// </summary> public bool ShowGrid { - get => _showGrid; - set => SetAndRaise(ShowGridProperty, ref _showGrid, value); + get => GetValue(ShowGridProperty); + set => SetValue(ShowGridProperty, value); } + public static readonly DirectProperty<AdvancedImageBox, Point> PointerPositionProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>( + nameof(PointerPosition), + o => o.PointerPosition); + + /// <summary> + /// Gets the current pointer position + /// </summary> public Point PointerPosition { get => _pointerPosition; - private set => RaiseAndSetIfChanged(ref _pointerPosition, value); + private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value); } + public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>( + nameof(IsPanning), + o => o.IsPanning); + + /// <summary> + /// Gets if control is currently panning + /// </summary> public bool IsPanning { get => _isPanning; protected set { - if (!RaiseAndSetIfChanged(ref _isPanning, value)) return; + if (!SetAndRaise(IsPanningProperty, ref _isPanning, value)) return; _startScrollPosition = Offset; if (value) @@ -596,94 +639,242 @@ namespace UVtools.WPF.Controls } } + public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>( + nameof(IsSelecting), + o => o.IsSelecting); + + /// <summary> + /// Gets if control is currently selecting a ROI + /// </summary> public bool IsSelecting { get => _isSelecting; - protected set => RaiseAndSetIfChanged(ref _isSelecting, value); + protected set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value); } + /// <summary> + /// Gets the center point of the viewport + /// </summary> public Point CenterPoint { get { var viewport = GetImageViewPort(); - return new Point(viewport.Width / 2, viewport.Height / 2); + return new(viewport.Width / 2, viewport.Height / 2); } } + public static readonly StyledProperty<bool> AutoPanProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(AutoPan), true); + + /// <summary> + /// Gets or sets if the control can pan with the mouse + /// </summary> public bool AutoPan { - get => _autoPan; - set => RaiseAndSetIfChanged(ref _autoPan, value); + get => GetValue(AutoPanProperty); + set => SetValue(AutoPanProperty, value); } + public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty = + AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton | MouseButtons.RightButton); + + /// <summary> + /// Gets or sets the mouse buttons to pan the image + /// </summary> public MouseButtons PanWithMouseButtons { - get => _panWithMouseButtons; - set => RaiseAndSetIfChanged(ref _panWithMouseButtons, value); + get => GetValue(PanWithMouseButtonsProperty); + set => SetValue(PanWithMouseButtonsProperty, value); } + public static readonly StyledProperty<bool> PanWithArrowsProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(PanWithArrows), true); + + /// <summary> + /// Gets or sets if the control can pan with the keyboard arrows + /// </summary> public bool PanWithArrows { - get => _panWithArrows; - set => RaiseAndSetIfChanged(ref _panWithArrows, value); + get => GetValue(PanWithArrowsProperty); + set => SetValue(PanWithArrowsProperty, value); } + public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty = + AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>(nameof(SelectWithMouseButtons), MouseButtons.LeftButton | MouseButtons.RightButton); + + + /// <summary> + /// Gets or sets the mouse buttons to select a region on image + /// </summary> public MouseButtons SelectWithMouseButtons { - get => _selectWithMouseButtons; - set => RaiseAndSetIfChanged(ref _selectWithMouseButtons, value); + get => GetValue(SelectWithMouseButtonsProperty); + set => SetValue(SelectWithMouseButtonsProperty, value); } - public bool InvertMouse + public static readonly StyledProperty<bool> InvertMousePanProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(InvertMousePan), false); + + /// <summary> + /// Gets or sets if mouse pan is inverted + /// </summary> + public bool InvertMousePan { - get => _invertMouse; - set => RaiseAndSetIfChanged(ref _invertMouse, value); + get => GetValue(InvertMousePanProperty); + set => SetValue(InvertMousePanProperty, value); } + public static readonly StyledProperty<bool> AutoCenterProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(AutoCenter), true); + + /// <summary> + /// Gets or sets if image is auto centered + /// </summary> public bool AutoCenter { - get => _autoCenter; - set => RaiseAndSetIfChanged(ref _autoCenter, value); + get => GetValue(AutoCenterProperty); + set => SetValue(AutoCenterProperty, value); } + public static readonly StyledProperty<SizeModes> SizeModeProperty = + AvaloniaProperty.Register<AdvancedImageBox, SizeModes>(nameof(SizeMode), SizeModes.Normal); + + /// <summary> + /// Gets or sets the image size mode + /// </summary> public SizeModes SizeMode { - get => _sizeMode; - set => RaiseAndSetIfChanged(ref _sizeMode, value); + get => GetValue(SizeModeProperty); + set + { + SetValue(SizeModeProperty, value); + SizeModeChanged(); + RaisePropertyChanged(nameof(IsHorizontalBarVisible)); + RaisePropertyChanged(nameof(IsVerticalBarVisible)); + } } - private bool _allowZoom = true; - public virtual bool AllowZoom + private void SizeModeChanged() { - get => _allowZoom; - set => RaiseAndSetIfChanged(ref _allowZoom, value); + switch (SizeMode) + { + case SizeModes.Normal: + HorizontalScrollBar.Visibility = ScrollBarVisibility.Auto; + VerticalScrollBar.Visibility = ScrollBarVisibility.Auto; + break; + case SizeModes.Stretch: + case SizeModes.Fit: + HorizontalScrollBar.Visibility = ScrollBarVisibility.Hidden; + VerticalScrollBar.Visibility = ScrollBarVisibility.Hidden; + break; + default: + throw new ArgumentOutOfRangeException(nameof(SizeMode), SizeMode, null); + } } + public static readonly StyledProperty<bool> AllowZoomProperty = + AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(AllowZoom), true); + + /// <summary> + /// Gets or sets if zoom is allowed + /// </summary> + public bool AllowZoom + { + get => GetValue(AllowZoomProperty); + set => SetValue(AllowZoomProperty, value); + } + + public static readonly DirectProperty<AdvancedImageBox, ZoomLevelCollection> ZoomLevelsProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>( + nameof(ZoomLevels), + o => o.ZoomLevels, + (o, v) => o.ZoomLevels = v); + ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default; /// <summary> /// Gets or sets the zoom levels. /// </summary> /// <value>The zoom levels.</value> - public virtual ZoomLevelCollection ZoomLevels + public ZoomLevelCollection ZoomLevels { get => _zoomLevels; - set => RaiseAndSetIfChanged(ref _zoomLevels, value); + set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value); } + public static readonly StyledProperty<int> MinZoomProperty = + AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(MinZoom), 10); + + /// <summary> + /// Gets or sets the minimum possible zoom. + /// </summary> + /// <value>The zoom.</value> + public int MinZoom + { + get => GetValue(MinZoomProperty); + set => SetValue(MinZoomProperty, value); + } + + public static readonly StyledProperty<int> MaxZoomProperty = + AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(MaxZoom), 3500); + + /// <summary> + /// Gets or sets the maximum possible zoom. + /// </summary> + /// <value>The zoom.</value> + public int MaxZoom + { + get => GetValue(MaxZoomProperty); + set => SetValue(MaxZoomProperty, value); + } + + + public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty = + AvaloniaProperty.RegisterDirect<AdvancedImageBox, int>( + nameof(OldZoom), + o => o.OldZoom); + private int _oldZoom = 100; - private int _zoom = 100; /// <summary> - /// Gets or sets the zoom. + /// Gets the previous zoom value /// </summary> /// <value>The zoom.</value> - public virtual int OldZoom + public int OldZoom { get => _oldZoom; - set => RaiseAndSetIfChanged(ref _oldZoom, value); + private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value); } + public static readonly StyledProperty<int> ZoomProperty = + AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(Zoom), 100); + + /// <summary> + /// Gets or sets the zoom. + /// </summary> + /// <value>The zoom.</value> + public int Zoom + { + get => GetValue(ZoomProperty); + set + { + var newZoom = value.Clamp(MinZoom, MaxZoom); + + var previousZoom = Zoom; + if (previousZoom == newZoom) return; + OldZoom = previousZoom; + SetValue(ZoomProperty, value); + + UpdateViewPort(); + TriggerRender(); + + RaisePropertyChanged(nameof(IsHorizontalBarVisible)); + RaisePropertyChanged(nameof(IsVerticalBarVisible)); + } + } + + /* /// <summary> /// Gets or sets the zoom. /// </summary> @@ -708,52 +899,90 @@ namespace UVtools.WPF.Controls //SetZoom(value, value > Zoom ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut); } } + */ - public virtual bool IsActualSize => Zoom == 100; + public bool IsActualSize => Zoom == 100; + + public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty = + AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(nameof(PixelGridColor), Brushes.DimGray); - private ISolidColorBrush _pixelGridColor = Brushes.DimGray; /// <summary> /// Gets or sets the color of the pixel grid. /// </summary> /// <value>The color of the pixel grid.</value> public virtual ISolidColorBrush PixelGridColor { - get => _pixelGridColor; - set => RaiseAndSetIfChanged(ref _pixelGridColor, value); + get => GetValue(PixelGridColorProperty); + set => SetValue(PixelGridColorProperty, value); } - private int _pixelGridThreshold = 5; + public static readonly StyledProperty<int> PixelGridZoomThresholdProperty = + AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(PixelGridZoomThreshold), 5); + /// <summary> /// Gets or sets the minimum size of zoomed pixel's before the pixel grid will be drawn /// </summary> /// <value>The pixel grid threshold.</value> - public virtual int PixelGridThreshold + public int PixelGridZoomThreshold { - get => _pixelGridThreshold; - set => RaiseAndSetIfChanged(ref _pixelGridThreshold, value); + get => GetValue(PixelGridZoomThresholdProperty); + set => SetValue(PixelGridZoomThresholdProperty, value); } + public static readonly StyledProperty<SelectionModes> SelectionModeProperty = + AvaloniaProperty.Register<AdvancedImageBox, SelectionModes>(nameof(SelectionMode), SelectionModes.None); + public SelectionModes SelectionMode { - get => _selectionMode; - set => RaiseAndSetIfChanged(ref _selectionMode, value); + get => GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); } + public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty = + AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255))); + public ISolidColorBrush SelectionColor { - get => _selectionColor; - set => RaiseAndSetIfChanged(ref _selectionColor, value); + get => GetValue(SelectionColorProperty); + set => SetValue(SelectionColorProperty, value); } + public static readonly StyledProperty<Rect> SelectionRegionProperty = + AvaloniaProperty.Register<AdvancedImageBox, Rect>(nameof(SelectionRegion), Rect.Empty); + + public Rect SelectionRegion { - get => _selectionRegion; + get => GetValue(SelectionRegionProperty); set { - if (!RaiseAndSetIfChanged(ref _selectionRegion, value)) return; + SetValue(SelectionRegionProperty, value); + //if (!RaiseAndSetIfChanged(ref _selectionRegion, value)) return; TriggerRender(); RaisePropertyChanged(nameof(HaveSelection)); + RaisePropertyChanged(nameof(SelectionRegionNet)); + RaisePropertyChanged(nameof(SelectionPixelSize)); + } + } + + public Rectangle SelectionRegionNet + { + get + { + var rect = SelectionRegion; + return new Rectangle((int) Math.Ceiling(rect.X), (int)Math.Ceiling(rect.Y), + (int)Math.Floor(rect.Width), (int)Math.Floor(rect.Height) + ); + } + } + + public PixelSize SelectionPixelSize + { + get + { + var rect = SelectionRegion; + return new PixelSize((int) Math.Floor(rect.Width), (int) Math.Floor(rect.Height)); } } @@ -765,23 +994,7 @@ namespace UVtools.WPF.Controls private Vector _startScrollPosition; private bool _isPanning; private bool _isSelecting; - private Bitmap _image; private Bitmap _trackerImage; - private bool _trackerImageAutoZoom = true; - private byte _gridCellSize; - private ISolidColorBrush _gridColor = Brushes.Gainsboro; - private ISolidColorBrush _gridColorAlternate = Brushes.White; - private bool _showGrid = true; - private bool _autoPan = true; - private MouseButtons _panWithMouseButtons = MouseButtons.LeftButton | MouseButtons.MiddleButton | MouseButtons.RightButton; - private bool _panWithArrows = true; - private MouseButtons _selectWithMouseButtons = MouseButtons.LeftButton | MouseButtons.RightButton; - private bool _invertMouse = false; - private bool _autoCenter = true; - private SizeModes _sizeMode = SizeModes.Normal; - private ISolidColorBrush _selectionColor = new SolidColorBrush(new Color(127, 0, 128, 255)); - private Rect _selectionRegion = Rect.Empty; - private SelectionModes _selectionMode = SelectionModes.None; private bool _canRender = true; private Point _pointerPosition; @@ -796,11 +1009,13 @@ namespace UVtools.WPF.Controls HorizontalScrollBar = this.FindControl<ScrollBar>("HorizontalScrollBar"); VerticalScrollBar = this.FindControl<ScrollBar>("VerticalScrollBar"); - ViewPortControl = this.FindControl<ContentControl>("ViewPort"); + ViewPort = this.FindControl<ContentPresenter>("ViewPort"); + + SizeModeChanged(); HorizontalScrollBar.Scroll += ScrollBarOnScroll; VerticalScrollBar.Scroll += ScrollBarOnScroll; - ViewPortControl.PointerWheelChanged += FillContainerOnPointerWheelChanged; + ViewPort.PointerWheelChanged += FillContainerOnPointerWheelChanged; } private void ScrollBarOnScroll(object? sender, ScrollEventArgs e) @@ -828,7 +1043,6 @@ namespace UVtools.WPF.Controls private void FillContainerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) { - Debug.WriteLine("mouse whell"); e.Handled = true; if (Image is null) return; if (AllowZoom && SizeMode == SizeModes.Normal) @@ -839,7 +1053,7 @@ namespace UVtools.WPF.Controls // TODO: Really should update the source method to handle multiple increments rather than calling it multiple times /*for (int i = 0; i < spins; i++) {*/ - ProcessMouseZoom(e.Delta.Y > 0, e.GetPosition(ViewPortControl)); + ProcessMouseZoom(e.Delta.Y > 0, e.GetPosition(ViewPort)); //} } } @@ -865,10 +1079,10 @@ namespace UVtools.WPF.Controls var result = action switch { ZoomActions.None => Zoom, - ZoomActions.ZoomIn => ZoomLevels.NextZoom(Zoom), - ZoomActions.ZoomOut => ZoomLevels.PreviousZoom(Zoom), + ZoomActions.ZoomIn => _zoomLevels.NextZoom(Zoom), + ZoomActions.ZoomOut => _zoomLevels.PreviousZoom(Zoom), ZoomActions.ActualSize => 100, - _ => throw new ArgumentOutOfRangeException(nameof(action)), + _ => throw new ArgumentOutOfRangeException(nameof(action), action, null), }; return result; } @@ -895,8 +1109,8 @@ namespace UVtools.WPF.Controls int currentZoom = Zoom; int newZoom = GetZoomLevel(action); - if (preservePosition && Zoom != currentZoom) - CanRender = false; + /*if (preservePosition && Zoom != currentZoom) + CanRender = false;*/ RestoreSizeMode(); Zoom = newZoom; @@ -949,7 +1163,7 @@ namespace UVtools.WPF.Controls /// </param> /// <returns><c>Point.Empty</c> if the point could not be matched to the source image, otherwise the new translated point</returns> public Point PointToImage(double x, double y, bool fitToBounds = true) - => PointToImage(x, y, fitToBounds); + => PointToImage(new Point(x, y), fitToBounds); /// <summary> /// Converts the given client size point to represent a coordinate on the source image. @@ -962,7 +1176,7 @@ namespace UVtools.WPF.Controls /// <returns><c>Point.Empty</c> if the point could not be matched to the source image, otherwise the new translated point</returns> public Point PointToImage(int x, int y, bool fitToBounds = true) { - return PointToImage(x, y, fitToBounds); + return PointToImage(new Point(x, y), fitToBounds); } /// <summary> @@ -985,10 +1199,11 @@ namespace UVtools.WPF.Controls x = (point.X + Offset.X - viewport.X) / ZoomFactor; y = (point.Y + Offset.Y - viewport.Y) / ZoomFactor; + var image = Image; if (fitToBounds) { - x = x.Clamp(0, Image.Size.Width-1); - y = y.Clamp(0, Image.Size.Height-1); + x = x.Clamp(0, image.Size.Width-1); + y = y.Clamp(0, image.Size.Height-1); } } else @@ -997,7 +1212,7 @@ namespace UVtools.WPF.Controls y = 0; } - return new Point(x, y); + return new(x, y); } /// <summary> @@ -1028,8 +1243,9 @@ namespace UVtools.WPF.Controls public virtual void ScrollTo(Point imageLocation, Point relativeDisplayPoint) { //CanRender = false; - var x = imageLocation.X * ZoomFactor - relativeDisplayPoint.X; - var y = imageLocation.Y * ZoomFactor - relativeDisplayPoint.Y; + var zoomFactor = ZoomFactor; + var x = imageLocation.X * zoomFactor - relativeDisplayPoint.X; + var y = imageLocation.Y * zoomFactor - relativeDisplayPoint.Y; _canRender = true; @@ -1077,34 +1293,35 @@ namespace UVtools.WPF.Controls } /// <summary> - /// Zooms to the maximum size for displaying the entire image within the bounds of the control. + /// Zooms to the maximum size for displaying the entire image within the bounds of the control. /// </summary> public virtual void ZoomToFit() { - if (Image is null) return; + var image = Image; + if (image is null) return; double zoom; double aspectRatio; - if (Image.Size.Width > Image.Size.Height) + if (image.Size.Width > image.Size.Height) { - aspectRatio = Viewport.Width / Image.Size.Width; + aspectRatio = ViewPortSize.Width / image.Size.Width; zoom = aspectRatio * 100.0; - if (Viewport.Height < Image.Size.Height * zoom / 100.0) + if (ViewPortSize.Height < image.Size.Height * zoom / 100.0) { - aspectRatio = Viewport.Height / Image.Size.Height; + aspectRatio = ViewPortSize.Height / image.Size.Height; zoom = aspectRatio * 100.0; } } else { - aspectRatio = Viewport.Height / Image.Size.Height; + aspectRatio = ViewPortSize.Height / image.Size.Height; zoom = aspectRatio * 100.0; - if (Viewport.Width < Image.Size.Width * zoom / 100.0) + if (ViewPortSize.Width < image.Size.Width * zoom / 100.0) { - aspectRatio = Viewport.Width / Image.Size.Width; + aspectRatio = ViewPortSize.Width / image.Size.Width; zoom = aspectRatio * 100.0; } } @@ -1143,18 +1360,18 @@ namespace UVtools.WPF.Controls /// </summary> /// <param name="rectangle">The rectangle to fit the view port to.</param> /// <param name="margin">Give a margin to rectangle by a value to zoom-out that pixel value</param> - public virtual void ZoomToRegion(Rectangle rectangle, double margin = 0) => ZoomToRegion(rectangle.ToAvalonia(), margin); + public void ZoomToRegion(Rectangle rectangle, double margin = 0) => ZoomToRegion(rectangle.ToAvalonia(), margin); /// <summary> /// Adjusts the view port to fit the given region /// </summary> /// <param name="rectangle">The rectangle to fit the view port to.</param> /// <param name="margin">Give a margin to rectangle by a value to zoom-out that pixel value</param> - public virtual void ZoomToRegion(Rect rectangle, double margin = 0) + public void ZoomToRegion(Rect rectangle, double margin = 0) { if (margin > 0) rectangle = rectangle.Inflate(margin); - var ratioX = Viewport.Width / rectangle.Width; - var ratioY = Viewport.Height / rectangle.Height; + var ratioX = ViewPortSize.Width / rectangle.Width; + var ratioY = ViewPortSize.Height / rectangle.Height; var zoomFactor = Math.Min(ratioX, ratioY); var cx = rectangle.X + rectangle.Width / 2; var cy = rectangle.Y + rectangle.Height / 2; @@ -1178,14 +1395,14 @@ namespace UVtools.WPF.Controls /// </summary> /// <param name="imageLocation">The point of the image to attempt to center.</param> public virtual void CenterAt(System.Drawing.Point imageLocation) - => ScrollTo(new Point(imageLocation.X, imageLocation.Y), new Point(Viewport.Width / 2, Viewport.Height / 2)); + => ScrollTo(new Point(imageLocation.X, imageLocation.Y), new Point(ViewPortSize.Width / 2, ViewPortSize.Height / 2)); /// <summary> /// Centers the given point in the image in the center of the control /// </summary> /// <param name="imageLocation">The point of the image to attempt to center.</param> public virtual void CenterAt(Point imageLocation) - => ScrollTo(imageLocation, new Point(Viewport.Width / 2, Viewport.Height / 2)); + => ScrollTo(imageLocation, new Point(ViewPortSize.Width / 2, ViewPortSize.Height / 2)); /// <summary> /// Centers the given point in the image in the center of the control @@ -1228,13 +1445,13 @@ namespace UVtools.WPF.Controls //var height = scaledImageHeight <= Viewport.Height ? Viewport.Height : scaledImageHeight; bool changed = false; - if (HorizontalScrollBar.Maximum != width) + if (Math.Abs(HorizontalScrollBar.Maximum - width) > 0.01) { HorizontalScrollBar.Maximum = width; changed = true; } - if (VerticalScrollBar.Maximum != scaledImageHeight) + if (Math.Abs(VerticalScrollBar.Maximum - scaledImageHeight) > 0.01) { VerticalScrollBar.Maximum = height; changed = true; @@ -1304,7 +1521,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="Point"/> which has been scaled to match the current zoom level</returns> public virtual Point GetScaledPoint(Point source) { - return new Point(source.X * ZoomFactor, source.Y * ZoomFactor); + return new(source.X * ZoomFactor, source.Y * ZoomFactor); } /// <summary> @@ -1314,7 +1531,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="PointF"/> which has been scaled to match the current zoom level</returns> public virtual PointF GetScaledPoint(PointF source) { - return new PointF((float)(source.X * this.ZoomFactor), (float)(source.Y * this.ZoomFactor)); + return new((float)(source.X * ZoomFactor), (float)(source.Y * ZoomFactor)); } /// <summary> @@ -1372,7 +1589,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="Rectangle"/> which has been scaled to match the current zoom level</returns> public virtual Rect GetScaledRectangle(Rect source) { - return new Rect(source.Left * ZoomFactor, source.Top * ZoomFactor, source.Width * ZoomFactor, source.Height * ZoomFactor); + return new(source.Left * ZoomFactor, source.Top * ZoomFactor, source.Width * ZoomFactor, source.Height * ZoomFactor); } /// <summary> @@ -1382,7 +1599,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="RectangleF"/> which has been scaled to match the current zoom level</returns> public virtual RectangleF GetScaledRectangle(RectangleF source) { - return new RectangleF((float)(source.Left * ZoomFactor), (float)(source.Top * ZoomFactor), (float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor)); + return new((float)(source.Left * ZoomFactor), (float)(source.Top * ZoomFactor), (float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor)); } /// <summary> @@ -1393,7 +1610,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="SizeF"/> which has been resized to match the current zoom level</returns> public SizeF GetScaledSize(float width, float height) { - return this.GetScaledSize(new SizeF(width, height)); + return GetScaledSize(new SizeF(width, height)); } /// <summary> @@ -1404,7 +1621,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="Size"/> which has been resized to match the current zoom level</returns> public Size GetScaledSize(int width, int height) { - return this.GetScaledSize(new Size(width, height)); + return GetScaledSize(new Size(width, height)); } /// <summary> @@ -1414,7 +1631,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="SizeF"/> which has been resized to match the current zoom level</returns> public virtual SizeF GetScaledSize(SizeF source) { - return new SizeF((float)(source.Width * this.ZoomFactor), (float)(source.Height * this.ZoomFactor)); + return new((float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor)); } /// <summary> @@ -1424,7 +1641,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="Size"/> which has been resized to match the current zoom level</returns> public virtual Size GetScaledSize(Size source) { - return new Size(source.Width * ZoomFactor, source.Height * ZoomFactor); + return new(source.Width * ZoomFactor, source.Height * ZoomFactor); } /// <summary> @@ -1433,12 +1650,13 @@ namespace UVtools.WPF.Controls /// <exception cref="System.InvalidOperationException">Thrown if no image is currently set</exception> public virtual void SelectAll() { - if (Image is null) return; - SelectionRegion = new Rect(0, 0, Image.Size.Width, Image.Size.Height); + var image = Image; + if (image is null) return; + SelectionRegion = new Rect(0, 0, image.Size.Width, image.Size.Height); } /// <summary> - /// Clears any existing selection region + /// Clears any existing selection region /// </summary> public virtual void SelectNone() { @@ -1454,25 +1672,29 @@ namespace UVtools.WPF.Controls public override void Render(DrawingContext context) { - Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); + //Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); base.Render(context); // Draw Grid - if (ShowGrid) + var gridCellSize = GridCellSize; + if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) { // draw the background - var currentColor = GridColor; - for (int y = 0; y < Viewport.Height; y += GridCellSize) + var gridColor = GridColor; + var altColor = GridColorAlternate; + var currentColor = gridColor; + for (int y = 0; y < ViewPortSize.Height; y += gridCellSize) { var firstRowColor = currentColor; - for (int x = 0; x < Viewport.Width; x += GridCellSize) + + for (int x = 0; x < ViewPortSize.Width; x += gridCellSize) { - context.FillRectangle(currentColor, new Rect(x, y, GridCellSize, GridCellSize)); - currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; + context.FillRectangle(currentColor, new Rect(x, y, gridCellSize, gridCellSize)); + currentColor = ReferenceEquals(currentColor, gridColor) ? altColor : gridColor; } - if (firstRowColor == currentColor) - currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; + if (Equals(firstRowColor, currentColor)) + currentColor = ReferenceEquals(currentColor, gridColor) ? altColor : gridColor; } } @@ -1481,18 +1703,22 @@ namespace UVtools.WPF.Controls context.FillRectangle(Background, new Rect(0, 0, Viewport.Width, Viewport.Height)); }*/ - if (Image is null) return; + var image = Image; + if (image is null) return; // Draw iamge - context.DrawImage(_image, + context.DrawImage(image, GetSourceImageRegion(), GetImageViewPort() ); + var zoomFactor = ZoomFactor; + + if (HaveTrackerImage && _pointerPosition.X >= 0 && _pointerPosition.Y >= 0) { var destSize = TrackerImageAutoZoom - ? new Size(_trackerImage.Size.Width * ZoomFactor, _trackerImage.Size.Height * ZoomFactor) - : _image.Size; + ? new Size(_trackerImage.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor) + : image.Size; var destPos = new Point( _pointerPosition.X - destSize.Width / 2, @@ -1505,20 +1731,19 @@ namespace UVtools.WPF.Controls //SkiaContext.SkCanvas.dr // Draw pixel grid - var pixelSize = ZoomFactor; - if (pixelSize > PixelGridThreshold) + if (zoomFactor > PixelGridZoomThreshold && SizeMode == SizeModes.Normal) { var viewport = GetImageViewPort(); - var offsetX = Offset.X % pixelSize; - var offsetY = Offset.Y % pixelSize; + var offsetX = Offset.X % zoomFactor; + var offsetY = Offset.Y % zoomFactor; Pen pen = new(PixelGridColor); - for (double x = viewport.X + pixelSize - offsetX; x < viewport.Right; x += pixelSize) + for (double x = viewport.X + zoomFactor - offsetX; x < viewport.Right; x += zoomFactor) { context.DrawLine(pen, new Avalonia.Point(x, viewport.X), new Avalonia.Point(x, viewport.Bottom)); } - for (double y = viewport.Y + pixelSize - offsetY; y < viewport.Bottom; y += pixelSize) + for (double y = viewport.Y + zoomFactor - offsetY; y < viewport.Bottom; y += zoomFactor) { context.DrawLine(pen, new Avalonia.Point(viewport.Y, y), new Avalonia.Point(viewport.Right, y)); } @@ -1529,9 +1754,10 @@ namespace UVtools.WPF.Controls if (!SelectionRegion.IsEmpty) { var rect = GetOffsetRectangle(SelectionRegion); - context.FillRectangle(SelectionColor, rect); - Color solidColor = Color.FromArgb(255, SelectionColor.Color.R, SelectionColor.Color.G, SelectionColor.Color.B); - context.DrawRectangle(new Pen(solidColor.ToUint32()), rect); + var selectionColor = SelectionColor; + context.FillRectangle(selectionColor, rect); + Color color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B); + context.DrawRectangle(new Pen(color.ToUint32()), rect); } } @@ -1544,7 +1770,7 @@ namespace UVtools.WPF.Controls { var offset = GetOffsetPoint(new Point (source.X, source.Y)); - return new Point((int)offset.X, (int)offset.Y); + return new((int)offset.X, (int)offset.Y); } /// <summary> @@ -1581,7 +1807,7 @@ namespace UVtools.WPF.Controls var offsetX = viewport.Left + Offset.X; var offsetY = viewport.Top + Offset.Y; - return new Point(scaled.X + offsetX, scaled.Y + offsetY); + return new(scaled.X + offsetX, scaled.Y + offsetY); } /// <summary> @@ -1596,7 +1822,7 @@ namespace UVtools.WPF.Controls var offsetX = viewport.Left - Offset.X; var offsetY = viewport.Top - Offset.Y; - return new Rect(new Point(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size); + return new(new Point(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size); } /// <summary> @@ -1609,7 +1835,7 @@ namespace UVtools.WPF.Controls /// <returns>A <see cref="Rectangle"/> which has been resized and repositioned to match the current zoom level and image offset</returns> public Rectangle GetOffsetRectangle(int x, int y, int width, int height) { - return this.GetOffsetRectangle(new Rectangle(x, y, width, height)); + return GetOffsetRectangle(new Rectangle(x, y, width, height)); } /// <summary> @@ -1637,8 +1863,7 @@ namespace UVtools.WPF.Controls var offsetX = viewport.Left + Offset.X; var offsetY = viewport.Top + Offset.Y; - return new Rectangle(new System.Drawing.Point((int)(scaled.Left + offsetX), (int)(scaled.Top + offsetY)), - new System.Drawing.Size((int)scaled.Size.Width, (int)scaled.Size.Height)); + return new(new System.Drawing.Point((int)(scaled.Left + offsetX), (int)(scaled.Top + offsetY)), new System.Drawing.Size((int)scaled.Size.Width, (int)scaled.Size.Height)); } /// <summary> @@ -1650,7 +1875,8 @@ namespace UVtools.WPF.Controls /// </returns> public Rectangle FitRectangle(Rectangle rectangle) { - if (Image is null) return Rectangle.Empty; + var image = Image; + if (image is null) return Rectangle.Empty; var x = rectangle.X; var y = rectangle.Y; var w = rectangle.Width; @@ -1666,17 +1892,17 @@ namespace UVtools.WPF.Controls y = 0; } - if (x + w > Image.Size.Width) + if (x + w > image.Size.Width) { - w = (int)(Image.Size.Width - x); + w = (int)(image.Size.Width - x); } - if (y + h > Image.Size.Height) + if (y + h > image.Size.Height) { - h = (int)(Image.Size.Height - y); + h = (int)(image.Size.Height - y); } - return new Rectangle(x, y, w, h); + return new(x, y, w, h); } /// <summary> @@ -1688,7 +1914,8 @@ namespace UVtools.WPF.Controls /// </returns> public Rect FitRectangle(Rect rectangle) { - if (Image is null) return Rect.Empty; + var image = Image; + if (image is null) return Rect.Empty; var x = rectangle.X; var y = rectangle.Y; var w = rectangle.Width; @@ -1706,94 +1933,119 @@ namespace UVtools.WPF.Controls y = 0; } - if (x + w > Image.Size.Width) + if (x + w > image.Size.Width) { - w = Image.Size.Width - x; + w = image.Size.Width - x; } - if (y + h > Image.Size.Height) + if (y + h > image.Size.Height) { - h = Image.Size.Height - y; + h = image.Size.Height - y; } - return new Rect(x, y, w, h); + return new(x, y, w, h); } /// <summary> /// Gets the source image region. /// </summary> /// <returns></returns> - public virtual Rect GetSourceImageRegion() + public Rect GetSourceImageRegion() { - if (Image is null) return Rect.Empty; + var image = Image; + if (image is null) return Rect.Empty; - if (SizeMode != SizeModes.Stretch) + switch (SizeMode) { - var viewPort = GetImageViewPort(); - double sourceLeft = (Offset.X / ZoomFactor); - double sourceTop = (Offset.Y / ZoomFactor); - double sourceWidth = (viewPort.Width / ZoomFactor); - double sourceHeight = (viewPort.Height / ZoomFactor); - - return new Rect(sourceLeft, sourceTop, sourceWidth, sourceHeight); + case SizeModes.Normal: + var offset = Offset; + var viewPort = GetImageViewPort(); + var zoomFactor = ZoomFactor; + double sourceLeft = (offset.X / zoomFactor); + double sourceTop = (offset.Y / zoomFactor); + double sourceWidth = (viewPort.Width / zoomFactor); + double sourceHeight = (viewPort.Height / zoomFactor); + + return new(sourceLeft, sourceTop, sourceWidth, sourceHeight); } - return new Rect(0, 0, Image.Size.Width, Image.Size.Height); + return new(0, 0, image.Size.Width, image.Size.Height); + } /// <summary> /// Gets the image view port. /// </summary> /// <returns></returns> - public virtual Rect GetImageViewPort() + public Rect GetImageViewPort() { - if (Viewport.Width == 0 && Viewport.Height == 0) return Rect.Empty; + if (ViewPortSize.Width == 0 && ViewPortSize.Height == 0) return Rect.Empty; double xOffset = 0; double yOffset = 0; - double width; - double height; + double width = 0; + double height = 0; - if (SizeMode != SizeModes.Stretch) + switch (SizeMode) { - if (AutoCenter) - { - xOffset = (!IsHorizontalBarVisible ? (Viewport.Width - ScaledImageWidth) / 2 : 0); - yOffset = (!IsVerticalBarVisible ? (Viewport.Height - ScaledImageHeight) / 2 : 0); - } + case SizeModes.Normal: + if (AutoCenter) + { + xOffset = (!IsHorizontalBarVisible ? (ViewPortSize.Width - ScaledImageWidth) / 2 : 0); + yOffset = (!IsVerticalBarVisible ? (ViewPortSize.Height - ScaledImageHeight) / 2 : 0); + } - width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), Viewport.Width); - height = Math.Min(ScaledImageHeight - Math.Abs(Offset.Y), Viewport.Height); - } - else - { - width = Viewport.Width; - height = Viewport.Height; + width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), ViewPortSize.Width); + height = Math.Min(ScaledImageHeight - Math.Abs(Offset.Y), ViewPortSize.Height); + break; + case SizeModes.Stretch: + width = ViewPortSize.Width; + height = ViewPortSize.Height; + break; + case SizeModes.Fit: + var image = Image; + double scaleFactor = Math.Min(ViewPortSize.Width / image.Size.Width, ViewPortSize.Height / image.Size.Height); + + width = Math.Floor(image.Size.Width * scaleFactor); + height = Math.Floor(image.Size.Height * scaleFactor); + + if (AutoCenter) + { + xOffset = (ViewPortSize.Width - width) / 2; + yOffset = (ViewPortSize.Height - height) / 2; + } + + break; + default: + throw new ArgumentOutOfRangeException(nameof(SizeMode), SizeMode, null); } - return new Rect(xOffset, yOffset, width, height); + return new(xOffset, yOffset, width, height); } /// <summary> - /// Gets the width of the scaled image. + /// Gets the width of the scaled image. /// </summary> /// <value>The width of the scaled image.</value> - protected virtual double ScaledImageWidth => Image.Size.Width * ZoomFactor; + protected double ScaledImageWidth => Image.Size.Width * ZoomFactor; /// <summary> - /// Gets the height of the scaled image. + /// Gets the height of the scaled image. /// </summary> /// <value>The height of the scaled image.</value> - protected virtual double ScaledImageHeight => Image.Size.Height * ZoomFactor; + protected double ScaledImageHeight => Image.Size.Height * ZoomFactor; - public double ZoomFactor => _zoom / 100.0; + /// <summary> + /// Gets the zoom factor, the zoom / 100 + /// </summary> + public double ZoomFactor => Zoom / 100.0; protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (e.Handled - || IsPanning - || IsSelecting + || _isPanning + || _isSelecting || Image is null) return; var pointer = e.GetCurrentPoint(this); @@ -1816,6 +2068,7 @@ namespace UVtools.WPF.Controls pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0 ) || !AutoPan + || SizeMode != SizeModes.Normal ) return; @@ -1824,8 +2077,8 @@ namespace UVtools.WPF.Controls var location = pointer.Position; - if (location.X > Viewport.Width) return; - if (location.Y > Viewport.Height) return; + if (location.X > ViewPortSize.Width) return; + if (location.Y > ViewPortSize.Height) return; _startMousePosition = location; } @@ -1854,18 +2107,18 @@ namespace UVtools.WPF.Controls var pointer = e.GetCurrentPoint(this); PointerPosition = pointer.Position; - if (!IsPanning && !IsSelecting) + if (!_isPanning && !_isSelecting) { TriggerRender(true); return; } - if (IsPanning) + if (_isPanning) { double x; double y; - if (!InvertMouse) + if (!InvertMousePan) { x = _startScrollPosition.X + (_startMousePosition.X - _pointerPosition.X); y = _startScrollPosition.Y + (_startMousePosition.Y - _pointerPosition.Y); @@ -1878,7 +2131,7 @@ namespace UVtools.WPF.Controls Offset = new Vector(x, y); } - else if (IsSelecting) + else if (_isSelecting) { double x; double y; @@ -1912,10 +2165,11 @@ namespace UVtools.WPF.Controls x -= imageOffset.X - Offset.X; y -= imageOffset.Y - Offset.Y; - x /= ZoomFactor; - y /= ZoomFactor; - w /= ZoomFactor; - h /= ZoomFactor; + var zoomFactor = ZoomFactor; + x /= zoomFactor; + y /= zoomFactor; + w /= zoomFactor; + h /= zoomFactor; if (w != 0 && h != 0) { @@ -1925,6 +2179,38 @@ namespace UVtools.WPF.Controls e.Handled = true; } + + public Bitmap GetSelectedBitmap() + { + var image = ImageAsWriteableBitmap; + if (image is null || !HaveSelection) return null; + var selection = SelectionRegionNet; + var pixelSize = SelectionPixelSize; + using var frameBuffer = image.Lock(); + + var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul); + using var newFrameBuffer = newBitmap.Lock(); + + int i = 0; + + unsafe + { + var inputPixels = (uint*) (void*) frameBuffer.Address; + var targetPixels = (uint*) (void*) newFrameBuffer.Address; + + for (int y = selection.Y; y < selection.Bottom; y++) + { + var thisY = y * frameBuffer.Size.Width; + for (int x = selection.X; x < selection.Right; x++) + { + targetPixels[i++] = inputPixels[thisY + x]; + } + } + } + + return newBitmap; + } + #endregion } } diff --git a/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs index f71bef9..2ab8826 100644 --- a/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolEditParametersControl.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Markup.Xaml; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Operations; +using UVtools.WPF.Extensions; using UVtools.WPF.Windows; namespace UVtools.WPF.Controls.Tools @@ -117,6 +118,7 @@ namespace UVtools.WPF.Controls.Tools if (Operation.Modifiers is null || Operation.Modifiers.Length == 0) { CanRun = false; + App.MainWindow.MessageBoxError("No available properties to edit on this file format.", BaseOperation.Title).GetAwaiter(); return; } diff --git a/UVtools.WPF/Extensions/BitmapExtension.cs b/UVtools.WPF/Extensions/BitmapExtension.cs index ae5f5ce..748f190 100644 --- a/UVtools.WPF/Extensions/BitmapExtension.cs +++ b/UVtools.WPF/Extensions/BitmapExtension.cs @@ -118,9 +118,7 @@ namespace UVtools.WPF.Extensions using var lockBuffer = writableBitmap.Lock(); - - - + unsafe { diff --git a/UVtools.WPF/Extensions/DrawingExtensions.cs b/UVtools.WPF/Extensions/DrawingExtensions.cs index b848c48..dbaea1e 100644 --- a/UVtools.WPF/Extensions/DrawingExtensions.cs +++ b/UVtools.WPF/Extensions/DrawingExtensions.cs @@ -15,22 +15,22 @@ namespace UVtools.WPF.Extensions { public static Avalonia.Media.Color ToAvalonia(this System.Drawing.Color color) { - return new Avalonia.Media.Color(color.A, color.R, color.G, color.B); + return new(color.A, color.R, color.G, color.B); } public static System.Drawing.Color ToDotNet(this Avalonia.Media.Color color) { - return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); + return Color.FromArgb(color.A, color.R, color.G, color.B); } public static Rect ToAvalonia(this Rectangle rectangle) { - return new Rect(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + return new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); } public static Rectangle ToDotNet(this Rect rectangle) { - return new Rectangle((int) rectangle.X, (int) rectangle.Y, (int) rectangle.Width, (int) rectangle.Height); + return new((int) rectangle.X, (int) rectangle.Y, (int) rectangle.Width, (int) rectangle.Height); } } } diff --git a/UVtools.WPF/LayerCache.cs b/UVtools.WPF/LayerCache.cs index d68f9a0..79bd632 100644 --- a/UVtools.WPF/LayerCache.cs +++ b/UVtools.WPF/LayerCache.cs @@ -68,12 +68,10 @@ namespace UVtools.WPF { get { - using (var framebuffer = Bitmap.Lock()) - { - var info = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, - framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); - return SKSurface.Create(info, framebuffer.Address, framebuffer.RowBytes).Canvas; - } + using var framebuffer = Bitmap.Lock(); + var info = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, + framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); + return SKSurface.Create(info, framebuffer.Address, framebuffer.RowBytes).Canvas; } } diff --git a/UVtools.WPF/MainWindow.LayerPreview.cs b/UVtools.WPF/MainWindow.LayerPreview.cs index 5553887..b837352 100644 --- a/UVtools.WPF/MainWindow.LayerPreview.cs +++ b/UVtools.WPF/MainWindow.LayerPreview.cs @@ -93,62 +93,54 @@ namespace UVtools.WPF LayerNavigationTooltipBorder = this.FindControl<Border>("Layer.Navigation.Tooltip.Border"); _issuesSliderCanvas = this.Find<Canvas>("Layer.Navigation.IssuesCanvas"); - - _showLayerImageDifference = Settings.LayerPreview.ShowLayerDifference; _showLayerOutlinePrintVolumeBoundary = Settings.LayerPreview.VolumeBoundsOutline; _showLayerOutlineLayerBoundary = Settings.LayerPreview.LayerBoundsOutline; _showLayerOutlineHollowAreas = Settings.LayerPreview.HollowOutline; - + LayerImageBox.ZoomLevels = new AdvancedImageBox.ZoomLevelCollection(AppSettings.ZoomLevels); - LayerImageBox.PropertyChanged += (sender, e) => + + LayerImageBox.GetObservable(AdvancedImageBox.ZoomProperty).Subscribe(zoom => { - if (e.PropertyName == nameof(LayerImageBox.Zoom)) + var oldZoom = LayerImageBox.OldZoom; + RaisePropertyChanged(nameof(LayerZoomStr)); + AddLogVerbose($"Zoomed from {oldZoom} to {zoom}"); + + if (_showLayerImageCrosshairs && + Issues.Count > 0 && + (oldZoom < 50 && + zoom >= 50 // Trigger refresh as crosshair thickness increases at lower zoom levels + || oldZoom > 100 && zoom <= 100 + || oldZoom is >= 50 and <= 100 && (zoom is < 50 or > 100) + || oldZoom <= AppSettings.CrosshairFadeLevel && + zoom > AppSettings.CrosshairFadeLevel // Trigger refresh as zoom level manually crosses fade threshold + || oldZoom > AppSettings.CrosshairFadeLevel && zoom <= AppSettings.CrosshairFadeLevel) + + ) { - RaisePropertyChanged(nameof(LayerZoomStr)); - AddLogVerbose($"Zoomed from {LayerImageBox.OldZoom} to {LayerImageBox.Zoom}"); - - if (ShowLayerImageCrosshairs && - Issues.Count > 0 && - (LayerImageBox.OldZoom < 50 && - LayerImageBox.Zoom >= 50 // Trigger refresh as crosshair thickness increases at lower zoom levels - || LayerImageBox.OldZoom > 100 && LayerImageBox.Zoom <= 100 - || LayerImageBox.OldZoom >= 50 && LayerImageBox.OldZoom <= 100 && (LayerImageBox.Zoom < 50 || LayerImageBox.Zoom > 100) - || LayerImageBox.OldZoom <= AppSettings.CrosshairFadeLevel && - LayerImageBox.Zoom > AppSettings.CrosshairFadeLevel // Trigger refresh as zoom level manually crosses fade threshold - || LayerImageBox.OldZoom > AppSettings.CrosshairFadeLevel && LayerImageBox.Zoom <= AppSettings.CrosshairFadeLevel) - - ) + if (Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues) { - if (Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues) - { - if (IssuesGrid.SelectedItems.Count == 0 || !IssuesGrid.SelectedItems.Cast<LayerIssue>().Any( - issue => // Find a valid candidate to update layer preview, otherwise quit - issue.LayerIndex == _actualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer && - issue.Type != LayerIssue.IssueType.TouchingBound)) return; - } - else - { - if (!Issues.Any( - issue => // Find a valid candidate to update layer preview, otherwise quit - issue.LayerIndex == _actualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer && - issue.Type != LayerIssue.IssueType.TouchingBound)) return; - } - - // A timer is used here rather than invoking ShowLayer directly to eliminate sublte visual flashing - // that will occur on the transition when the crosshair fades or unfades if ShowLayer is called directly. - ShowLayer(); + if (IssuesGrid.SelectedItems.Count == 0 || !IssuesGrid.SelectedItems.Cast<LayerIssue>().Any( + issue => // Find a valid candidate to update layer preview, otherwise quit + issue.LayerIndex == _actualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer && + issue.Type != LayerIssue.IssueType.TouchingBound)) return; + } + else + { + if (!Issues.Any( + issue => // Find a valid candidate to update layer preview, otherwise quit + issue.LayerIndex == _actualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer && + issue.Type != LayerIssue.IssueType.TouchingBound)) return; } - return; - } - - if (e.PropertyName == nameof(LayerImageBox.SelectionRegion)) - { - RaisePropertyChanged(nameof(LayerROIStr)); + // A timer is used here rather than invoking ShowLayer directly to eliminate sublte visual flashing + // that will occur on the transition when the crosshair fades or unfades if ShowLayer is called directly. + ShowLayer(); } + }); - }; + LayerImageBox.GetObservable(AdvancedImageBox.SelectionRegionProperty) + .Subscribe(rect => RaisePropertyChanged(nameof(LayerROIStr))); LayerImageBox.PointerMoved += LayerImageBoxOnPointerMoved; LayerImageBox.KeyUp += LayerImageBox_KeyUp; @@ -1248,7 +1240,7 @@ namespace UVtools.WPF if (!_showLayerImageFlipped) return; if (_showLayerImageFlippedHorizontally) { - rectangle.Location = new Point(LayerCache.Image.Width - 1 - rectangle.Right, rectangle.Y); + rectangle.Location = new Point(LayerCache.Image.Width - rectangle.Right, rectangle.Y); } if (_showLayerImageFlippedVertically) @@ -1801,7 +1793,8 @@ namespace UVtools.WPF public async void SaveCurrentLayerImage() { - SaveFileDialog dialog = new SaveFileDialog + if (!IsFileLoaded) return; + SaveFileDialog dialog = new() { Filters = Helpers.PngFileFilter, DefaultExtension = ".png", @@ -1814,6 +1807,22 @@ namespace UVtools.WPF LayerCache.ImageBgr.Save(result); } + public async void SaveCurrentROIImage() + { + if (!IsFileLoaded || !LayerImageBox.HaveSelection) return; + SaveFileDialog dialog = new() + { + Filters = Helpers.PngFileFilter, + DefaultExtension = ".png", + InitialFileName = $"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_layer{ActualLayer}_ROI.png" + }; + + var result = await dialog.ShowAsync(this); + if (string.IsNullOrEmpty(result)) return; + + LayerImageBox.GetSelectedBitmap()?.Save(result); + } + const byte _pixelEditorCursorMinDiamater = 10; public void UpdatePixelEditorCursor() { diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml index ce4c87d..50fd61d 100644 --- a/UVtools.WPF/MainWindow.axaml +++ b/UVtools.WPF/MainWindow.axaml @@ -284,11 +284,13 @@ VerticalAlignment="Center"> <TextBlock Text="{Binding SlicerFile.LayerHeight, StringFormat=Layer height: \{0\}mm}"/> - <TextBlock Text=" | "/> - <TextBlock Text="{Binding SlicerFile.BottomLayerCount, StringFormat=Bottom layers: {0}}"/> + <TextBlock IsVisible="{Binding SlicerFile.CanUseBottomLayerCount}" Text=" | "/> + <TextBlock IsVisible="{Binding SlicerFile.CanUseBottomLayerCount}" + Text="{Binding SlicerFile.BottomLayerCount, StringFormat=Bottom layers: {0}}"/> - <TextBlock Text=" | "/> - <TextBlock Text="{Binding SlicerFile.ExposureRepresentation, StringFormat=Exposure: {0}}"/> + <TextBlock IsVisible="{Binding SlicerFile.CanUseAnyExposureTime}" Text=" | "/> + <TextBlock IsVisible="{Binding SlicerFile.CanUseAnyExposureTime}" + Text="{Binding SlicerFile.ExposureRepresentation, StringFormat=Exposure: {0}}"/> <TextBlock IsVisible="{Binding SlicerFile.CanUseAnyLiftHeight}" Text=" | "/> <TextBlock IsVisible="{Binding SlicerFile.CanUseAnyLiftHeight}" @@ -1875,8 +1877,20 @@ ToolTip.Tip="Save layer image to a file" VerticalAlignment="Stretch" Margin="1,0,0,0"> - <StackPanel Orientation="Horizontal"> - <Image Source="/Assets/Icons/save-16x16.png"/> + <Button.ContextMenu> + <ContextMenu PlacementMode="Bottom"> + <MenuItem + Command="{Binding SaveCurrentROIImage}" + Header="Save the selected region (ROI)"> + <MenuItem.Icon> + <Image Source="/Assets/Icons/object-group-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + </ContextMenu> + </Button.ContextMenu> + <StackPanel Orientation="Horizontal"> + <Image Source="/Assets/Icons/save-16x16.png"/> + <TextBlock Text=" ⮟"/> </StackPanel> </Button> @@ -1891,9 +1905,8 @@ > <uc:AdvancedImageBox ShowGrid="{Binding Settings.LayerPreview.ShowBackgroudGrid}" - GridCellSize="15" - Name="LayerImage" - /> + GridCellSize="15" + Name="LayerImage"/> </Border> diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index b8c454e..de6127a 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -508,7 +508,7 @@ namespace UVtools.WPF var clientSizeObs = this.GetObservable(ClientSizeProperty); clientSizeObs.Subscribe(size => UpdateLayerTrackerHighlightIssues()); var windowStateObs = this.GetObservable(WindowStateProperty); - windowStateObs.Subscribe(size => UpdateLayerTrackerHighlightIssues()); + windowStateObs.Subscribe(windowsState => UpdateLayerTrackerHighlightIssues()); DataContext = this; diff --git a/UVtools.WPF/Structures/AppVersionChecker.cs b/UVtools.WPF/Structures/AppVersionChecker.cs index 83ba22a..89cceaa 100644 --- a/UVtools.WPF/Structures/AppVersionChecker.cs +++ b/UVtools.WPF/Structures/AppVersionChecker.cs @@ -116,7 +116,6 @@ namespace UVtools.WPF.Structures Debug.WriteLine($"Version checker: v{App.VersionStr} <=> v{tag_name}"); Version checkVersion = new(tag_name); Changelog = json.body; - //if (string.Compare(tag_name, App.VersionStr, StringComparison.OrdinalIgnoreCase) > 0) if (App.Version.CompareTo(checkVersion) < 0) { diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index f5b81db..3eb829a 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ <PackageLicenseFile>LICENSE</PackageLicenseFile> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <RepositoryType>Git</RepositoryType> - <Version>2.11.2</Version> + <Version>2.12.0</Version> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs index c66f81e..38ebee4 100644 --- a/UVtools.WPF/UserSettings.cs +++ b/UVtools.WPF/UserSettings.cs @@ -224,7 +224,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush TooltipOverlayBackgroundBrush { - get => new SolidColorBrush(_tooltipOverlayBackgroundColor.ToAvalonia()); + get => new(_tooltipOverlayBackgroundColor.ToAvalonia()); set => TooltipOverlayBackgroundColor = new Color(value); } @@ -247,7 +247,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush VolumeBoundsOutlineBrush { - get => new SolidColorBrush(_volumeBoundsOutlineColor.ToAvalonia()); + get => new(_volumeBoundsOutlineColor.ToAvalonia()); set => VolumeBoundsOutlineColor = new Color(value); } @@ -276,7 +276,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush LayerBoundsOutlineBrush { - get => new SolidColorBrush(_layerBoundsOutlineColor.ToAvalonia()); + get => new(_layerBoundsOutlineColor.ToAvalonia()); set => LayerBoundsOutlineColor = new Color(value); } @@ -305,7 +305,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush HollowOutlineBrush { - get => new SolidColorBrush(_hollowOutlineColor.ToAvalonia()); + get => new(_hollowOutlineColor.ToAvalonia()); set => HollowOutlineColor = new Color(value); } @@ -334,7 +334,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush MaskOutlineBrush { - get => new SolidColorBrush(_maskOutlineColor.ToAvalonia()); + get => new(_maskOutlineColor.ToAvalonia()); set => MaskOutlineColor = new Color(value); } @@ -363,7 +363,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush PreviousLayerDifferenceBrush { - get => new SolidColorBrush(_previousLayerDifferenceColor.ToAvalonia()); + get => new(_previousLayerDifferenceColor.ToAvalonia()); set => PreviousLayerDifferenceColor = new Color(value); } @@ -380,7 +380,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush NextLayerDifferenceBrush { - get => new SolidColorBrush(_nextLayerDifferenceColor.ToAvalonia()); + get => new(_nextLayerDifferenceColor.ToAvalonia()); set => NextLayerDifferenceColor = new Color(value); } @@ -397,7 +397,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush BothLayerDifferenceBrush { - get => new SolidColorBrush(_bothLayerDifferenceColor.ToAvalonia()); + get => new(_bothLayerDifferenceColor.ToAvalonia()); set => BothLayerDifferenceColor = new Color(value); } @@ -426,7 +426,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush IslandBrush { - get => new SolidColorBrush(_islandColor.ToAvalonia()); + get => new(_islandColor.ToAvalonia()); set => IslandColor = new Color(value); } @@ -443,7 +443,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush IslandHighlightBrush { - get => new SolidColorBrush(_islandHighlightColor.ToAvalonia()); + get => new(_islandHighlightColor.ToAvalonia()); set => IslandHighlightColor = new Color(value); } @@ -460,7 +460,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush OverhangBrush { - get => new SolidColorBrush(_overhangColor.ToAvalonia()); + get => new(_overhangColor.ToAvalonia()); set => OverhangColor = new Color(value); } @@ -477,7 +477,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush OverhangHighlightBrush { - get => new SolidColorBrush(_overhangHighlightColor.ToAvalonia()); + get => new(_overhangHighlightColor.ToAvalonia()); set => OverhangHighlightColor = new Color(value); } @@ -494,7 +494,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush ResinTrapBrush { - get => new SolidColorBrush(_resinTrapColor.ToAvalonia()); + get => new(_resinTrapColor.ToAvalonia()); set => ResinTrapColor = new Color(value); } @@ -511,7 +511,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush ResinTrapHighlightBrush { - get => new SolidColorBrush(_resinTrapHighlightColor.ToAvalonia()); + get => new(_resinTrapHighlightColor.ToAvalonia()); set => ResinTrapHighlightColor = new Color(value); } public Color TouchingBoundsColor @@ -527,7 +527,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush TouchingBoundsBrush { - get => new SolidColorBrush(_touchingBoundsColor.ToAvalonia()); + get => new(_touchingBoundsColor.ToAvalonia()); set => TouchingBoundsColor = new Color(value); } @@ -544,7 +544,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush CrosshairBrush { - get => new SolidColorBrush(_crosshairColor.ToAvalonia()); + get => new(_crosshairColor.ToAvalonia()); set => CrosshairColor = new Color(value); } @@ -877,15 +877,15 @@ namespace UVtools.WPF [Serializable] public sealed class PixelEditorUserSettings : BindableBase { - private Color _addPixelColor = new Color(255, 144, 238, 144); - private Color _addPixelHighlightColor = new Color(255, 0, 255, 0); - private Color _removePixelColor = new Color(255, 219, 112, 147); - private Color _removePixelHighlightColor = new Color(255, 139, 0, 0); - private Color _supportsColor = new Color(255, 0, 255, 255); - private Color _supportsHighlightColor = new Color(255, 0, 139, 139); - private Color _drainHoleColor = new Color(255, 142, 69, 133); - private Color _drainHoleHighlightColor = new Color(255, 159, 0, 197); - private Color _cursorColor = new Color(150, 52, 152, 219); + private Color _addPixelColor = new(255, 144, 238, 144); + private Color _addPixelHighlightColor = new(255, 0, 255, 0); + private Color _removePixelColor = new(255, 219, 112, 147); + private Color _removePixelHighlightColor = new(255, 139, 0, 0); + private Color _supportsColor = new(255, 0, 255, 255); + private Color _supportsHighlightColor = new(255, 0, 139, 139); + private Color _drainHoleColor = new(255, 142, 69, 133); + private Color _drainHoleHighlightColor = new(255, 159, 0, 197); + private Color _cursorColor = new(150, 52, 152, 219); private bool _partialUpdateIslandsOnEditing = true; private bool _closeEditorOnApply; @@ -902,7 +902,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush AddPixelBrush { - get => new SolidColorBrush(_addPixelColor.ToAvalonia()); + get => new(_addPixelColor.ToAvalonia()); set => AddPixelColor = new Color(value); } @@ -919,7 +919,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush AddPixelHighlightBrush { - get => new SolidColorBrush(_addPixelHighlightColor.ToAvalonia()); + get => new(_addPixelHighlightColor.ToAvalonia()); set => AddPixelHighlightColor = new Color(value); } @@ -936,7 +936,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush RemovePixelBrush { - get => new SolidColorBrush(_removePixelColor.ToAvalonia()); + get => new(_removePixelColor.ToAvalonia()); set => RemovePixelColor = new Color(value); } @@ -953,7 +953,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush RemovePixelHighlightBrush { - get => new SolidColorBrush(_removePixelHighlightColor.ToAvalonia()); + get => new(_removePixelHighlightColor.ToAvalonia()); set => RemovePixelHighlightColor = new Color(value); } @@ -970,7 +970,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush SupportsBrush { - get => new SolidColorBrush(_supportsColor.ToAvalonia()); + get => new(_supportsColor.ToAvalonia()); set => SupportsColor = new Color(value); } @@ -987,7 +987,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush SupportsHighlightBrush { - get => new SolidColorBrush(_supportsHighlightColor.ToAvalonia()); + get => new(_supportsHighlightColor.ToAvalonia()); set => SupportsHighlightColor = new Color(value); } @@ -1004,7 +1004,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush DrainHoleBrush { - get => new SolidColorBrush(_drainHoleColor.ToAvalonia()); + get => new(_drainHoleColor.ToAvalonia()); set => DrainHoleColor = new Color(value); } @@ -1021,7 +1021,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush DrainHoleHighlightBrush { - get => new SolidColorBrush(_drainHoleHighlightColor.ToAvalonia()); + get => new(_drainHoleHighlightColor.ToAvalonia()); set => DrainHoleHighlightColor = new Color(value); } @@ -1038,7 +1038,7 @@ namespace UVtools.WPF [XmlIgnore] public SolidColorBrush CursorBrush { - get => new SolidColorBrush(_cursorColor.ToAvalonia()); + get => new(_cursorColor.ToAvalonia()); set => CursorColor = new Color(value); } diff --git a/UVtools.WPF/Windows/ToolWindow.axaml b/UVtools.WPF/Windows/ToolWindow.axaml index 5b244dc..15f20dc 100644 --- a/UVtools.WPF/Windows/ToolWindow.axaml +++ b/UVtools.WPF/Windows/ToolWindow.axaml @@ -224,6 +224,7 @@ <StackPanel Margin="15" Spacing="5"> <TextBlock VerticalAlignment="Center" + IsVisible="{Binding IsROIVisible}" Text="{Binding ROI, StringFormat=Region: \{0\}}" /> <CheckBox diff --git a/build/CreateRelease.WPF.ps1 b/build/CreateRelease.WPF.ps1 index e2f658c..24e73e9 100644 --- a/build/CreateRelease.WPF.ps1 +++ b/build/CreateRelease.WPF.ps1 @@ -225,7 +225,7 @@ if($enableMSI) # Clean and build MSI Remove-Item "$installer\obj" -Recurse -ErrorAction Ignore Remove-Item "$installer\bin" -Recurse -ErrorAction Ignore - iex "& $msbuild $installer\$installer.wixproj" + Invoke-Expression "& $msbuild $installer\$installer.wixproj" } Write-Output "Coping $runtime MSI to: $msiTargetFile" |