/* * GNU AFFERO GENERAL PUBLIC LICENSE * Version 3, 19 November 2007 * Copyright (C) 2007 Free Software Foundation, Inc. * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Threading; using Emgu.CV; using Emgu.CV.Structure; using Emgu.CV.Util; using MessageBox.Avalonia.Enums; using UVtools.Core; using UVtools.Core.Extensions; using UVtools.Core.Operations; using UVtools.WPF.Extensions; using Brushes = Avalonia.Media.Brushes; namespace UVtools.WPF { public partial class MainWindow { #region Members private RangeObservableCollection _issues = new(); private bool _firstTimeOnIssues = true; public DataGrid IssuesGrid; private int _issueSelectedIndex = -1; #endregion #region Properties public RangeObservableCollection Issues { get => _issues; private set => RaiseAndSetIfChanged(ref _issues, value); } public readonly List IgnoredIssues = new(); public bool IssueCanGoPrevious => Issues.Count > 0 && _issueSelectedIndex > 0; public bool IssueCanGoNext => Issues.Count > 0 && _issueSelectedIndex < Issues.Count - 1; #endregion #region Methods public void InitIssues() { IssuesGrid = this.FindControl("IssuesGrid"); IssuesGrid.CellPointerPressed += IssuesGridOnCellPointerPressed; IssuesGrid.SelectionChanged += IssuesGridOnSelectionChanged; IssuesGrid.KeyUp += IssuesGridOnKeyUp; Issues.CollectionChanged += (sender, e) => { UpdateLayerTrackerHighlightIssues(); }; } public void IssueGoPrevious() { if (!IssueCanGoPrevious) return; IssueSelectedIndex--; } public void IssueGoNext() { if (!IssueCanGoNext) return; IssueSelectedIndex++; } public async void OnClickIssueRemove() { if (IssuesGrid.SelectedItems.Count == 0) return; if (await this.MessageBoxQuestion($"Are you sure you want to remove all selected {IssuesGrid.SelectedItems.Count} issues?\n\n" + "Warning: Removing an island can cause other issues to appear if there is material present in the layers above it.\n" + "Always check previous and next layers before performing an island removal.", $"Remove {IssuesGrid.SelectedItems.Count} Issues?") != ButtonResult.Yes) return; Dictionary> processIssues = new(); List layersRemove = new(); foreach (LayerIssue issue in IssuesGrid.SelectedItems) { if (issue.Type == LayerIssue.IssueType.TouchingBound) continue; if (!processIssues.TryGetValue(issue.Layer.Index, out var issueList)) { issueList = new List(); processIssues.Add(issue.Layer.Index, issueList); } issueList.Add(issue); if (issue.Type == LayerIssue.IssueType.EmptyLayer) { layersRemove.Add(issue.Layer.Index); } } IsGUIEnabled = false; ShowProgressWindow("Removing selected issues", false); Clipboard.Snapshot(); var task = await Task.Factory.StartNew(() => { Progress.Reset("Removing selected issues", (uint)processIssues.Count); bool result = false; try { Parallel.ForEach(processIssues, layerIssues => { if (Progress.Token.IsCancellationRequested) return; using (var image = SlicerFile[layerIssues.Key].LayerMat) { var bytes = image.GetDataSpan(); bool edited = false; foreach (var issue in layerIssues.Value) { if (issue.Type == LayerIssue.IssueType.Island) { foreach (var pixel in issue) { bytes[image.GetPixelPos(pixel.X, pixel.Y)] = 0; } edited = true; } else if (issue.Type == LayerIssue.IssueType.ResinTrap) { using (var contours = new VectorOfVectorOfPoint(new VectorOfPoint(issue.Pixels))) { CvInvoke.DrawContours(image, contours, -1, new MCvScalar(255), -1); } edited = true; } } if (edited) { SlicerFile[layerIssues.Key].LayerMat = image; result = true; } } Progress.LockAndIncrement(); }); if (layersRemove.Count > 0) { OperationLayerRemove.RemoveLayers(SlicerFile, layersRemove); result = true; } } catch (Exception ex) { Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(ex.ToString(), "Removal failed")); } return result; }); IsGUIEnabled = true; if (!task) { Clipboard.RestoreSnapshot(); return; } var whiteListLayers = new List(); // Update GUI var issueRemoveList = new List(); foreach (LayerIssue issue in IssuesGrid.SelectedItems) { if (issue.Type != LayerIssue.IssueType.Island && issue.Type != LayerIssue.IssueType.ResinTrap && issue.Type != LayerIssue.IssueType.EmptyLayer) continue; issueRemoveList.Add(issue); if (issue.Type == LayerIssue.IssueType.Island) { var nextLayer = issue.Layer.Index + 1; if (nextLayer >= SlicerFile.LayerCount) continue; if (whiteListLayers.Contains(nextLayer)) continue; whiteListLayers.Add(nextLayer); } //Issues.Remove(issue); } Clipboard.Clip($"Manually removed {issueRemoveList.Count} issues"); Issues.RemoveRange(issueRemoveList); if (layersRemove.Count > 0) { ResetDataContext(); } if (Settings.PixelEditor.PartialUpdateIslandsOnEditing) { await UpdateIslandsOverhangs(whiteListLayers); } ShowLayer(); // It will call latter so its a extra call CanSave = true; } public async void OnClickIssueIgnore() { if ((_globalModifiers & KeyModifiers.Alt) != 0) { if(IgnoredIssues.Count == 0) return; if (await this.MessageBoxQuestion( $"Are you sure you want to re-enable {IgnoredIssues.Count} ignored issues?\n" + "A full re-detect will be required to get the ignored issues.\n", $"Re-enable {IgnoredIssues.Count} Issues?") != ButtonResult.Yes) return; IgnoredIssues.Clear(); return; } if (IssuesGrid.SelectedItems.Count == 0) return; if (await this.MessageBoxQuestion( $"Are you sure you want to hide and ignore all selected {IssuesGrid.SelectedItems.Count} issues?\n" + "The ignored issues won't be re-detected.\n", $"Ignore {IssuesGrid.SelectedItems.Count} Issues?") != ButtonResult.Yes) return; var list = IssuesGrid.SelectedItems.Cast().ToArray(); IgnoredIssues.AddRange(list); IssuesGrid.SelectedItems.Clear(); Issues.RemoveRange(list); ShowLayer(); } private async Task UpdateIslandsOverhangs(List whiteListLayers) { if (whiteListLayers.Count == 0) return; var islandConfig = GetIslandDetectionConfiguration(); var overhangConfig = GetOverhangDetectionConfiguration(); var resinTrapConfig = new ResinTrapDetectionConfiguration(false); var touchingBoundConfig = new TouchingBoundDetectionConfiguration(false); var printHeightConfig = new PrintHeightDetectionConfiguration(false); islandConfig.Enabled = true; islandConfig.WhiteListLayers = whiteListLayers; overhangConfig.Enabled = true; overhangConfig.WhiteListLayers = whiteListLayers; IsGUIEnabled = false; ShowProgressWindow("Updating Issues"); var issueList = Issues.ToList(); issueList.RemoveAll(issue => islandConfig.WhiteListLayers.Contains(issue.LayerIndex) && (issue.Type == LayerIssue.IssueType.Island || issue.Type == LayerIssue.IssueType.Overhang)); /*foreach (var layerIndex in islandConfig.WhiteListLayers) { issueList.RemoveAll(issue => issue.LayerIndex == layerIndex && (issue.Type == LayerIssue.IssueType.Island || issue.Type == LayerIssue.IssueType.Overhang)); }*/ var resultIssues = await Task.Factory.StartNew(() => { try { var issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapConfig, touchingBoundConfig, printHeightConfig, false, IgnoredIssues, Progress); issues.RemoveAll(issue => issue.Type != LayerIssue.IssueType.Island && issue.Type != LayerIssue.IssueType.Overhang); // Remove all non islands and overhangs return issues; } catch (OperationCanceledException) { } catch (Exception ex) { Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(ex.ToString(), "Error while trying to compute issues")); } return null; }); IsGUIEnabled = true; if (resultIssues is not null && resultIssues.Count > 0) issueList.AddRange(resultIssues); issueList = issueList.OrderBy(issue => issue.Type) .ThenBy(issue => issue.LayerIndex) .ThenBy(issue => issue.PixelsCount).ToList(); Issues.ReplaceCollection(issueList); } public int IssueSelectedIndex { get => _issueSelectedIndex; set { if (!RaiseAndSetIfChanged(ref _issueSelectedIndex, value)) return; RaisePropertyChanged(nameof(IssueSelectedIndexStr)); RaisePropertyChanged(nameof(IssueCanGoPrevious)); RaisePropertyChanged(nameof(IssueCanGoNext)); } } public string IssueSelectedIndexStr => (_issueSelectedIndex + 1).ToString().PadLeft(Issues.Count.ToString().Length, '0'); private void IssuesGridOnSelectionChanged(object? sender, SelectionChangedEventArgs e) { if (DataContext is null) return; if (IssuesGrid.SelectedItem is not LayerIssue issue) { ShowLayer(); return; } if (issue.Type is LayerIssue.IssueType.TouchingBound or LayerIssue.IssueType.EmptyLayer || (issue.X == -1 && issue.Y == -1)) { ZoomToFit(); } else if (issue.X >= 0 && issue.Y >= 0) { if (Settings.LayerPreview.ZoomIssues ^ (_globalModifiers & KeyModifiers.Alt) != 0) { ZoomToIssue(issue); } 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); } } } ForceUpdateActualLayer(issue.LayerIndex); } private void IssuesGridOnCellPointerPressed(object? sender, DataGridCellPointerPressedEventArgs e) { if (e.PointerPressedEventArgs.ClickCount == 2) return; if (IssuesGrid.SelectedItem is not LayerIssue) return; // Double clicking an issue will center and zoom into the // selected issue. Left click on an issue will zoom to fit. var pointer = e.PointerPressedEventArgs.GetCurrentPoint(IssuesGrid); if (pointer.Properties.IsRightButtonPressed) { ZoomToFit(); return; } //ForceUpdateActualLayer(issue.LayerIndex); } private void IssuesGridOnKeyUp(object? sender, KeyEventArgs e) { switch (e.Key) { case Key.Escape: IssuesGrid.SelectedItems.Clear(); break; case Key.Multiply: var selectedItems = IssuesGrid.SelectedItems.OfType().ToList(); IssuesGrid.SelectedItems.Clear(); foreach (LayerIssue item in Issues) { if (!selectedItems.Contains(item)) IssuesGrid.SelectedItems.Add(item); } break; case Key.Delete: OnClickIssueRemove(); break; } } public async void OnClickRepairIssues() { await ShowRunOperation(typeof(OperationRepairLayers)); } public async Task OnClickDetectIssues() { if (!IsFileLoaded) return; await ComputeIssues( GetIslandDetectionConfiguration(), GetOverhangDetectionConfiguration(), GetResinTrapDetectionConfiguration(), GetTouchingBoundsDetectionConfiguration(), GetPrintHeightDetectionConfiguration(), Settings.Issues.ComputeEmptyLayers); } private async Task ComputeIssues(IslandDetectionConfiguration islandConfig = null, OverhangDetectionConfiguration overhangConfig = null, ResinTrapDetectionConfiguration resinTrapConfig = null, TouchingBoundDetectionConfiguration touchingBoundConfig = null, PrintHeightDetectionConfiguration printHeightConfig = null, bool emptyLayersConfig = true) { Issues.Clear(); IsGUIEnabled = false; ShowProgressWindow("Computing Issues"); var resultIssues = await Task.Factory.StartNew(() => { try { var issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapConfig, touchingBoundConfig, printHeightConfig, emptyLayersConfig, IgnoredIssues, Progress); return issues; } catch (OperationCanceledException) { } catch (Exception ex) { Dispatcher.UIThread.InvokeAsync(async () => await this.MessageBoxError(ex.ToString(), "Error while trying compute issues")); } return null; }); IsGUIEnabled = true; if (resultIssues is null) { return; } Issues.AddRange(resultIssues); ShowLayer(); RaisePropertyChanged(nameof(IssueSelectedIndexStr)); RaisePropertyChanged(nameof(IssueCanGoPrevious)); RaisePropertyChanged(nameof(IssueCanGoNext)); } public Dictionary GetIssuesCountPerLayer() { if (Issues is null || Issues.Count == 0) return null; Dictionary layerIndexIssueCount = new(); foreach (var issue in Issues) { if (!layerIndexIssueCount.ContainsKey(issue.LayerIndex)) { layerIndexIssueCount.Add(issue.LayerIndex, 1); } else { layerIndexIssueCount[issue.LayerIndex]++; } } return layerIndexIssueCount; } void UpdateLayerTrackerHighlightIssues() { _issuesSliderCanvas.Children.Clear(); var issuesCountPerLayer = GetIssuesCountPerLayer(); if (issuesCountPerLayer is null) { return; } //var tickFrequencySize = LayerSlider.Track.Bounds.Height * LayerSlider.TickFrequency / (LayerSlider.Maximum - LayerSlider.Minimum); var tickFrequencySize = _issuesSliderCanvas.Bounds.Height * LayerSlider.TickFrequency / (LayerSlider.Maximum - LayerSlider.Minimum); foreach (var value in issuesCountPerLayer) { var yPos = tickFrequencySize * value.Key; var line = new Line{StrokeThickness = 1, Stroke = Brushes.Red, EndPoint = new Avalonia.Point(_issuesSliderCanvas.Width, 0)}; _issuesSliderCanvas.Children.Add(line); Canvas.SetBottom(line, yPos); } } public void IssuesClear(bool clearIgnored = true) { Issues.Clear(); if(clearIgnored) IgnoredIssues.Clear(); } public IslandDetectionConfiguration GetIslandDetectionConfiguration(bool enable) { return new() { Enabled = enable, EnhancedDetection = Settings.Issues.IslandEnhancedDetection, AllowDiagonalBonds = Settings.Issues.IslandAllowDiagonalBonds, BinaryThreshold = Settings.Issues.IslandBinaryThreshold, RequiredAreaToProcessCheck = Settings.Issues.IslandRequiredAreaToProcessCheck, RequiredPixelBrightnessToProcessCheck = Settings.Issues.IslandRequiredPixelBrightnessToProcessCheck, RequiredPixelsToSupportMultiplier = Settings.Issues.IslandRequiredPixelsToSupportMultiplier, RequiredPixelsToSupport = Settings.Issues.IslandRequiredPixelsToSupport, RequiredPixelBrightnessToSupport = Settings.Issues.IslandRequiredPixelBrightnessToSupport }; } public IslandDetectionConfiguration GetIslandDetectionConfiguration() => GetIslandDetectionConfiguration(Settings.Issues.ComputeIslands); public OverhangDetectionConfiguration GetOverhangDetectionConfiguration(bool enable) { return new() { Enabled = enable, IndependentFromIslands = Settings.Issues.OverhangIndependentFromIslands, ErodeIterations = Settings.Issues.OverhangErodeIterations, }; } public OverhangDetectionConfiguration GetOverhangDetectionConfiguration() => GetOverhangDetectionConfiguration(Settings.Issues.ComputeOverhangs); public ResinTrapDetectionConfiguration GetResinTrapDetectionConfiguration(bool enable) { return new() { Enabled = enable, BinaryThreshold = Settings.Issues.ResinTrapBinaryThreshold, RequiredAreaToProcessCheck = Settings.Issues.ResinTrapRequiredAreaToProcessCheck, RequiredBlackPixelsToDrain = Settings.Issues.ResinTrapRequiredBlackPixelsToDrain, MaximumPixelBrightnessToDrain = Settings.Issues.ResinTrapMaximumPixelBrightnessToDrain }; } public ResinTrapDetectionConfiguration GetResinTrapDetectionConfiguration() => GetResinTrapDetectionConfiguration(Settings.Issues.ComputeResinTraps); public TouchingBoundDetectionConfiguration GetTouchingBoundsDetectionConfiguration(bool enable) { return new() { Enabled = enable, MinimumPixelBrightness = UserSettings.Instance.Issues.TouchingBoundMinimumPixelBrightness, MarginLeft = UserSettings.Instance.Issues.TouchingBoundMarginLeft, MarginTop = UserSettings.Instance.Issues.TouchingBoundMarginTop, MarginRight = UserSettings.Instance.Issues.TouchingBoundMarginRight, MarginBottom = UserSettings.Instance.Issues.TouchingBoundMarginBottom, }; } public TouchingBoundDetectionConfiguration GetTouchingBoundsDetectionConfiguration() => GetTouchingBoundsDetectionConfiguration(Settings.Issues.ComputeTouchingBounds); public PrintHeightDetectionConfiguration GetPrintHeightDetectionConfiguration(bool enable) { return new () { Enabled = enable, Offset = (float) Settings.Issues.PrintHeightOffset }; } public PrintHeightDetectionConfiguration GetPrintHeightDetectionConfiguration() => GetPrintHeightDetectionConfiguration(Settings.Issues.ComputePrintHeight); #endregion } }