Files
UVtools/UVtools.WPF/MainWindow.LayerPreview.cs
T
Tiago Conceição 3f838001c7 v2.7.0
* **Core:**
   * Write "Unhandled Exceptions" to an "errors.log" file at user settings directory
   * Increase layer height max precision from 2 to 3 decimals
* **Settings - Layer Preview:**
   * Allow to set hollow line thickness to -1 to fill the area
   * Add tooltip for "Auto rotate on load": Auto rotate the layer preview on file load for a landscape viewport
   * Add option to masks outline color and thickness
   * Add option to clear ROI when adding masks
   * Add option "Auto flip on load": Auto flip the layer preview on file load if the file is marked to print mirrored on the printer LCD
* **Layer preview:**
   * Add selectable rotation directions 90º (CW and CCW)
   * Add preview flip (CTRL+F) horizontally and/or vertically
   * Add maskable regions to process on a layer (SHIFT + Alt + Click) on a area
   * ROI: Shortcut "Shift + left click" now also selects hollow black areas inside a white perimeter
   * ROI: Shortcut "ESC + Shift" to clear only the ROI and leave masks in
   * Fix a crash when using the pixel picker tool outside image bounds
* **Pixel editor:**
   * Change drawings brush diameter limit from 255 to 4000 maximum
   * When using layers below go lower than layer 0 it no longer apply the modifications
