mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-09 01:52:32 +02:00
4600e81b08
- **(Add) Suggestions:**
- A new module that detect bad or parameters out of a defined range and suggest a change on the file, those can be auto applied if configured to do so
- **Avaliable suggestions:**
- **Bottom layer count:** Bottom layers should be kept to a minimum, usually from 2 to 3, it function is to provide a good adhesion to the first layer on the build plate, using a high count have disadvantages.
- **Wait time before cure:** Rest some time before cure the layer is crucial to let the resin settle after the lift sequence and allow some time for the arm settle at the correct Z position as the resin will offer some resistance and push the structure.
This lead to better quality with more successful prints, less lamination problems, better first layers with more success of stick to the build plate and less elephant foot effect.
- **Wait time after cure:** Rest some time after cure the layer and before the lift sequence can be important to allow the layer to cooldown a bit and detach better from the FEP.
- **Layer height:** Using the right layer height is important to get successful prints:
Thin layers may cause problems on adhesion, lamination, will print much slower and have no real visual benefits.
Thick layers may not fully cure no matter the exposure time you use, causing lamination and other hazards. Read your resin dtasheet to know the limits.
Using layer height with too many decimal digits may produce a wrong positioning due stepper step loss and/or Z axis quality.
- **Core:**
- Convert the project to Nullable aware and "null-safe"
- **File Formats:**
- (Add) `Volume` property to get the total model volume
- (Add) `SanitizeLayers` method to reassign indexes and force attribute parent file
- (Improvement) Merge `LayerManager` into `FileFormat` and cleanup: This affects the whole project and external scripts.
If using scripts please update them, search for `.LayerManager.` and replace by `.`
- (Change) Chitubox encrypted format can now be saved as normal
- (Fix) Converted files layers was pointing to the source file and related to it
- **Layers:**
- (Add) Methods: `ResetParameters`, `CopyParametersTo`, `CopyExposureTo`, `CopyWaitTimesTo`
- (Improvement) `IsBottomLayer` property will also return true when the index is inside bottom layer count
- **Scripting:**
- (Add) Configuration variable: `MinimumVersionToRun` - Sets the minimum version able to run the script
- (Improvement) Allow run scripts written in C# 10 with the new namespace; style as well as nullables methods
- (Improvement) Convert scripts to use Nullable code
- **UI:**
- (Add) Fluent Dark theme
- (Add) Default Light theme
- (Add) Default Dark theme
- (Change) Use fontawesome and material design to render the icons instead of static png images
- (Change) Some icons
- (Change) Move log tab to clipboard tab
- (Change) Tooltip overlay default color
- (Improvement) Windows position for tool windows, sometimes framework can return negative values affecting positions, now limits to 0 (#387)
- (Fix) Center image icon for layer action button
- (Fix) Center image icon for save layer image button
- **Tools:**
- (Add) Layer re-height: Offset mode, change layers position by a defined offset (#423)
- (Improvement) Rotate: Unable to use an angle of 0
- (Improvement) Remove layers: Will not recalcualte and reset properties of layers anymore, allowing removing layers on dynamic layer height models and others
- (Improvement) Clone layers: Will not recalcualte and reset properties of layers anymore, allowing cloning layers on dynamic layer height models and others
- (Fix) Exposure time finder: Very small printers may not print the stock object as it is configured, lead to a unknown error while generating the test. It will now show a better error message and advice a solution (#426)
- **Terminal:**
- (Add) More default namespaces
- (Improvement) Set a MinHeight for the rows to prevent spliter from eat the elements
- (Change) Set working space to the MainWindow instead of TerminalWindow
- **(Upgrade) .NET from 5.0.14 to 6.0.3**
- This brings big performance improvements, better JIT, faster I/O operations and others
- Read more: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6
- Due this macOS requirement starts at 10.15 (Catalina)
- Read more: https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md
- (Add) Native support for MacOS ARM64 architecture (Mac M1 and upcomming Mac's) (#187)
- (Exchange) Dependency Newtonsoft Json by System.Text.Json to parse the json documents
- (Remove) "Automations - Light-off delay" in favor of new suggestion "wait time before cure" module
- (Fix) File - Send to: Winrar or 7zip have a wrong extension on the list (uvt) when should be (uvj)
- (Upgrade) AvaloniaUI from 0.10.12 to 0.10.13
2257 lines
87 KiB
C#
2257 lines
87 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.Controls.Primitives;
|
|
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.AvaloniaControls;
|
|
using UVtools.Core;
|
|
using UVtools.Core.EmguCV;
|
|
using UVtools.Core.Extensions;
|
|
using UVtools.Core.Layers;
|
|
using UVtools.Core.PixelEditor;
|
|
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 Slider LayerSlider;
|
|
public Track LayerSlicerTrack;
|
|
public Panel LayerNavigationTooltipPanel;
|
|
public Border LayerNavigationTooltipBorder;
|
|
private Canvas _issuesSliderCanvas;
|
|
|
|
|
|
private Timer _layerNavigationTooltipTimer = new(0.1) { AutoReset = false };
|
|
private Timer _layerNavigationSliderDebounceTimer = new(25) { AutoReset = false };
|
|
private uint _actualLayerSlider;
|
|
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 _showLayerOutlineContourBoundary;
|
|
private bool _showLayerOutlineHollowAreas;
|
|
private bool _showLayerOutlineCentroids;
|
|
private bool _showLayerOutlineEdgeDetection;
|
|
private bool _showLayerOutlineDistanceDetection;
|
|
private bool _showLayerOutlineSkeletonize;
|
|
|
|
|
|
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<Slider>("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");
|
|
|
|
LayerSlider.TemplateApplied += (sender, e) =>
|
|
{
|
|
LayerSlicerTrack = e.NameScope.Find<Track>("PART_Track");
|
|
};
|
|
|
|
_showLayerImageDifference = Settings.LayerPreview.ShowLayerDifference;
|
|
_showLayerOutlinePrintVolumeBoundary = Settings.LayerPreview.VolumeBoundsOutline;
|
|
_showLayerOutlineLayerBoundary = Settings.LayerPreview.LayerBoundsOutline;
|
|
_showLayerOutlineContourBoundary = Settings.LayerPreview.ContourBoundsOutline;
|
|
_showLayerOutlineHollowAreas = Settings.LayerPreview.HollowOutline;
|
|
_showLayerOutlineCentroids = Settings.LayerPreview.CentroidOutline;
|
|
|
|
LayerImageBox.ZoomLevels = new AdvancedImageBox.ZoomLevelCollection(AppSettings.ZoomLevels);
|
|
|
|
LayerImageBox.GetObservable(AdvancedImageBox.ZoomProperty).Subscribe(zoom =>
|
|
{
|
|
if (!IsFileLoaded) return;
|
|
var newZoom = zoom;
|
|
var oldZoom = LayerImageBox.OldZoom;
|
|
RaisePropertyChanged(nameof(LayerZoomStr));
|
|
AddLogVerbose($"Zoomed from {oldZoom} to {newZoom}");
|
|
|
|
if (_showLayerImageCrosshairs &&
|
|
SlicerFile.IssueManager.Count > 0 &&
|
|
(oldZoom < 50 &&
|
|
newZoom >= 50 // Trigger refresh as crosshair thickness increases at lower zoom levels
|
|
|| oldZoom > 100 && newZoom <= 100
|
|
|| oldZoom is >= 50 and <= 100 && (newZoom is < 50 or > 100)
|
|
|| oldZoom <= AppSettings.CrosshairFadeLevel &&
|
|
newZoom > AppSettings.CrosshairFadeLevel // Trigger refresh as zoom level manually crosses fade threshold
|
|
|| oldZoom > AppSettings.CrosshairFadeLevel && newZoom <= AppSettings.CrosshairFadeLevel)
|
|
|
|
)
|
|
{
|
|
if (Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues)
|
|
{
|
|
if (IssuesGrid.SelectedItems.Count == 0 || !IssuesGrid.SelectedItems.Cast<MainIssue>().Any(
|
|
mainIssue => // Find a valid candidate to update layer preview, otherwise quit
|
|
mainIssue.IsIssueInBetween(_actualLayer)
|
|
&& mainIssue.Type is not MainIssue.IssueType.TouchingBound and not MainIssue.IssueType.EmptyLayer)) return;
|
|
}
|
|
else
|
|
{
|
|
if (!SlicerFile.IssueManager.Any(
|
|
mainIssue => // Find a valid candidate to update layer preview, otherwise quit
|
|
mainIssue.IsIssueInBetween(_actualLayer)
|
|
&& mainIssue.Type is not MainIssue.IssueType.TouchingBound and not MainIssue.IssueType.EmptyLayer)) 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();
|
|
}
|
|
});
|
|
|
|
LayerImageBox.GetObservable(AdvancedImageBox.SelectionRegionProperty)
|
|
.Subscribe(rect => RaisePropertyChanged(nameof(LayerROIStr)));
|
|
|
|
LayerImageBox.PointerMoved += LayerImageBoxOnPointerMoved;
|
|
LayerImageBox.KeyDown += LayerImageBox_KeyDown;
|
|
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)));
|
|
};
|
|
_layerNavigationSliderDebounceTimer.Interval = Settings.LayerPreview.LayerSliderDebounce == 0 ? 1 : Settings.LayerPreview.LayerSliderDebounce;
|
|
_layerNavigationSliderDebounceTimer.Elapsed += (sender, args) =>
|
|
{
|
|
Dispatcher.UIThread.InvokeAsync(ShowLayer);
|
|
};
|
|
}
|
|
|
|
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 ShowLayerOutlineContourBoundary
|
|
{
|
|
get => _showLayerOutlineContourBoundary;
|
|
set
|
|
{
|
|
if (!RaiseAndSetIfChanged(ref _showLayerOutlineContourBoundary, value)) return;
|
|
ShowLayer();
|
|
}
|
|
}
|
|
|
|
public bool ShowLayerOutlineHollowAreas
|
|
{
|
|
get => _showLayerOutlineHollowAreas;
|
|
set
|
|
{
|
|
if (!RaiseAndSetIfChanged(ref _showLayerOutlineHollowAreas, value)) return;
|
|
ShowLayer();
|
|
}
|
|
}
|
|
|
|
public bool ShowLayerOutlineCentroids
|
|
{
|
|
get => _showLayerOutlineCentroids;
|
|
set
|
|
{
|
|
if (!RaiseAndSetIfChanged(ref _showLayerOutlineCentroids, value)) return;
|
|
ShowLayer();
|
|
}
|
|
}
|
|
|
|
public bool ShowLayerOutlineEdgeDetection
|
|
{
|
|
get => _showLayerOutlineEdgeDetection;
|
|
set
|
|
{
|
|
if (!RaiseAndSetIfChanged(ref _showLayerOutlineEdgeDetection, value)) return;
|
|
ShowLayer();
|
|
}
|
|
}
|
|
|
|
public bool ShowLayerOutlineDistanceDetection
|
|
{
|
|
get => _showLayerOutlineDistanceDetection;
|
|
set
|
|
{
|
|
if (!RaiseAndSetIfChanged(ref _showLayerOutlineDistanceDetection, value)) return;
|
|
ShowLayer();
|
|
}
|
|
}
|
|
|
|
public bool ShowLayerOutlineSkeletonize
|
|
{
|
|
get => _showLayerOutlineSkeletonize;
|
|
set
|
|
{
|
|
if (!RaiseAndSetIfChanged(ref _showLayerOutlineSkeletonize, 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.LastLayerIndex}";
|
|
public string ActualLayerTooltip => SlicerFile is null ? "???" : $"{Layer.ShowHeight(SlicerFile[_actualLayer]?.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);
|
|
var text = $"Pixels: {LayerCache.Layer.NonZeroPixelCount} ({pixelPercent}%)";
|
|
var volume = LayerCache.Layer.Volume;
|
|
if (volume > 0)
|
|
{
|
|
text += $"\nVolume: {volume:F2}mm³";
|
|
}
|
|
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
|
|
{
|
|
get
|
|
{
|
|
var pixelSize = SlicerFile?.PixelSizeMicronsMax;
|
|
var text = $"Zoom: [ {LayerImageBox.Zoom / 100m}x{(AppSettings.LockedZoomLevel == LayerImageBox.Zoom ? " 🔒 ]" : " ]")}";
|
|
if (pixelSize > 0) text += $"\nPixel: {SlicerFile.PixelSizeMicronsMax}µm";
|
|
return text;
|
|
}
|
|
}
|
|
|
|
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 ActualLayerSlider
|
|
{
|
|
get => _actualLayerSlider;
|
|
set
|
|
{
|
|
if (DataContext is null) return;
|
|
if (!RaiseAndSetIfChanged(ref _actualLayerSlider, value)) return;
|
|
if (Settings.LayerPreview.LayerSliderDebounce == 0)
|
|
{
|
|
ActualLayer = _actualLayerSlider;
|
|
}
|
|
else
|
|
{
|
|
_layerNavigationSliderDebounceTimer.Stop();
|
|
_layerNavigationSliderDebounceTimer.Start();
|
|
ActualLayer = _actualLayerSlider;
|
|
}
|
|
}
|
|
}
|
|
|
|
public uint ActualLayer
|
|
{
|
|
get => _actualLayer;
|
|
set
|
|
{
|
|
if (DataContext is null) return;
|
|
if (!RaiseAndSetIfChanged(ref _actualLayer, value)) return;
|
|
|
|
|
|
if (!_layerNavigationSliderDebounceTimer.Enabled) // Doesn't come from ActualLayerSlider timer
|
|
{
|
|
ActualLayerSlider = _actualLayer; // sync when required
|
|
ShowLayer(); // Show layer only if timer is not present
|
|
}
|
|
|
|
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 (LayerSlicerTrack != null)
|
|
{
|
|
double trackerPos = LayerSlicerTrack.Thumb.Bounds.Height / 2 + LayerSlicerTrack.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 = GetTransposedRectangle(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 void SelectModelVolumeRoi()
|
|
{
|
|
ROI = SlicerFile.BoundingRectangle;
|
|
}
|
|
|
|
public void SelectLayerVolumeRoi()
|
|
{
|
|
ROI = LayerCache.Layer.BoundingRectangle;
|
|
}
|
|
|
|
|
|
public List<Point[]> MaskPoints => _maskPoints;
|
|
|
|
/*private set
|
|
{
|
|
if(!RaiseAndSetIfChanged(ref _maskPoints, value)) return;
|
|
ShowLayer();
|
|
}*/
|
|
public void AddMaskPoints(Point[] points, bool refreshLayer = true)
|
|
{
|
|
if (_maskPoints.RemoveAll(points1 => points1.SequenceEqual(points)) <= 0)
|
|
{
|
|
_maskPoints.Add(points);
|
|
}
|
|
|
|
if(_maskPoints.Count > 0 && Settings.LayerPreview.MaskClearROIAfterSet) ClearROI();
|
|
|
|
if(refreshLayer) ShowLayer();
|
|
RaisePropertyChanged(nameof(LayerROIStr));
|
|
}
|
|
|
|
public void AddMaskPoints(IEnumerable<Point[]> pointsOfPoints, bool clear = true)
|
|
{
|
|
if (clear)
|
|
{
|
|
_maskPoints.Clear();
|
|
_maskPoints.AddRange(pointsOfPoints);
|
|
}
|
|
else
|
|
{
|
|
foreach (var points in pointsOfPoints)
|
|
{
|
|
if (_maskPoints.RemoveAll(points1 => points1.SequenceEqual(points)) <= 0)
|
|
{
|
|
_maskPoints.Add(points);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
ShowLayer();
|
|
RaisePropertyChanged(nameof(LayerROIStr));
|
|
}
|
|
|
|
public void SelectLayerPositiveAreasMask()
|
|
{
|
|
AddMaskPoints(LayerCache.LayerContours.ToArrayOfArray());
|
|
if (_maskPoints.Count > 0 && Settings.LayerPreview.MaskClearROIAfterSet) ClearROI();
|
|
}
|
|
|
|
public void SelectLayerHollowAreasMask()
|
|
{
|
|
var contours = EmguContours.GetNegativeContours(LayerCache.LayerContours, LayerCache.LayerContourHierarchy);
|
|
AddMaskPoints(contours.ToArrayOfArray());
|
|
if (_maskPoints.Count > 0 && Settings.LayerPreview.MaskClearROIAfterSet) ClearROI();
|
|
}
|
|
|
|
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 (!IsFileLoaded) return;
|
|
if (!CanGoDown) return;
|
|
ActualLayer = 0;
|
|
}
|
|
|
|
public void GoPreviousLayer()
|
|
{
|
|
if (!IsFileLoaded) return;
|
|
if (!CanGoDown) return;
|
|
ActualLayer--;
|
|
}
|
|
|
|
public void GoNextLayer()
|
|
{
|
|
if (!IsFileLoaded) return;
|
|
if (!CanGoUp) return;
|
|
ActualLayer++;
|
|
}
|
|
|
|
public void GoLastLayer()
|
|
{
|
|
if (!IsFileLoaded) return;
|
|
if (!CanGoUp) return;
|
|
ActualLayer = SliderMaximumValue;
|
|
}
|
|
|
|
public void GoMassLayer(string which)
|
|
{
|
|
if (!IsFileLoaded) return;
|
|
var layer = which switch
|
|
{
|
|
"SB" => SlicerFile.SmallestBottomLayer,
|
|
"LB" => SlicerFile.LargestBottomLayer,
|
|
"SN" => SlicerFile.SmallestNormalLayer,
|
|
"LN" => SlicerFile.LargestNormalLayer,
|
|
_ => null
|
|
};
|
|
if (layer is null) return;
|
|
ActualLayer = layer.Index;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
var watch = Stopwatch.StartNew();
|
|
LayerCache.Layer = SlicerFile[_actualLayer];
|
|
if (LayerCache.Image is null)
|
|
{
|
|
RefreshCurrentLayerData();
|
|
return;
|
|
}
|
|
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.Gray2Bgr);
|
|
}
|
|
else if (_showLayerOutlineDistanceDetection)
|
|
{
|
|
using var distance = new Mat();
|
|
CvInvoke.DistanceTransform(LayerCache.Image, distance, null, DistType.C, 3);
|
|
//distance.ConvertTo(distance, DepthType.Cv8U);
|
|
CvInvoke.Normalize(distance, distance, byte.MinValue, byte.MaxValue, NormType.MinMax, DepthType.Cv8U);
|
|
CvInvoke.CvtColor(distance, LayerCache.ImageBgr, ColorConversion.Gray2Bgr);
|
|
}
|
|
else if (_showLayerOutlineSkeletonize)
|
|
{
|
|
using var skeletonize = LayerCache.Image.Skeletonize();
|
|
CvInvoke.CvtColor(skeletonize, LayerCache.ImageBgr, ColorConversion.Gray2Bgr);
|
|
}
|
|
else if (_showLayerImageDifference)
|
|
{
|
|
//if (_actualLayer > 0 && _actualLayer < SlicerFile.LayerCount - 1)
|
|
// {
|
|
var previousLayer = _actualLayer > 0 ? SlicerFile[_actualLayer - 1] : null;
|
|
var nextLayer = _actualLayer < SlicerFile.LastLayerIndex ? SlicerFile[_actualLayer + 1] : null;
|
|
Mat previousImage = null;
|
|
Mat nextImage = null;
|
|
|
|
// Optimize empties for now...
|
|
var rect = Rectangle.Empty;
|
|
if (!LayerCache.Layer.IsEmpty)
|
|
{
|
|
rect = LayerCache.Layer.BoundingRectangle;
|
|
}
|
|
else if (previousLayer is not null && !previousLayer.IsEmpty)
|
|
{
|
|
rect = previousLayer.BoundingRectangle;
|
|
}
|
|
else if (nextLayer is not null && !nextLayer.IsEmpty)
|
|
{
|
|
rect = nextLayer.BoundingRectangle;
|
|
}
|
|
|
|
if (previousLayer is not null && !previousLayer.IsEmpty)
|
|
{
|
|
rect = Rectangle.Union(rect, previousLayer.BoundingRectangle);
|
|
}
|
|
if (nextLayer is not null && !nextLayer.IsEmpty)
|
|
{
|
|
rect = Rectangle.Union(rect, nextLayer.BoundingRectangle);
|
|
}
|
|
|
|
/*var rect = Rectangle.Union(
|
|
Rectangle.Union(LayerCache.Layer.BoundingRectangle, previousLayer.BoundingRectangle),
|
|
nextLayer.BoundingRectangle);*/
|
|
|
|
if (!rect.IsEmpty && (previousLayer is not null || nextLayer is not null))
|
|
{
|
|
byte* previousSpan = null;
|
|
byte* nextSpan = null;
|
|
// Can improve performance on >4K images?
|
|
Parallel.Invoke(
|
|
() =>
|
|
{
|
|
if (previousLayer is null) return;
|
|
previousImage = previousLayer.LayerMat;
|
|
previousSpan = previousImage.GetBytePointer();
|
|
},
|
|
() =>
|
|
{
|
|
if (nextLayer is null) return;
|
|
nextImage = nextLayer.LayerMat;
|
|
nextSpan = nextImage.GetBytePointer();
|
|
});
|
|
|
|
/*using (var previousImage = SlicerFile[_actualLayer - 1].LayerMat)
|
|
using (var nextImage = SlicerFile[_actualLayer + 1].LayerMat)
|
|
{*/
|
|
//var previousSpan = previousImage.GetPixelSpan<byte>();
|
|
//var nextSpan = nextImage.GetPixelSpan<byte>();
|
|
|
|
|
|
int width = LayerCache.Image.GetRealStep();
|
|
int channels = LayerCache.ImageBgr.NumberOfChannels;
|
|
bool showSimilarityInstead =
|
|
Settings.LayerPreview.LayerDifferenceHighlightSimilarityInstead;
|
|
|
|
Parallel.For(rect.Y, rect.Bottom, CoreSettings.ParallelOptions, y =>
|
|
{
|
|
for (int x = rect.X; x < rect.Right; x++)
|
|
{
|
|
int pixel = y * width + x;
|
|
if (showSimilarityInstead)
|
|
{
|
|
if (imageSpan[pixel] == 0) continue;
|
|
}
|
|
else
|
|
{
|
|
if (imageSpan[pixel] != 0) continue;
|
|
}
|
|
|
|
byte brightness = 0;
|
|
|
|
var color = Color.Empty;
|
|
if (previousSpan is not null && nextSpan is not null && previousSpan[pixel] > 0 && nextSpan[pixel] > 0)
|
|
{
|
|
brightness = Math.Max(previousSpan[pixel], nextSpan[pixel]);
|
|
color = Settings.LayerPreview.BothLayerDifferenceColor;
|
|
}
|
|
else if (previousSpan is not null && previousSpan[pixel] > 0)
|
|
{
|
|
brightness = previousSpan[pixel];
|
|
color = Settings.LayerPreview.PreviousLayerDifferenceColor;
|
|
}
|
|
else if (nextSpan is not null && nextSpan[pixel] > 0)
|
|
{
|
|
brightness = nextSpan[pixel];
|
|
color = Settings.LayerPreview.NextLayerDifferenceColor;
|
|
}
|
|
|
|
if (color.IsEmpty) continue;
|
|
|
|
color = color.FactorColor(brightness);
|
|
|
|
var bgrPixel = pixel * channels;
|
|
imageBgrSpan[bgrPixel] = color.B; // B
|
|
imageBgrSpan[bgrPixel + 1] = color.G; // G
|
|
imageBgrSpan[bgrPixel + 2] = color.R; // R
|
|
//imageBgrSpan[++bgrPixel] = color.A; // A
|
|
}
|
|
});
|
|
}
|
|
|
|
previousImage?.Dispose();
|
|
nextImage?.Dispose();
|
|
// }
|
|
}
|
|
|
|
|
|
var selectedIssues = IssuesGrid.SelectedItems;
|
|
|
|
if (_showLayerImageIssues && SlicerFile.IssueManager.Count > 0)
|
|
{
|
|
//var count = 0;
|
|
foreach (var issue in SlicerFile.IssueManager.GetIssuesBy(_actualLayer)
|
|
.Where(issue => issue.Parent.Type
|
|
is not MainIssue.IssueType.PrintHeight
|
|
and not MainIssue.IssueType.EmptyLayer))
|
|
{
|
|
//count++;
|
|
var color = Color.Empty;
|
|
bool drawCrosshair = false;
|
|
|
|
switch (issue.Parent.Type)
|
|
{
|
|
case MainIssue.IssueType.Island:
|
|
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue.Parent)
|
|
? Settings.LayerPreview.IslandHighlightColor
|
|
: Settings.LayerPreview.IslandColor;
|
|
drawCrosshair = true;
|
|
|
|
break;
|
|
case MainIssue.IssueType.Overhang:
|
|
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue.Parent)
|
|
? Settings.LayerPreview.OverhangHighlightColor
|
|
: Settings.LayerPreview.OverhangColor;
|
|
drawCrosshair = true;
|
|
|
|
break;
|
|
case MainIssue.IssueType.ResinTrap:
|
|
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue.Parent)
|
|
? Settings.LayerPreview.ResinTrapHighlightColor
|
|
: Settings.LayerPreview.ResinTrapColor;
|
|
drawCrosshair = true;
|
|
break;
|
|
case MainIssue.IssueType.SuctionCup:
|
|
color = selectedIssues.Count > 0 && selectedIssues.Contains(issue.Parent)
|
|
? Settings.LayerPreview.SuctionCupHighlightColor
|
|
: Settings.LayerPreview.SuctionCupColor;
|
|
drawCrosshair = true;
|
|
break;
|
|
case MainIssue.IssueType.TouchingBound:
|
|
color = Settings.LayerPreview.TouchingBoundsColor;
|
|
break;
|
|
case MainIssue.IssueType.Debug:
|
|
color = new Color(255, 15, 112, 16);
|
|
break;
|
|
}
|
|
|
|
if (color.IsEmpty) continue;
|
|
|
|
if (drawCrosshair && _showLayerImageCrosshairs &&
|
|
!Settings.LayerPreview.CrosshairShowOnlyOnSelectedIssues &&
|
|
LayerImageBox.Zoom <= AppSettings.CrosshairFadeLevel)
|
|
{
|
|
DrawCrosshair(issue.BoundingRectangle);
|
|
}
|
|
|
|
/*var point = issue.FirstPoint;
|
|
if (!point.IsBothNegative())
|
|
{
|
|
CvInvoke.PutText(LayerCache.ImageBgr, count.ToString(), point, FontFace.HersheyDuplex, 2, new MCvScalar(0,255,0), 2, LineType.AntiAlias);
|
|
}*/
|
|
|
|
switch (issue)
|
|
{
|
|
case IssueOfContours issueOfContours:
|
|
{
|
|
using var vec = new VectorOfVectorOfPoint(issueOfContours.Contours);
|
|
CvInvoke.DrawContours(LayerCache.ImageBgr, vec, -1, new MCvScalar(color.B, color.G, color.R), -1);
|
|
break;
|
|
}
|
|
case IssueOfPoints issueOfPoints:
|
|
{
|
|
foreach (var pixel in issueOfPoints.Points)
|
|
{
|
|
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
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if (_showLayerOutlinePrintVolumeBoundary)
|
|
{
|
|
CvInvoke.Rectangle(LayerCache.ImageBgr, SlicerFile.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 (_showLayerOutlineContourBoundary)
|
|
{
|
|
int lastParent = -1;
|
|
uint reps = 0;
|
|
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
|
|
{
|
|
var parent = LayerCache.LayerContourHierarchy[i, EmguContour.HierarchyParent];
|
|
if (parent == -1)
|
|
{
|
|
reps = 0;
|
|
}
|
|
if(parent == -1 || parent != lastParent) reps++;
|
|
if (reps % 2 == 0)
|
|
{
|
|
lastParent = parent;
|
|
continue;
|
|
}
|
|
|
|
CvInvoke.Rectangle(LayerCache.ImageBgr, CvInvoke.BoundingRectangle(LayerCache.LayerContours[i]),
|
|
new MCvScalar(
|
|
Settings.LayerPreview.ContourBoundsOutlineColor.B,
|
|
Settings.LayerPreview.ContourBoundsOutlineColor.G,
|
|
Settings.LayerPreview.ContourBoundsOutlineColor.R),
|
|
Settings.LayerPreview.ContourBoundsOutlineThickness);
|
|
|
|
lastParent = parent;
|
|
}
|
|
}
|
|
|
|
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
|
|
*/
|
|
using var vec = EmguContours.GetNegativeContours(LayerCache.LayerContours, LayerCache.LayerContourHierarchy);
|
|
if (vec.Size > 0)
|
|
{
|
|
CvInvoke.DrawContours(LayerCache.ImageBgr, vec, -1,
|
|
new MCvScalar(Settings.LayerPreview.HollowOutlineColor.B,
|
|
Settings.LayerPreview.HollowOutlineColor.G,
|
|
Settings.LayerPreview.HollowOutlineColor.R),
|
|
Settings.LayerPreview.HollowOutlineLineThickness);
|
|
}
|
|
}
|
|
|
|
if (_showLayerOutlineCentroids)
|
|
{
|
|
int lastParent = -1;
|
|
uint reps = 0;
|
|
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
|
|
{
|
|
var parent = LayerCache.LayerContourHierarchy[i, EmguContour.HierarchyParent];
|
|
if (parent == -1)
|
|
{
|
|
reps = 0;
|
|
}
|
|
if (parent == -1 || parent != lastParent) reps++;
|
|
if (reps % 2 == 0)
|
|
{
|
|
if (Settings.LayerPreview.CentroidOutlineHollow)
|
|
{
|
|
CvInvoke.Circle(LayerCache.ImageBgr, EmguContour.GetCentroid(LayerCache.LayerContours[i]),
|
|
Settings.LayerPreview.CentroidOutlineDiameter / 2, new MCvScalar(
|
|
Settings.LayerPreview.HollowOutlineColor.B,
|
|
Settings.LayerPreview.HollowOutlineColor.G,
|
|
Settings.LayerPreview.HollowOutlineColor.R), 1, LineType.AntiAlias);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CvInvoke.Circle(LayerCache.ImageBgr, EmguContour.GetCentroid(LayerCache.LayerContours[i]),
|
|
Settings.LayerPreview.CentroidOutlineDiameter / 2, new MCvScalar(
|
|
Settings.LayerPreview.CentroidOutlineColor.B,
|
|
Settings.LayerPreview.CentroidOutlineColor.G,
|
|
Settings.LayerPreview.CentroidOutlineColor.R), -1, LineType.AntiAlias);
|
|
}
|
|
|
|
|
|
|
|
lastParent = parent;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
LayerCache.ImageBgr.DrawPolygon((byte)operationDrawing.BrushShape, operationDrawing.BrushSize / 2, operationDrawing.Location,
|
|
new MCvScalar(color.B, color.G, color.R), operationDrawing.RotationAngle, operationDrawing.Thickness, operationDrawing.LineType);
|
|
/*switch (operationDrawing.BrushShape)
|
|
{
|
|
case PixelDrawing.BrushShapeType.Square:
|
|
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);*/
|
|
LayerCache.ImageBgr.PutTextRotated(operationText.Text, operationText.Location,
|
|
operationText.Font, operationText.FontScale, new MCvScalar(color.B, color.G, color.R),
|
|
operationText.Thickness, operationText.LineType, operationText.Mirror, operationText.LineAlignment, operationText.Angle);
|
|
}
|
|
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;
|
|
|
|
using var vec = EmguContours.GetContoursInside(LayerCache.LayerContours, LayerCache.LayerContourHierarchy, operation.Location);
|
|
if (vec.Size > 0) CvInvoke.DrawContours(LayerCache.ImageBgr, vec, -1, new MCvScalar(color.B, color.G, color.R), -1);
|
|
|
|
/*var hollowGroups = EmguContours.GetPositiveContoursInGroups(LayerCache.LayerContours, LayerCache.LayerContourHierarchy);
|
|
|
|
foreach (var vec in hollowGroups)
|
|
{
|
|
CvInvoke.PutText(LayerCache.ImageBgr, vec.Size.ToString(), vec[0][0], FontFace.HersheyDuplex, 2, new MCvScalar(255, 0, 0), 2);
|
|
CvInvoke.DrawContours(LayerCache.ImageBgr, vec, -1,
|
|
new MCvScalar(color.B, color.G, color.R), -1);
|
|
vec.Dispose();
|
|
}*/
|
|
}
|
|
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 &&
|
|
SlicerFile.IssueManager.Count > 0 &&
|
|
IssuesGrid.SelectedItems.Count > 0 &&
|
|
LayerImageBox.Zoom <=
|
|
AppSettings.CrosshairFadeLevel && // Only draw crosshairs when zoom level is below the configurable crosshair fade threshold.
|
|
!_isPixelEditorActive)
|
|
{
|
|
|
|
|
|
// 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.
|
|
foreach (var issue in SlicerFile.IssueManager.GetIssuesBy(_actualLayer)
|
|
.Where(issue => issue.Parent.Type
|
|
is not MainIssue.IssueType.TouchingBound
|
|
and not MainIssue.IssueType.PrintHeight
|
|
and not MainIssue.IssueType.EmptyLayer))
|
|
{
|
|
DrawCrosshair(issue.BoundingRectangle);
|
|
}
|
|
}
|
|
|
|
if (_showLayerImageFlipped && (_showLayerImageFlippedHorizontally || _showLayerImageFlippedVertically))
|
|
{
|
|
var flipType = FlipType.Both;
|
|
|
|
if (_showLayerImageFlippedHorizontally && _showLayerImageFlippedVertically)
|
|
flipType = FlipType.Both;
|
|
else if (_showLayerImageFlippedHorizontally)
|
|
flipType = FlipType.Horizontal;
|
|
else if (_showLayerImageFlippedVertically)
|
|
flipType = FlipType.Vertical;
|
|
|
|
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 Point(Math.Max(0, rect.X - Settings.LayerPreview.CrosshairMargin - 1),
|
|
rect.Y + rect.Height / 2);
|
|
var endPoint =
|
|
new 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 Point(rect.X + rect.Width / 2,
|
|
Math.Max(0, rect.Y - Settings.LayerPreview.CrosshairMargin - 1));
|
|
endPoint = new 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 - 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(Issue issue)
|
|
{
|
|
if (issue.BoundingRectangle.IsEmpty /*|| issue.PixelsCount == 1*/)
|
|
{
|
|
return GetTransposedRectangle(LayerCache.Layer.BoundingRectangle);
|
|
}
|
|
//return new Rectangle(GetTransposedPoint(issue.FirstPoint, true), new Size(1, 1));
|
|
|
|
return GetTransposedRectangle(issue.BoundingRectangle);
|
|
}
|
|
|
|
public void CenterLayer(int zoomLevel = 0)
|
|
{
|
|
if (zoomLevel < 0) zoomLevel = AppSettings.LockedZoomLevel;
|
|
if (zoomLevel > 0) LayerImageBox.Zoom = zoomLevel;
|
|
LayerImageBox.CenterToImage();
|
|
}
|
|
|
|
/// <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(Issue issue, bool forceRefreshLayer = false)
|
|
{
|
|
if (issue.Type is MainIssue.IssueType.EmptyLayer || issue.BoundingRectangle.IsEmpty)
|
|
{
|
|
ZoomToFit();
|
|
if (forceRefreshLayer) ForceUpdateActualLayer(issue.LayerIndex);
|
|
return;
|
|
}
|
|
|
|
if (Settings.LayerPreview.ZoomIssues ^ (_globalModifiers & KeyModifiers.Alt) != 0)
|
|
{
|
|
CenterLayerAt(GetTransposedIssueBounds(issue), AppSettings.LockedZoomLevel);
|
|
}
|
|
else
|
|
{
|
|
//CenterLayerAt(GetTransposedIssueBounds(issue));
|
|
// If issue is not already visible, center on it and bring it into view.
|
|
// Issues already in view will not be centered, though their color may
|
|
// change and the crosshair may move to reflect active selections.
|
|
|
|
if (!LayerImageBox.GetSourceImageRegion().Contains(GetTransposedIssueBounds(issue).ToAvalonia()))
|
|
{
|
|
CenterAtIssue(issue);
|
|
}
|
|
}
|
|
|
|
if(forceRefreshLayer) ForceUpdateActualLayer(issue.LayerIndex);
|
|
}
|
|
|
|
/// <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(Issue issue)
|
|
{
|
|
if (issue.Parent.Type is MainIssue.IssueType.EmptyLayer || issue.BoundingRectangle.IsEmpty)
|
|
{
|
|
ZoomToFit();
|
|
}
|
|
|
|
if (!issue.BoundingRectangle.IsEmpty)
|
|
{
|
|
CenterLayerAt(GetTransposedIssueBounds(issue));
|
|
}
|
|
}
|
|
|
|
public void ZoomToNormal()
|
|
{
|
|
LayerImageBox.Zoom = (_globalModifiers & KeyModifiers.Shift) != 0 ? AppSettings.LockedZoomLevel : 100;
|
|
LayerImageBox.CenterToImage();
|
|
}
|
|
|
|
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.
|
|
var issues = SlicerFile.IssueManager.GetIssuesBy(_actualLayer);
|
|
for (var i = issues.Length-1; i >= 0; i--)
|
|
{
|
|
if (!GetTransposedIssueBounds(issues[i]).Contains(location)) continue;
|
|
|
|
SuppressIssueGridSelectionEvent = true;
|
|
IssuesGrid.SelectedItem = issues[i].Parent;
|
|
SuppressIssueGridSelectionEvent = false;
|
|
|
|
ZoomToIssue(issues[i], true);
|
|
|
|
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_KeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.Up)
|
|
{
|
|
GoNextLayer();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.Key == Key.Down)
|
|
{
|
|
GoPreviousLayer();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 is Key.LeftShift or Key.RightShift || (e.KeyModifiers & KeyModifiers.Shift) != 0) // Ctrl + Shift
|
|
{
|
|
if (e.Key == Key.R)
|
|
{
|
|
ShowLayerImageRotated = true;
|
|
if (_showLayerImageRotateCwDirection)
|
|
{
|
|
ShowLayerImageRotateCWDirection = false;
|
|
ShowLayerImageRotateCCWDirection = true;
|
|
}
|
|
else
|
|
{
|
|
ShowLayerImageRotateCCWDirection = false;
|
|
ShowLayerImageRotateCWDirection = true;
|
|
}
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.Key == Key.F)
|
|
{
|
|
ShowLayerImageFlipped = true;
|
|
if (!_showLayerImageFlippedHorizontally && !_showLayerImageFlippedVertically)
|
|
{
|
|
ShowLayerImageFlippedHorizontally = true;
|
|
}
|
|
else if (_showLayerImageFlippedHorizontally && !_showLayerImageFlippedVertically)
|
|
{
|
|
ShowLayerImageFlippedHorizontally = false;
|
|
ShowLayerImageFlippedVertically = true;
|
|
}
|
|
else if (!_showLayerImageFlippedHorizontally && _showLayerImageFlippedVertically)
|
|
{
|
|
ShowLayerImageFlippedHorizontally = true;
|
|
}
|
|
else if (_showLayerImageFlippedHorizontally && _showLayerImageFlippedVertically)
|
|
{
|
|
ShowLayerImageFlippedVertically = false;
|
|
}
|
|
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.Key == Key.B)
|
|
{
|
|
SelectModelVolumeRoi();
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
if (e.Key is Key.D0 or 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)
|
|
{
|
|
var realLocation = GetTransposedPoint(location);
|
|
unsafe
|
|
{
|
|
var brightness = LayerCache.ImageSpan[LayerCache.Image.GetPixelPos(realLocation)];
|
|
LayerPixelPicker.Set(realLocation, brightness, SlicerFile.PixelToDisplayPosition(realLocation, 2));
|
|
}
|
|
|
|
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);
|
|
|
|
for (int i = LayerCache.LayerContours.Size-1; i >= 0; i--)
|
|
{
|
|
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], point, false) < 0) continue;
|
|
var rectangle = 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();
|
|
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 = roiRectangle;
|
|
|
|
return (uint)rectangles.Count;
|
|
}
|
|
|
|
public bool SelectObjectMask(Point location)
|
|
{
|
|
var point = GetTransposedPoint(location);
|
|
|
|
using var vec = EmguContours.GetContoursInside(LayerCache.LayerContours, LayerCache.LayerContourHierarchy, point, (_globalModifiers & KeyModifiers.Control) != 0);
|
|
AddMaskPoints(vec.ToArrayOfArray(), false);
|
|
return vec.Size > 0;
|
|
}
|
|
|
|
public void OnLayerPixelPickerClicked()
|
|
{
|
|
if (!LayerPixelPicker.IsSet) return;
|
|
CenterLayerAt(GetTransposedPoint(LayerPixelPicker.Location, true), -1);
|
|
}
|
|
|
|
public async void SaveCurrentLayerImage()
|
|
{
|
|
if (!IsFileLoaded) return;
|
|
SaveFileDialog dialog = new()
|
|
{
|
|
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);
|
|
}
|
|
|
|
public async void SaveCurrentROIImage()
|
|
{
|
|
if (!IsFileLoaded || !LayerImageBox.HaveSelection) return;
|
|
SaveFileDialog dialog = new()
|
|
{
|
|
Filters = Helpers.PngFileFilter,
|
|
DefaultExtension = ".png",
|
|
InitialFileName = $"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_layer{ActualLayer}_ROI.png"
|
|
};
|
|
|
|
var result = await dialog.ShowAsync(this);
|
|
if (string.IsNullOrEmpty(result)) return;
|
|
|
|
LayerImageBox.GetSelectedBitmap()?.Save(result);
|
|
}
|
|
|
|
const byte _pixelEditorCursorMinDiamater = 10;
|
|
public void UpdatePixelEditorCursor()
|
|
{
|
|
Mat cursor = null;
|
|
var _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)
|
|
{
|
|
if ((byte)DrawingPixelDrawing.BrushShape >= 1)
|
|
{
|
|
int cursorSize = DrawingPixelDrawing.BrushSize;
|
|
if (DrawingPixelDrawing.Thickness > 1)
|
|
{
|
|
cursorSize += DrawingPixelDrawing.Thickness;
|
|
}
|
|
cursor = EmguExtensions.InitMat(new Size(cursorSize, cursorSize), 4);
|
|
|
|
cursor.DrawPolygon((byte) DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.BrushSize / 2, cursor.Size.ToPoint().Half(),
|
|
_pixelEditorCursorColor, DrawingPixelDrawing.RotationAngle, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.LineType);
|
|
|
|
if (DrawingPixelDrawing.BrushShape != PixelDrawing.BrushShapeType.Circle)
|
|
{
|
|
if (_showLayerImageFlipped && (_showLayerImageFlippedHorizontally || _showLayerImageFlippedVertically))
|
|
{
|
|
var flipType = FlipType.Both;
|
|
|
|
if (_showLayerImageFlippedHorizontally && _showLayerImageFlippedVertically)
|
|
flipType = FlipType.Both;
|
|
else if (_showLayerImageFlippedHorizontally)
|
|
flipType = FlipType.Horizontal;
|
|
else if (_showLayerImageFlippedVertically)
|
|
flipType = FlipType.Vertical;
|
|
|
|
CvInvoke.Flip(cursor, cursor, flipType);
|
|
}
|
|
|
|
if (_showLayerImageRotated)
|
|
{
|
|
CvInvoke.Rotate(cursor, cursor,
|
|
_showLayerImageRotateCcwDirection
|
|
? RotateFlags.Rotate90CounterClockwise
|
|
: RotateFlags.Rotate90Clockwise);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*switch (DrawingPixelDrawing.BrushShape)
|
|
{
|
|
case PixelDrawing.BrushShapeType.Square:
|
|
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);
|
|
var size = EmguExtensions.GetTextSizeExtended(text, DrawingPixelText.Font, DrawingPixelText.FontScale, DrawingPixelText.Thickness, ref baseLine, DrawingPixelText.LineAlignment);
|
|
//var rotatedSize = size.Rotate(DrawingPixelText.Angle);
|
|
//Point point = (rotatedSize.Inflate(rotatedSize)).Rotate(DrawingPixelText.Angle, rotatedSize.ToPoint());
|
|
cursor = EmguExtensions.InitMat(size.Add(), 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);
|
|
|
|
cursor.PutTextExtended(text, size.ToPoint(), DrawingPixelText.Font, DrawingPixelText.FontScale, _pixelEditorCursorColor, DrawingPixelText.Thickness, DrawingPixelText.LineType, DrawingPixelText.Mirror, DrawingPixelText.LineAlignment);
|
|
//CvInvoke.PutText(cursor, text, size.ToPoint(), DrawingPixelText.Font, DrawingPixelText.FontScale, _pixelEditorCursorColor, DrawingPixelText.Thickness, DrawingPixelText.LineType, DrawingPixelText.Mirror);
|
|
cursor.RotateAdjustBounds(DrawingPixelText.Angle);
|
|
//cursor.Rotate(DrawingPixelText.Angle);
|
|
//cursor.PutTextRotated(text, cursor.Size.ToPoint().Half(), DrawingPixelText.Font, DrawingPixelText.FontScale, _pixelEditorCursorColor, DrawingPixelText.Thickness, DrawingPixelText.LineType, DrawingPixelText.Mirror, DrawingPixelText.Angle);
|
|
if (_showLayerImageFlipped && (_showLayerImageFlippedHorizontally || _showLayerImageFlippedVertically))
|
|
{
|
|
var flipType = FlipType.Both;
|
|
|
|
if (_showLayerImageFlippedHorizontally && _showLayerImageFlippedVertically)
|
|
flipType = FlipType.Both;
|
|
else if (_showLayerImageFlippedHorizontally)
|
|
flipType = FlipType.Horizontal;
|
|
else if (_showLayerImageFlippedVertically)
|
|
flipType = FlipType.Vertical;
|
|
|
|
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);
|
|
}*/
|
|
}
|
|
} |