diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d59303..949e2cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ## 01/10/2020 - v0.8.4.1 +* (Fix) Tool - Layer Clone: Layer information was the same as heights, fixed to show the result of operation in layers * (Fix) Tool - Pattern: Unable to use an anchor ## 01/10/2020 - v0.8.4.0 diff --git a/UVtools.Core/Layer/LayerManager.cs b/UVtools.Core/Layer/LayerManager.cs index e8a0ced..632dc92 100644 --- a/UVtools.Core/Layer/LayerManager.cs +++ b/UVtools.Core/Layer/LayerManager.cs @@ -1513,7 +1513,7 @@ namespace UVtools.Core //new ParallelOptions{MaxDegreeOfParallelism = 1}, i => { - var mat = CvInvoke.Imread(operation.Files[i], ImreadModes.Grayscale); + var mat = CvInvoke.Imread(operation.Files[i].TagString, ImreadModes.Grayscale); uint layerIndex = (uint) (startIndex + i); if (operation.MergeImages) { diff --git a/UVtools.Core/Operations/Operation.cs b/UVtools.Core/Operations/Operation.cs index ff7f177..fae625b 100644 --- a/UVtools.Core/Operations/Operation.cs +++ b/UVtools.Core/Operations/Operation.cs @@ -15,6 +15,8 @@ namespace UVtools.Core.Operations public abstract class Operation : BindableBase { private Rectangle _roi = Rectangle.Empty; + private uint _layerIndexEnd; + private uint _layerIndexStart; public const byte ClassNameLength = 9; /// @@ -84,12 +86,20 @@ namespace UVtools.Core.Operations /// /// Gets the start layer index where operation will starts in /// - public virtual uint LayerIndexStart { get; set; } + public virtual uint LayerIndexStart + { + get => _layerIndexStart; + set => RaiseAndSetIfChanged(ref _layerIndexStart, value); + } /// /// Gets the end layer index where operation will ends in /// - public virtual uint LayerIndexEnd { get; set; } + public virtual uint LayerIndexEnd + { + get => _layerIndexEnd; + set => RaiseAndSetIfChanged(ref _layerIndexEnd, value); + } public uint LayerRangeCount => LayerIndexEnd - LayerIndexStart + 1; diff --git a/UVtools.Core/Operations/OperationLayerClone.cs b/UVtools.Core/Operations/OperationLayerClone.cs index 1d6214e..e1e896c 100644 --- a/UVtools.Core/Operations/OperationLayerClone.cs +++ b/UVtools.Core/Operations/OperationLayerClone.cs @@ -12,13 +12,15 @@ namespace UVtools.Core.Operations { public sealed class OperationLayerClone : Operation { + private uint _clones = 1; + #region Overrides public override Enumerations.LayerRangeSelection LayerRangeSelection => Enumerations.LayerRangeSelection.Current; public override bool CanROI { get; set; } = false; public override bool PassActualLayerIndex => true; - public override string Title => "Clone layer(s)"; + public override string Title => "Clone layers"; public override string Description => "Clone layers.\n\n" + "Useful to increase the height of the model or add additional structure by duplicating layers. For example, can be used to increase the raft height for added stability."; @@ -50,7 +52,11 @@ namespace UVtools.Core.Operations /// /// Gets or sets the number of clones /// - public uint Clones { get; set; } = 1; + public uint Clones + { + get => _clones; + set => RaiseAndSetIfChanged(ref _clones, value); + } #endregion } diff --git a/UVtools.Core/Operations/OperationLayerImport.cs b/UVtools.Core/Operations/OperationLayerImport.cs index 7756a6a..e6aa9b9 100644 --- a/UVtools.Core/Operations/OperationLayerImport.cs +++ b/UVtools.Core/Operations/OperationLayerImport.cs @@ -8,8 +8,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Drawing; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Emgu.CV; @@ -20,6 +22,13 @@ namespace UVtools.Core.Operations { public sealed class OperationLayerImport : Operation { + private uint _insertAfterLayerIndex; + private bool _replaceStartLayer; + private bool _replaceSubsequentLayers; + private bool _discardRemainingLayers; + private bool _mergeImages; + private ObservableCollection _files = new ObservableCollection(); + #region Overrides public override Enumerations.LayerRangeSelection LayerRangeSelection => Enumerations.LayerRangeSelection.None; @@ -44,10 +53,10 @@ namespace UVtools.Core.Operations public override StringTag Validate(params object[] parameters) { - var result = new ConcurrentBag(); + var result = new ConcurrentBag(); Parallel.ForEach(Files, file => { - using (Mat mat = CvInvoke.Imread(file, ImreadModes.AnyColor)) + using (Mat mat = CvInvoke.Imread(file.TagString, ImreadModes.AnyColor)) { if (mat.Size != FileResolution) { @@ -69,7 +78,7 @@ namespace UVtools.Core.Operations message.AppendLine("... Too many to show ..."); break; } - message.AppendLine(Path.GetFileNameWithoutExtension(file)); + message.AppendLine(file.Content); } return new StringTag(message.ToString(), result); @@ -79,13 +88,42 @@ namespace UVtools.Core.Operations #region Properties public Size FileResolution { get; } - public uint InsertAfterLayerIndex { get; set; } - public bool ReplaceStartLayer { get; set; } - public bool ReplaceSubsequentLayers { get; set; } - public bool DiscardRemainingLayers { get; set; } - public bool MergeImages { get; set; } - public List Files { get; } = new List(); + public uint InsertAfterLayerIndex + { + get => _insertAfterLayerIndex; + set => RaiseAndSetIfChanged(ref _insertAfterLayerIndex, value); + } + + public bool ReplaceStartLayer + { + get => _replaceStartLayer; + set => RaiseAndSetIfChanged(ref _replaceStartLayer, value); + } + + public bool ReplaceSubsequentLayers + { + get => _replaceSubsequentLayers; + set => RaiseAndSetIfChanged(ref _replaceSubsequentLayers, value); + } + + public bool DiscardRemainingLayers + { + get => _discardRemainingLayers; + set => RaiseAndSetIfChanged(ref _discardRemainingLayers, value); + } + + public bool MergeImages + { + get => _mergeImages; + set => RaiseAndSetIfChanged(ref _mergeImages, value); + } + + public ObservableCollection Files + { + get => _files; + set => RaiseAndSetIfChanged(ref _files, value); + } public int Count => Files.Count; #endregion @@ -103,9 +141,22 @@ namespace UVtools.Core.Operations #endregion #region Methods + + public void AddFile(string file) + { + Files.Add(new StringTag(Path.GetFileNameWithoutExtension(file), file)); + } + public void Sort() { - Files.Sort((file1, file2) => string.Compare(Path.GetFileNameWithoutExtension(file1), Path.GetFileNameWithoutExtension(file2), StringComparison.Ordinal)); + var sortedFiles = Files.ToList(); + sortedFiles.Sort((file1, file2) => string.Compare(Path.GetFileNameWithoutExtension(file1.TagString), Path.GetFileNameWithoutExtension(file2.TagString), StringComparison.Ordinal)); + Files.Clear(); + foreach (var file in sortedFiles) + { + Files.Add(file); + } + //Files.Sort((file1, file2) => string.Compare(Path.GetFileNameWithoutExtension(file1), Path.GetFileNameWithoutExtension(file2), StringComparison.Ordinal)); } diff --git a/UVtools.Core/Operations/OperationMask.cs b/UVtools.Core/Operations/OperationMask.cs index 7083e7a..ed1c3c3 100644 --- a/UVtools.Core/Operations/OperationMask.cs +++ b/UVtools.Core/Operations/OperationMask.cs @@ -20,7 +20,7 @@ namespace UVtools.Core.Operations "Mask the intensity of the LCD output using a greyscale input image.\n\n" + "Useful to correct LCD light uniformity for a specific printer.\n\n" + "NOTE: This operation should be run only after repairs and other transformations. The provided" + - "input mask image must match the ouput resolution of the target printer."; + "input mask image must match the output resolution of the target printer."; public override string ConfirmationText => $"mask layers from {LayerIndexStart} through {LayerIndexEnd}"; @@ -42,5 +42,13 @@ namespace UVtools.Core.Operations } public Mat Mask { get; set; } + + public bool HaveMask => !(Mask is null); + + public void InvertMask() + { + if (!HaveMask) return; + CvInvoke.BitwiseNot(Mask, Mask); + } } } diff --git a/UVtools.Core/Operations/OperationPixelDimming.cs b/UVtools.Core/Operations/OperationPixelDimming.cs index e9cad5b..22941d6 100644 --- a/UVtools.Core/Operations/OperationPixelDimming.cs +++ b/UVtools.Core/Operations/OperationPixelDimming.cs @@ -14,6 +14,11 @@ namespace UVtools.Core.Operations { public class OperationPixelDimming : Operation { + private uint _borderSize = 5; + private bool _bordersOnly; + private Matrix _evenPattern; + private Matrix _oddPattern; + #region Overrides public override string Title => "Pixel dimming"; public override string Description => @@ -51,10 +56,30 @@ namespace UVtools.Core.Operations #region Properties - public uint BorderSize { get; set; } - public bool BordersOnly { get; set; } - public Matrix EvenPattern { get; set; } - public Matrix OddPattern { get; set; } + public uint BorderSize + { + get => _borderSize; + set => RaiseAndSetIfChanged(ref _borderSize, value); + } + + public bool BordersOnly + { + get => _bordersOnly; + set => RaiseAndSetIfChanged(ref _bordersOnly, value); + } + + public Matrix EvenPattern + { + get => _evenPattern; + set => RaiseAndSetIfChanged(ref _evenPattern, value); + } + + public Matrix OddPattern + { + get => _oddPattern; + set => RaiseAndSetIfChanged(ref _oddPattern, value); + } + #endregion } } diff --git a/UVtools.Core/PixelEditor/PixelDrainHole.cs b/UVtools.Core/PixelEditor/PixelDrainHole.cs index 3b58237..1f03edb 100644 --- a/UVtools.Core/PixelEditor/PixelDrainHole.cs +++ b/UVtools.Core/PixelEditor/PixelDrainHole.cs @@ -11,9 +11,13 @@ namespace UVtools.Core.PixelEditor { public class PixelDrainHole : PixelOperation { + public override PixelOperationType OperationType => PixelOperationType.DrainHole; + public byte Diameter { get; } - public PixelDrainHole(uint layerIndex, Point location, byte diameter) : base(PixelOperationType.DrainHole, layerIndex, location) + public PixelDrainHole(){} + + public PixelDrainHole(uint layerIndex, Point location, byte diameter) : base(layerIndex, location) { Diameter = diameter; Size = new Size(diameter, diameter); diff --git a/UVtools.Core/PixelEditor/PixelDrawing.cs b/UVtools.Core/PixelEditor/PixelDrawing.cs index acfb1d0..0e792b9 100644 --- a/UVtools.Core/PixelEditor/PixelDrawing.cs +++ b/UVtools.Core/PixelEditor/PixelDrawing.cs @@ -13,6 +13,9 @@ namespace UVtools.Core.PixelEditor { public class PixelDrawing : PixelOperation { + private BrushShapeType _brushShape = BrushShapeType.Rectangle; + private ushort _brushSize = 1; + private short _thickness = -1; public const byte MinRectangleBrush = 1; public const byte MinCircleBrush = 7; public enum BrushShapeType : byte @@ -21,22 +24,47 @@ namespace UVtools.Core.PixelEditor Circle } - public BrushShapeType BrushShape { get; } + public override PixelOperationType OperationType => PixelOperationType.Drawing; - public ushort BrushSize { get; } - public short Thickness { get; } + public static BrushShapeType[] BrushShapeTypes => (BrushShapeType[])Enum.GetValues(typeof(BrushShapeType)); - //public ushort LayersBelow { get; } + public BrushShapeType BrushShape + { + get => _brushShape; + set + { + if (!RaiseAndSetIfChanged(ref _brushShape, value)) return; + if (_brushShape == BrushShapeType.Circle) + { + BrushSize = Math.Max(MinCircleBrush, BrushSize); + } + } + } - //public ushort LayersAbove { get; } + public ushort BrushSize + { + get => _brushSize; + set => RaiseAndSetIfChanged(ref _brushSize, value); + } - public bool IsAdd { get; } + public short Thickness + { + get => _thickness; + set => RaiseAndSetIfChanged(ref _thickness, value); + } + + public bool IsAdd { get; } public byte Color { get; } public Rectangle Rectangle { get; } - public PixelDrawing(uint layerIndex, Point location, LineType lineType, BrushShapeType brushShape, ushort brushSize, short thickness, bool isAdd) : base(PixelOperationType.Drawing, layerIndex, location, lineType) + public PixelDrawing() + { + + } + + public PixelDrawing(uint layerIndex, Point location, LineType lineType, BrushShapeType brushShape, ushort brushSize, short thickness, bool isAdd) : base(layerIndex, location, lineType) { BrushShape = brushShape; BrushSize = brushSize; @@ -50,5 +78,7 @@ namespace UVtools.Core.PixelEditor Rectangle = new Rectangle(Math.Max(0, location.X - shiftPos), Math.Max(0, location.Y - shiftPos), brushSize-1, brushSize-1); Size = new Size(BrushSize, BrushSize); } + + } } diff --git a/UVtools.Core/PixelEditor/PixelEraser.cs b/UVtools.Core/PixelEditor/PixelEraser.cs index 362d999..07d3856 100644 --- a/UVtools.Core/PixelEditor/PixelEraser.cs +++ b/UVtools.Core/PixelEditor/PixelEraser.cs @@ -13,7 +13,13 @@ namespace UVtools.Core.PixelEditor { public const byte Diameter = 4; - public PixelEraser(uint layerIndex, Point location) : base(PixelOperationType.Eraser, layerIndex, location) + public override PixelOperationType OperationType => PixelOperationType.Eraser; + + public PixelEraser() + { + } + + public PixelEraser(uint layerIndex, Point location) : base(layerIndex, location) { Size = new Size(Diameter, Diameter); } diff --git a/UVtools.Core/PixelEditor/PixelOperation.cs b/UVtools.Core/PixelEditor/PixelOperation.cs index d64fce6..6fb3397 100644 --- a/UVtools.Core/PixelEditor/PixelOperation.cs +++ b/UVtools.Core/PixelEditor/PixelOperation.cs @@ -5,13 +5,18 @@ * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ + +using System; using System.Drawing; using Emgu.CV.CvEnum; +using UVtools.Core.Objects; namespace UVtools.Core.PixelEditor { - public class PixelOperation + public abstract class PixelOperation : BindableBase { + private uint _index; + public enum PixelOperationType : byte { Drawing, @@ -24,12 +29,16 @@ namespace UVtools.Core.PixelEditor /// /// Gets or sets the index number to show on GUI /// - public uint Index { get; set; } + public uint Index + { + get => _index; + set => RaiseAndSetIfChanged(ref _index, value); + } /// /// Gets the /// - public PixelOperationType OperationType { get; } + public abstract PixelOperationType OperationType { get; } /// /// Gets the layer index @@ -44,19 +53,38 @@ namespace UVtools.Core.PixelEditor /// /// Gets the for the draw operation /// - public LineType LineType { get; } + public LineType LineType { get; set; } = LineType.AntiAlias; + + public LineType[] LineTypes => new[] + { + LineType.FourConnected, + LineType.EightConnected, + LineType.AntiAlias + }; + + public uint LayersBelow { get; set; } + + public uint LayersAbove { get; set; } /// /// Gets the total size of the operation /// public Size Size { get; private protected set; } = Size.Empty; - public PixelOperation(PixelOperationType operationType, uint layerIndex, Point location, LineType lineType = LineType.AntiAlias) + protected PixelOperation() + { + } + + protected PixelOperation(uint layerIndex, Point location, LineType lineType = LineType.AntiAlias) { - OperationType = operationType; Location = location; LayerIndex = layerIndex; LineType = lineType; } + + public PixelOperation Clone() + { + return (PixelOperation) MemberwiseClone(); + } } } diff --git a/UVtools.Core/PixelEditor/PixelSupport.cs b/UVtools.Core/PixelEditor/PixelSupport.cs index a67a9e7..66ce20f 100644 --- a/UVtools.Core/PixelEditor/PixelSupport.cs +++ b/UVtools.Core/PixelEditor/PixelSupport.cs @@ -11,13 +11,17 @@ namespace UVtools.Core.PixelEditor { public class PixelSupport : PixelOperation { - public byte TipDiameter { get; } + public override PixelOperationType OperationType => PixelOperationType.Supports; - public byte PillarDiameter { get; } + public byte TipDiameter { get; set; } - public byte BaseDiameter { get; } + public byte PillarDiameter { get; set; } - public PixelSupport(uint layerIndex, Point location, byte tipDiameter, byte pillarDiameter, byte baseDiameter) : base(PixelOperationType.Supports, layerIndex, location) + public byte BaseDiameter { get; set; } + + public PixelSupport(){} + + public PixelSupport(uint layerIndex, Point location, byte tipDiameter, byte pillarDiameter, byte baseDiameter) : base(layerIndex, location) { TipDiameter = tipDiameter; PillarDiameter = pillarDiameter; diff --git a/UVtools.Core/PixelEditor/PixelText.cs b/UVtools.Core/PixelEditor/PixelText.cs index d6f4158..7f84b7c 100644 --- a/UVtools.Core/PixelEditor/PixelText.cs +++ b/UVtools.Core/PixelEditor/PixelText.cs @@ -14,12 +14,14 @@ namespace UVtools.Core.PixelEditor { public class PixelText : PixelOperation { - public FontFace Font { get; } + public override PixelOperationType OperationType => PixelOperationType.Text; - public double FontScale { get; } - public ushort Thickness { get; } - public string Text { get; } - public bool Mirror { get; } + public FontFace Font { get; set; } + + public double FontScale { get; set; } + public ushort Thickness { get; set; } + public string Text { get; set; } + public bool Mirror { get; set; } public bool IsAdd { get; } @@ -27,7 +29,9 @@ namespace UVtools.Core.PixelEditor public Rectangle Rectangle { get; } - public PixelText(uint layerIndex, Point location, LineType lineType, FontFace font, double fontScale, ushort thickness, string text, bool mirror, bool isAdd) : base(PixelOperationType.Text, layerIndex, location, lineType) + public PixelText(){} + + public PixelText(uint layerIndex, Point location, LineType lineType, FontFace font, double fontScale, ushort thickness, string text, bool mirror, bool isAdd) : base(layerIndex, location, lineType) { Font = font; FontScale = fontScale; @@ -41,5 +45,7 @@ namespace UVtools.Core.PixelEditor Size = CvInvoke.GetTextSize(text, font, fontScale, thickness, ref baseLine); Rectangle = new Rectangle(location, Size); } + + } } diff --git a/UVtools.GUI/Controls/Tools/CtrlToolLayerClone.cs b/UVtools.GUI/Controls/Tools/CtrlToolLayerClone.cs index 5486dc3..20d6ca1 100644 --- a/UVtools.GUI/Controls/Tools/CtrlToolLayerClone.cs +++ b/UVtools.GUI/Controls/Tools/CtrlToolLayerClone.cs @@ -35,8 +35,8 @@ namespace UVtools.GUI.Controls.Tools { uint extraLayers = (uint)Math.Max(0, (ParentToolWindow.nmLayerRangeEnd.Value - ParentToolWindow.nmLayerRangeStart.Value + 1) * nmClones.Value); float extraHeight = (float)Math.Round(extraLayers * Program.SlicerFile.LayerHeight, 2); - lbLayersCount.Text = $"Layers: {Program.SlicerFile.TotalHeight} → {Program.SlicerFile.TotalHeight + extraLayers} (+ {extraLayers})"; - lbHeights.Text = $"Heights: {Program.SlicerFile.TotalHeight}mm → {Program.SlicerFile.TotalHeight + extraHeight}mm (+ {extraHeight}mm)"; + lbLayersCount.Text = $"Result layers: {Program.SlicerFile.LayerCount} → {Program.SlicerFile.LayerCount + extraLayers} (+ {extraLayers})"; + lbHeights.Text = $"Result heights: {Program.SlicerFile.TotalHeight}mm → {Program.SlicerFile.TotalHeight + extraHeight}mm (+ {extraHeight}mm)"; } public override bool UpdateOperation() diff --git a/UVtools.GUI/Controls/Tools/CtrlToolLayerImport.cs b/UVtools.GUI/Controls/Tools/CtrlToolLayerImport.cs index 53090af..e900652 100644 --- a/UVtools.GUI/Controls/Tools/CtrlToolLayerImport.cs +++ b/UVtools.GUI/Controls/Tools/CtrlToolLayerImport.cs @@ -10,8 +10,6 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Drawing; -using System.IO; -using System.Text; using System.Windows.Forms; using UVtools.Core.Objects; using UVtools.Core.Operations; @@ -55,7 +53,7 @@ namespace UVtools.GUI.Controls.Tools message.Content += "\nDo you want to remove all invalid files from list?"; if (MessageBoxError(message.ToString(), MessageBoxButtons.YesNo) == DialogResult.Yes) { - ConcurrentBag result = (ConcurrentBag)message.Tag; + ConcurrentBag result = (ConcurrentBag)message.Tag; foreach (var file in result) { Operation.Files.Remove(file); @@ -104,7 +102,10 @@ namespace UVtools.GUI.Controls.Tools { if (fileOpen.ShowDialog() != DialogResult.OK) return; - Operation.Files.AddRange(fileOpen.FileNames); + foreach (var filename in fileOpen.FileNames) + { + Operation.AddFile(filename); + } if (cbAutoSort.Checked) { @@ -121,7 +122,7 @@ namespace UVtools.GUI.Controls.Tools { foreach (StringTag selectedItem in lbFiles.SelectedItems) { - Operation.Files.Remove(selectedItem.TagString); + Operation.Files.Remove(selectedItem); } UpdateListBox(); @@ -189,8 +190,7 @@ namespace UVtools.GUI.Controls.Tools foreach (var file in Operation.Files) { - var stringTag = new StringTag(Path.GetFileNameWithoutExtension(file), file); - lbFiles.Items.Add(stringTag); + lbFiles.Items.Add(file); } ButtonOkEnabled = btnRemove.Enabled = btnSort.Enabled = btnClear.Enabled = Operation.Files.Count > 0; diff --git a/UVtools.GUI/Controls/Tools/CtrlToolPixelDimming.resx b/UVtools.GUI/Controls/Tools/CtrlToolPixelDimming.resx index b4eb075..3cab13f 100644 --- a/UVtools.GUI/Controls/Tools/CtrlToolPixelDimming.resx +++ b/UVtools.GUI/Controls/Tools/CtrlToolPixelDimming.resx @@ -131,6 +131,6 @@ Enable the "Fade in/out" to fade the iteration over layers, you can use a start WARNING: Using high iteration values can destroy your model depending on the mutator being used, please use low values or with caution! - 25 + 121 \ No newline at end of file diff --git a/UVtools.GUI/FrmMain.cs b/UVtools.GUI/FrmMain.cs index 40d7353..6710c6d 100644 --- a/UVtools.GUI/FrmMain.cs +++ b/UVtools.GUI/FrmMain.cs @@ -2806,57 +2806,56 @@ namespace UVtools.GUI // the cross cursor is displayed even before the pblayer control has focus. // This ensures the user is aware that even in this case, a click in the layer // preview will draw a pixel. - if (ReferenceEquals(sender, this) && !ReferenceEquals(SlicerFile, null)) + if (!ReferenceEquals(sender, this) || ReferenceEquals(SlicerFile, null)) return; + + // This event repeats for as long as the key is pressed, so if we've + // already set the cursor from a previous key down event, just return. + if (!ReferenceEquals(pbLayer.Cursor.Tag, null) || pbLayer.Cursor == pixelEditCursor || pbLayer.Cursor == Cursors.Cross + || pbLayer.Cursor == Cursors.Hand || pbLayer.SelectionMode == ImageBoxSelectionMode.Rectangle) return; + + // Pixel Edit is active, Shift is down, and the cursor is over the image region. + if (pbLayer.ClientRectangle.Contains(pbLayer.PointToClient(MousePosition))) { - // This event repeats for as long as the key is pressed, so if we've - // already set the cursor from a previous key down event, just return. - if (!ReferenceEquals(pbLayer.Cursor.Tag, null) || pbLayer.Cursor == pixelEditCursor || pbLayer.Cursor == Cursors.Cross - || pbLayer.Cursor == Cursors.Hand || pbLayer.SelectionMode == ImageBoxSelectionMode.Rectangle) return; - - // Pixel Edit is active, Shift is down, and the cursor is over the image region. - if (pbLayer.ClientRectangle.Contains(pbLayer.PointToClient(MousePosition))) + if (e.Modifiers == Keys.Shift) { - if (e.Modifiers == Keys.Shift) + if (btnLayerImagePixelEdit.Checked) { - if (btnLayerImagePixelEdit.Checked) - { - pbLayer.PanMode = ImageBoxPanMode.None; - lbLayerImageTooltipOverlay.Text = "Pixel editing is on:\n" + - "» Click over a pixel to draw\n" + - "» Hold CTRL to clear pixels"; - - UpdatePixelEditorCursor(); - } - else - { - pbLayer.Cursor = Cursors.Cross; - pbLayer.SelectionMode = ImageBoxSelectionMode.Rectangle; - lbLayerImageTooltipOverlay.Text = "ROI selection mode:\n" + - "» Left-click drag to select a fixed region\n" + - "» Left-click + ALT drag to select specific objects\n" + - "» Right click on a specific object to select it\n" + - "Press Esc to clear the ROI"; - } - - lbLayerImageTooltipOverlay.Visible = Settings.Default.LayerTooltipOverlay; - - return; - } - if (e.Modifiers == Keys.Control) - { - pbLayer.Cursor = Cursors.Hand; pbLayer.PanMode = ImageBoxPanMode.None; - lbLayerImageTooltipOverlay.Text = "Issue selection mode:\n" + - "» Click over an issue to select it"; + lbLayerImageTooltipOverlay.Text = "Pixel editing is on:\n" + + "» Click over a pixel to draw\n" + + "» Hold CTRL to clear pixels"; - lbLayerImageTooltipOverlay.Visible = Settings.Default.LayerTooltipOverlay; - - return; + UpdatePixelEditorCursor(); + } + else + { + pbLayer.Cursor = Cursors.Cross; + pbLayer.SelectionMode = ImageBoxSelectionMode.Rectangle; + lbLayerImageTooltipOverlay.Text = "ROI selection mode:\n" + + "» Left-click drag to select a fixed region\n" + + "» Left-click + ALT drag to select specific objects\n" + + "» Right click on a specific object to select it\n" + + "Press Esc to clear the ROI"; } - } - return; + lbLayerImageTooltipOverlay.Visible = Settings.Default.LayerTooltipOverlay; + + return; + } + if (e.Modifiers == Keys.Control) + { + pbLayer.Cursor = Cursors.Hand; + pbLayer.PanMode = ImageBoxPanMode.None; + lbLayerImageTooltipOverlay.Text = "Issue selection mode:\n" + + "» Click over an issue to select it"; + + lbLayerImageTooltipOverlay.Visible = Settings.Default.LayerTooltipOverlay; + + return; + } } + + return; } private void EventKeyUp(object sender, KeyEventArgs e) diff --git a/UVtools.WPF/Assets/Icons/code-branch-24x24.png b/UVtools.WPF/Assets/Icons/code-branch-24x24.png new file mode 100644 index 0000000..30ca801 Binary files /dev/null and b/UVtools.WPF/Assets/Icons/code-branch-24x24.png differ diff --git a/UVtools.WPF/Assets/Icons/eraser-24x24.png b/UVtools.WPF/Assets/Icons/eraser-24x24.png new file mode 100644 index 0000000..0c3db2e Binary files /dev/null and b/UVtools.WPF/Assets/Icons/eraser-24x24.png differ diff --git a/UVtools.WPF/Assets/Icons/font-24x24.png b/UVtools.WPF/Assets/Icons/font-24x24.png new file mode 100644 index 0000000..f19f1cf Binary files /dev/null and b/UVtools.WPF/Assets/Icons/font-24x24.png differ diff --git a/UVtools.WPF/Assets/Icons/minus_16x16.png b/UVtools.WPF/Assets/Icons/minus-16x16.png similarity index 100% rename from UVtools.WPF/Assets/Icons/minus_16x16.png rename to UVtools.WPF/Assets/Icons/minus-16x16.png diff --git a/UVtools.WPF/Assets/Icons/pencil-alt-24x24.png b/UVtools.WPF/Assets/Icons/pencil-alt-24x24.png new file mode 100644 index 0000000..82143e6 Binary files /dev/null and b/UVtools.WPF/Assets/Icons/pencil-alt-24x24.png differ diff --git a/UVtools.WPF/Assets/Icons/ring-24x24.png b/UVtools.WPF/Assets/Icons/ring-24x24.png new file mode 100644 index 0000000..41a18f5 Binary files /dev/null and b/UVtools.WPF/Assets/Icons/ring-24x24.png differ diff --git a/UVtools.WPF/Controls/AdvancedImageBox.cs b/UVtools.WPF/Controls/AdvancedImageBox.cs index 1a8c0c9..e33ca7e 100644 --- a/UVtools.WPF/Controls/AdvancedImageBox.cs +++ b/UVtools.WPF/Controls/AdvancedImageBox.cs @@ -19,6 +19,7 @@ using UVtools.Core.Extensions; using UVtools.WPF.Extensions; using Bitmap = Avalonia.Media.Imaging.Bitmap; using Brushes = Avalonia.Media.Brushes; +using Color = Avalonia.Media.Color; using Pen = Avalonia.Media.Pen; using Point = Avalonia.Point; using Size = Avalonia.Size; @@ -383,7 +384,7 @@ namespace UVtools.WPF.Controls } [Flags] - public enum PanMouseButtons : byte + public enum MouseButtons : byte { None = 0, LeftButton = 1, @@ -395,7 +396,7 @@ namespace UVtools.WPF.Controls /// Describes the zoom action occuring /// [Flags] - public enum ImageZoomActions : byte + public enum ZoomActions : byte { /// /// No action. @@ -418,6 +419,24 @@ namespace UVtools.WPF.Controls ActualSize = 4 } + public enum SelectionModes + { + /// + /// No selection. + /// + None, + + /// + /// Rectangle selection. + /// + Rectangle, + + /// + /// Zoom selection. + /// + Zoom + } + #endregion #region Constants @@ -465,6 +484,7 @@ namespace UVtools.WPF.Controls { SizedContainer.Width = 0; SizedContainer.Height = 0; + SelectNone(); } else { @@ -525,6 +545,12 @@ namespace UVtools.WPF.Controls } } + public bool IsSelecting + { + get => _isSelecting; + protected set => RaiseAndSetIfChanged(ref _isSelecting, value); + } + public Point CenterPoint { get @@ -540,7 +566,7 @@ namespace UVtools.WPF.Controls set => RaiseAndSetIfChanged(ref _autoPan, value); } - public PanMouseButtons PanWithMouseButtons + public MouseButtons PanWithMouseButtons { get => _panWithMouseButtons; set => RaiseAndSetIfChanged(ref _panWithMouseButtons, value); @@ -552,6 +578,12 @@ namespace UVtools.WPF.Controls set => RaiseAndSetIfChanged(ref _panWithArrows, value); } + public MouseButtons SelectWithMouseButtons + { + get => _selectWithMouseButtons; + set => RaiseAndSetIfChanged(ref _selectWithMouseButtons, value); + } + public bool InvertMouse { get => _invertMouse; @@ -653,31 +685,53 @@ namespace UVtools.WPF.Controls set => RaiseAndSetIfChanged(ref _pixelGridThreshold, value); } + public SelectionModes SelectionMode + { + get => _selectionMode; + set => RaiseAndSetIfChanged(ref _selectionMode, value); + } + + public ISolidColorBrush SelectionColor + { + get => _selectionColor; + set => RaiseAndSetIfChanged(ref _selectionColor, value); + } + public Rect SelectionRegion { get => _selectionRegion; - set => RaiseAndSetIfChanged(ref _selectionRegion, value); + set + { + if(!RaiseAndSetIfChanged(ref _selectionRegion, value)) return; + InvalidateArrange(); + RaisePropertyChanged(nameof(HaveSelection)); + } } + public bool HaveSelection => !SelectionRegion.IsEmpty; + //Our render target we compile everything to and present to the user - private RenderTargetBitmap RenderTarget; - private ISkiaDrawingContextImpl SkiaContext; private Point _startMousePosition; private Vector _startScrollPosition; private bool _isPanning; + private bool _isSelecting; private Bitmap _image; private byte _gridCellSize; private ISolidColorBrush _gridColor = Brushes.Gainsboro; private ISolidColorBrush _gridColorAlternate = Brushes.White; private bool _showGrid = true; private bool _autoPan = true; - private PanMouseButtons _panWithMouseButtons = PanMouseButtons.LeftButton | PanMouseButtons.MiddleButton | PanMouseButtons.RightButton; + 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; + public ContentControl FillContainer { get; } = new ContentControl { @@ -727,29 +781,29 @@ namespace UVtools.WPF.Controls } private void ProcessMouseZoom(bool isZoomIn, Point cursorPosition) - => PerformZoom(isZoomIn ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut, true, cursorPosition); + => PerformZoom(isZoomIn ? ZoomActions.ZoomIn : ZoomActions.ZoomOut, true, cursorPosition); /// /// Returns an appropriate zoom level based on the specified action, relative to the current zoom level. /// /// The action to determine the zoom level. /// Thrown if an unsupported action is specified. - private int GetZoomLevel(ImageZoomActions action) + private int GetZoomLevel(ZoomActions action) { int result; switch (action) { - case ImageZoomActions.None: + case ZoomActions.None: result = Zoom; break; - case ImageZoomActions.ZoomIn: + case ZoomActions.ZoomIn: result = ZoomLevels.NextZoom(Zoom); break; - case ImageZoomActions.ZoomOut: + case ZoomActions.ZoomOut: result = ZoomLevels.PreviousZoom(Zoom); break; - case ImageZoomActions.ActualSize: + case ZoomActions.ActualSize: result = 100; break; default: @@ -772,10 +826,10 @@ namespace UVtools.WPF.Controls } } - private void PerformZoom(ImageZoomActions action, bool preservePosition) + private void PerformZoom(ZoomActions action, bool preservePosition) => PerformZoom(action, preservePosition, CenterPoint); - private void PerformZoom(ImageZoomActions action, bool preservePosition, Point relativePoint) + private void PerformZoom(ZoomActions action, bool preservePosition, Point relativePoint) { Point currentPixel = PointToImage(relativePoint); int currentZoom = Zoom; @@ -945,7 +999,7 @@ namespace UVtools.WPF.Controls /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. public virtual void ZoomIn(bool preservePosition) { - PerformZoom(ImageZoomActions.ZoomIn, preservePosition); + PerformZoom(ZoomActions.ZoomIn, preservePosition); } /// @@ -960,7 +1014,7 @@ namespace UVtools.WPF.Controls /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. public virtual void ZoomOut(bool preservePosition) { - PerformZoom(ImageZoomActions.ZoomOut, preservePosition); + PerformZoom(ZoomActions.ZoomOut, preservePosition); } /// @@ -1065,6 +1119,13 @@ namespace UVtools.WPF.Controls } + /// + /// Centers the given point in the image in the center of the control + /// + /// The point of the image to attempt to center. + public virtual void CenterAt(System.Drawing.Point imageLocation) + => ScrollTo(new Point(imageLocation.X, imageLocation.Y), new Point(Viewport.Width / 2, Viewport.Height / 2)); + /// /// Centers the given point in the image in the center of the control /// @@ -1344,7 +1405,7 @@ namespace UVtools.WPF.Controls //ImageControl.InvalidateVisual(); } - public override void Render(DrawingContext context) + public override void Render(DrawingContext context) { Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); // base.Render(context); @@ -1363,7 +1424,8 @@ namespace UVtools.WPF.Controls currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; } - if(firstRowColor == currentColor) currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; + if (firstRowColor == currentColor) + currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; } } @@ -1377,7 +1439,7 @@ namespace UVtools.WPF.Controls context.DrawImage(Image, GetSourceImageRegion(), GetImageViewPort() - ); + ); //SkiaContext.SkCanvas.dr // Draw pixel grid var pixelSize = ZoomFactor; @@ -1400,13 +1462,205 @@ namespace UVtools.WPF.Controls context.DrawRectangle(pen, viewport); } + + 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); + } } + /// + /// Returns the source repositioned to include the current image offset and scaled by the current zoom level + /// + /// The source to offset. + /// A which has been repositioned to match the current zoom level and image offset + public virtual Point GetOffsetPoint(System.Drawing.Point source) + { + var offset = GetOffsetPoint(new Point(source.X, source.Y)); + + return new Point((int)offset.X, (int)offset.Y); + } + + /// + /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level + /// + /// The source X co-ordinate. + /// The source Y co-ordinate. + /// A which has been repositioned to match the current zoom level and image offset + public Point GetOffsetPoint(int x, int y) + { + return GetOffsetPoint(new System.Drawing.Point(x, y)); + } + + /// + /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level + /// + /// The source X co-ordinate. + /// The source Y co-ordinate. + /// A which has been repositioned to match the current zoom level and image offset + public Point GetOffsetPoint(double x, double y) + { + return GetOffsetPoint(new Point(x, y)); + } + + /// + /// Returns the source repositioned to include the current image offset and scaled by the current zoom level + /// + /// The source to offset. + /// A which has been repositioned to match the current zoom level and image offset + public virtual Point GetOffsetPoint(Point source) + { + Rect viewport = GetImageViewPort(); + var scaled = GetScaledPoint(source); + var offsetX = viewport.Left + Offset.X; + var offsetY = viewport.Top + Offset.Y; + + return new Point(scaled.X + offsetX, scaled.Y + offsetY); + } + + /// + /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset + /// + /// The source to offset. + /// A which has been resized and repositioned to match the current zoom level and image offset + public virtual Rect GetOffsetRectangle(Rect source) + { + var viewport = GetImageViewPort(); + var scaled = GetScaledRectangle(source); + 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); + } + + /// + /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset + /// + /// The X co-ordinate of the source rectangle. + /// The Y co-ordinate of the source rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + /// A which has been resized and repositioned to match the current zoom level and image offset + public Rectangle GetOffsetRectangle(int x, int y, int width, int height) + { + return this.GetOffsetRectangle(new Rectangle(x, y, width, height)); + } + + /// + /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset + /// + /// The X co-ordinate of the source rectangle. + /// The Y co-ordinate of the source rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + /// A which has been resized and repositioned to match the current zoom level and image offset + public Rect GetOffsetRectangle(double x, double y, double width, double height) + { + return GetOffsetRectangle(new Rect(x, y, width, height)); + } + + /// + /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset + /// + /// The source to offset. + /// A which has been resized and repositioned to match the current zoom level and image offset + public virtual Rectangle GetOffsetRectangle(Rectangle source) + { + var viewport = GetImageViewPort(); + var scaled = GetScaledRectangle(source); + 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)); + } + + /// + /// Fits a given to match image boundaries + /// + /// The rectangle. + /// + /// A structure remapped to fit the image boundaries + /// + public Rectangle FitRectangle(Rectangle rectangle) + { + if (Image is null) return Rectangle.Empty; + var x = rectangle.X; + var y = rectangle.Y; + var w = rectangle.Width; + var h = rectangle.Height; + + if (x < 0) + { + x = 0; + } + + if (y < 0) + { + y = 0; + } + + if (x + w > Image.Size.Width) + { + w = (int) (Image.Size.Width - x); + } + + if (y + h > Image.Size.Height) + { + h = (int) (Image.Size.Height - y); + } + + return new Rectangle(x, y, w, h); + } + /// - /// Gets the source image region. + /// Fits a given to match image boundaries /// - /// - public virtual Rect GetSourceImageRegion() + /// The rectangle. + /// + /// A structure remapped to fit the image boundaries + /// + public Rect FitRectangle(Rect rectangle) + { + if(Image is null) return Rect.Empty; + var x = rectangle.X; + var y = rectangle.Y; + var w = rectangle.Width; + var h = rectangle.Height; + + if (x < 0) + { + w -= -x; + x = 0; + } + + if (y < 0) + { + h -= -y; + y = 0; + } + + if (x + w > Image.Size.Width) + { + w = Image.Size.Width - x; + } + + if (y + h > Image.Size.Height) + { + h = Image.Size.Height - y; + } + + return new Rect(x, y, w, h); + } + + /// + /// Gets the source image region. + /// + /// + public virtual Rect GetSourceImageRegion() { if (Image is null) return Rect.Empty; @@ -1474,21 +1728,42 @@ namespace UVtools.WPF.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); - if (e.Handled) return; + if (e.Handled + || IsPanning + || IsSelecting + || Image is null) return; var pointer = e.GetCurrentPoint(this); - if (!( - pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & PanMouseButtons.LeftButton) != 0 || - pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & PanMouseButtons.MiddleButton) != 0 || - pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & PanMouseButtons.RightButton) != 0 + + if (SelectionMode != SelectionModes.None) + { + if (!( + pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 || + pointer.Properties.IsMiddleButtonPressed && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 || + pointer.Properties.IsRightButtonPressed && (SelectWithMouseButtons & MouseButtons.RightButton) != 0 ) - || !AutoPan || IsPanning || Image == null) return; + ) return; + IsSelecting = true; + } + else + { + if (!( + pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 || + pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 || + pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0 + ) + || !AutoPan + + ) return; + + IsPanning = true; + } + var location = pointer.Position; if (location.X > Viewport.Width) return; if (location.Y > Viewport.Height) return; _startMousePosition = location; - IsPanning = true; } protected override void OnPointerReleased(PointerReleasedEventArgs e) @@ -1496,7 +1771,8 @@ namespace UVtools.WPF.Controls base.OnPointerReleased(e); if (e.Handled) return; - if (IsPanning) IsPanning = false; + IsPanning = false; + IsSelecting = false; } protected override void OnPointerMoved(PointerEventArgs e) @@ -1504,25 +1780,74 @@ namespace UVtools.WPF.Controls base.OnPointerMoved(e); if (e.Handled) return; - if (!IsPanning) return; + if (!IsPanning && !IsSelecting) return; var pointer = e.GetCurrentPoint(this); var location = pointer.Position; - - double x; - double y; - if (!InvertMouse) + + if (IsPanning) { - x = _startScrollPosition.X + (_startMousePosition.X - location.X); - y = _startScrollPosition.Y + (_startMousePosition.Y - location.Y); + double x; + double y; + + if (!InvertMouse) + { + x = _startScrollPosition.X + (_startMousePosition.X - location.X); + y = _startScrollPosition.Y + (_startMousePosition.Y - location.Y); + } + else + { + x = (_startScrollPosition.X - (_startMousePosition.X - location.X)); + y = (_startScrollPosition.Y - (_startMousePosition.Y - location.Y)); + } + + Offset = new Vector(x, y); } - else + else if (IsSelecting) { - x = (_startScrollPosition.X - (_startMousePosition.X - location.X)); - y = (_startScrollPosition.Y - (_startMousePosition.Y - location.Y)); + double x; + double y; + double w; + double h; + + var imageOffset = GetImageViewPort().Position; + + if (location.X < _startMousePosition.X) + { + x = location.X; + w = _startMousePosition.X - location.X; + } + else + { + x = _startMousePosition.X; + w = location.X - _startMousePosition.X; + } + + if (location.Y < _startMousePosition.Y) + { + y = location.Y; + h = _startMousePosition.Y - location.Y; + } + else + { + y = _startMousePosition.Y; + h = location.Y - _startMousePosition.Y; + } + + x -= imageOffset.X - Offset.X; + y -= imageOffset.Y - Offset.Y; + + x /= ZoomFactor; + y /= ZoomFactor; + w /= ZoomFactor; + h /= ZoomFactor; + + if (w != 0 && h != 0) + { + SelectionRegion = FitRectangle(new Rect(x, y, w, h)); + } } - Offset = new Vector(x, y); e.Handled = true; } #endregion diff --git a/UVtools.WPF/Controls/DummyControl.axaml b/UVtools.WPF/Controls/DummyControl.axaml new file mode 100644 index 0000000..9e91fca --- /dev/null +++ b/UVtools.WPF/Controls/DummyControl.axaml @@ -0,0 +1,8 @@ + + + diff --git a/UVtools.WPF/Controls/DummyControl.axaml.cs b/UVtools.WPF/Controls/DummyControl.axaml.cs new file mode 100644 index 0000000..a5c2cb7 --- /dev/null +++ b/UVtools.WPF/Controls/DummyControl.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace UVtools.WPF.Controls +{ + public class DummyControl : UserControl + { + public DummyControl() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UVtools.WPF/Controls/StaticControls.cs b/UVtools.WPF/Controls/StaticControls.cs new file mode 100644 index 0000000..3d0f943 --- /dev/null +++ b/UVtools.WPF/Controls/StaticControls.cs @@ -0,0 +1,11 @@ +using Avalonia.Input; + +namespace UVtools.WPF.Controls +{ + public static class StaticControls + { + public static Cursor ArrowCursor = new Cursor(StandardCursorType.Arrow); + public static Cursor CrossCursor = new Cursor(StandardCursorType.Cross); + public static Cursor HandCursor = new Cursor(StandardCursorType.Hand); + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolArithmeticControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolArithmeticControl.axaml.cs index f3b93ad..73fef89 100644 --- a/UVtools.WPF/Controls/Tools/ToolArithmeticControl.axaml.cs +++ b/UVtools.WPF/Controls/Tools/ToolArithmeticControl.axaml.cs @@ -1,5 +1,6 @@ using Avalonia.Markup.Xaml; using UVtools.Core.Operations; +using UVtools.WPF.Windows; namespace UVtools.WPF.Controls.Tools { @@ -11,11 +12,28 @@ namespace UVtools.WPF.Controls.Tools { InitializeComponent(); BaseOperation = Operation = new OperationArithmetic(); + Operation.PropertyChanged += (sender, e) => + { + if (e.PropertyName == nameof(Operation.Sentence)) + { + ParentWindow.ButtonOkEnabled = !string.IsNullOrWhiteSpace(Operation.Sentence); + } + }; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + public override void Callback(ToolWindow.Callbacks callback) + { + switch (callback) + { + case ToolWindow.Callbacks.Init: + ParentWindow.ButtonOkEnabled = false; + break; + } + } } } diff --git a/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml new file mode 100644 index 0000000..d877261 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs new file mode 100644 index 0000000..a394fa5 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerCloneControl.axaml.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Markup.Xaml; +using UVtools.Core.Operations; + +namespace UVtools.WPF.Controls.Tools +{ + public class ToolLayerCloneControl : ToolControl + { + public OperationLayerClone Operation { get; } + + public uint ExtraLayers => (uint)Math.Max(0, ((int)Operation.LayerIndexEnd - Operation.LayerIndexStart + 1) * Operation.Clones); + + public string InfoLayersStr + { + get + { + uint extraLayers = ExtraLayers; + return $"Layers: {App.SlicerFile.LayerCount} → {App.SlicerFile.LayerCount + extraLayers} (+ {extraLayers})"; + } + } + + public string InfoHeightsStr + { + get + { + float extraHeight = (float)Math.Round(ExtraLayers * App.SlicerFile.LayerHeight, 2); + return $"Height: {App.SlicerFile.TotalHeight}mm → {Math.Round(App.SlicerFile.TotalHeight + extraHeight, 2)}mm (+ {extraHeight}mm)"; + } + } + + public ToolLayerCloneControl() + { + InitializeComponent(); + BaseOperation = Operation = new OperationLayerClone(); + Operation.PropertyChanged += (sender, args) => + { + RaisePropertyChanged(nameof(InfoLayersStr)); + RaisePropertyChanged(nameof(InfoHeightsStr)); + }; + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml b/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml new file mode 100644 index 0000000..ce0164f --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs new file mode 100644 index 0000000..7e08757 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerImportControl.axaml.cs @@ -0,0 +1,192 @@ +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using DynamicData; +using MessageBox.Avalonia.Enums; +using UVtools.Core.Objects; +using UVtools.Core.Operations; +using UVtools.WPF.Extensions; +using UVtools.WPF.Windows; +using Path = System.IO.Path; + +namespace UVtools.WPF.Controls.Tools +{ + public class ToolLayerImportControl : ToolControl + { + private bool _isAutoSortLayersByFileNameChecked; + private StringTag _selectedFile; + private Bitmap _previewImage; + + private ListBox FilesListBox; + + public OperationLayerImport Operation { get; } + + public uint MaximumLayer => App.SlicerFile.LastLayerIndex; + + public string InfoLayerHeightStr => $"({App.SlicerFile.GetHeightFromLayer(Operation.InsertAfterLayerIndex)}mm)"; + + public bool IsAutoSortLayersByFileNameChecked + { + get => _isAutoSortLayersByFileNameChecked; + set => RaiseAndSetIfChanged(ref _isAutoSortLayersByFileNameChecked, value); + } + + public string InfoImportResult + { + get + { + if (Operation.Files.Count <= 0) return null; + uint modelTotalLayers = Operation.CalculateTotalLayers(App.SlicerFile.LayerCount); + string textFactor = "grow"; + if (modelTotalLayers < App.SlicerFile.LayerCount) + { + textFactor = "shrink"; + } + else if (modelTotalLayers == App.SlicerFile.LayerCount) + { + textFactor = "keep"; + } + return + $"{Operation.Files.Count} layers will be imported into model starting from layer {Operation.InsertAfterLayerIndex} {InfoLayerHeightStr}.\n" + + $"Model will {textFactor} from layers {App.SlicerFile.LayerCount} ({App.SlicerFile.TotalHeight}mm) to {modelTotalLayers} ({App.SlicerFile.GetHeightFromLayer(modelTotalLayers, false)}mm)"; + } + + } + + public StringTag SelectedFile + { + get => _selectedFile; + set + { + if(!RaiseAndSetIfChanged(ref _selectedFile, value)) return; + if (_selectedFile is null) + { + PreviewImage = null; + return; + } + PreviewImage = new Bitmap(_selectedFile.TagString); + } + } + + public Bitmap PreviewImage + { + get => _previewImage; + set => RaiseAndSetIfChanged(ref _previewImage, value); + } + + + public ToolLayerImportControl() + { + InitializeComponent(); + BaseOperation = Operation = new OperationLayerImport(App.SlicerFile.Resolution); + Operation.Files.CollectionChanged += (sender, args) => RefreshGUI(); + Operation.PropertyChanged += (sender, args) => RefreshGUI(); + FilesListBox = this.Find("FilesListBox"); + FilesListBox.DoubleTapped += (sender, args) => + { + if (!(FilesListBox.SelectedItem is StringTag file)) return; + App.StartProcess(file.TagString); + }; + FilesListBox.KeyUp += (sender, e) => + { + switch (e.Key) + { + case Key.Escape: + FilesListBox.SelectedItems.Clear(); + e.Handled = true; + break; + case Key.Delete: + RemoveFiles(); + e.Handled = true; + break; + case Key.A: + if ((e.KeyModifiers & KeyModifiers.Control) != 0) + { + FilesListBox.SelectAll(); + e.Handled = true; + } + break; + } + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public override async Task ValidateForm() + { + UpdateOperation(); + var message = Operation.Validate(); + if (message is null) return true; + + + message.Content += "\nDo you want to remove all invalid files from list?"; + if (await ParentWindow.MessageBoxQuestion(message.ToString()) == ButtonResult.Yes) + { + ConcurrentBag result = (ConcurrentBag)message.Tag; + foreach (var file in result) + { + Operation.Files.Remove(file); + } + } + + return false; + } + + public override void Callback(ToolWindow.Callbacks callback) + { + switch (callback) + { + case ToolWindow.Callbacks.Init: + ParentWindow.ButtonOkEnabled = false; + break; + } + } + + public void RefreshGUI() + { + RaisePropertyChanged(nameof(InfoLayerHeightStr)); + RaisePropertyChanged(nameof(InfoImportResult)); + ParentWindow.ButtonOkEnabled = Operation.Files.Count > 0; + } + + public async void AddFiles() + { + var dialog = new OpenFileDialog + { + AllowMultiple = true, + Filters = Helpers.ImagesFileFilter + }; + + var files = await dialog.ShowAsync(ParentWindow); + if (files is null || files.Length == 0) return; + foreach (var filename in files) + { + Operation.AddFile(filename); + } + + if (_isAutoSortLayersByFileNameChecked) + { + Operation.Sort(); + } + } + + public void RemoveFiles() + { + Operation.Files.RemoveMany(FilesListBox.SelectedItems.OfType()); + } + + public void ClearFiles() + { + Operation.Files.Clear(); + PreviewImage = null; + } + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolLayerRemoveControl.axaml b/UVtools.WPF/Controls/Tools/ToolLayerRemoveControl.axaml new file mode 100644 index 0000000..2185926 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerRemoveControl.axaml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolLayerRemoveControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerRemoveControl.axaml.cs new file mode 100644 index 0000000..e025811 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerRemoveControl.axaml.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Markup.Xaml; +using UVtools.Core.Operations; + +namespace UVtools.WPF.Controls.Tools +{ + public class ToolLayerRemoveControl : ToolControl + { + public OperationLayerRemove Operation { get; } + + public uint ExtraLayers => (uint)Math.Max(0, (int)Operation.LayerIndexEnd - Operation.LayerIndexStart + 1); + + public string InfoLayersStr + { + get + { + uint extraLayers = ExtraLayers; + return $"Layers: {App.SlicerFile.LayerCount} → {App.SlicerFile.LayerCount - extraLayers} (- {extraLayers})"; + } + } + + public string InfoHeightsStr + { + get + { + float extraHeight = (float)Math.Round(ExtraLayers * App.SlicerFile.LayerHeight, 2); + return $"Height: {App.SlicerFile.TotalHeight}mm → {Math.Round(App.SlicerFile.TotalHeight - extraHeight, 2)}mm (- {extraHeight}mm)"; + } + } + + public ToolLayerRemoveControl() + { + InitializeComponent(); + BaseOperation = Operation = new OperationLayerRemove(); + Operation.PropertyChanged += (sender, args) => + { + RaisePropertyChanged(nameof(InfoLayersStr)); + RaisePropertyChanged(nameof(InfoHeightsStr)); + }; + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UVtools.WPF/Controls/Tools/ToolMaskControl.axaml b/UVtools.WPF/Controls/Tools/ToolMaskControl.axaml new file mode 100644 index 0000000..da77c3c --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolMaskControl.axaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -918,10 +1183,36 @@ + + + + + + + + + @@ -944,9 +1235,11 @@