* **File formats:**
   * Add an internal GCodeBuilder to generate identical gcode within formats with auto convertion, managing features and parsing information
   * Internally rearrange formats properties and pass values to the base class
   * Fix "Save As" filename when using formats with dual extensions
   * CBDDLP and CTB: "LightPWM" was setting "BottomLightPWM" instead
   * CWS: Fix problem with filenames with dots (.) and ending with numbers (#171)
   * CWS: Improved the enconding and decoding performance
   * CWS: Implement all available print paramenters and per layer, missing values are got from gcode interpretation
   * CWS: Use set "light off delay" layer value instead of calculating it
   * CWS: Get light off delay per layer parsed from gcode
   * CWS - RGB flavour (Bene4 Mono): Warn about wrong resolution if width not multiples of 3 (#171)
   * ZCode: Allow to set Bottom and Light intensity (LightPWM) on paramters and per layer
   * ZCode: Allow to change bottom light pwm independent from normal light pwm
   * LGS: Light off and bottom light off was setting the value on the wrong units
   * UVJ: Unable to set per layer parameters
* **Issues:**
   * When computing islands and resin traps together, they will not compute in parallel anymore to prevent CPU and tasks exaustion, it will calculate islands first and then resin traps, this should also speed up the process on weaker machines
   * Gather resin trap areas together when computing for other issues to spare a decoding cycle latter
   * When using a threshold for islands detection it was also appling it to the overhangs
   * Fix the spare decoding conditional cycle for partial scans
   * Change resin trap search from parallel to sync to prevent fake detections and missing joints at cost of speed (#13)
* **Tools:**
   * Add layer selector: 'From first to current layer' and 'From current to last layer'
   * I printed this file: Multiplier - Number of time(s) the file has been printed. Half numbers can be used to consume from a failed print. Example: 0.5x if a print canceled at 50% progress
   * Pixel dimming: Increase wall thickness default from 5px to 10px
   * Import layers: Importing layers was not marking layers as modified, then the save file won't save the new images in, to prevent other similar bugs, all layers that got replaced will be auto marked as modified
2021-03-19 04:48:45 +00:00

1826 lines
75 KiB
C#

/*
* 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.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.CV.Util;
using UVtools.Core;
using UVtools.Core.Extensions;
using UVtools.Core.PixelEditor;
using UVtools.WPF.Controls;
using UVtools.WPF.Extensions;
using UVtools.WPF.Structures;
using Color = UVtools.WPF.Structures.Color;
using Helpers = UVtools.WPF.Controls.Helpers;
using Point = System.Drawing.Point;
using Size = System.Drawing.Size;
namespace UVtools.WPF
{
public partial class MainWindow
{
#region Enum
public enum ZoomToFitType : byte
{
Auto,
Image,
Volume,
Selection
};
#endregion
public AdvancedImageBox LayerImageBox { get; private set; }
public SliderEx LayerSlider;
public Panel LayerNavigationTooltipPanel;
public Border LayerNavigationTooltipBorder;
private Canvas _issuesSliderCanvas;
private Timer _layerNavigationTooltipTimer = new Timer(0.1) { AutoReset = false };
private uint _actualLayer;
private bool _showLayerImageRotated;
private bool _showLayerImageRotateCwDirection = true;
private bool _showLayerImageRotateCcwDirection;
private bool _showLayerImageFlipped;
private bool _showLayerImageFlippedHorizontally = true;
private bool _showLayerImageFlippedVertically;
private bool _showLayerImageDifference;
private bool _showLayerImageIssues = true;
private bool _showLayerImageCrosshairs = true;
private bool _isPixelEditorActive;
private bool _showLayerOutlinePrintVolumeBoundary;
private bool _showLayerOutlineLayerBoundary;
private bool _showLayerOutlineHollowAreas;
private bool _showLayerOutlineEdgeDetection;
private bool _isTooltipOverlayVisible;
private string _tooltipOverlayText;
private long _showLayerRenderMs;
public LayerCache LayerCache = new ();
private Point _lastPixelMouseLocation = Point.Empty;
private readonly List<Point[]> _maskPoints = new ();
public void InitLayerPreview()
{
LayerImageBox = this.FindControl<AdvancedImageBox>("LayerImage");
LayerSlider = this.FindControl<SliderEx>("Layer.Navigation.Slider");
LayerNavigationTooltipPanel = this.FindControl<Panel>("Layer.Navigation.Tooltip.Panel");
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) =>
{
if (e.PropertyName == nameof(LayerImageBox.Zoom))
{
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 (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();
}
return;
}
if (e.PropertyName == nameof(LayerImageBox.SelectionRegion))
{
RaisePropertyChanged(nameof(LayerROIStr));
}
};
LayerImageBox.PointerMoved += LayerImageBoxOnPointerMoved;
LayerImageBox.KeyUp += LayerImageBox_KeyUp;
LayerImageBox.PointerReleased += LayerImageBox_PointerReleased;
LayerImageBox.PointerPressed += LayerImageBoxOnPointerPressed;
LayerImageBox.DoubleTapped += LayerImageBoxOnDoubleTapped;
_issuesSliderCanvas.PointerWheelChanged += LayerSliderOnPointerWheelChanged;
LayerSlider.PointerWheelChanged += LayerSliderOnPointerWheelChanged;
//this.FindControl<Grid>("LayerNavigationSliderGrid").PointerWheelChanged += LayerSliderOnPointerWheelChanged;
_layerNavigationTooltipTimer.Elapsed += (sender, args) =>
{
Dispatcher.UIThread.InvokeAsync(() => RaisePropertyChanged(nameof(LayerNavigationTooltipMargin)));
};
}
private void LayerSliderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (e.Delta.Y > 0)
ActualLayer++;
else if (e.Delta.Y < 0 && _actualLayer > 0)
ActualLayer--;
}
public bool ShowLayerImageRotated
{
get => _showLayerImageRotated;
set
{
var rect = LayerImageBox.SelectionRegion;
if (!rect.IsEmpty)
{
rect = GetTransposedRectangle(rect.ToDotNet(), true).ToAvalonia();
}
if (!RaiseAndSetIfChanged(ref _showLayerImageRotated, value) || !IsFileLoaded) return;
if (!rect.IsEmpty)
{
LayerImageBox.SelectionRegion = GetTransposedRectangle(rect.ToDotNet()).ToAvalonia();
}
ZoomToFit();
ShowLayer();
}
}
public bool ShowLayerImageRotateCWDirection
{
get => _showLayerImageRotateCwDirection;
set
{
var rect = LayerImageBox.SelectionRegion;
if (!rect.IsEmpty)
{
rect = GetTransposedRectangle(rect.ToDotNet(), true).ToAvalonia();
}
if (!RaiseAndSetIfChanged(ref _showLayerImageRotateCwDirection, value)) return;
if (!_showLayerImageRotated) return;
if (!rect.IsEmpty)
{
LayerImageBox.SelectionRegion = GetTransposedRectangle(rect.ToDotNet()).ToAvalonia();
}
ZoomToFit();
ShowLayer();
}
}
public bool ShowLayerImageRotateCCWDirection
{
get => _showLayerImageRotateCcwDirection;
set
{
var rect = LayerImageBox.SelectionRegion;
if (!rect.IsEmpty)
{
rect = GetTransposedRectangle(rect.ToDotNet(), true).ToAvalonia();
}
if (!RaiseAndSetIfChanged(ref _showLayerImageRotateCcwDirection, value)) return;
if (!_showLayerImageRotated) return;
if (!rect.IsEmpty)
{
LayerImageBox.SelectionRegion = GetTransposedRectangle(rect.ToDotNet()).ToAvalonia();
}
ZoomToFit();
ShowLayer();
}
}
public bool ShowLayerImageFlipped
{
get => _showLayerImageFlipped;
set
{
var rect = LayerImageBox.SelectionRegion;
if (!rect.IsEmpty)
{
rect = GetTransposedRectangle(rect.ToDotNet(), true).ToAvalonia();
}
if (!RaiseAndSetIfChanged(ref _showLayerImageFlipped, value)) return;
if (!rect.IsEmpty)
{
LayerImageBox.SelectionRegion = GetTransposedRectangle(rect.ToDotNet()).ToAvalonia();
}
ShowLayer();
}
}
public bool ShowLayerImageFlippedHorizontally
{
get => _showLayerImageFlippedHorizontally;
set
{
var rect = LayerImageBox.SelectionRegion;
if (!rect.IsEmpty)
{
rect = GetTransposedRectangle(rect.ToDotNet(), true).ToAvalonia();
}
if (!RaiseAndSetIfChanged(ref _showLayerImageFlippedHorizontally, value)) return;
if (!_showLayerImageFlipped) return;
if (!rect.IsEmpty)
{
LayerImageBox.SelectionRegion = GetTransposedRectangle(rect.ToDotNet()).ToAvalonia();
}
ShowLayer();
}
}
public bool ShowLayerImageFlippedVertically
{
get => _showLayerImageFlippedVertically;
set
{
var rect = LayerImageBox.SelectionRegion;
if (!rect.IsEmpty)
{
rect = GetTransposedRectangle(rect.ToDotNet(), true).ToAvalonia();
}
if (!RaiseAndSetIfChanged(ref _showLayerImageFlippedVertically, value)) return;
if (!_showLayerImageFlipped) return;
if (!rect.IsEmpty)
{
LayerImageBox.SelectionRegion = GetTransposedRectangle(rect.ToDotNet()).ToAvalonia();
}
ShowLayer();
}
}
public bool ShowLayerImageDifference
{
get => _showLayerImageDifference;
set
{
if (!RaiseAndSetIfChanged(ref _showLayerImageDifference, value)) return;
ShowLayer();
}
}
public bool ShowLayerImageIssues
{
get => _showLayerImageIssues;
set
{
if (RaiseAndSetIfChanged(ref _showLayerImageIssues, value))
{
ShowLayer();
}
}
}
public bool ShowLayerImageCrosshairs
{
get => _showLayerImageCrosshairs;
set
{
if (!RaiseAndSetIfChanged(ref _showLayerImageCrosshairs, value)) return;
ShowLayer();
}
}
public bool ShowLayerOutlinePrintVolumeBoundary
{
get => _showLayerOutlinePrintVolumeBoundary;
set
{
if (!RaiseAndSetIfChanged(ref _showLayerOutlinePrintVolumeBoundary, value)) return;
ShowLayer();
}
}
public bool ShowLayerOutlineLayerBoundary
{
get => _showLayerOutlineLayerBoundary;
set
{
if (!RaiseAndSetIfChanged(ref _showLayerOutlineLayerBoundary, value)) return;
ShowLayer();
}
}
public bool ShowLayerOutlineHollowAreas
{
get => _showLayerOutlineHollowAreas;
set
{
if (!RaiseAndSetIfChanged(ref _showLayerOutlineHollowAreas, value)) return;
ShowLayer();
}
}
public bool ShowLayerOutlineEdgeDetection
{
get => _showLayerOutlineEdgeDetection;
set
{
if (!RaiseAndSetIfChanged(ref _showLayerOutlineEdgeDetection, value)) return;
ShowLayer();
}
}
public bool IsPixelEditorActive
{
get => _isPixelEditorActive;
set
{
if (!RaiseAndSetIfChanged(ref _isPixelEditorActive, value)) return;
if (_isPixelEditorActive)
{
SelectedTabItem = TabPixelEditor;
}
else
{
DrawModifications(true);
}
}
}
public string MinimumLayerString => SlicerFile is null ? "???" : $"{SlicerFile.LayerHeight}mm\n0";
public string MaximumLayerString => SlicerFile is null ? "???" : $"{SlicerFile.PrintHeight}mm\n{SlicerFile.LayerCount - 1}";
public string ActualLayerTooltip => SlicerFile is null ? "???" : $"{Layer.ShowHeight(LayerCache.Layer?.PositionZ ?? 0)}mm\n" +
$"{ActualLayer}\n" +
$"{(ActualLayer + 1) * 100 / SlicerFile.LayerCount}%";
public uint SliderMaximumValue => SlicerFile?.LastLayerIndex ?? 0;
public bool CanGoUp => _actualLayer < SliderMaximumValue;
public bool CanGoDown => _actualLayer > 0;
public bool IsTooltipOverlayVisible
{
get => _isTooltipOverlayVisible;
set => RaiseAndSetIfChanged(ref _isTooltipOverlayVisible, value);
}
public string TooltipOverlayText
{
get => _tooltipOverlayText;
set => RaiseAndSetIfChanged(ref _tooltipOverlayText, value);
}
public string LayerPixelCountStr
{
get
{
if (!LayerCache.IsCached) return "Pixels: 0";
var pixelPercent = Math.Round(LayerCache.Layer.NonZeroPixelCount * 100.0 / (SlicerFile.ResolutionX * SlicerFile.ResolutionY), 2);
string text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)";
var exposedMillimeters = LayerCache.Layer.ExposureMillimeters;
if (exposedMillimeters > 0)
{
text += $"\nMillimeters: {exposedMillimeters}";
}
return text;
}
}
public string LayerBoundsStr
{
get
{
if (LayerCache.Layer is null) return "Bounds: NS";
var text = $"Bounds: {LayerCache.Layer.BoundingRectangle} ({LayerCache.Layer.BoundingRectangle.Area()}px²)";
var rectMillimeters = LayerCache.Layer.BoundingRectangleMillimeters;
if (!rectMillimeters.IsEmpty)
{
text += $"\nBounds: {rectMillimeters} ({rectMillimeters.Area(2)}mm²)";
}
return text;
}
}
public string LayerROIStr
{
get
{
var roi = ROI;
if (roi.IsEmpty)
{
return _maskPoints.Count > 0 ? $"Masks: {_maskPoints.Count}" : "ROI: NS";
}
var text = $"ROI: {roi} ({roi.Area()}px²)";
var roiMillimeters = ROIMillimeters;
if (!roiMillimeters.IsEmpty)
{
text += $"\nROI: {roiMillimeters} ({roiMillimeters.Area(2)}mm²)";
}
return text;
}
}
public long ShowLayerRenderMs
{
get => _showLayerRenderMs;
set => RaiseAndSetIfChanged(ref _showLayerRenderMs, value);
}
public PixelPicker LayerPixelPicker { get; } = new ();
public string LayerZoomStr => $"{LayerImageBox.Zoom / 100m}x" +
(AppSettings.LockedZoomLevel == LayerImageBox.Zoom ? " 🔒" : string.Empty);
public string LayerResolutionStr
{
get
{
if (SlicerFile is null) return "Unloaded";
var text = $"{SlicerFile.Resolution} px";
var display = SlicerFile.Display;
if (!display.IsEmpty)
{
text += $"\n{SlicerFile.Display} mm";
}
return text;
}
}
public uint ActualLayer
{
get => _actualLayer;
set
{
if (DataContext is null) return;
if (!RaiseAndSetIfChanged(ref _actualLayer, value)) return;
ShowLayer();
InvalidateLayerNavigation();
}
}
public void ForceUpdateActualLayer(uint layerIndex = 0)
{
//_actualLayer = layerIndex;
/*ShowLayer();
InvalidateLayerNavigation();
RaisePropertyChanged(nameof(ActualLayer));*/
_actualLayer = uint.MaxValue;
ActualLayer = layerIndex;
}
public void InvalidateLayerNavigation()
{
RaisePropertyChanged(nameof(CanGoDown));
RaisePropertyChanged(nameof(CanGoUp));
RaisePropertyChanged(nameof(ActualLayerTooltip));
//RaisePropertyChanged(nameof(LayerNavigationTooltipMargin));
RaisePropertyChanged(nameof(LayerPixelCountStr));
RaisePropertyChanged(nameof(LayerBoundsStr));
_layerNavigationTooltipTimer.Start();
}
public Thickness LayerNavigationTooltipMargin
{
get
{
double top = 0;
if (LayerSlider?.Track != null)
{
double trackerPos = LayerSlider.Track.Thumb.Bounds.Height / 2 + LayerSlider.Track.Thumb.Bounds.Top;
double halfTooltipHeight = LayerNavigationTooltipBorder.Bounds.Height / 2;
top = (trackerPos - halfTooltipHeight).Clamp(0,
LayerSlider.Bounds.Height - LayerNavigationTooltipBorder.Bounds.Height);
}
return new Thickness(
0,
top,
5,
0);
}
}
#region ROI & Mask
public Rectangle ROI
{
get
{
var rect = LayerImageBox.SelectionRegion;
return rect.IsEmpty ? Rectangle.Empty : GetTransposedRectangle(rect.ToDotNet(), true);
}
set => LayerImageBox.SelectionRegion = value.ToAvalonia();
}
public RectangleF ROIMillimeters
{
get
{
var roi = ROI;
var pixelSize = SlicerFile.PixelSize;
if(roi.IsEmpty || pixelSize.IsEmpty) return RectangleF.Empty;
return new RectangleF(
(float)Math.Round(roi.X * pixelSize.Width, 2),
(float)Math.Round(roi.Y * pixelSize.Height, 2),
(float)Math.Round(roi.Width * pixelSize.Width, 2),
(float)Math.Round(roi.Height * pixelSize.Height, 2));
}
}
public List<Point[]> MaskPoints => _maskPoints;
/*private set
{
if(!RaiseAndSetIfChanged(ref _maskPoints, value)) return;
ShowLayer();
}*/
public void AddMaskPoints(Point[] points)
{
if (_maskPoints.RemoveAll(points1 => points1.SequenceEqual(points)) <= 0)
{
_maskPoints.Add(points);
}
if(_maskPoints.Count > 0 && Settings.LayerPreview.MaskClearROIAfterSet) ClearROI();
ShowLayer();
RaisePropertyChanged(nameof(LayerROIStr));
}
public void AddMaskPoints(Point[][] points)
{
_maskPoints.Clear();
_maskPoints.AddRange(points);
ShowLayer();
RaisePropertyChanged(nameof(LayerROIStr));
}
public void ClearMask()
{
if (_maskPoints.Count <= 0) return;
_maskPoints.Clear();
ShowLayer();
RaisePropertyChanged(nameof(LayerROIStr));
}
public void ClearROI()
{
ROI = Rectangle.Empty;
}
public void ClearROIAndMask()
{
ClearROI();
ClearMask();
}
public void OnROIClick()
{
ZoomToFit(ZoomToFitType.Selection);
}
#endregion
public void GoFirstLayer()
{
if (SlicerFile is null) return;
if (!CanGoDown) return;
ActualLayer = 0;
}
public void GoPreviousLayer()
{
if (SlicerFile is null) return;
if (!CanGoDown) return;
ActualLayer--;
}
public void GoNextLayer()
{
if (SlicerFile is null) return;
if (!CanGoUp) return;
ActualLayer++;
}
public void GoLastLayer()
{
if (SlicerFile is null) return;
if (!CanGoUp) return;
ActualLayer = SliderMaximumValue;
}
public void RefreshLayerImage()
{
LayerImageBox.Image = LayerCache.ImageBgr.ToBitmap();
}
/// <summary>
/// Shows a layer number
/// </summary>
unsafe void ShowLayer()
{
if (!IsFileLoaded) return;
var sanitizedLayerIndex = Math.Min(_actualLayer, SlicerFile.LastLayerIndex);
if (sanitizedLayerIndex != _actualLayer)
{
_actualLayer = sanitizedLayerIndex;
InvalidateLayerNavigation();
}
Stopwatch watch = Stopwatch.StartNew();
LayerCache.Layer = SlicerFile[_actualLayer];
try
{
//var imageSpan = LayerCache.Image.GetPixelSpan<byte>();
//var imageBgrSpan = LayerCache.ImageBgr.GetPixelSpan<byte>();
var imageSpan = LayerCache.ImageSpan;
var imageBgrSpan = LayerCache.ImageBgrSpan;
if (_showLayerOutlineEdgeDetection)
{
using var canny = new Mat();
CvInvoke.Canny(LayerCache.Image, canny, 80, 40, 3, true);
CvInvoke.CvtColor(canny, LayerCache.ImageBgr, ColorConversion.Gray2Bgra);
}
else if (_showLayerImageDifference)
{
if (_actualLayer > 0 && _actualLayer < SlicerFile.LayerCount - 1)
{
Mat previousImage = null;
Mat nextImage = null;
// Can improve performance on >4K images?
Parallel.Invoke(
() => { previousImage = SlicerFile[_actualLayer - 1].LayerMat; },
() => { nextImage = SlicerFile[_actualLayer + 1].LayerMat; });
/*using (var previousImage = SlicerFile[_actualLayer - 1].LayerMat)
using (var nextImage = SlicerFile[_actualLayer + 1].LayerMat)
{*/
//var previousSpan = previousImage.GetPixelSpan<byte>();
//var nextSpan = nextImage.GetPixelSpan<byte>();
var previousSpan = previousImage.GetBytePointer();
var nextSpan = nextImage.GetBytePointer();
int width = LayerCache.Image.Width;
int channels = LayerCache.ImageBgr.NumberOfChannels;
Parallel.For(0, LayerCache.Image.Height, y =>
{
for (int x = 0; x < width; x++)
{
int pixel = y * width + x;
if (imageSpan[pixel] != 0) continue;
Color color = Color.Empty;
if (previousSpan[pixel] > 0 && nextSpan[pixel] > 0)
{
color = Settings.LayerPreview.NextLayerDifferenceColor;
}
else if (previousSpan[pixel] > 0)
{
color = Settings.LayerPreview.PreviousLayerDifferenceColor;
}
else if (nextSpan[pixel] > 0)
{
color = Settings.LayerPreview.NextLayerDifferenceColor;
}
if (color.IsEmpty) continue;
var bgrPixel = pixel * channels;
imageBgrSpan[bgrPixel] = color.B; // B
imageBgrSpan[++bgrPixel] = color.G; // G
imageBgrSpan[++bgrPixel] = color.R; // R
//imageBgrSpan[++bgrPixel] = color.A; // A
}
});
previousImage.Dispose();
nextImage.Dispose();
}
}
var selectedIssues = IssuesGrid.SelectedItems;
if (_showLayerImageIssues && Issues.Count > 0)
{
foreach (var issue in Issues)
{
if (issue.LayerIndex != ActualLayer) continue;
if (!issue.HaveValidPoint) continue;
Color color = Color.Empty;
if (issue.Type == LayerIssue.IssueType.ResinTrap)
{
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue)
? Settings.LayerPreview.ResinTrapHighlightColor
: Settings.LayerPreview.ResinTrapColor;
using (var vec = new VectorOfVectorOfPoint(new VectorOfPoint(issue.Pixels)))
{
CvInvoke.DrawContours(LayerCache.ImageBgr, vec, -1,
new MCvScalar(color.B, color.G, color.R), -1);
}
if (_showLayerImageCrosshairs &&
!Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues &&
LayerImageBox.Zoom <= AppSettings.CrosshairFadeLevel)
{
DrawCrosshair(issue.BoundingRectangle);
}
continue;
}
switch (issue.Type)
{
case LayerIssue.IssueType.Island:
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue)
? Settings.LayerPreview.IslandHighlightColor
: Settings.LayerPreview.IslandColor;
if (_showLayerImageCrosshairs &&
!Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues &&
LayerImageBox.Zoom <= AppSettings.CrosshairFadeLevel)
{
DrawCrosshair(issue.BoundingRectangle);
}
break;
case LayerIssue.IssueType.Overhang:
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue)
? Settings.LayerPreview.OverhangHighlightColor
: Settings.LayerPreview.OverhangColor;
if (_showLayerImageCrosshairs &&
!Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues &&
LayerImageBox.Zoom <= AppSettings.CrosshairFadeLevel)
{
DrawCrosshair(issue.BoundingRectangle);
}
break;
case LayerIssue.IssueType.TouchingBound:
color = Settings.LayerPreview.TouchingBoundsColor;
break;
}
if (color.IsEmpty) continue;
foreach (var pixel in issue)
{
int pixelPos = LayerCache.Image.GetPixelPos(pixel);
byte brightness = imageSpan[pixelPos];
if (brightness == 0) continue;
int pixelBgrPos = pixelPos * LayerCache.ImageBgr.NumberOfChannels;
var newColor = color.FactorColor(brightness, 80);
imageBgrSpan[pixelBgrPos] = newColor.B; // B
imageBgrSpan[pixelBgrPos + 1] = newColor.G; // G
imageBgrSpan[pixelBgrPos + 2] = newColor.R; // R
}
}
}
if (_showLayerOutlinePrintVolumeBoundary)
{
CvInvoke.Rectangle(LayerCache.ImageBgr, SlicerFile.LayerManager.BoundingRectangle,
new MCvScalar(Settings.LayerPreview.VolumeBoundsOutlineColor.B,
Settings.LayerPreview.VolumeBoundsOutlineColor.G,
Settings.LayerPreview.VolumeBoundsOutlineColor.R),
Settings.LayerPreview.VolumeBoundsOutlineThickness);
}
if (_showLayerOutlineLayerBoundary && !SlicerFile[_actualLayer].BoundingRectangle.IsEmpty)
{
CvInvoke.Rectangle(LayerCache.ImageBgr, SlicerFile[_actualLayer].BoundingRectangle,
new MCvScalar(Settings.LayerPreview.LayerBoundsOutlineColor.B,
Settings.LayerPreview.LayerBoundsOutlineColor.G, Settings.LayerPreview.LayerBoundsOutlineColor.R),
Settings.LayerPreview.LayerBoundsOutlineThickness);
}
if (_showLayerOutlineHollowAreas)
{
//CvInvoke.Threshold(ActualLayerImage, grayscale, 1, 255, ThresholdType.Binary);
/*
* hierarchy[i][0]: the index of the next contour of the same level
* hierarchy[i][1]: the index of the previous contour of the same level
* hierarchy[i][2]: the index of the first child
* hierarchy[i][3]: the index of the parent
*/
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
{
if ((int)LayerCache.LayerHierarchyJagged.GetValue(0, i, 2) == -1 &&
(int)LayerCache.LayerHierarchyJagged.GetValue(0, i, 3) != -1)
{
//var r = CvInvoke.BoundingRectangle(contours[i]);
//CvInvoke.Rectangle(ActualLayerImageBgr, r, new MCvScalar(0, 0, 255), 2);
CvInvoke.DrawContours(LayerCache.ImageBgr, LayerCache.LayerContours, i,
new MCvScalar(Settings.LayerPreview.HollowOutlineColor.B,
Settings.LayerPreview.HollowOutlineColor.G,
Settings.LayerPreview.HollowOutlineColor.R),
Settings.LayerPreview.HollowOutlineLineThickness);
}
}
}
if (_maskPoints is not null && _maskPoints.Count > 0)
{
using var vec = new VectorOfVectorOfPoint(_maskPoints.ToArray());
CvInvoke.DrawContours(LayerCache.ImageBgr, vec, -1,
new MCvScalar(Settings.LayerPreview.MaskOutlineColor.B,
Settings.LayerPreview.MaskOutlineColor.G,
Settings.LayerPreview.MaskOutlineColor.R),
Settings.LayerPreview.MaskOutlineLineThickness);
}
for (var index = 0; index < Drawings.Count; index++)
{
if (Drawings[index].LayerIndex != ActualLayer) continue;
var operation = Drawings[index];
if (operation.OperationType == PixelOperation.PixelOperationType.Drawing)
{
var operationDrawing = (PixelDrawing) operation;
var color = operationDrawing.IsAdd
? (DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.AddPixelHighlightColor
: Settings.PixelEditor.AddPixelColor)
: (DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.RemovePixelHighlightColor
: Settings.PixelEditor.RemovePixelColor);
if (operationDrawing.BrushSize == 1)
{
LayerCache.ImageBgr.SetByte(operation.Location.X, operation.Location.Y,
new[] {color.B, color.G, color.R});
continue;
}
switch (operationDrawing.BrushShape)
{
case PixelDrawing.BrushShapeType.Rectangle:
CvInvoke.Rectangle(LayerCache.ImageBgr, operationDrawing.Rectangle,
new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness,
operationDrawing.LineType);
break;
case PixelDrawing.BrushShapeType.Circle:
CvInvoke.Circle(LayerCache.ImageBgr, operation.Location, operationDrawing.BrushSize / 2,
new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness,
operationDrawing.LineType);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
else if (operation.OperationType == PixelOperation.PixelOperationType.Text)
{
var operationText = (PixelText) operation;
var color = operationText.IsAdd
? (DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.AddPixelHighlightColor
: Settings.PixelEditor.AddPixelColor)
: (DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.RemovePixelHighlightColor
: Settings.PixelEditor.RemovePixelColor);
CvInvoke.PutText(LayerCache.ImageBgr, operationText.Text, operationText.Location,
operationText.Font, operationText.FontScale, new MCvScalar(color.B, color.G, color.R),
operationText.Thickness, operationText.LineType, operationText.Mirror);
}
else if (operation.OperationType == PixelOperation.PixelOperationType.Eraser)
{
//var pixelBrightness = LayerCache.Image.GetPixelPos(operation.Location);
if (imageSpan[LayerCache.Image.GetPixelPos(operation.Location)] < 10) continue;
var color = DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.RemovePixelHighlightColor
: Settings.PixelEditor.RemovePixelColor;
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
{
/*if (pixelBrightness < 10 && (int) LayerCache.LayerHierarchyJagged.GetValue(0, i, 2) != -1 ||
(int) LayerCache.LayerHierarchyJagged.GetValue(0, i, 3) == -1)
{
continue;
}*/
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], operation.Location, false) >= 0)
{
CvInvoke.DrawContours(LayerCache.ImageBgr, LayerCache.LayerContours, i,
new MCvScalar(color.B, color.G, color.R), -1);
break;
}
}
}
else if (operation.OperationType == PixelOperation.PixelOperationType.Supports)
{
var operationSupport = (PixelSupport) operation;
var color = DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.SupportsHighlightColor
: Settings.PixelEditor.SupportsColor;
CvInvoke.Circle(LayerCache.ImageBgr, operation.Location, operationSupport.TipDiameter / 2,
new MCvScalar(color.B, color.G, color.R), -1);
}
else if (operation.OperationType == PixelOperation.PixelOperationType.DrainHole)
{
var operationDrainHole = (PixelDrainHole) operation;
var color = DrawingsGrid.SelectedItems.Contains(operation)
? Settings.PixelEditor.DrainHoleHighlightColor
: Settings.PixelEditor.DrainHoleColor;
CvInvoke.Circle(LayerCache.ImageBgr, operation.Location, operationDrainHole.Diameter / 2,
new MCvScalar(color.B, color.G, color.R), -1);
}
}
// Show crosshairs for selected issues if crosshair mode is enabled via toolstrip button.
// Even when enabled, crosshairs are hidden in pixel edit mode when SHIFT is pressed.
if (_showLayerImageCrosshairs &&
Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues &&
Issues.Count > 0 &&
IssuesGrid.SelectedItems.Count > 0 &&
LayerImageBox.Zoom <=
AppSettings.CrosshairFadeLevel && // Only draw crosshairs when zoom level is below the configurable crosshair fade threshold.
!_isPixelEditorActive)
{
foreach (LayerIssue issue in selectedIssues)
{
// Don't render crosshairs for selected issue that are not on the current layer, or for
// issue types that don't have a specific location or bounds.
if (issue.LayerIndex != ActualLayer || issue.Type == LayerIssue.IssueType.EmptyLayer
|| issue.Type == LayerIssue.IssueType.TouchingBound)
continue;
DrawCrosshair(issue.BoundingRectangle);
}
}
if (_showLayerImageFlipped)
{
var flipType = FlipType.None;
if (_showLayerImageFlippedHorizontally)
flipType |= FlipType.Horizontal;
if (_showLayerImageFlippedVertically)
flipType |= FlipType.Vertical;
if (flipType != FlipType.None)
CvInvoke.Flip(LayerCache.ImageBgr, LayerCache.ImageBgr, flipType);
}
if (_showLayerImageRotated)
{
CvInvoke.Rotate(LayerCache.ImageBgr, LayerCache.ImageBgr, _showLayerImageRotateCcwDirection ? RotateFlags.Rotate90CounterClockwise : RotateFlags.Rotate90Clockwise);
}
LayerImageBox.Image = LayerCache.Bitmap = LayerCache.ImageBgr.ToBitmap();
RefreshCurrentLayerData();
watch.Stop();
ShowLayerRenderMs = watch.ElapsedMilliseconds;
AddLogVerbose($"Show Layer: {_actualLayer}", watch.Elapsed.TotalSeconds);
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
/// <summary>
/// Draw a crosshair around a rectangle
/// </summary>
/// <param name="rect"></param>
public void DrawCrosshair(Rectangle rect)
{
// Gradually increase line thickness from 1 to 3 at the lower-end of the zoom range.
// This prevents the crosshair lines from disappearing due to being too thin to
// render at very low zoom factors.
var lineThickness = (LayerImageBox.Zoom > 100) ? 1 : (LayerImageBox.Zoom < 50) ? 3 : 2;
var color = new MCvScalar(Settings.LayerPreview.CrosshairColor.B, Settings.LayerPreview.CrosshairColor.G,
Settings.LayerPreview.CrosshairColor.R);
// LEFT
var startPoint = new System.Drawing.Point(Math.Max(0, rect.X - Settings.LayerPreview.CrosshairMargin - 1),
rect.Y + rect.Height / 2);
var endPoint =
new System.Drawing.Point(
Settings.LayerPreview.CrosshairLength == 0
? 0
: (int)Math.Max(0, startPoint.X - Settings.LayerPreview.CrosshairLength + 1),
startPoint.Y);
CvInvoke.Line(LayerCache.ImageBgr,
startPoint,
endPoint,
color,
lineThickness);
// RIGHT
startPoint.X = Math.Min(LayerCache.ImageBgr.Width,
rect.Right + Settings.LayerPreview.CrosshairMargin);
endPoint.X = Settings.LayerPreview.CrosshairLength == 0
? LayerCache.ImageBgr.Width
: (int)Math.Min(LayerCache.ImageBgr.Width, startPoint.X + Settings.LayerPreview.CrosshairLength - 1);
CvInvoke.Line(LayerCache.ImageBgr,
startPoint,
endPoint,
color,
lineThickness);
// TOP
startPoint = new System.Drawing.Point(rect.X + rect.Width / 2,
Math.Max(0, rect.Y - Settings.LayerPreview.CrosshairMargin - 1));
endPoint = new System.Drawing.Point(startPoint.X,
(int)(Settings.LayerPreview.CrosshairLength == 0
? 0
: Math.Max(0, startPoint.Y - Settings.LayerPreview.CrosshairLength + 1)));
CvInvoke.Line(LayerCache.ImageBgr,
startPoint,
endPoint,
color,
lineThickness);
// Bottom
startPoint.Y = Math.Min(LayerCache.ImageBgr.Height, rect.Bottom + Settings.LayerPreview.CrosshairMargin);
endPoint.Y = Settings.LayerPreview.CrosshairLength == 0
? LayerCache.ImageBgr.Height
: (int)Math.Min(LayerCache.ImageBgr.Height, startPoint.Y + Settings.LayerPreview.CrosshairLength - 1);
CvInvoke.Line(LayerCache.ImageBgr,
startPoint,
endPoint,
color,
lineThickness);
}
public Point GetTransposedPoint(Point point, bool inverse = false)
{
if (point.IsEmpty) return point;
void Flip()
{
if (!_showLayerImageFlipped) return;
if (_showLayerImageFlippedHorizontally)
{
point = new Point(LayerCache.Image.Width - 1 - point.X, point.Y);
}
if (_showLayerImageFlippedVertically)
{
point = new Point(point.X, LayerCache.Image.Height - 1 - point.Y);
}
}
void Rotate()
{
if (!_showLayerImageRotated) return;
if (_showLayerImageRotateCcwDirection)
{
point = inverse
? new Point(point.Y, LayerCache.Image.Width - 1 - point.X) // 90º CCW
: new Point(LayerCache.Image.Width - 1 - point.Y, point.X); // 90º CW
}
else
{
point = inverse
? new Point(LayerCache.Image.Height - 1 - point.Y, point.X) // 90º CW
: new Point(point.Y, LayerCache.Image.Height - 1 - point.X); // 90º CCW
}
}
if (inverse)
{
Flip();
Rotate();
}
else
{
Rotate();
Flip();
}
return point;
}
public Rectangle GetTransposedRectangle(RectangleF rectangleF, bool inverse = true) =>
GetTransposedRectangle(Rectangle.Round(rectangleF), inverse);
public Rectangle GetTransposedRectangle(Rectangle rectangle, bool inverse = false)
{
if (rectangle.IsEmpty) return rectangle;
void Flip()
{
if (!_showLayerImageFlipped) return;
if (_showLayerImageFlippedHorizontally)
{
rectangle.Location = new Point(LayerCache.Image.Width - 1 - rectangle.Right, rectangle.Y);
}
if (_showLayerImageFlippedVertically)
{
rectangle.Location = new Point(rectangle.X, LayerCache.Image.Height - 1 - rectangle.Bottom);
}
}
void Rotate()
{
if (!_showLayerImageRotated) return;
if (_showLayerImageRotateCcwDirection)
{
rectangle = !inverse
? new Rectangle(rectangle.Y, LayerCache.Image.Width - rectangle.Right, rectangle.Height, rectangle.Width) // 90º CCW
: new Rectangle(LayerCache.Image.Width - rectangle.Bottom, rectangle.X, rectangle.Height, rectangle.Width); // 90º CW
}
else
{
rectangle = !inverse
? new Rectangle(LayerCache.Image.Height - rectangle.Bottom, rectangle.X, rectangle.Height, rectangle.Width) // 90º CW
: new Rectangle(rectangle.Y, LayerCache.Image.Height - rectangle.Right, rectangle.Height, rectangle.Width); // 90º CCW
}
}
if (!inverse)
{
Flip();
Rotate();
}
else
{
Rotate();
Flip();
}
return rectangle;
/*return inverse
? new Rectangle(LayerCache.Image.Height - rectangle.Bottom,
rectangle.Left, rectangle.Height, rectangle.Width)
//: new Rectangle(ActualLayerImage.Width - rectangle.Bottom, rectangle.Left, rectangle.Width, rectangle.Height);
//: new Rectangle(ActualLayerImage.Width - rectangle.Bottom, ActualLayerImage.Height-rectangle.Right, rectangle.Width, rectangle.Height); // Rotate90FlipX: // = Rotate270FlipY
//: new Rectangle(rectangle.Top, rectangle.Left, rectangle.Width, rectangle.Height); // Rotate270FlipX: // = Rotate90FlipY
: new Rectangle(rectangle.Top, LayerCache.Image.Height - rectangle.Right, rectangle.Height, rectangle.Width); // Rotate90FlipNone: // = Rotate270FlipXY*/
}
/// <summary>
/// Gets the bounding rectangle of the passed issue, automatically adjusting
/// the coordinates and width/height to account for whether or not the layer
/// preview image is rotated. Used to ensure images are properly zoomed or
/// centered independent of the layer preview rotation.
/// </summary>
private Rectangle GetTransposedIssueBounds(LayerIssue issue)
{
if (issue.X >= 0 && issue.Y >= 0 && (issue.BoundingRectangle.IsEmpty || issue.Size == 1))
return new Rectangle(GetTransposedPoint(issue.FirstPoint, true), new Size(1, 1));
//return new Rectangle(LayerCache.Image.Height - 1 - issue.Y, issue.X, 1, 1);
return GetTransposedRectangle(issue.BoundingRectangle);
}
/// <summary>
/// Centers layer view on a X,Y coordinate
/// </summary>
/// <param name="x">X coordinate</param>
/// <param name="y">X coordinate</param>
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
public void CenterLayerAt(double x, double y, int zoomLevel = 0)
{
if (zoomLevel < 0) zoomLevel = AppSettings.LockedZoomLevel;
if (zoomLevel > 0) LayerImageBox.Zoom = zoomLevel;
LayerImageBox.CenterAt(x, y);
}
/// <summary>
/// Centers layer view on a X,Y coordinate
/// </summary>
/// <param name="x">X coordinate</param>
/// <param name="y">X coordinate</param>
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
public void CenterLayerAt(int x, int y, int zoomLevel = 0)
{
if (zoomLevel < 0) zoomLevel = AppSettings.LockedZoomLevel;
if (zoomLevel > 0) LayerImageBox.Zoom = zoomLevel;
LayerImageBox.CenterAt(x, y);
}
public void CenterLayerAt(Rectangle rectangle, int zoomLevel = 0, bool zoomToRegion = false)
{
var viewPort = LayerImageBox.GetSourceImageRegion();
if (zoomToRegion ||
rectangle.Width * AppSettings.LockedZoomLevel / LayerImageBox.Zoom > viewPort.Width ||
rectangle.Height * AppSettings.LockedZoomLevel / LayerImageBox.Zoom > viewPort.Height)
{
Debug.WriteLine("zoom to region");
//SupressLayerZoomEvent = true;
LayerImageBox.ZoomToRegion(rectangle, 10);
//SupressLayerZoomEvent = false;
//LayerImageBox.ZoomOut(true);
return;
}
Debug.WriteLine($"Center at {zoomLevel}");
CenterLayerAt(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2, zoomLevel);
}
/// <summary>
/// Centers layer view on a <see cref="Point"/>
/// </summary>
/// <param name="point">Point holding X and Y coordinates</param>
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
public void CenterLayerAt(Point point, int zoomLevel = 0) => CenterLayerAt(point.X, point.Y, zoomLevel);
/// <summary>
/// Zoom the layer preview to the passed issue, or if appropriate for issue type,
/// Zoom to fit the plate or print bounds.
/// </summary>
private void ZoomToIssue(LayerIssue issue)
{
if (issue.Type == LayerIssue.IssueType.TouchingBound || issue.Type == LayerIssue.IssueType.EmptyLayer ||
(issue.X == -1 && issue.Y == -1))
{
ZoomToFit();
return;
}
if (issue.X >= 0 && issue.Y >= 0)
{
// Check to see if this zoom action will cross the crosshair fade threshold
/*if (tsLayerImageShowCrosshairs.Checked && !ReferenceEquals(Issues, null) && flvIssues.SelectedIndices.Count > 0
&& pbLayer.Zoom <= CrosshairFadeLevel && LockedZoomLevel > CrosshairFadeLevel)
{
// Refresh the preview without the crosshairs before zooming-in.
// Prevents zoomed-in crosshairs from breifly being displayed before
// the Layer Preview is refreshed post-zoom.
tsLayerImageShowCrosshairs.Checked = false;
ShowLayer();
tsLayerImageShowCrosshairs.Checked = true;
}*/
CenterLayerAt(GetTransposedIssueBounds(issue), AppSettings.LockedZoomLevel);
}
}
/// <summary>
/// Center the layer preview on the passed issue, or if appropriate for issue type,
/// Zoom to fit the plate or print bounds.
/// </summary>
private void CenterAtIssue(LayerIssue issue)
{
if (issue.Type == LayerIssue.IssueType.TouchingBound || issue.Type == LayerIssue.IssueType.EmptyLayer ||
(issue.X == -1 && issue.Y == -1))
{
ZoomToFit();
}
if (issue.X >= 0 && issue.Y >= 0)
{
CenterLayerAt(GetTransposedIssueBounds(issue));
}
}
public void ZoomToFitSimple()
{
ZoomToFit(ZoomToFitType.Image);
}
public void ZoomToFitPrintVolume()
{
ZoomToFit(ZoomToFitType.Volume);
}
private void ZoomToFit(ZoomToFitType fitType = ZoomToFitType.Auto)
{
if (!IsFileLoaded) return;
const byte margin = 10;
// If ALT key is pressed when ZoomToFit is performed, the configured option for
// zoom to plate vs. zoom to print bounds will be inverted.
switch (fitType)
{
case ZoomToFitType.Auto:
if (Settings.LayerPreview.ZoomToFitPrintVolumeBounds ^ (_globalModifiers & KeyModifiers.Alt) != 0)
{
/*if (!_showLayerImageRotated)
{
LayerImageBox.ZoomToRegion(SlicerFile.LayerManager.BoundingRectangle, margin);
}
else
{
LayerImageBox.ZoomToRegion(LayerCache.Image.Height - 1 - SlicerFile.LayerManager.BoundingRectangle.Bottom,
SlicerFile.LayerManager.BoundingRectangle.X,
SlicerFile.LayerManager.BoundingRectangle.Height,
SlicerFile.LayerManager.BoundingRectangle.Width, margin
);
}*/
LayerImageBox.ZoomToRegion(GetTransposedRectangle(SlicerFile.BoundingRectangle), margin);
}
else
{
LayerImageBox.ZoomToFit();
}
break;
case ZoomToFitType.Image:
LayerImageBox.ZoomToFit();
break;
case ZoomToFitType.Volume:
LayerImageBox.ZoomToRegion(GetTransposedRectangle(SlicerFile.BoundingRectangle), margin);
break;
case ZoomToFitType.Selection:
LayerImageBox.ZoomToSelectionRegion(margin);
break;
default:
throw new ArgumentOutOfRangeException(nameof(fitType), fitType, null);
}
}
/// <summary>
/// If there is an issue under the point location passed, that issue will be selected and
/// scrolled into view on the IssueList.
/// </summary>
private void SelectIssueAtPoint(Point location)
{
//location = GetTransposedPoint(location);
// If location clicked is within an issue, activate it.
for (var i = 0; i < Issues.Count; i++)
{
LayerIssue issue = Issues[i];
if (issue.LayerIndex != ActualLayer) continue;
if (!GetTransposedIssueBounds(issue).Contains(location)) continue;
IssueSelectedIndex = i;
SelectedTabItem = TabIssues;
break;
}
}
private void LayerImageBox_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
var pointer = e.GetCurrentPoint(LayerImageBox);
if (!LayerImageBox.IsPointInImage(pointer.Position)) return;
Point location = LayerImageBox.PointToImage(pointer.Position).ToDotNet();
if (LayerImageBox.SelectionMode == AdvancedImageBox.SelectionModes.Rectangle)
{
if (e.InitialPressMouseButton == MouseButton.Left)
{
if ((e.KeyModifiers & KeyModifiers.Alt) != 0)
{
if (SelectObjectRoi(ROI) == 0) SelectObjectRoi(location);
return;
}
return;
}
if (e.InitialPressMouseButton == MouseButton.Right)
{
if (!LayerImageBox.IsPointInImage(pointer.Position)) return;
if ((e.KeyModifiers & KeyModifiers.Alt) != 0)
{
SelectObjectMask(location);
return;
}
SelectObjectRoi(location);
return;
}
return;
}
if ((e.KeyModifiers & KeyModifiers.Control) != 0)
{
// Check to see if the clicked location is an issue,
// and if so, select it in the ListView.
SelectIssueAtPoint(location);
return;
}
// Shift must be pressed for any pixel edit action, middle button is ignored.
if (!IsPixelEditorActive || e.InitialPressMouseButton == MouseButton.Middle ||
(e.KeyModifiers & KeyModifiers.Shift) == 0) return;
_lastPixelMouseLocation = Point.Empty;
// Left or Alt-Right Adds pixel, Right or Alt-Left removes pixel
DrawPixel(e.InitialPressMouseButton == MouseButton.Left ^ (e.KeyModifiers & KeyModifiers.Alt) != 0, location, e.KeyModifiers);
}
private void LayerImageBoxOnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.ClickCount != 2 || (e.KeyModifiers & KeyModifiers.Alt) != 0 || (e.KeyModifiers & KeyModifiers.Shift) != 0) return;
var pointer = e.GetCurrentPoint(LayerImageBox);
if (pointer.Properties.IsLeftButtonPressed)
{
if (!LayerImageBox.IsPointInImage(pointer.Position)) return;
var location = LayerImageBox.PointToImage(pointer.Position).ToDotNet();
CenterLayerAt(location, AppSettings.LockedZoomLevel);
// Check to see if the clicked location is an issue, and if so, select it in the ListView.
SelectIssueAtPoint(location);
return;
}
if (pointer.Properties.IsRightButtonPressed)
{
ZoomToFit();
return;
}
e.Handled = true;
}
private void LayerImageBoxOnDoubleTapped(object? sender, RoutedEventArgs e)
{
if ((_globalModifiers & KeyModifiers.Alt) != 0 || (_globalModifiers & KeyModifiers.Shift) != 0) return;
e.Handled = true;
}
private void LayerImageBox_KeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
if (e.KeyModifiers == KeyModifiers.Shift)
{
ClearROI();
}
/*else if(e.KeyModifiers == KeyModifiers.Alt)
{
ClearMask();
}*/
else
{
ClearROIAndMask();
}
e.Handled = true;
return;
}
if ((e.KeyModifiers & KeyModifiers.Control) != 0)
{
if (e.Key == Key.D0 || e.Key == Key.NumPad0)
{
ZoomToFit();
e.Handled = true;
return;
}
if (e.Key == Key.R)
{
ShowLayerImageRotated = !_showLayerImageRotated;
e.Handled = true;
return;
}
if (e.Key == Key.F)
{
ShowLayerImageFlipped = !_showLayerImageFlipped;
e.Handled = true;
return;
}
}
}
private void LayerImageBoxOnPointerMoved(object? sender, PointerEventArgs e)
{
var pointer = e.GetCurrentPoint(LayerImageBox);
if (!LayerImageBox.IsPointInImage(pointer.Position)) return;
var location = LayerImageBox.PointToImage(pointer.Position).ToDotNet();
if ((e.KeyModifiers & KeyModifiers.Control) != 0)
{
Point realLocation = GetTransposedPoint(location);
unsafe
{
var brightness = LayerCache.ImageSpan[LayerCache.Image.GetPixelPos(realLocation)];
LayerPixelPicker.Set(realLocation, brightness);
}
RaisePropertyChanged(nameof(LayerPixelPicker));
}
if ((e.KeyModifiers & KeyModifiers.Shift) == 0) return;
if (_lastPixelMouseLocation == location) return;
_lastPixelMouseLocation = location;
// Bail here if we're not in a draw operation, if the mouse button is not either
// left or right, or if the location of the mouse pointer is not within the image.
if (SelectedPixelOperationTabIndex != (int)PixelOperation.PixelOperationType.Drawing) return;
if (!IsPixelEditorActive || pointer.Properties.IsMiddleButtonPressed) return;
//if (!pbLayer.IsPointInImage(e.Location)) return;
if (pointer.Properties.IsRightButtonPressed)
{
// Right or Alt-Left will remove a pixel
DrawPixel(false ^ (e.KeyModifiers & KeyModifiers.Alt) != 0, location, e.KeyModifiers);
return;
}
if (pointer.Properties.IsLeftButtonPressed)
{
// Left or Alt-Right will add a pixel
DrawPixel(true ^ (e.KeyModifiers & KeyModifiers.Alt) != 0, location, e.KeyModifiers);
return;
}
}
public bool SelectObjectRoi(Point location)
{
var point = GetTransposedPoint(location);
var brightness = LayerCache.Image.GetByte(point);
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
{
if (brightness == 0 &&
((int) LayerCache.LayerHierarchyJagged.GetValue(0, i, 2) != -1 ||
(int) LayerCache.LayerHierarchyJagged.GetValue(0, i, 3) == -1)) continue;
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], point, false) >= 0)
{
var rectangle =
GetTransposedRectangle(CvInvoke.BoundingRectangle(LayerCache.LayerContours[i]));
ROI = rectangle;
return true;
}
}
return false;
}
public uint SelectObjectRoi(Rectangle roiRectangle)
{
if (roiRectangle.IsEmpty) return 0;
List<Rectangle> rectangles = new List<Rectangle>();
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
{
var rectangle = CvInvoke.BoundingRectangle(LayerCache.LayerContours[i]);
//roi.Intersect(rectangle);
if (roiRectangle.IntersectsWith(rectangle))
{
rectangles.Add(rectangle);
}
}
roiRectangle = rectangles.Count == 0 ? Rectangle.Empty : rectangles[0];
for (var i = 1; i < rectangles.Count; i++)
{
var rectangle = rectangles[i];
roiRectangle = Rectangle.Union(roiRectangle, rectangle);
}
ROI = GetTransposedRectangle(roiRectangle);
return (uint)rectangles.Count;
}
public bool SelectObjectMask(Point location)
{
var point = GetTransposedPoint(location);
var brightness = LayerCache.Image.GetByte(point);
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
{
if (brightness == 0 &&
((int)LayerCache.LayerHierarchyJagged.GetValue(0, i, 2) != -1 ||
(int)LayerCache.LayerHierarchyJagged.GetValue(0, i, 3) == -1)) continue;
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], point, false) >= 0)
{
//var rectangle = GetTransposedRectangle(CvInvoke.BoundingRectangle(LayerCache.LayerContours[i]));
//ROI = rectangle;
AddMaskPoints(LayerCache.LayerContours[i].ToArray());
return true;
}
}
return false;
}
public void OnLayerPixelPickerClicked()
{
if (!LayerPixelPicker.IsSet) return;
CenterLayerAt(GetTransposedPoint(LayerPixelPicker.Location, true), -1);
}
public async void SaveCurrentLayerImage()
{
SaveFileDialog dialog = new SaveFileDialog
{
Filters = Helpers.PngFileFilter,
DefaultExtension = ".png",
InitialFileName = $"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_layer{ActualLayer}.png"
};
var result = await dialog.ShowAsync(this);
if (string.IsNullOrEmpty(result)) return;
LayerCache.ImageBgr.Save(result);
}
const byte _pixelEditorCursorMinDiamater = 10;
public void UpdatePixelEditorCursor()
{
Mat cursor = null;
MCvScalar _pixelEditorCursorColor = new MCvScalar(
Settings.PixelEditor.CursorColor.B,
Settings.PixelEditor.CursorColor.G,
Settings.PixelEditor.CursorColor.R,
Settings.PixelEditor.CursorColor.A);
switch ((PixelOperation.PixelOperationType)SelectedPixelOperationTabIndex)
{
case PixelOperation.PixelOperationType.Drawing:
if (DrawingPixelDrawing.BrushSize > 1)
{
cursor = EmguExtensions.InitMat(new Size(DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.BrushSize), 4);
switch (DrawingPixelDrawing.BrushShape)
{
case PixelDrawing.BrushShapeType.Rectangle:
CvInvoke.Rectangle(cursor,
new Rectangle(Point.Empty, new Size(DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.BrushSize)),
_pixelEditorCursorColor, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.LineType);
_pixelEditorCursorColor.V3 = 255;
CvInvoke.Rectangle(cursor,
new Rectangle(Point.Empty, new Size(DrawingPixelDrawing.BrushSize-1, DrawingPixelDrawing.BrushSize-1)),
_pixelEditorCursorColor, 1, DrawingPixelDrawing.LineType);
break;
case PixelDrawing.BrushShapeType.Circle:
var center = new Point(DrawingPixelDrawing.BrushSize / 2, DrawingPixelDrawing.BrushSize / 2);
CvInvoke.Circle(cursor,
center,
center.X,
_pixelEditorCursorColor,
DrawingPixelDrawing.Thickness, DrawingPixelDrawing.LineType
);
_pixelEditorCursorColor.V3 = 255;
CvInvoke.Circle(cursor,
center,
center.X,
_pixelEditorCursorColor,
1, DrawingPixelDrawing.LineType
);
break;
}
}
break;
case PixelOperation.PixelOperationType.Text:
var text = DrawingPixelText.Text;
if (string.IsNullOrEmpty(text) || DrawingPixelText.FontScale < 0.2) return;
int baseLine = 0;
var size = CvInvoke.GetTextSize(text, DrawingPixelText.Font, DrawingPixelText.FontScale, DrawingPixelText.Thickness, ref baseLine);
cursor = EmguExtensions.InitMat(new Size(size.Width * 2, size.Height * 2), 4);
//CvInvoke.Rectangle(cursor, new Rectangle(Point.Empty, size), _pixelEditorCursorColor, -1, DrawingPixelText.LineType);
//_pixelEditorCursorColor.V3 = 255;
//CvInvoke.Rectangle(cursor, new Rectangle(new Point(size.Width, 0), size), _pixelEditorCursorColor, 1, DrawingPixelText.LineType);
CvInvoke.PutText(cursor, text, new Point(size.Width, size.Height), DrawingPixelText.Font, DrawingPixelText.FontScale, _pixelEditorCursorColor, DrawingPixelText.Thickness, DrawingPixelText.LineType, DrawingPixelText.Mirror);
if (_showLayerImageFlipped)
{
var flipType = FlipType.None;
if (_showLayerImageFlippedHorizontally)
flipType |= FlipType.Horizontal;
if (_showLayerImageFlippedVertically)
flipType |= FlipType.Vertical;
if (flipType != FlipType.None)
CvInvoke.Flip(cursor, cursor, flipType);
}
if (_showLayerImageRotated)
{
CvInvoke.Rotate(cursor, cursor, _showLayerImageRotateCcwDirection ? RotateFlags.Rotate90CounterClockwise : RotateFlags.Rotate90Clockwise);
}
break;
case PixelOperation.PixelOperationType.Supports:
case PixelOperation.PixelOperationType.DrainHole:
var diameter = SelectedPixelOperationTabIndex == (byte)PixelOperation.PixelOperationType.Supports ?
DrawingPixelSupport.TipDiameter : DrawingPixelDrainHole.Diameter;
if (diameter >= _pixelEditorCursorMinDiamater)
{
cursor = EmguExtensions.InitMat(new System.Drawing.Size(diameter, diameter), 4);
var center = new Point(diameter / 2, diameter / 2);
CvInvoke.Circle(cursor,
center,
center.X,
_pixelEditorCursorColor,
-1, LineType.AntiAlias
);
_pixelEditorCursorColor.V3 = 255;
CvInvoke.Circle(cursor,
center,
center.X,
_pixelEditorCursorColor,
1, LineType.AntiAlias
);
}
break;
}
if (cursor is not null)
{
LayerImageBox.TrackerImage = cursor.ToBitmap();
//cursor.Save("D:\\Cursor.png");
//LayerImageBox.TrackerImage.Save("D:\\CursorAVA.png");
}
/*else if (tabControlPixelEditor.SelectedIndex == (byte)PixelOperation.PixelOperationType.Text)
{
var text = tbPixelEditorTextText.Text;
if (string.IsNullOrEmpty(text) || nmPixelEditorTextFontScale.Value < 0.2m) return;
LineType lineType = (LineType)cbPixelEditorTextLineType.SelectedItem;
FontFace fontFace = (FontFace)cbPixelEditorTextFontFace.SelectedItem;
double scale = (double) nmPixelEditorTextFontScale.Value * pbLayer.Zoom / 100;
int thickness = (int) nmPixelEditorTextThickness.Value;
int baseLine = 0;
var size = CvInvoke.GetTextSize(text, fontFace, scale, thickness, ref baseLine);
mat = new Mat(size, DepthType.Cv8U, 4);
CvInvoke.PutText(mat, text, new Point(0,0), fontFace, scale, new MCvScalar(255,100,255, 255), thickness, lineType, cbPixelEditorTextMirror.Checked);
}*/
}
}
}