mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-08 17:42:31 +02:00
4397 lines
177 KiB
C#
4397 lines
177 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;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Drawing.Imaging;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using BrightIdeasSoftware;
|
|
using Cyotek.Windows.Forms;
|
|
using Emgu.CV;
|
|
using Emgu.CV.CvEnum;
|
|
using Emgu.CV.Structure;
|
|
using Emgu.CV.Util;
|
|
using UVtools.Core;
|
|
using UVtools.Core.Extensions;
|
|
using UVtools.Core.FileFormats;
|
|
using UVtools.Core.Operations;
|
|
using UVtools.Core.PixelEditor;
|
|
using UVtools.GUI.Controls;
|
|
using UVtools.GUI.Controls.Tools;
|
|
using UVtools.GUI.Extensions;
|
|
using UVtools.GUI.Forms;
|
|
using UVtools.GUI.Properties;
|
|
|
|
namespace UVtools.GUI
|
|
{
|
|
public partial class FrmMain : Form
|
|
{
|
|
#region Enums
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public static readonly OperationMenuItem[] MenuTools = {
|
|
new OperationMenuItem(new OperationEditParameters(), Resources.Wrench_16x16),
|
|
new OperationMenuItem(new OperationRepairLayers(), Resources.toolbox_16x16),
|
|
new OperationMenuItem(new OperationMove(), Resources.move_16x16),
|
|
new OperationMenuItem(new OperationResize(), Resources.crop_16x16),
|
|
new OperationMenuItem(new OperationFlip(), Resources.flip_16x16),
|
|
new OperationMenuItem(new OperationRotate(), Resources.sync_16x16),
|
|
new OperationMenuItem(new OperationSolidify(), Resources.square_solid_16x16),
|
|
new OperationMenuItem(new OperationMorph(), Resources.Geometry_16x16),
|
|
new OperationMenuItem(new OperationMask(), Resources.mask_16x16),
|
|
new OperationMenuItem(new OperationPixelDimming(), Resources.pixel_16x16),
|
|
new OperationMenuItem(new OperationThreshold(), Resources.th_16x16),
|
|
new OperationMenuItem(new OperationBlur(), Resources.blur_16x16),
|
|
new OperationMenuItem(new OperationPattern(), Resources.pattern_16x16),
|
|
new OperationMenuItem(new OperationLayerReHeight(), Resources.ladder_16x16),
|
|
new OperationMenuItem(new OperationChangeResolution(), Resources.resize_16x16),
|
|
};
|
|
|
|
public static readonly OperationMenuItem[] LayerActions = {
|
|
new OperationMenuItem(new OperationLayerImport(), Resources.file_import_16x16),
|
|
new OperationMenuItem(new OperationLayerClone(), Resources.copy_16x16),
|
|
new OperationMenuItem(new OperationLayerRemove(), Resources.trash_16x16),
|
|
};
|
|
|
|
public FrmLoading FrmLoading { get; }
|
|
|
|
public Rectangle ROI
|
|
{
|
|
get
|
|
{
|
|
var rectangleF = pbLayer.SelectionRegion;
|
|
return rectangleF.IsEmpty ? Rectangle.Empty : GetTransposedRectangle(rectangleF, false);
|
|
}
|
|
set => pbLayer.SelectionRegion = value;
|
|
}
|
|
|
|
public static FileFormat SlicerFile
|
|
{
|
|
get => Program.SlicerFile;
|
|
set => Program.SlicerFile = value;
|
|
}
|
|
|
|
public LayerCache LayerCache { get; set; } = new LayerCache();
|
|
|
|
public uint ActualLayer { get; set; }
|
|
|
|
public Mat ActualLayerImage { get; private set; }
|
|
|
|
public Mat ActualLayerImageBgr { get; private set; } = new Mat();
|
|
|
|
public List<LayerIssue> Issues { get; set; }
|
|
|
|
public int TotalIssues => Issues?.Count ?? 0;
|
|
|
|
public bool IsChangingLayer { get; set; }
|
|
|
|
// Supported ZoomLevels for Layer Preview.
|
|
// These settings eliminate very small zoom factors from the ImageBox default values,
|
|
// while ensuring that 4K/5K build plates can still easily fit on screen.
|
|
public static readonly int[] ZoomLevels =
|
|
{20, 25, 30, 50, 75, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600};
|
|
|
|
// Count of the bottom portion of the full zoom range which will be skipped for
|
|
// assignable actions such as auto-zoom level, and crosshair fade level. If values
|
|
// are added/removed from ZoomLevels above, this value may also need to be adjusted.
|
|
public static int ZoomLevelSkipCount { get; } = 7; // Start at 2x which is index 7.
|
|
|
|
/// <summary>
|
|
/// Returns the zoom level at which the crosshairs will fade and no longer be displayed
|
|
/// </summary>
|
|
public int CrosshairFadeLevel => ZoomLevels[Settings.Default.CrosshairFadeLevel + ZoomLevelSkipCount];
|
|
|
|
/// <summary>
|
|
/// Returns the zoom level that will be used for autozoom actions
|
|
/// </summary>
|
|
private int LockedZoomLevel { get; set; } = 1200;
|
|
|
|
|
|
/// <summary>
|
|
/// Minimum Zoom level to which autozoom can be locked.
|
|
/// </summary>
|
|
private static int MinLockedZoomLevel { get; } = 200;
|
|
|
|
public PixelHistory PixelHistory { get; } = new PixelHistory();
|
|
|
|
// Track last open tab for when PixelEditor tab is removed.
|
|
public TabPage ControlLeftLastTab { get; set; }
|
|
|
|
public uint SavesCount { get; set; }
|
|
|
|
private bool SupressLayerZoomEvent { get; set; }
|
|
|
|
public bool IsLogVerbose => btnLogVerbose.Checked;
|
|
|
|
private Cursor pixelEditCursor;
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
public FrmMain()
|
|
{
|
|
//SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
|
|
InitializeComponent();
|
|
FrmLoading = new FrmLoading();
|
|
Program.SetAllControlsFontSize(Controls, 11);
|
|
Program.SetAllControlsFontSize(FrmLoading.Controls, 11);
|
|
|
|
if (Settings.Default.UpgradeSettings)
|
|
{
|
|
Settings.Default.Upgrade();
|
|
Settings.Default.UpgradeSettings = false;
|
|
Settings.Default.Save();
|
|
}
|
|
|
|
ControlLeftLastTab = tbpThumbnailsAndInfo;
|
|
Clear();
|
|
|
|
btnLayerImageLayerDifference.Checked = Settings.Default.LayerDifferenceDefault;
|
|
tsIssuesDetectIslands.Checked = Settings.Default.ComputeIslands;
|
|
tsIssuesDetectResinTraps.Checked = Settings.Default.ComputeResinTraps;
|
|
tsIssuesDetectTouchingBounds.Checked = Settings.Default.ComputeTouchingBounds;
|
|
tsIssuesDetectEmptyLayers.Checked = Settings.Default.ComputeEmptyLayers;
|
|
btnLayerImageLayerOutlinePrintVolumeBounds.Checked = Settings.Default.OutlinePrintVolumeBounds;
|
|
btnLayerImageLayerOutlineLayerBounds.Checked = Settings.Default.OutlineLayerBounds;
|
|
btnLayerImageLayerOutlineHollowAreas.Checked = Settings.Default.OutlineHollowAreas;
|
|
lbLayerImageTooltipOverlay.TransparentBackColor = Settings.Default.LayerTooltipOverlayColor;
|
|
lbLayerImageTooltipOverlay.Opacity = Settings.Default.LayerTooltipOverlayOpacity;
|
|
|
|
|
|
// Initialize pbLayer zoom levels to use the discrete factors from ZoomLevels
|
|
pbLayer.ZoomLevels = new ZoomLevelCollection(ZoomLevels);
|
|
// Initialize the zoom level used for autozoom based on the stored default settings.
|
|
LockedZoomLevel = ZoomLevels[Settings.Default.ZoomLockLevel + ZoomLevelSkipCount];
|
|
|
|
if (Settings.Default.StartMaximized || Width >= Screen.FromControl(this).WorkingArea.Width ||
|
|
Height >= Screen.FromControl(this).WorkingArea.Height)
|
|
{
|
|
WindowState = FormWindowState.Maximized;
|
|
}
|
|
|
|
|
|
DragEnter += (s, e) =>
|
|
{
|
|
if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy;
|
|
};
|
|
DragDrop += (s, e) => { ProcessFile((string[]) e.Data.GetData(DataFormats.FileDrop)); };
|
|
|
|
foreach (var menuItem in MenuTools)
|
|
{
|
|
var item = new ToolStripMenuItem(menuItem.Operation.Title)
|
|
{
|
|
AutoToolTip = true,
|
|
ToolTipText = menuItem.Operation.Description,
|
|
Tag = menuItem.Operation,
|
|
Image = menuItem.Icon
|
|
};
|
|
item.Click += EventClick;
|
|
menuTools.DropDownItems.Add(item);
|
|
}
|
|
foreach (var menuItem in LayerActions)
|
|
{
|
|
var item = new ToolStripMenuItem(menuItem.Operation.Title)
|
|
{
|
|
ToolTipText = menuItem.Operation.Description,
|
|
Tag = menuItem.Operation,
|
|
AutoToolTip = true,
|
|
Image = menuItem.Icon
|
|
};
|
|
item.Click += EventClick;
|
|
btnLayerImageActions.DropDownItems.Add(item);
|
|
}
|
|
|
|
foreach (PixelDrawing.BrushShapeType brushShape in (PixelDrawing.BrushShapeType[]) Enum.GetValues(
|
|
typeof(PixelDrawing.BrushShapeType)))
|
|
{
|
|
cbPixelEditorDrawingBrushShape.Items.Add(brushShape);
|
|
}
|
|
|
|
cbPixelEditorDrawingBrushShape.SelectedIndex = 0;
|
|
|
|
foreach (LineType lineType in (LineType[]) Enum.GetValues(
|
|
typeof(LineType)))
|
|
{
|
|
if (lineType == LineType.Filled) continue;
|
|
cbPixelEditorDrawingLineType.Items.Add(lineType);
|
|
cbPixelEditorTextLineType.Items.Add(lineType);
|
|
}
|
|
|
|
cbPixelEditorDrawingLineType.SelectedItem = LineType.AntiAlias;
|
|
cbPixelEditorTextLineType.SelectedItem = LineType.AntiAlias;
|
|
|
|
foreach (FontFace font in (FontFace[]) Enum.GetValues(
|
|
typeof(FontFace)))
|
|
{
|
|
cbPixelEditorTextFontFace.Items.Add(font);
|
|
}
|
|
|
|
cbPixelEditorTextFontFace.SelectedIndex = 0;
|
|
pixelEditCursor = CursorResourceLoader.LoadEmbeddedCursor(Properties.Resources.pixel_edit);
|
|
|
|
|
|
tbLayer.MouseWheel += TbLayerOnMouseWheel;
|
|
|
|
flvIssues.ShowGroups = btnIssueGroup.Checked;
|
|
flvIssues.AlwaysGroupByColumn = flvIssuesColType;
|
|
flvIssues.PrimarySortColumn = flvIssuesColType;
|
|
flvIssues.PrimarySortOrder = SortOrder.Ascending;
|
|
flvIssues.SecondarySortColumn = flvIssuesColLayerIndex;
|
|
flvIssues.SecondarySortOrder = SortOrder.Ascending;
|
|
|
|
Generator.GenerateColumns(lvLog, typeof(LogItem), true);
|
|
lvLog.PrimarySortColumn = lvLog.AllColumns[0];
|
|
lvLog.PrimarySortOrder = SortOrder.Descending;
|
|
|
|
flvPixelHistory.ShowGroups = true;
|
|
flvPixelHistory.AlwaysGroupByColumn = flvPixelHistoryColOperation;
|
|
flvPixelHistory.PrimarySortColumn = flvPixelHistoryColNumber;
|
|
flvPixelHistory.PrimarySortOrder = SortOrder.Descending;
|
|
|
|
Generator.GenerateColumns(flvProperties, typeof(SlicerPropertyItem), true);
|
|
flvProperties.ShowGroups = true;
|
|
flvProperties.AlwaysGroupByColumn = flvProperties.AllColumns[2];
|
|
|
|
if (Settings.Default.CheckForUpdatesOnStartup)
|
|
{
|
|
Task.Factory.StartNew(AppLoadTask);
|
|
}
|
|
}
|
|
|
|
private void AppLoadTask()
|
|
{
|
|
try
|
|
{
|
|
using (WebClient client = new WebClient())
|
|
{
|
|
string htmlCode = client.DownloadString($"{About.Website}/releases");
|
|
const string searchFor = "/releases/tag/";
|
|
var startIndex = htmlCode.IndexOf(searchFor, StringComparison.InvariantCultureIgnoreCase) +
|
|
searchFor.Length;
|
|
var endIndex = htmlCode.IndexOf("\"", startIndex, StringComparison.InvariantCultureIgnoreCase);
|
|
var version = htmlCode.Substring(startIndex, endIndex - startIndex);
|
|
if (string.Compare(version, $"v{FrmAbout.AssemblyVersion}", StringComparison.OrdinalIgnoreCase) > 0)
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
menuNewVersion.Text = $"New version {version} is available!";
|
|
menuNewVersion.Tag = $"{About.Website}/releases/tag/{version}";
|
|
menuNewVersion.Visible = true;
|
|
});
|
|
}
|
|
|
|
//Debug.WriteLine(version);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.WriteLine(e.Message);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Overrides
|
|
|
|
protected override CreateParams CreateParams
|
|
{
|
|
get
|
|
{
|
|
CreateParams cp = base.CreateParams;
|
|
cp.ExStyle |= 0x02000000;
|
|
return cp;
|
|
}
|
|
}
|
|
|
|
protected override void OnShown(EventArgs e)
|
|
{
|
|
base.OnShown(e);
|
|
AddLog("UVtools Start");
|
|
ProcessFile(Program.Args);
|
|
}
|
|
|
|
protected override void OnKeyPress(KeyPressEventArgs e)
|
|
{
|
|
if (SlicerFile is null || IsChangingLayer)
|
|
{
|
|
return;
|
|
}
|
|
if (e.KeyChar == 's')
|
|
{
|
|
if (!CanGlobalHotKey) return;
|
|
ShowLayer(false);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyChar == 'w')
|
|
{
|
|
if (!CanGlobalHotKey) return;
|
|
ShowLayer(true);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
base.OnKeyPress(e);
|
|
}
|
|
|
|
protected override void OnKeyUp(KeyEventArgs e)
|
|
{
|
|
|
|
if (SlicerFile is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.Home)
|
|
{
|
|
if (!CanGlobalHotKey) return;
|
|
btnFirstLayer.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
if (e.KeyCode == Keys.End)
|
|
{
|
|
if (!CanGlobalHotKey) return;
|
|
btnLastLayer.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.Control)
|
|
{
|
|
if (e.KeyCode == Keys.F)
|
|
{
|
|
btnFindLayer.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.R)
|
|
{
|
|
btnLayerImageRotate.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.NumPad0 || e.KeyCode == Keys.D0)
|
|
{
|
|
ZoomToFit();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.Left)
|
|
{
|
|
tsIssuePrevious.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.Right)
|
|
{
|
|
tsIssueNext.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (e.KeyCode == Keys.F5)
|
|
{
|
|
ShowLayer();
|
|
}
|
|
}
|
|
|
|
base.OnKeyUp(e);
|
|
}
|
|
|
|
protected override void OnResize(EventArgs e)
|
|
{
|
|
base.OnResize(e);
|
|
if (Settings.Default.LayerZoomToFit)
|
|
{
|
|
ZoomToFit();
|
|
}
|
|
|
|
tbLayer.Invalidate();
|
|
tbLayer.Update();
|
|
tbLayer.Refresh();
|
|
lbActualLayer.Location = new Point(lbActualLayer.Location.X,
|
|
((int) (tbLayer.Height - (float) tbLayer.Height / tbLayer.Maximum * tbLayer.Value) -
|
|
lbActualLayer.Height / 2)
|
|
.Clamp(1, tbLayer.Height - lbActualLayer.Height));
|
|
|
|
UpdateLayerTrackerHighlightIssues();
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
private void EventClick(object sender, EventArgs e)
|
|
{
|
|
if (sender is ToolStripSplitButton splitButton && !ReferenceEquals(sender, tsIssuesDetect))
|
|
{
|
|
splitButton.ShowDropDown();
|
|
return;
|
|
}
|
|
|
|
if (sender is ToolStripMenuItem menuItem)
|
|
{
|
|
/*******************
|
|
* Main Menu *
|
|
******************/
|
|
// File
|
|
if (ReferenceEquals(menuItem, menuFileOpen))
|
|
{
|
|
using (OpenFileDialog openFile = new OpenFileDialog())
|
|
{
|
|
openFile.CheckFileExists = true;
|
|
|
|
openFile.Filter = FileFormat.AllFileFilters;
|
|
openFile.FilterIndex = Settings.Default.DefaultOpenFileExtension + 1;
|
|
openFile.InitialDirectory = Settings.Default.FileOpenDefaultDirectory;
|
|
if (openFile.ShowDialog() == DialogResult.OK)
|
|
{
|
|
try
|
|
{
|
|
ProcessFile(openFile.FileName);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
MessageBox.Show(exception.ToString(), "Error while trying to open the file",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileOpenNewWindow))
|
|
{
|
|
using (OpenFileDialog openFile = new OpenFileDialog())
|
|
{
|
|
openFile.CheckFileExists = true;
|
|
openFile.Filter = FileFormat.AllFileFilters;
|
|
openFile.FilterIndex = Settings.Default.DefaultOpenFileExtension + 1;
|
|
openFile.InitialDirectory = Settings.Default.FileOpenDefaultDirectory;
|
|
if (openFile.ShowDialog() == DialogResult.OK)
|
|
{
|
|
Program.NewInstance(openFile.FileName);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileReload))
|
|
{
|
|
ProcessFile(ActualLayer);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileSave))
|
|
{
|
|
SaveFile();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileSaveAs))
|
|
{
|
|
using (SaveFileDialog dialog = new SaveFileDialog())
|
|
{
|
|
var ext = Path.GetExtension(SlicerFile.FileFullPath);
|
|
dialog.Filter = $"{ext.Remove(0, 1)} files (*{ext})|*{ext}";
|
|
dialog.AddExtension = true;
|
|
dialog.InitialDirectory = string.IsNullOrEmpty(Settings.Default.FileSaveDefaultDirectory)
|
|
? Path.GetDirectoryName(SlicerFile.FileFullPath)
|
|
: Settings.Default.FileSaveDefaultDirectory;
|
|
dialog.FileName =
|
|
$"{Settings.Default.FileSaveNamePreffix}{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}{Settings.Default.FileSaveNameSuffix}";
|
|
if (dialog.ShowDialog() != DialogResult.OK) return;
|
|
SaveFile(dialog.FileName);
|
|
}
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileClose))
|
|
{
|
|
if (menuFileSave.Enabled)
|
|
{
|
|
if (MessageBox.Show("There are unsaved changes. Do you want close this file without saving?",
|
|
"Close file without save?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) !=
|
|
DialogResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
Clear();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileSettings))
|
|
{
|
|
using (FrmSettings frmSettings = new FrmSettings())
|
|
{
|
|
if (frmSettings.ShowDialog() != DialogResult.OK) return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuFileExit))
|
|
{
|
|
if (menuFileSave.Enabled)
|
|
{
|
|
if (MessageBox.Show("There are unsaved changes, do you want exit without saving?",
|
|
"Exit without save?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) !=
|
|
DialogResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
Application.Exit();
|
|
return;
|
|
}
|
|
|
|
|
|
if (ReferenceEquals(menuItem, menuFileExtract))
|
|
{
|
|
using (FolderBrowserDialog folder = new FolderBrowserDialog())
|
|
{
|
|
string fileNameNoExt = Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath);
|
|
folder.SelectedPath = string.IsNullOrEmpty(Settings.Default.FileExtractDefaultDirectory)
|
|
? Path.GetDirectoryName(SlicerFile.FileFullPath)
|
|
: Settings.Default.FileExtractDefaultDirectory;
|
|
folder.Description =
|
|
$"A \"{fileNameNoExt}\" folder will be created within your selected folder to dump the contents.";
|
|
if (folder.ShowDialog() == DialogResult.OK)
|
|
{
|
|
string finalPath = Path.Combine(folder.SelectedPath, fileNameNoExt);
|
|
try
|
|
{
|
|
DisableGUI();
|
|
FrmLoading.SetDescription($"Extracting {Path.GetFileName(SlicerFile.FileFullPath)}");
|
|
|
|
var task = Task.Factory.StartNew(() =>
|
|
{
|
|
try
|
|
{
|
|
SlicerFile.Extract(finalPath, true, true, FrmLoading.RestartProgress());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
var loadingResult = FrmLoading.ShowDialog();
|
|
/*if (loadingResult != DialogResult.OK)
|
|
{
|
|
return;
|
|
}*/
|
|
|
|
if (MessageBox.Show(
|
|
$"Extraction to {finalPath} completed in ({FrmLoading.StopWatch.ElapsedMilliseconds / 1000}s)\n\n" +
|
|
"'Yes' to open target folder, 'No' to continue.",
|
|
"Extraction complete", MessageBoxButtons.YesNo, MessageBoxIcon.Question) ==
|
|
DialogResult.Yes)
|
|
{
|
|
using (Process.Start(finalPath))
|
|
{
|
|
}
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
MessageBox.Show(exception.ToString(), "Error while try extracting the file",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Edit & mutate
|
|
if (menuItem.Tag is Operation baseOperation)
|
|
{
|
|
ShowRunOperation(baseOperation.GetType());
|
|
return;
|
|
}
|
|
|
|
// About
|
|
if (ReferenceEquals(menuItem, menuHelpAbout))
|
|
{
|
|
Program.FrmAbout.ShowDialog();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuHelpWebsite))
|
|
{
|
|
using (Process.Start(About.Website))
|
|
{
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuHelpDonate))
|
|
{
|
|
MessageBox.Show(
|
|
"All my work here is given for free (OpenSource), it took some hours to build, test and polish the program.\n" +
|
|
"If would like to contribute for a better program and for my work, I would appreciate the tip.\n" +
|
|
"A browser window will be open and you will be forwarded to my paypal address after you click 'OK'.\nHappy Printing!",
|
|
"Donation", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
using (Process.Start(About.Donate))
|
|
{
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuHelpBenchmark))
|
|
{
|
|
using (var frmBenchmark = new FrmBenchmark())
|
|
{
|
|
frmBenchmark.ShowDialog();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(menuItem, menuHelpInstallPrinters))
|
|
{
|
|
var PEFolder =
|
|
$"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}PrusaSlicer";
|
|
if (!Directory.Exists(PEFolder))
|
|
{
|
|
var result = MessageBox.Show(
|
|
"Unable to detect PrusaSlicer on your system, please ensure you have lastest version installed.\n\n" +
|
|
"Click 'OK' to open the PrusaSlicer webpage for download\n" +
|
|
"Click 'Cancel' to cancel this operation",
|
|
"Unable to detect PrusaSlicer",
|
|
MessageBoxButtons.OKCancel, MessageBoxIcon.Error);
|
|
|
|
switch (result)
|
|
{
|
|
case DialogResult.OK:
|
|
using (Process.Start("https://www.prusa3d.com/prusaslicer/"))
|
|
{
|
|
}
|
|
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
using (FrmInstallPEProfiles form = new FrmInstallPEProfiles())
|
|
{
|
|
form.ShowDialog();
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ReferenceEquals(sender, menuNewVersion))
|
|
{
|
|
try
|
|
{
|
|
using (Process.Start(menuNewVersion.Tag.ToString()))
|
|
{
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.WriteLine(exception);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/************************
|
|
* Thumbnail Menu *
|
|
***********************/
|
|
if (ReferenceEquals(sender, tsThumbnailsPrevious))
|
|
{
|
|
byte i = (byte) tsThumbnailsCount.Tag;
|
|
if (i == 0)
|
|
{
|
|
// This should never happen!
|
|
tsThumbnailsPrevious.Enabled = false;
|
|
return;
|
|
}
|
|
|
|
tsThumbnailsCount.Tag = --i;
|
|
|
|
if (i == 0)
|
|
{
|
|
tsThumbnailsPrevious.Enabled = false;
|
|
}
|
|
|
|
pbThumbnail.Image = SlicerFile.Thumbnails[i]?.ToBitmap();
|
|
|
|
tsThumbnailsCount.Text = $"{i + 1}/{SlicerFile.CreatedThumbnailsCount}";
|
|
tsThumbnailsNext.Enabled = true;
|
|
|
|
tsThumbnailsResolution.Text = pbThumbnail.Image.PhysicalDimension.ToString();
|
|
|
|
AdjustThumbnailSplitter();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsThumbnailsNext))
|
|
{
|
|
byte i = byte.Parse(tsThumbnailsCount.Tag.ToString());
|
|
if (i >= SlicerFile.CreatedThumbnailsCount - 1)
|
|
{
|
|
// This should never happen!
|
|
tsThumbnailsNext.Enabled = false;
|
|
return;
|
|
}
|
|
|
|
tsThumbnailsCount.Tag = ++i;
|
|
|
|
if (i >= SlicerFile.CreatedThumbnailsCount - 1)
|
|
{
|
|
tsThumbnailsNext.Enabled = false;
|
|
}
|
|
|
|
pbThumbnail.Image = SlicerFile.Thumbnails[i]?.ToBitmap();
|
|
|
|
tsThumbnailsCount.Text = $"{i + 1}/{SlicerFile.CreatedThumbnailsCount}";
|
|
tsThumbnailsPrevious.Enabled = true;
|
|
|
|
tsThumbnailsResolution.Text = pbThumbnail.Image.PhysicalDimension.ToString();
|
|
|
|
AdjustThumbnailSplitter();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsThumbnailsImport))
|
|
{
|
|
using (var fileOpen = new OpenFileDialog
|
|
{
|
|
CheckFileExists = true,
|
|
Filter = "Image Files(*.PNG;*.BMP;*.JPEG;*.JPG;*.GIF)|*.PNG;*.BMP;*.JPEG;*.JPG;*.GIF"
|
|
})
|
|
{
|
|
if (fileOpen.ShowDialog() != DialogResult.OK) return;
|
|
byte i = byte.Parse(tsThumbnailsCount.Tag.ToString());
|
|
SlicerFile.SetThumbnail(i, fileOpen.FileName);
|
|
pbThumbnail.Image = SlicerFile.Thumbnails[i].ToBitmap();
|
|
SlicerFile.RequireFullEncode = true;
|
|
menuFileSave.Enabled = true;
|
|
}
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsThumbnailsExportFile))
|
|
{
|
|
using (SaveFileDialog dialog = new SaveFileDialog())
|
|
{
|
|
byte i = byte.Parse(tsThumbnailsCount.Tag.ToString());
|
|
if (ReferenceEquals(SlicerFile.Thumbnails[i], null))
|
|
{
|
|
return; // This should never happen!
|
|
}
|
|
|
|
dialog.Filter = "Image Files|.*png";
|
|
dialog.AddExtension = true;
|
|
dialog.FileName =
|
|
$"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_thumbnail{i + 1}.png";
|
|
|
|
if (dialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
SlicerFile.Thumbnails[i].Save(dialog.FileName);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsThumbnailsExportClipboard))
|
|
{
|
|
Clipboard.SetImage(pbThumbnail.Image);
|
|
return;
|
|
}
|
|
|
|
/************************
|
|
* Properties Menu *
|
|
***********************/
|
|
if (ReferenceEquals(sender, tsPropertiesExportFile))
|
|
{
|
|
using (SaveFileDialog dialog = new SaveFileDialog())
|
|
{
|
|
dialog.Filter = "Ini Files|.*ini";
|
|
dialog.AddExtension = true;
|
|
dialog.FileName = $"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_properties.ini";
|
|
|
|
if (dialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
using (TextWriter tw = new StreamWriter(dialog.OpenFile()))
|
|
{
|
|
foreach (var config in SlicerFile.Configs)
|
|
{
|
|
var type = config.GetType();
|
|
tw.WriteLine($"[{type.Name}]");
|
|
foreach (var property in type.GetProperties())
|
|
{
|
|
tw.WriteLine($"{property.Name} = {property.GetValue(config)}");
|
|
}
|
|
|
|
tw.WriteLine();
|
|
}
|
|
|
|
tw.Close();
|
|
}
|
|
|
|
if (MessageBox.Show(
|
|
"Properties save was successful. Do you want open the file in the default editor?",
|
|
"Properties save complete", MessageBoxButtons.YesNo, MessageBoxIcon.Question) ==
|
|
DialogResult.Yes)
|
|
{
|
|
using (Process.Start(dialog.FileName))
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsPropertiesExportClipboard))
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
foreach (var config in SlicerFile.Configs)
|
|
{
|
|
var type = config.GetType();
|
|
sb.AppendLine($"[{type.Name}]");
|
|
foreach (var property in type.GetProperties())
|
|
{
|
|
sb.AppendLine($"{property.Name} = {property.GetValue(config)}");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
}
|
|
|
|
Clipboard.SetText(sb.ToString());
|
|
return;
|
|
}
|
|
|
|
/************************
|
|
* GCode Menu *
|
|
***********************/
|
|
if (ReferenceEquals(sender, btnGCodeRebuild))
|
|
{
|
|
SlicerFile.RebuildGCode();
|
|
UpdateGCode();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnGCodeSaveFile))
|
|
{
|
|
using (SaveFileDialog dialog = new SaveFileDialog())
|
|
{
|
|
dialog.Filter = "Text Files|.*txt";
|
|
dialog.AddExtension = true;
|
|
dialog.FileName = $"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_gcode.txt";
|
|
|
|
if (dialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
using (TextWriter tw = new StreamWriter(dialog.OpenFile()))
|
|
{
|
|
tw.Write(SlicerFile.GCode);
|
|
tw.Close();
|
|
}
|
|
|
|
if (MessageBox.Show(
|
|
"GCode save was successful. Do you want open the file in the default editor?",
|
|
"GCode save complete", MessageBoxButtons.YesNo, MessageBoxIcon.Question) !=
|
|
DialogResult.Yes) return;
|
|
using (Process.Start(dialog.FileName))
|
|
{
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnGCodeSaveClipboard))
|
|
{
|
|
Clipboard.SetText(SlicerFile.GCode.ToString());
|
|
return;
|
|
}
|
|
|
|
/************************
|
|
* Issues Menu *
|
|
***********************/
|
|
if (ReferenceEquals(sender, tsIssuePrevious))
|
|
{
|
|
if (!tsIssuePrevious.Enabled) return;
|
|
int index = Convert.ToInt32(tsIssueCount.Tag);
|
|
flvIssues.SelectedIndices.Clear();
|
|
//flvIssues.SelectObject(Issues[--index]);
|
|
flvIssues.SelectedIndex = --index;
|
|
flvIssues.EnsureVisible(index); // Keep selection on screen
|
|
EventItemActivate(flvIssues, EventArgs.Empty);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsIssueNext))
|
|
{
|
|
if (!tsIssueNext.Enabled) return;
|
|
int index = Convert.ToInt32(tsIssueCount.Tag);
|
|
flvIssues.SelectedIndices.Clear();
|
|
//flvIssues.SelectObject(Issues[++index]);
|
|
flvIssues.SelectedIndex = ++index;
|
|
flvIssues.EnsureVisible(index); // Keep selection on screen
|
|
EventItemActivate(flvIssues, EventArgs.Empty);
|
|
return;
|
|
}
|
|
|
|
|
|
if (ReferenceEquals(sender, tsIssueRemove))
|
|
{
|
|
if (!tsIssueRemove.Enabled || ReferenceEquals(Issues, null)) return;
|
|
|
|
if (MessageBox.Show("Are you sure you want to remove all selected 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 Issues?",
|
|
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
|
|
|
|
Dictionary<uint, List<LayerIssue>> processIssues = new Dictionary<uint, List<LayerIssue>>();
|
|
List<uint> layersRemove = new List<uint>();
|
|
|
|
|
|
foreach (LayerIssue issue in flvIssues.SelectedObjects)
|
|
{
|
|
//if (!issue.HaveValidPoint) continue;
|
|
if (issue.Type == LayerIssue.IssueType.TouchingBound) continue;
|
|
|
|
if (!processIssues.TryGetValue(issue.Layer.Index, out var issueList))
|
|
{
|
|
issueList = new List<LayerIssue>();
|
|
processIssues.Add(issue.Layer.Index, issueList);
|
|
}
|
|
|
|
issueList.Add(issue);
|
|
if (issue.Type == LayerIssue.IssueType.EmptyLayer)
|
|
{
|
|
layersRemove.Add(issue.Layer.Index);
|
|
}
|
|
}
|
|
|
|
|
|
DisableGUI();
|
|
FrmLoading.SetDescription("Removing selected issues");
|
|
var progress = FrmLoading.RestartProgress(false);
|
|
|
|
progress.Reset("Removing selected issues", (uint) processIssues.Count);
|
|
Task<bool> task = Task<bool>.Factory.StartNew(() =>
|
|
{
|
|
bool result = false;
|
|
try
|
|
{
|
|
Parallel.ForEach(processIssues, layerIssues =>
|
|
{
|
|
if (progress.Token.IsCancellationRequested) return;
|
|
using (var image = SlicerFile[layerIssues.Key].LayerMat)
|
|
{
|
|
var bytes = image.GetPixelSpan<byte>();
|
|
|
|
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);
|
|
//CvInvoke.DrawContours(image, contours, -1, new MCvScalar(255), 2);
|
|
}
|
|
|
|
edited = true;
|
|
}
|
|
|
|
}
|
|
|
|
if (edited)
|
|
{
|
|
SlicerFile[layerIssues.Key].LayerMat = image;
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
progress++;
|
|
});
|
|
|
|
if (layersRemove.Count > 0)
|
|
{
|
|
SlicerFile.LayerManager.RemoveLayers(layersRemove);
|
|
result = true;
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(ex.Message, "Removal failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
FrmLoading.ShowDialog();
|
|
|
|
if (!task.Result) return;
|
|
|
|
var whiteListLayers = new List<uint>();
|
|
|
|
// Update GUI
|
|
List<LayerIssue> removeList = new List<LayerIssue>();
|
|
foreach (LayerIssue issue in flvIssues.SelectedObjects)
|
|
{
|
|
//if (!issue.HaveValidPoint) continue;
|
|
if (issue.Type == LayerIssue.IssueType.TouchingBound) continue;
|
|
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);
|
|
removeList.Add(issue);
|
|
}
|
|
|
|
flvIssues.RemoveObjects(removeList);
|
|
|
|
if (layersRemove.Count > 0)
|
|
{
|
|
UpdateLayerLimits();
|
|
RefreshInfo();
|
|
}
|
|
|
|
if (Settings.Default.PartialUpdateIslandsOnEditing)
|
|
{
|
|
UpdateIslands(whiteListLayers);
|
|
}
|
|
|
|
//ShowLayer(); // It will call latter so its a extra call
|
|
UpdateIssuesInfo();
|
|
menuFileSave.Enabled = true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsIssuesRepair))
|
|
{
|
|
ShowRunOperation(typeof(OperationRepairLayers));
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsIssuesDetect))
|
|
{
|
|
/*if (MessageBox.Show("Are you sure you want to compute issues?", "Issues Compute",
|
|
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;*/
|
|
|
|
ComputeIssues(GetIslandDetectionConfiguration(), GetResinTrapDetectionConfiguration(),
|
|
GetTouchingBoundsDetectionConfiguration(), tsIssuesDetectEmptyLayers.Checked);
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnIssueGroup))
|
|
{
|
|
flvIssues.ClearObjects();
|
|
flvIssues.ShowGroups = btnIssueGroup.Checked;
|
|
UpdateIssuesList();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnIssueResort))
|
|
{
|
|
flvIssues.PrimarySortColumn = flvIssuesColType;
|
|
flvIssues.PrimarySortOrder = SortOrder.Ascending;
|
|
flvIssues.SecondarySortColumn = flvIssuesColLayerIndex;
|
|
flvIssues.SecondarySortOrder = SortOrder.Ascending;
|
|
flvIssues.RebuildColumns();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnPixelHistoryRemove))
|
|
{
|
|
if (!btnPixelHistoryRemove.Enabled) return;
|
|
if (flvPixelHistory.SelectedIndices.Count == 0) return;
|
|
foreach (PixelOperation operation in flvPixelHistory.SelectedObjects)
|
|
{
|
|
PixelHistory.Items.Remove(operation);
|
|
}
|
|
|
|
RefreshPixelHistory();
|
|
|
|
ShowLayer();
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnPixelHistoryClear))
|
|
{
|
|
if (!btnPixelHistoryApply.Enabled) return;
|
|
if (PixelHistory.Count == 0) return;
|
|
if (MessageBox.Show(
|
|
"Are you sure you want to clear all operations?",
|
|
"Clear operations?", MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question) != DialogResult.Yes) return;
|
|
|
|
PixelHistory.Clear();
|
|
RefreshPixelHistory();
|
|
ShowLayer();
|
|
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnPixelHistoryApply))
|
|
{
|
|
if (!btnPixelHistoryApply.Enabled) return;
|
|
DrawModifications(false);
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLogClear))
|
|
{
|
|
if (MessageBox.Show(
|
|
"Are you sure you want to clear the log?",
|
|
"Clear log?", MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question) != DialogResult.Yes) return;
|
|
|
|
lvLog.ClearObjects();
|
|
AddLog("Log cleared");
|
|
}
|
|
|
|
|
|
/************************
|
|
* Layer Menu *
|
|
***********************/
|
|
if (ReferenceEquals(sender, btnLayerImageRotate) ||
|
|
ReferenceEquals(sender, btnLayerImageLayerDifference) ||
|
|
ReferenceEquals(sender, btnLayerImageHighlightIssues) ||
|
|
ReferenceEquals(sender, btnLayerImageShowCrosshairs) ||
|
|
ReferenceEquals(sender, btnLayerImageLayerOutlinePrintVolumeBounds) ||
|
|
ReferenceEquals(sender, btnLayerImageLayerOutlineLayerBounds) ||
|
|
ReferenceEquals(sender, btnLayerImageLayerOutlineHollowAreas) ||
|
|
ReferenceEquals(sender, btnLayerImageLayerOutlineEdgeDetection)
|
|
|
|
)
|
|
{
|
|
ShowLayer();
|
|
if (ReferenceEquals(sender, btnLayerImageRotate))
|
|
{
|
|
// Arrange selection rotation
|
|
var rectangleF = pbLayer.SelectionRegion;
|
|
if (!rectangleF.IsEmpty)
|
|
{
|
|
var rectangle = Rectangle.Round(rectangleF);
|
|
pbLayer.SelectionRegion = GetTransposedRectangle(rectangle, btnLayerImageRotate.Checked, true);
|
|
}
|
|
|
|
ZoomToFit();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerImagePixelEdit))
|
|
{
|
|
if (btnLayerImagePixelEdit.Checked)
|
|
{
|
|
tabControlLeft.TabPages.Add(tabPagePixelEditor);
|
|
tabControlLeft.SelectedTab = tabPagePixelEditor;
|
|
}
|
|
else
|
|
{
|
|
DrawModifications(true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerImageExportFile))
|
|
{
|
|
using (SaveFileDialog dialog = new SaveFileDialog())
|
|
{
|
|
if (ReferenceEquals(pbLayer, null))
|
|
{
|
|
return; // This should never happen!
|
|
}
|
|
|
|
dialog.Filter = "Image Files|.*png";
|
|
dialog.AddExtension = true;
|
|
dialog.FileName =
|
|
$"{Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath)}_layer{ActualLayer}.png";
|
|
|
|
if (dialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
pbLayer.Image.Save(dialog.FileName);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerImageExportClipboard))
|
|
{
|
|
if (ReferenceEquals(pbLayer, null))
|
|
{
|
|
return; // This should never happen!
|
|
}
|
|
|
|
Clipboard.SetImage(pbLayer.Image);
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnFirstLayer))
|
|
{
|
|
ShowLayer(0);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnPreviousLayer))
|
|
{
|
|
ShowLayer(false);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnNextLayer))
|
|
{
|
|
ShowLayer(true);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLastLayer))
|
|
{
|
|
ShowLayer(SlicerFile.LayerCount - 1);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnFindLayer))
|
|
{
|
|
using (FrmInputBox inputBox = new FrmInputBox("Go to layer", "Select a layer to go to",
|
|
ActualLayer, null, 0, SlicerFile.LayerCount - 1, 0, "Layer"))
|
|
{
|
|
if (inputBox.ShowDialog() == DialogResult.OK)
|
|
{
|
|
ShowLayer((uint) inputBox.NewValue);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerBounds))
|
|
{
|
|
CenterLayerAt(GetTransposedRectangle(SlicerFile[ActualLayer].BoundingRectangle), 0, true);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerROI))
|
|
{
|
|
var roi = pbLayer.SelectionRegion;
|
|
if (roi.IsEmpty) return;
|
|
|
|
if ((ModifierKeys & Keys.Shift) != 0)
|
|
{
|
|
pbLayer.SelectNone();
|
|
return;
|
|
}
|
|
|
|
CenterLayerAt(roi, 0, true);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerMouseLocation))
|
|
{
|
|
if (btnLayerMouseLocation.Tag is Point point)
|
|
{
|
|
CenterLayerAt(point);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, btnLayerResolution))
|
|
{
|
|
pbLayer.ZoomToFit();
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void ValueChanged(object sender, EventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, tbLayer))
|
|
{
|
|
if (tbLayer.Value == ActualLayer) return;
|
|
/*Debug.WriteLine($"{tbLayer.Height} + {tbLayer.Value} * ({tbLayer.Height} / {tbLayer.Maximum})");
|
|
lbLayerActual.Location = new Point(lbLayerActual.Location.X, (int) (tbLayer.Height - tbLayer.Value * ((float)tbLayer.Height / tbLayer.Maximum)));
|
|
Debug.WriteLine(lbLayerActual.Location);*/
|
|
ShowLayer((uint) tbLayer.Value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void ConvertToItemOnClick(object sender, EventArgs e)
|
|
{
|
|
ToolStripMenuItem menuItem = (ToolStripMenuItem) sender;
|
|
FileFormat fileFormat = (FileFormat) menuItem.Tag;
|
|
using (SaveFileDialog dialog = new SaveFileDialog())
|
|
{
|
|
dialog.FileName = Path.GetFileNameWithoutExtension(SlicerFile.FileFullPath);
|
|
dialog.Filter = fileFormat.FileFilter;
|
|
dialog.InitialDirectory = string.IsNullOrEmpty(Settings.Default.FileConvertDefaultDirectory)
|
|
? Path.GetDirectoryName(SlicerFile.FileFullPath)
|
|
: Settings.Default.FileConvertDefaultDirectory;
|
|
|
|
//using (FileFormat instance = (FileFormat)Activator.CreateInstance(type))
|
|
//using (CbddlpFile file = new CbddlpFile())
|
|
|
|
|
|
|
|
if (dialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
DisableGUI();
|
|
FrmLoading.SetDescription(
|
|
$"Converting {Path.GetFileName(SlicerFile.FileFullPath)} to {Path.GetExtension(dialog.FileName)}");
|
|
|
|
Task<bool> task = Task<bool>.Factory.StartNew(() =>
|
|
{
|
|
try
|
|
{
|
|
return SlicerFile.Convert(fileFormat, dialog.FileName, FrmLoading.RestartProgress());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
string extraMessage = string.Empty;
|
|
if (SlicerFile.FileFullPath.EndsWith(".sl1"))
|
|
{
|
|
extraMessage = "Note: When converting from SL1 make sure you have the correct printer selected, you MUST use a UVtools base printer.\n" +
|
|
"Go to \"Help\" -> \"Install profiles into PrusaSlicer\" to install printers.\n";
|
|
}
|
|
|
|
MessageBox.Show($"Convertion was not successful! Maybe not implemented...\n{extraMessage}{ex.Message}",
|
|
"Convertion unsuccessful", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
if (FrmLoading.ShowDialog() == DialogResult.OK && task.Result)
|
|
{
|
|
if (MessageBox.Show(
|
|
$"Conversion completed in {FrmLoading.StopWatch.ElapsedMilliseconds / 1000}s\n\n" +
|
|
$"Do you want to open {Path.GetFileName(dialog.FileName)} in a new window?",
|
|
"Conversion complete", MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Information) == DialogResult.Yes)
|
|
{
|
|
Program.NewInstance(dialog.FileName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(dialog.FileName))
|
|
{
|
|
File.Delete(dialog.FileName);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine(ex);
|
|
}
|
|
|
|
//MessageBox.Show("Convertion was unsuccessful! Maybe not implemented...", "Convertion unsuccessful", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void pbLayer_SelectionRegionChanged(object sender, EventArgs e)
|
|
{
|
|
var roi = ROI;
|
|
btnLayerROI.Text = roi.IsEmpty ? "ROI: (NS)" : $"ROI: {roi}";
|
|
btnLayerROI.Enabled = !roi.IsEmpty;
|
|
}
|
|
|
|
private void pbLayer_Zoomed(object sender, ImageBoxZoomEventArgs e)
|
|
{
|
|
if (SupressLayerZoomEvent) return;
|
|
AddLogVerbose($"Zoomed from {e.OldZoom} to {e.NewZoom}");
|
|
// Update zoom level display in the toolstrip
|
|
if (e.NewZoom == LockedZoomLevel)
|
|
{
|
|
tsLayerImageZoom.Text = $"Zoom: [ {e.NewZoom / 100f}x";
|
|
tsLayerImageZoomLock.Visible = true;
|
|
}
|
|
else
|
|
{
|
|
tsLayerImageZoom.Text = $"Zoom: [ {e.NewZoom / 100f}x ]";
|
|
tsLayerImageZoomLock.Visible = false;
|
|
}
|
|
|
|
if (pbLayer.Cursor.Tag is string str || ReferenceEquals(pbLayer.Cursor, pixelEditCursor))
|
|
{
|
|
UpdatePixelEditorCursor();
|
|
}
|
|
|
|
// Refresh the layer to properly render the crosshair at various zoom transitions
|
|
if (btnLayerImageShowCrosshairs.Checked &&
|
|
!ReferenceEquals(Issues, null) &&
|
|
(e.OldZoom < 50 &&
|
|
e.NewZoom >= 50 // Trigger refresh as crosshair thickness increases at lower zoom levels
|
|
|| e.OldZoom > 100 && e.NewZoom <= 100
|
|
|| e.OldZoom >= 50 && e.OldZoom <= 100 && (e.NewZoom < 50 || e.NewZoom > 100)
|
|
|| e.OldZoom <= CrosshairFadeLevel &&
|
|
e.NewZoom > CrosshairFadeLevel // Trigger refresh as zoom level manually crosses fade threshold
|
|
|| e.OldZoom > CrosshairFadeLevel && e.NewZoom <= CrosshairFadeLevel)
|
|
|
|
)
|
|
{
|
|
if (Settings.Default.CrosshairShowOnlyOnSelectedIssues)
|
|
{
|
|
if (flvIssues.SelectedIndices.Count == 0 || !flvIssues.SelectedObjects.Cast<LayerIssue>().Any(
|
|
issue => // Find a valid candidate to update layer preview, otherwise quit
|
|
issue.LayerIndex == ActualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer &&
|
|
issue.Type != LayerIssue.IssueType.TouchingBound)) return;
|
|
}
|
|
else
|
|
{
|
|
if (!Issues.Any(
|
|
issue => // Find a valid candidate to update layer preview, otherwise quit
|
|
issue.LayerIndex == ActualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer &&
|
|
issue.Type != LayerIssue.IssueType.TouchingBound)) return;
|
|
}
|
|
|
|
// A timer is used here rather than invoking ShowLayer directly to eliminate sublte visual flashing
|
|
// that will occur on the transition when the crosshair fades or unfades if ShowLayer is called directly.
|
|
layerZoomTimer.Start();
|
|
}
|
|
}
|
|
|
|
private Point _lastPixelMouseLocation = Point.Empty;
|
|
|
|
private void pbLayer_MouseMove(object sender, MouseEventArgs e)
|
|
{
|
|
if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
var location = pbLayer.PointToImage(e.Location);
|
|
|
|
if ((ModifierKeys & Keys.Control) != 0)
|
|
{
|
|
Point realLocation = GetTransposedPoint(location);
|
|
btnLayerMouseLocation.Text =
|
|
$"{{X={realLocation.X}, Y={realLocation.Y}, B={ActualLayerImage.GetByte(realLocation)}}}";
|
|
btnLayerMouseLocation.Tag = location;
|
|
btnLayerMouseLocation.Enabled = true;
|
|
}
|
|
|
|
if ((ModifierKeys & Keys.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 (tabControlPixelEditor.SelectedIndex != (int) PixelOperation.PixelOperationType.Drawing) return;
|
|
if (!btnLayerImagePixelEdit.Checked || (e.Button & MouseButtons.Middle) != 0) return;
|
|
//if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
|
|
if (e.Button == MouseButtons.Right)
|
|
{
|
|
// Right or Alt-Left will remove a pixel
|
|
DrawPixel(false ^ (ModifierKeys & Keys.Alt) != 0, location);
|
|
return;
|
|
}
|
|
|
|
if (e.Button == MouseButtons.Left)
|
|
{
|
|
// Left or Alt-Right will add a pixel
|
|
DrawPixel(true ^ (ModifierKeys & Keys.Alt) != 0, location);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventItemActivate(object sender, EventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, flvIssues))
|
|
{
|
|
if (!(flvIssues.SelectedObject is LayerIssue issue)) return;
|
|
|
|
if (issue.Type == LayerIssue.IssueType.TouchingBound || issue.Type == LayerIssue.IssueType.EmptyLayer ||
|
|
(issue.X == -1 && issue.Y == -1))
|
|
{
|
|
ZoomToFit();
|
|
}
|
|
else if (issue.X >= 0 && issue.Y >= 0)
|
|
{
|
|
if (Settings.Default.ZoomIssues ^ (ModifierKeys & Keys.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 (!Rectangle.Round(pbLayer.GetSourceImageRegion()).Contains(GetTransposedIssueBounds(issue)))
|
|
{
|
|
CenterAtIssue(issue);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unconditionally refresh layer preview here, even if layer is already active.
|
|
// This ensures highlight colors are updated to reflect active selections within
|
|
// the current layer and the crosshair is refreshed.
|
|
ShowLayer(issue.Layer.Index);
|
|
|
|
tsIssueCount.Tag = flvIssues.SelectedIndices[0];
|
|
UpdateIssuesInfo();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, flvPixelHistory))
|
|
{
|
|
if (!(flvPixelHistory.SelectedObject is PixelOperation operation)) return;
|
|
|
|
Point location = GetTransposedPoint(operation.Location, false);
|
|
|
|
if (Settings.Default.ZoomIssues ^ (ModifierKeys & Keys.Alt) != 0)
|
|
{
|
|
CenterLayerAt(new Rectangle(location, operation.Size), LockedZoomLevel);
|
|
}
|
|
else
|
|
{
|
|
CenterLayerAt(location);
|
|
}
|
|
|
|
// Unconditionally refresh layer preview here to ensure highlighting for pixel
|
|
// operations properly reflects the active selection.
|
|
ShowLayer(operation.LayerIndex);
|
|
}
|
|
}
|
|
|
|
private void TbLayerOnMouseWheel(object sender, MouseEventArgs e)
|
|
{
|
|
((HandledMouseEventArgs) e).Handled = true;
|
|
if (!tbLayer.Enabled) return;
|
|
if (e.Delta > 0)
|
|
{
|
|
ShowLayer(true);
|
|
}
|
|
else if (e.Delta < 0)
|
|
{
|
|
ShowLayer(false);
|
|
}
|
|
}
|
|
|
|
private void flvIssues_ItemsChanged(object sender, ItemsChangedEventArgs e)
|
|
{
|
|
UpdateLayerTrackerHighlightIssues();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Closes file and clear UI
|
|
/// </summary>
|
|
void Clear()
|
|
{
|
|
UpdateTitle();
|
|
|
|
SlicerFile?.Dispose();
|
|
SlicerFile = null;
|
|
ActualLayer = 0;
|
|
|
|
// GUI CLEAN
|
|
pbThumbnail.Image = null;
|
|
pbLayer.Image = null;
|
|
pbLayer.SelectNone();
|
|
lbLayerImageTooltipOverlay.Visible = false;
|
|
pbThumbnail.Image = null;
|
|
tbGCode.Clear();
|
|
tabPageIssues.Tag = null;
|
|
|
|
Issues = null;
|
|
flvIssues.ClearObjects();
|
|
UpdateIssuesInfo();
|
|
|
|
PixelHistory.Clear();
|
|
RefreshPixelHistory();
|
|
|
|
lbMaxLayer.Text =
|
|
lbActualLayer.Text =
|
|
lbInitialLayer.Text = "???";
|
|
flvProperties.ClearObjects();
|
|
//pbLayers.Value = 0;
|
|
tbLayer.Value = 0;
|
|
|
|
statusBar.Items.Clear();
|
|
menuFileConvert.DropDownItems.Clear();
|
|
|
|
menuEdit.DropDownItems.Clear();
|
|
|
|
foreach (ToolStripItem item in menuTools.DropDownItems)
|
|
{
|
|
item.Enabled = false;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsThumbnails.Items)
|
|
{
|
|
item.Enabled = false;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsLayer.Items)
|
|
{
|
|
item.Enabled = false;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsLayerInfo.Items)
|
|
{
|
|
item.Enabled = false;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsProperties.Items)
|
|
{
|
|
item.Enabled = false;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsIssues.Items)
|
|
{
|
|
item.Enabled = false;
|
|
}
|
|
|
|
tsLayerImagePixelCount.Text = "Pixels: 0";
|
|
btnLayerBounds.Text = "Bounds: (Unloaded)";
|
|
btnLayerROI.Text = "ROI: (NA)";
|
|
btnLayerMouseLocation.Text = "{X=0, Y=0}";
|
|
|
|
tsThumbnailsResolution.Text =
|
|
tsLayerPreviewTime.Text =
|
|
btnLayerResolution.Text =
|
|
tsPropertiesLabelCount.Text =
|
|
tsPropertiesLabelGroups.Text = string.Empty;
|
|
|
|
|
|
menuFileReload.Enabled =
|
|
menuFileSave.Enabled =
|
|
menuFileSaveAs.Enabled =
|
|
menuFileClose.Enabled =
|
|
menuFileExtract.Enabled =
|
|
menuFileConvert.Enabled =
|
|
tbLayer.Enabled =
|
|
//pbLayers.Enabled =
|
|
menuEdit.Enabled =
|
|
menuTools.Enabled =
|
|
btnFirstLayer.Enabled =
|
|
btnNextLayer.Enabled =
|
|
btnPreviousLayer.Enabled =
|
|
btnLastLayer.Enabled =
|
|
btnFindLayer.Enabled =
|
|
|
|
btnLayerImagePixelEdit.Checked =
|
|
|
|
false;
|
|
|
|
tsThumbnailsCount.Text = "0/0";
|
|
tsThumbnailsCount.Tag = null;
|
|
|
|
tabControlLeft.TabPages.Remove(tabPageGCode);
|
|
tabControlLeft.TabPages.Remove(tabPageIssues);
|
|
tabControlLeft.TabPages.Remove(tabPagePixelEditor);
|
|
tabControlLeft.SelectedIndex = 0;
|
|
}
|
|
|
|
void DisableGUI()
|
|
{
|
|
mainTable.Enabled =
|
|
menu.Enabled = false;
|
|
}
|
|
|
|
void EnableGUI(bool closeLoading = false)
|
|
{
|
|
if (closeLoading)
|
|
{
|
|
if (ReferenceEquals(FrmLoading.Progress, null))
|
|
{
|
|
FrmLoading.DialogResult = DialogResult.OK;
|
|
}
|
|
else
|
|
{
|
|
FrmLoading.DialogResult = FrmLoading.Progress.Token.IsCancellationRequested
|
|
? DialogResult.Cancel
|
|
: DialogResult.OK;
|
|
}
|
|
|
|
//FrmLoading.Close();
|
|
}
|
|
|
|
mainTable.Enabled =
|
|
menu.Enabled = true;
|
|
}
|
|
|
|
void ProcessFile(uint actualLayer = 0)
|
|
{
|
|
if (ReferenceEquals(SlicerFile, null)) return;
|
|
ProcessFile(SlicerFile.FileFullPath, actualLayer);
|
|
}
|
|
|
|
void ProcessFile(string[] files)
|
|
{
|
|
if (ReferenceEquals(files, null)) return;
|
|
foreach (string file in files)
|
|
{
|
|
try
|
|
{
|
|
ProcessFile(file);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
MessageBox.Show(exception.ToString(), "Error opening the file", MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
void ProcessFile(string fileName, uint actualLayer = 0)
|
|
{
|
|
Clear();
|
|
|
|
var fileNameOnly = Path.GetFileName(fileName);
|
|
SlicerFile = FileFormat.FindByExtension(fileName, true, true);
|
|
if (ReferenceEquals(SlicerFile, null)) return;
|
|
|
|
DisableGUI();
|
|
FrmLoading.SetDescription($"Decoding {fileNameOnly}");
|
|
|
|
var task = Task.Factory.StartNew(() =>
|
|
{
|
|
try
|
|
{
|
|
SlicerFile.Decode(fileName, FrmLoading.RestartProgress());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
SlicerFile.Clear();
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
var loadingResult = FrmLoading.ShowDialog();
|
|
if (loadingResult != DialogResult.OK)
|
|
{
|
|
SlicerFile = null;
|
|
return;
|
|
}
|
|
|
|
if (SlicerFile.LayerCount == 0)
|
|
{
|
|
MessageBox.Show("It seems this file has no layers. Possible causes could be:\n" +
|
|
"- File is empty\n" +
|
|
"- File is corrupted\n" +
|
|
"- File has not been sliced\n" +
|
|
"- An internal programing error\n\n" +
|
|
"Please check your file and retry", "Error reading file",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
//Clear();
|
|
return;
|
|
}
|
|
|
|
ActualLayerImage?.Dispose();
|
|
ActualLayerImage = SlicerFile[0].LayerMat;
|
|
|
|
if (Settings.Default.LayerAutoRotateBestView)
|
|
{
|
|
btnLayerImageRotate.Checked = ActualLayerImage.Height > ActualLayerImage.Width;
|
|
}
|
|
|
|
if (!ReferenceEquals(SlicerFile.ConvertToFormats, null))
|
|
{
|
|
foreach (var fileFormatType in SlicerFile.ConvertToFormats)
|
|
{
|
|
FileFormat fileFormat = FileFormat.FindByType(fileFormatType);
|
|
//if (fileFormat.GetType() == SlicerFile.GetType()) continue;
|
|
|
|
string extensions = fileFormat.FileExtensions.Length > 0
|
|
? $" ({fileFormat.GetFileExtensions()})"
|
|
: string.Empty;
|
|
ToolStripMenuItem menuItem =
|
|
new ToolStripMenuItem(fileFormat.GetType().Name.Replace("File", extensions))
|
|
{
|
|
Tag = fileFormat,
|
|
Image = Resources.layers_16x16
|
|
};
|
|
menuItem.Click += ConvertToItemOnClick;
|
|
menuFileConvert.DropDownItems.Add(menuItem);
|
|
}
|
|
|
|
menuFileConvert.Enabled = menuFileConvert.DropDownItems.Count > 0;
|
|
}
|
|
|
|
scLeft.Panel1Collapsed = SlicerFile.CreatedThumbnailsCount == 0;
|
|
if (SlicerFile.CreatedThumbnailsCount > 0)
|
|
{
|
|
tsThumbnailsCount.Tag = 0;
|
|
tsThumbnailsCount.Text = $"1/{SlicerFile.CreatedThumbnailsCount}";
|
|
pbThumbnail.Image = SlicerFile.Thumbnails[0]?.ToBitmap();
|
|
tsThumbnailsResolution.Text = pbThumbnail.Image.PhysicalDimension.ToString();
|
|
|
|
for (var i = 1; i < tsThumbnails.Items.Count; i++)
|
|
{
|
|
tsThumbnails.Items[i].Enabled = true;
|
|
}
|
|
|
|
tsThumbnailsNext.Enabled = SlicerFile.CreatedThumbnailsCount > 1;
|
|
AdjustThumbnailSplitter();
|
|
}
|
|
|
|
foreach (ToolStripItem item in menuTools.DropDownItems)
|
|
{
|
|
item.Enabled = true;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsLayer.Items)
|
|
{
|
|
item.Enabled = true;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsLayerInfo.Items)
|
|
{
|
|
if(ReferenceEquals(item, btnLayerROI) ||
|
|
ReferenceEquals(item, btnLayerMouseLocation)
|
|
) continue;
|
|
item.Enabled = true;
|
|
}
|
|
|
|
foreach (ToolStripItem item in tsProperties.Items)
|
|
{
|
|
item.Enabled = true;
|
|
}
|
|
|
|
tsPropertiesLabelCount.Text = $"Properties: {flvProperties.GetItemCount()}";
|
|
tsPropertiesLabelGroups.Text = $"Groups: {flvProperties.OLVGroups?.Count ?? 0}";
|
|
|
|
menuFileReload.Enabled =
|
|
menuFileClose.Enabled =
|
|
menuFileExtract.Enabled =
|
|
menuFileSaveAs.Enabled =
|
|
tbLayer.Enabled =
|
|
//pbLayers.Enabled =
|
|
menuEdit.Enabled =
|
|
menuTools.Enabled =
|
|
|
|
tsIssuesRepair.Enabled =
|
|
tsIssuesDetect.Enabled =
|
|
|
|
btnFindLayer.Enabled =
|
|
true;
|
|
|
|
|
|
tabControlLeft.TabPages.Insert(1, tabPageIssues);
|
|
if (!ReferenceEquals(SlicerFile.GCode, null))
|
|
{
|
|
tabControlLeft.TabPages.Insert(1, tabPageGCode);
|
|
}
|
|
|
|
|
|
|
|
tabControlLeft.SelectedIndex = 0;
|
|
btnLayerResolution.Text = $"{{Width={SlicerFile.ResolutionX}, Height={SlicerFile.ResolutionY}}}";
|
|
|
|
UpdateLayerLimits();
|
|
ShowLayer(Math.Min(actualLayer, SlicerFile.LayerCount - 1));
|
|
|
|
RefreshInfo();
|
|
|
|
if (Settings.Default.LayerZoomToFit)
|
|
{
|
|
ZoomToFit();
|
|
}
|
|
|
|
UpdateTitle();
|
|
|
|
if (Settings.Default.ComputeIssuesOnLoad)
|
|
{
|
|
tsIssuesDetect.PerformButtonClick();
|
|
}
|
|
}
|
|
|
|
public bool SaveFile(string filepath = null)
|
|
{
|
|
if (filepath is null)
|
|
{
|
|
if (SavesCount == 0 && Settings.Default.FileSavePromptOverwrite)
|
|
{
|
|
if (MessageBox.Show(
|
|
"Original input file will be overwritten. Do you wish to proceed?", "Overwrite file?",
|
|
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
|
|
return false;
|
|
}
|
|
|
|
filepath = SlicerFile.FileFullPath;
|
|
}
|
|
|
|
var oldFile = SlicerFile.FileFullPath;
|
|
var tempFile = filepath + FileFormat.TemporaryFileAppend;
|
|
|
|
DisableGUI();
|
|
FrmLoading.SetDescription($"Saving {Path.GetFileName(filepath)}");
|
|
|
|
Task<bool> task = Task<bool>.Factory.StartNew(() =>
|
|
{
|
|
bool result = false;
|
|
|
|
try
|
|
{
|
|
SlicerFile.SaveAs(tempFile, FrmLoading.RestartProgress());
|
|
if (File.Exists(filepath))
|
|
{
|
|
File.Delete(filepath);
|
|
}
|
|
File.Move(tempFile, filepath);
|
|
SlicerFile.FileFullPath = filepath;
|
|
result = true;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
SlicerFile.FileFullPath = oldFile;
|
|
if (File.Exists(tempFile))
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(ex.Message, "Error while saving the file", MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker)delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
FrmLoading.ShowDialog();
|
|
|
|
if (task.Result)
|
|
{
|
|
SavesCount++;
|
|
menuFileSave.Enabled = false;
|
|
UpdateTitle();
|
|
}
|
|
|
|
return task.Result;
|
|
}
|
|
|
|
private void UpdateLayerLimits()
|
|
{
|
|
lbMaxLayer.Text = $"{SlicerFile.TotalHeight}mm\n{SlicerFile.LayerCount - 1}";
|
|
lbInitialLayer.Text = $"{SlicerFile.LayerHeight}mm\n0";
|
|
tbLayer.Maximum = (int) SlicerFile.LayerCount - 1;
|
|
}
|
|
|
|
private void UpdateTitle()
|
|
{
|
|
Text = ReferenceEquals(SlicerFile, null)
|
|
? $"{FrmAbout.AssemblyTitle} Version: {FrmAbout.AssemblyVersion}"
|
|
: $"{FrmAbout.AssemblyTitle} File: {Path.GetFileName(SlicerFile.FileFullPath)} ({Math.Round(FrmLoading.StopWatch.ElapsedMilliseconds / 1000m, 2)}s) Version: {FrmAbout.AssemblyVersion}";
|
|
|
|
#if DEBUG
|
|
Text += " [DEBUG]";
|
|
#endif
|
|
}
|
|
|
|
private void RefreshInfo()
|
|
{
|
|
/*menuEdit.DropDownItems.Clear();
|
|
|
|
if (!ReferenceEquals(SlicerFile.PrintParameterModifiers, null))
|
|
{
|
|
foreach (var modifier in SlicerFile.PrintParameterModifiers)
|
|
{
|
|
ToolStripItem item = new ToolStripMenuItem
|
|
{
|
|
Text =
|
|
$"{modifier.Name} ({SlicerFile.GetValueFromPrintParameterModifier(modifier)}{modifier.ValueUnit})",
|
|
Tag = modifier,
|
|
Image = Resources.Wrench_16x16,
|
|
};
|
|
menuEdit.DropDownItems.Add(item);
|
|
|
|
item.Click += EventClick;
|
|
}
|
|
}*/
|
|
|
|
flvProperties.ClearObjects();
|
|
|
|
if (!ReferenceEquals(SlicerFile.Configs, null))
|
|
{
|
|
List<SlicerPropertyItem> items = new List<SlicerPropertyItem>();
|
|
foreach (object config in SlicerFile.Configs)
|
|
{
|
|
foreach (PropertyInfo propertyInfo in config.GetType()
|
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (propertyInfo.Name.Equals("Item")) continue;
|
|
var value = propertyInfo.GetValue(config);
|
|
if (ReferenceEquals(value, null)) continue;
|
|
if (value is IList list)
|
|
{
|
|
items.Add(new SlicerPropertyItem(propertyInfo.Name, list.Count.ToString(),
|
|
config.GetType().Name));
|
|
}
|
|
else
|
|
{
|
|
items.Add(
|
|
new SlicerPropertyItem(propertyInfo.Name, value.ToString(), config.GetType().Name));
|
|
}
|
|
}
|
|
}
|
|
|
|
flvProperties.SetObjects(items);
|
|
}
|
|
|
|
tsPropertiesLabelCount.Text = $"Properties: {flvProperties.GetItemCount()}";
|
|
tsPropertiesLabelGroups.Text = $"Groups: {flvProperties.OLVGroups?.Count ?? 0}";
|
|
|
|
UpdateGCode();
|
|
|
|
statusBar.Items.Clear();
|
|
AddStatusBarItem(nameof(SlicerFile.LayerHeight), SlicerFile.LayerHeight, "mm");
|
|
AddStatusBarItem(nameof(SlicerFile.BottomLayerCount), SlicerFile.BottomLayerCount);
|
|
AddStatusBarItem(nameof(SlicerFile.BottomExposureTime), SlicerFile.BottomExposureTime, "s");
|
|
AddStatusBarItem(nameof(SlicerFile.ExposureTime), SlicerFile.ExposureTime, "s");
|
|
AddStatusBarItem(nameof(SlicerFile.PrintTime), Math.Round(SlicerFile.PrintTime / 3600, 2), "h");
|
|
AddStatusBarItem(nameof(SlicerFile.UsedMaterial), Math.Round(SlicerFile.UsedMaterial, 2), "ml");
|
|
AddStatusBarItem(nameof(SlicerFile.MaterialCost), SlicerFile.MaterialCost, "€");
|
|
AddStatusBarItem(nameof(SlicerFile.MaterialName), SlicerFile.MaterialName);
|
|
AddStatusBarItem(nameof(SlicerFile.MachineName), SlicerFile.MachineName);
|
|
|
|
btnLayerResolution.Text = ActualLayerImage.Size.ToString();
|
|
}
|
|
|
|
private void UpdateGCode()
|
|
{
|
|
if (SlicerFile.GCode is null) return;
|
|
tbGCode.Text = SlicerFile.GCode.ToString();
|
|
tsGCodeLabelLines.Text = $"Lines: {tbGCode.Lines.Length}";
|
|
tsGcodeLabelChars.Text = $"Chars: {tbGCode.Text.Length}";
|
|
}
|
|
|
|
/// <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 = (pbLayer.Zoom > 100) ? 1 : (pbLayer.Zoom < 50) ? 3 : 2;
|
|
var color = new MCvScalar(Settings.Default.CrosshairColor.B, Settings.Default.CrosshairColor.G,
|
|
Settings.Default.CrosshairColor.R);
|
|
|
|
|
|
// LEFT
|
|
Point startPoint = new Point(Math.Max(0, rect.X - Settings.Default.CrosshairLineMargin - 1),
|
|
rect.Y + rect.Height / 2);
|
|
Point endPoint =
|
|
new Point(
|
|
Settings.Default.CrosshairLineLength == 0
|
|
? 0
|
|
: (int) Math.Max(0, startPoint.X - Settings.Default.CrosshairLineLength + 1),
|
|
startPoint.Y);
|
|
|
|
CvInvoke.Line(ActualLayerImageBgr,
|
|
startPoint,
|
|
endPoint,
|
|
color,
|
|
lineThickness);
|
|
|
|
|
|
// RIGHT
|
|
startPoint.X = Math.Min(ActualLayerImageBgr.Width,
|
|
rect.Right + Settings.Default.CrosshairLineMargin);
|
|
endPoint.X = Settings.Default.CrosshairLineLength == 0
|
|
? ActualLayerImageBgr.Width
|
|
: (int) Math.Min(ActualLayerImageBgr.Width, startPoint.X + Settings.Default.CrosshairLineLength - 1);
|
|
|
|
CvInvoke.Line(ActualLayerImageBgr,
|
|
startPoint,
|
|
endPoint,
|
|
color,
|
|
lineThickness);
|
|
|
|
// TOP
|
|
startPoint = new Point(rect.X + rect.Width / 2,
|
|
Math.Max(0, rect.Y - Settings.Default.CrosshairLineMargin - 1));
|
|
endPoint = new Point(startPoint.X,
|
|
(int) (Settings.Default.CrosshairLineLength == 0
|
|
? 0
|
|
: Math.Max(0, startPoint.Y - Settings.Default.CrosshairLineLength + 1)));
|
|
|
|
|
|
CvInvoke.Line(ActualLayerImageBgr,
|
|
startPoint,
|
|
endPoint,
|
|
color,
|
|
lineThickness);
|
|
|
|
// Bottom
|
|
startPoint.Y = Math.Min(ActualLayerImageBgr.Height, rect.Bottom + Settings.Default.CrosshairLineMargin);
|
|
endPoint.Y = Settings.Default.CrosshairLineLength == 0
|
|
? ActualLayerImageBgr.Height
|
|
: (int) Math.Min(ActualLayerImageBgr.Height, startPoint.Y + Settings.Default.CrosshairLineLength - 1);
|
|
|
|
CvInvoke.Line(ActualLayerImageBgr,
|
|
startPoint,
|
|
endPoint,
|
|
color,
|
|
lineThickness);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reshow current layer
|
|
/// </summary>
|
|
void ShowLayer() => ShowLayer(Math.Min(ActualLayer, SlicerFile.LayerCount - 1));
|
|
|
|
void ShowLayer(bool direction)
|
|
{
|
|
if (direction)
|
|
{
|
|
if (ActualLayer < SlicerFile.LayerCount - 1)
|
|
{
|
|
ShowLayer(ActualLayer + 1);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ActualLayer > 0)
|
|
{
|
|
ShowLayer(ActualLayer - 1);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a layer number
|
|
/// </summary>
|
|
/// <param name="layerNum">Layer number</param>
|
|
unsafe void ShowLayer(uint layerNum)
|
|
{
|
|
if (SlicerFile is null) return;
|
|
|
|
//int layerOnSlider = (int)(SlicerFile.LayerCount - layerNum - 1);
|
|
if (tbLayer.Value != layerNum)
|
|
{
|
|
tbLayer.Value = (int) layerNum;
|
|
return;
|
|
}
|
|
|
|
if (IsChangingLayer) return;
|
|
IsChangingLayer = true;
|
|
|
|
AddLogVerbose($"Show Layer: {layerNum}");
|
|
|
|
ActualLayer = layerNum;
|
|
btnLastLayer.Enabled = btnNextLayer.Enabled = layerNum < SlicerFile.LayerCount - 1;
|
|
btnFirstLayer.Enabled = btnPreviousLayer.Enabled = layerNum > 0;
|
|
|
|
|
|
var layer = SlicerFile[ActualLayer];
|
|
|
|
try
|
|
{
|
|
// OLD
|
|
//if(!ReferenceEquals(pbLayer.Image, null))
|
|
//pbLayer.Image?.Dispose(); // SLOW! LET GC DO IT
|
|
//pbLayer.Image = Image.FromStream(SlicerFile.LayerEntries[layerNum].Open());
|
|
//pbLayer.Image.RotateFlip(RotateFlipType.Rotate90FlipNone);
|
|
|
|
|
|
Stopwatch watch = Stopwatch.StartNew();
|
|
|
|
|
|
ActualLayerImage?.Dispose();
|
|
ActualLayerImage = SlicerFile[layerNum].LayerMat;
|
|
LayerCache.Image = ActualLayerImage;
|
|
|
|
CvInvoke.CvtColor(ActualLayerImage, ActualLayerImageBgr, ColorConversion.Gray2Bgr);
|
|
|
|
//var imageSpan = ActualLayerImage.GetPixelSpan<byte>();
|
|
//var imageBgrSpan = ActualLayerImageBgr.GetPixelSpan<byte>();
|
|
var imageSpan = ActualLayerImage.GetBytePointer();
|
|
var imageBgrSpan = ActualLayerImageBgr.GetBytePointer();
|
|
|
|
|
|
if (btnLayerImageLayerOutlineEdgeDetection.Checked)
|
|
{
|
|
using (var grayscale = new Mat())
|
|
{
|
|
CvInvoke.Canny(ActualLayerImage, grayscale, 80, 40, 3, true);
|
|
CvInvoke.CvtColor(grayscale, ActualLayerImageBgr, ColorConversion.Gray2Bgr);
|
|
}
|
|
}
|
|
else if (btnLayerImageLayerDifference.Checked)
|
|
{
|
|
if (layerNum > 0 && layerNum < SlicerFile.LayerCount - 1)
|
|
{
|
|
Mat previousImage = null;
|
|
Mat nextImage = null;
|
|
|
|
// Can improve performance on >4K images?
|
|
Parallel.Invoke(
|
|
() => { previousImage = SlicerFile[ActualLayer - 1].LayerMat; },
|
|
() => { nextImage = SlicerFile[ActualLayer + 1].LayerMat; });
|
|
|
|
/*using (var previousImage = SlicerFile[_actualLayer - 1].LayerMat)
|
|
using (var nextImage = SlicerFile[_actualLayer + 1].LayerMat)
|
|
{*/
|
|
//var previousSpan = previousImage.GetPixelSpan<byte>();
|
|
//var nextSpan = nextImage.GetPixelSpan<byte>();
|
|
|
|
var previousSpan = previousImage.GetBytePointer();
|
|
var nextSpan = nextImage.GetBytePointer();
|
|
|
|
int width = ActualLayerImage.Width;
|
|
int channels = ActualLayerImageBgr.NumberOfChannels;
|
|
Parallel.For(0, ActualLayerImageBgr.Height, y =>
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int pixel = y * width + x;
|
|
if (imageSpan[pixel] != 0) continue;
|
|
Color color = Color.Empty;
|
|
if (previousSpan[pixel] > 0 && nextSpan[pixel] > 0)
|
|
{
|
|
color = Settings.Default.PreviousNextLayerColor;
|
|
}
|
|
else if (previousSpan[pixel] > 0)
|
|
{
|
|
color = Settings.Default.PreviousLayerColor;
|
|
}
|
|
else if (nextSpan[pixel] > 0)
|
|
{
|
|
color = Settings.Default.NextLayerColor;
|
|
}
|
|
|
|
if (color.IsEmpty) continue;
|
|
var bgrPixel = pixel * channels;
|
|
imageBgrSpan[bgrPixel] = color.B; // B
|
|
imageBgrSpan[++bgrPixel] = color.G; // G
|
|
imageBgrSpan[++bgrPixel] = color.R; // R
|
|
//imageBgrSpan[++bgrPixel] = color.A; // A
|
|
}
|
|
});
|
|
|
|
previousImage.Dispose();
|
|
nextImage.Dispose();
|
|
}
|
|
}
|
|
|
|
var selectedIssues = flvIssues.SelectedObjects;
|
|
//List<LayerIssue> selectedIssues = (from object t in selectedIssuesRaw where ((LayerIssue) t).LayerIndex == ActualLayer select (LayerIssue) t).ToList();
|
|
|
|
if (btnLayerImageHighlightIssues.Checked &&
|
|
!ReferenceEquals(Issues, null))
|
|
{
|
|
foreach (var issue in Issues)
|
|
{
|
|
if (issue.LayerIndex != ActualLayer) continue;
|
|
if (!issue.HaveValidPoint) continue;
|
|
|
|
Color color = Color.Empty;
|
|
|
|
if (issue.Type == LayerIssue.IssueType.ResinTrap)
|
|
{
|
|
color = selectedIssues.Contains(issue)
|
|
? Settings.Default.ResinTrapHLColor
|
|
: Settings.Default.ResinTrapColor;
|
|
|
|
|
|
using (var vec = new VectorOfVectorOfPoint(new VectorOfPoint(issue.Pixels)))
|
|
{
|
|
CvInvoke.DrawContours(ActualLayerImageBgr, vec, -1,
|
|
new MCvScalar(color.B, color.G, color.R), -1);
|
|
//CvInvoke.DrawContours(ActualLayerImageBgr, new VectorOfVectorOfPoint(new VectorOfPoint(issue.Pixels)), -1, new MCvScalar(0, 0, 255), 1);
|
|
}
|
|
|
|
if (btnLayerImageShowCrosshairs.Checked &&
|
|
!Settings.Default.CrosshairShowOnlyOnSelectedIssues &&
|
|
pbLayer.Zoom <= CrosshairFadeLevel)
|
|
{
|
|
DrawCrosshair(issue.BoundingRectangle);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
switch (issue.Type)
|
|
{
|
|
case LayerIssue.IssueType.Island:
|
|
color = selectedIssues.Contains(issue)
|
|
? Settings.Default.IslandHLColor
|
|
: Settings.Default.IslandColor;
|
|
if (btnLayerImageShowCrosshairs.Checked &&
|
|
!Settings.Default.CrosshairShowOnlyOnSelectedIssues &&
|
|
pbLayer.Zoom <= CrosshairFadeLevel)
|
|
{
|
|
DrawCrosshair(issue.BoundingRectangle);
|
|
}
|
|
|
|
break;
|
|
case LayerIssue.IssueType.TouchingBound:
|
|
color = Settings.Default.TouchingBoundsColor;
|
|
break;
|
|
}
|
|
|
|
foreach (var pixel in issue)
|
|
{
|
|
int pixelPos = ActualLayerImage.GetPixelPos(pixel);
|
|
byte brightness = imageSpan[pixelPos];
|
|
if (brightness == 0) continue;
|
|
|
|
int pixelBgrPos = pixelPos * ActualLayerImageBgr.NumberOfChannels;
|
|
|
|
var newColor = color.FactorColor(brightness, 80);
|
|
|
|
imageBgrSpan[pixelBgrPos] = newColor.B; // B
|
|
imageBgrSpan[pixelBgrPos + 1] = newColor.G; // G
|
|
imageBgrSpan[pixelBgrPos + 2] = newColor.R; // R
|
|
}
|
|
}
|
|
}
|
|
|
|
if (btnLayerImageLayerOutlinePrintVolumeBounds.Checked)
|
|
{
|
|
CvInvoke.Rectangle(ActualLayerImageBgr, SlicerFile.LayerManager.BoundingRectangle,
|
|
new MCvScalar(Settings.Default.OutlinePrintVolumeBoundsColor.B,
|
|
Settings.Default.OutlinePrintVolumeBoundsColor.G,
|
|
Settings.Default.OutlinePrintVolumeBoundsColor.R),
|
|
Settings.Default.OutlinePrintVolumeBoundsLineThickness);
|
|
}
|
|
|
|
if (btnLayerImageLayerOutlineLayerBounds.Checked)
|
|
{
|
|
CvInvoke.Rectangle(ActualLayerImageBgr, SlicerFile[layerNum].BoundingRectangle,
|
|
new MCvScalar(Settings.Default.OutlineLayerBoundsColor.B,
|
|
Settings.Default.OutlineLayerBoundsColor.G, Settings.Default.OutlineLayerBoundsColor.R),
|
|
Settings.Default.OutlineLayerBoundsLineThickness);
|
|
}
|
|
|
|
if (btnLayerImageLayerOutlineHollowAreas.Checked)
|
|
{
|
|
//CvInvoke.Threshold(ActualLayerImage, grayscale, 1, 255, ThresholdType.Binary);
|
|
|
|
/*
|
|
* hierarchy[i][0]: the index of the next contour of the same level
|
|
* hierarchy[i][1]: the index of the previous contour of the same level
|
|
* hierarchy[i][2]: the index of the first child
|
|
* hierarchy[i][3]: the index of the parent
|
|
*/
|
|
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
|
|
{
|
|
if ((int)LayerCache.LayerHierarchyJagged.GetValue(0, i, 2) == -1 &&
|
|
(int)LayerCache.LayerHierarchyJagged.GetValue(0, i, 3) != -1)
|
|
{
|
|
//var r = CvInvoke.BoundingRectangle(contours[i]);
|
|
//CvInvoke.Rectangle(ActualLayerImageBgr, r, new MCvScalar(0, 0, 255), 2);
|
|
CvInvoke.DrawContours(ActualLayerImageBgr, LayerCache.LayerContours, i,
|
|
new MCvScalar(Settings.Default.OutlineHollowAreasColor.B,
|
|
Settings.Default.OutlineHollowAreasColor.G,
|
|
Settings.Default.OutlineHollowAreasColor.R),
|
|
Settings.Default.OutlineHollowAreasLineThickness);
|
|
}
|
|
/*else
|
|
{
|
|
CvInvoke.DrawContours(ActualLayerImageBgr, contours, i,
|
|
new MCvScalar(Settings.Default.ResinTrapColor.B,
|
|
Settings.Default.IslandColor.G, Settings.Default.IslandColor.R),
|
|
2);
|
|
}*/
|
|
|
|
//if ((int) arr.GetValue(0, i, 2) == -1 && (int) arr.GetValue(0, i, 3) != -1)
|
|
// CvInvoke.DrawContours(ActualLayerImageBgr, contours, i, new MCvScalar(0, 0, 0), -1);
|
|
}
|
|
}
|
|
|
|
for (var index = 0; index < PixelHistory.Count; index++)
|
|
{
|
|
if (PixelHistory[index].LayerIndex != ActualLayer) continue;
|
|
var operation = PixelHistory[index];
|
|
if (operation.OperationType == PixelOperation.PixelOperationType.Drawing)
|
|
{
|
|
var operationDrawing = (PixelDrawing) operation;
|
|
var color = operationDrawing.IsAdd
|
|
? (flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorAddPixelHLColor
|
|
: Settings.Default.PixelEditorAddPixelColor)
|
|
: (flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorRemovePixelHLColor
|
|
: Settings.Default.PixelEditorRemovePixelColor);
|
|
if (operationDrawing.BrushSize == 1)
|
|
{
|
|
ActualLayerImageBgr.SetByte(operation.Location.X, operation.Location.Y,
|
|
new[] {color.B, color.G, color.R});
|
|
continue;
|
|
}
|
|
|
|
switch (operationDrawing.BrushShape)
|
|
{
|
|
case PixelDrawing.BrushShapeType.Rectangle:
|
|
CvInvoke.Rectangle(ActualLayerImageBgr, operationDrawing.Rectangle,
|
|
new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness,
|
|
operationDrawing.LineType);
|
|
break;
|
|
case PixelDrawing.BrushShapeType.Circle:
|
|
CvInvoke.Circle(ActualLayerImageBgr, 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
|
|
? (flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorAddPixelHLColor
|
|
: Settings.Default.PixelEditorAddPixelColor)
|
|
: (flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorRemovePixelHLColor
|
|
: Settings.Default.PixelEditorRemovePixelColor);
|
|
|
|
CvInvoke.PutText(ActualLayerImageBgr, operationText.Text, operationText.Location,
|
|
operationText.Font, operationText.FontScale, new MCvScalar(color.B, color.G, color.R),
|
|
operationText.Thickness, operationText.LineType, operationText.Mirror);
|
|
}
|
|
else if (operation.OperationType == PixelOperation.PixelOperationType.Eraser)
|
|
{
|
|
if (imageSpan[ActualLayerImage.GetPixelPos(operation.Location)] < 10) continue;
|
|
var color = flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorRemovePixelHLColor
|
|
: Settings.Default.PixelEditorRemovePixelColor;
|
|
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
|
|
{
|
|
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], operation.Location, false) >= 0)
|
|
{
|
|
CvInvoke.DrawContours(ActualLayerImageBgr, LayerCache.LayerContours, i,
|
|
new MCvScalar(color.B, color.G, color.R), -1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (operation.OperationType == PixelOperation.PixelOperationType.Supports)
|
|
{
|
|
var operationSupport = (PixelSupport) operation;
|
|
var color = flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorSupportHLColor
|
|
: Settings.Default.PixelEditorSupportColor;
|
|
|
|
CvInvoke.Circle(ActualLayerImageBgr, 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 = flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorDrainHoleHLColor
|
|
: Settings.Default.PixelEditorDrainHoleColor;
|
|
|
|
CvInvoke.Circle(ActualLayerImageBgr, 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 (btnLayerImageShowCrosshairs.Checked &&
|
|
Settings.Default.CrosshairShowOnlyOnSelectedIssues &&
|
|
!ReferenceEquals(Issues, null) &&
|
|
flvIssues.SelectedIndices.Count > 0 &&
|
|
pbLayer.Zoom <=
|
|
CrosshairFadeLevel && // Only draw crosshairs when zoom level is below the configurable crosshair fade threshold.
|
|
!(btnLayerImagePixelEdit.Checked && (ModifierKeys & Keys.Shift) != 0))
|
|
{
|
|
|
|
|
|
foreach (LayerIssue issue in selectedIssues)
|
|
{
|
|
// Don't render crosshairs for selected issue that are not on the current layer, or for
|
|
// issue types that don't have a specific location or bounds.
|
|
if (issue.LayerIndex != ActualLayer || issue.Type == LayerIssue.IssueType.EmptyLayer
|
|
|| issue.Type == LayerIssue.IssueType.TouchingBound)
|
|
continue;
|
|
|
|
DrawCrosshair(issue.BoundingRectangle);
|
|
}
|
|
}
|
|
|
|
if (btnLayerImageRotate.Checked)
|
|
{
|
|
CvInvoke.Rotate(ActualLayerImageBgr, ActualLayerImageBgr, RotateFlags.Rotate90Clockwise);
|
|
/*var roi = Rectangle.Round(pbLayer.SelectionRegion);
|
|
if (roi != Rectangle.Empty)
|
|
{
|
|
pbLayer.SelectionRegion = G
|
|
}*/
|
|
}
|
|
|
|
|
|
//watch.Restart();
|
|
var imageBmp = ActualLayerImageBgr.ToBitmap();
|
|
//imageBmp.MakeTransparent();
|
|
pbLayer.Image = imageBmp;
|
|
pbLayer.Image.Tag = ActualLayerImageBgr;
|
|
//Debug.WriteLine(watch.ElapsedMilliseconds);
|
|
|
|
|
|
byte percent = (byte) ((layerNum + 1) * 100 / SlicerFile.LayerCount);
|
|
|
|
float pixelPercent =
|
|
(float) Math.Round(
|
|
layer.NonZeroPixelCount * 100f / (SlicerFile.ResolutionX * SlicerFile.ResolutionY), 2);
|
|
tsLayerImagePixelCount.Text = $"Pixels: {layer.NonZeroPixelCount} ({pixelPercent}%)";
|
|
btnLayerBounds.Text = $"Bounds: {layer.BoundingRectangle}";
|
|
tsLayerImagePixelCount.Invalidate();
|
|
btnLayerBounds.Invalidate();
|
|
tsLayerInfo.Update();
|
|
tsLayerInfo.Refresh();
|
|
|
|
watch.Stop();
|
|
tsLayerPreviewTime.Text = $"{watch.ElapsedMilliseconds}ms";
|
|
//lbLayers.Text = $"{SlicerFile.GetHeightFromLayer(layerNum)} / {SlicerFile.TotalHeight}mm\n{layerNum} / {SlicerFile.LayerCount-1}\n{percent}%";
|
|
lbActualLayer.Text = $"{layer.PositionZ}mm\n{ActualLayer}\n{percent}%";
|
|
lbActualLayer.Location = new Point(lbActualLayer.Location.X,
|
|
((int) (tbLayer.Height - (float) tbLayer.Height / tbLayer.Maximum * tbLayer.Value) -
|
|
lbActualLayer.Height / 2)
|
|
.Clamp(1, tbLayer.Height - lbActualLayer.Height));
|
|
|
|
//pbLayers.Value = percent;
|
|
lbActualLayer.Invalidate();
|
|
lbActualLayer.Update();
|
|
lbActualLayer.Refresh();
|
|
pbLayer.Invalidate();
|
|
pbLayer.Update();
|
|
pbLayer.Refresh();
|
|
//Application.DoEvents();
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.WriteLine(e);
|
|
}
|
|
|
|
IsChangingLayer = false;
|
|
}
|
|
|
|
void AddStatusBarItem(string name, object item, string extraText = "")
|
|
{
|
|
if (ReferenceEquals(item, null)) return;
|
|
//if (item.ToString().Equals(0)) return;
|
|
if (statusBar.Items.Count > 0)
|
|
statusBar.Items.Add(new ToolStripSeparator());
|
|
|
|
ToolStripLabel label = new ToolStripLabel($"{name}: {item}{extraText}");
|
|
statusBar.Items.Add(label);
|
|
}
|
|
|
|
void AdjustThumbnailSplitter()
|
|
{
|
|
scLeft.SplitterDistance = Math.Min(pbThumbnail.Image.Height + 5, 400);
|
|
}
|
|
|
|
private void EventSelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, flvIssues))
|
|
{
|
|
tsIssueRemove.Enabled = flvIssues.SelectedIndices.Count > 0;
|
|
|
|
// If selected index change has resulted in a single selected issue,
|
|
// activate it immediately. Otherwise, selection was cleared or multiple
|
|
// items are selected, and in either case, update layer preview
|
|
// to refresh selection highlighting and crosshair locations.
|
|
if (flvIssues.SelectedIndices.Count == 1)
|
|
{
|
|
EventItemActivate(flvIssues, EventArgs.Empty);
|
|
}
|
|
else
|
|
{
|
|
ShowLayer();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, flvPixelHistory))
|
|
{
|
|
btnPixelHistoryRemove.Enabled = flvPixelHistory.SelectedIndices.Count > 0;
|
|
|
|
// If single item is selected, activate it, otherwise, just refresh layer preview.
|
|
if (flvPixelHistory.SelectedIndices.Count == 1)
|
|
{
|
|
EventItemActivate(flvPixelHistory, EventArgs.Empty);
|
|
}
|
|
else
|
|
{
|
|
ShowLayer();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tabControlLeft))
|
|
{
|
|
if (!ReferenceEquals(tabControlLeft.SelectedTab, tabPagePixelEditor))
|
|
{
|
|
// Remember the last tab to be selected. This is the tab that will
|
|
// opened when Pixel Editor closes and it's tab is removed from the control.
|
|
ControlLeftLastTab = tabControlLeft.SelectedTab;
|
|
}
|
|
|
|
if (ReferenceEquals(tabControlLeft.SelectedTab, tabPageIssues))
|
|
{
|
|
if (!ReferenceEquals(tabPageIssues.Tag, null) ||
|
|
!Settings.Default.AutoComputeIssuesClickOnTab) return;
|
|
tsIssuesDetect.PerformButtonClick();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, cbPixelEditorDrawingBrushShape))
|
|
{
|
|
if (cbPixelEditorDrawingBrushShape.SelectedIndex == (int) PixelDrawing.BrushShapeType.Rectangle)
|
|
{
|
|
nmPixelEditorDrawingBrushSize.Minimum = PixelDrawing.MinRectangleBrush;
|
|
return;
|
|
}
|
|
|
|
if (cbPixelEditorDrawingBrushShape.SelectedIndex == (int) PixelDrawing.BrushShapeType.Circle)
|
|
{
|
|
nmPixelEditorDrawingBrushSize.Minimum = PixelDrawing.MinCircleBrush;
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
// We handle this event at the to top level rather than pbLayer to ensure
|
|
// the cross cursor is displayed even before the pblayer control has focus.
|
|
// This ensures the user is aware that even in this case, a click in the layer
|
|
// preview will draw a pixel.
|
|
if (ReferenceEquals(sender, this) && !ReferenceEquals(SlicerFile, null))
|
|
{
|
|
// This event repeats for as long as the key is pressed, so if we've
|
|
// already set the cursor from a previous key down event, just return.
|
|
if (!ReferenceEquals(pbLayer.Cursor.Tag, null) || pbLayer.Cursor == pixelEditCursor || pbLayer.Cursor == Cursors.Cross
|
|
|| pbLayer.Cursor == Cursors.Hand || pbLayer.SelectionMode == ImageBoxSelectionMode.Rectangle) return;
|
|
|
|
// Pixel Edit is active, Shift is down, and the cursor is over the image region.
|
|
if (pbLayer.ClientRectangle.Contains(pbLayer.PointToClient(MousePosition)))
|
|
{
|
|
if (e.Modifiers == Keys.Shift)
|
|
{
|
|
if (btnLayerImagePixelEdit.Checked)
|
|
{
|
|
pbLayer.PanMode = ImageBoxPanMode.None;
|
|
lbLayerImageTooltipOverlay.Text = "Pixel editing is on:\n" +
|
|
"» Click over a pixel to draw\n" +
|
|
"» Hold CTRL to clear pixels";
|
|
|
|
UpdatePixelEditorCursor();
|
|
}
|
|
else
|
|
{
|
|
pbLayer.Cursor = Cursors.Cross;
|
|
pbLayer.SelectionMode = ImageBoxSelectionMode.Rectangle;
|
|
lbLayerImageTooltipOverlay.Text = "ROI selection mode:\n" +
|
|
"» Left-click drag to select a fixed region\n" +
|
|
"» Left-click + ALT drag to select specific objects\n" +
|
|
"» Right click on a specific object to select it\n" +
|
|
"Press Esc to clear the ROI";
|
|
}
|
|
|
|
lbLayerImageTooltipOverlay.Visible = Settings.Default.LayerTooltipOverlay;
|
|
|
|
return;
|
|
}
|
|
if (e.Modifiers == Keys.Control)
|
|
{
|
|
pbLayer.Cursor = Cursors.Hand;
|
|
pbLayer.PanMode = ImageBoxPanMode.None;
|
|
lbLayerImageTooltipOverlay.Text = "Issue selection mode:\n" +
|
|
"» Click over an issue to select it";
|
|
|
|
lbLayerImageTooltipOverlay.Visible = Settings.Default.LayerTooltipOverlay;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventKeyUp(object sender, KeyEventArgs e)
|
|
{
|
|
// As with EventKeyDown, we handle this event at the to top level
|
|
// to ensure cursor and pan functionality are restored regardless
|
|
// of which form has focus when shift is released.
|
|
if (ReferenceEquals(sender, this))
|
|
{
|
|
if (e.KeyCode == Keys.ShiftKey || ((ModifierKeys & Keys.Shift) == 0 && e.KeyCode == Keys.ControlKey))
|
|
{
|
|
pbLayer.Cursor = Cursors.Default;
|
|
pbLayer.PanMode = ImageBoxPanMode.Left;
|
|
pbLayer.SelectionMode = ImageBoxSelectionMode.None;
|
|
lbLayerImageTooltipOverlay.Visible = false;
|
|
//if (!ReferenceEquals(SlicerFile, null)) ShowLayer(); // Not needed?
|
|
e.Handled = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, pbLayer))
|
|
{
|
|
if (e.KeyCode == Keys.Escape)
|
|
{
|
|
pbLayer.SelectNone();
|
|
e.Handled = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, flvProperties))
|
|
{
|
|
if (e.KeyCode == Keys.Escape)
|
|
{
|
|
flvProperties.SelectedIndices.Clear();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
|
|
if (e.KeyCode == Keys.Multiply)
|
|
{
|
|
int[] selectedArray = new int[flvProperties.SelectedIndices.Count];
|
|
flvProperties.SelectedIndices.CopyTo(selectedArray, 0);
|
|
flvProperties.SelectedIndices.Clear();
|
|
for (int i = 0; i < flvProperties.GetItemCount(); i++)
|
|
{
|
|
if (selectedArray.Contains(i)) continue;
|
|
flvProperties.SelectedIndices.Add(i);
|
|
}
|
|
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, flvIssues))
|
|
{
|
|
if (e.KeyCode == Keys.Escape)
|
|
{
|
|
flvIssues.SelectedIndices.Clear();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.Multiply)
|
|
{
|
|
int[] selectedArray = new int[flvIssues.SelectedIndices.Count];
|
|
flvIssues.SelectedIndices.CopyTo(selectedArray, 0);
|
|
flvIssues.SelectedIndices.Clear();
|
|
for (int i = 0; i < flvIssues.GetItemCount(); i++)
|
|
{
|
|
if (selectedArray.Contains(i)) continue;
|
|
flvIssues.SelectedIndices.Add(i);
|
|
}
|
|
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
/*if (e.Control && e.KeyCode == Keys.C)
|
|
{
|
|
StringBuilder clip = new StringBuilder();
|
|
foreach (ListViewItem item in lvIssues.Items)
|
|
{
|
|
if (!item.Selected) continue;
|
|
if (clip.Length > 0) clip.AppendLine();
|
|
clip.Append($"{item.Text}: {item.SubItems[1].Text} Layer: {item.SubItems[2].Text} X,Y: {{{item.SubItems[3].Text}}} Pixels: {item.SubItems[4].Text}");
|
|
}
|
|
|
|
if (clip.Length > 0)
|
|
{
|
|
Clipboard.SetText(clip.ToString());
|
|
}
|
|
e.Handled = true;
|
|
return;
|
|
}*/
|
|
|
|
if (e.KeyCode == Keys.Delete)
|
|
{
|
|
tsIssueRemove.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, flvPixelHistory))
|
|
{
|
|
if (e.KeyCode == Keys.Escape)
|
|
{
|
|
flvPixelHistory.SelectedIndices.Clear();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (e.KeyCode == Keys.Multiply)
|
|
{
|
|
int[] selectedArray = new int[flvPixelHistory.SelectedIndices.Count];
|
|
flvPixelHistory.SelectedIndices.CopyTo(selectedArray, 0);
|
|
flvPixelHistory.SelectedIndices.Clear();
|
|
for (int i = 0; i < flvPixelHistory.GetItemCount(); i++)
|
|
{
|
|
if (selectedArray.Contains(i)) continue;
|
|
flvPixelHistory.SelectedIndices.Add(i);
|
|
}
|
|
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
/*if (e.Control && e.KeyCode == Keys.C)
|
|
{
|
|
StringBuilder clip = new StringBuilder();
|
|
foreach (ListViewItem item in flvPixelHistory.Items)
|
|
{
|
|
if (!item.Selected) continue;
|
|
if (clip.Length > 0) clip.AppendLine();
|
|
clip.Append($"{item.Text}: {item.SubItems[1].Text} Layer: {item.SubItems[2].Text} X,Y: {{{item.SubItems[3].Text}}}");
|
|
}
|
|
|
|
if (clip.Length > 0)
|
|
{
|
|
Clipboard.SetText(clip.ToString());
|
|
}
|
|
e.Handled = true;
|
|
return;
|
|
}*/
|
|
|
|
if (e.KeyCode == Keys.Delete)
|
|
{
|
|
btnPixelHistoryRemove.PerformClick();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventMouseDown(object sender, MouseEventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, btnNextLayer) || ReferenceEquals(sender, btnPreviousLayer))
|
|
{
|
|
layerScrollTimer.Tag = ReferenceEquals(sender, btnNextLayer);
|
|
layerScrollTimer.Start();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsIssueNext) || ReferenceEquals(sender, tsIssuePrevious))
|
|
{
|
|
issueScrollTimer.Tag = ReferenceEquals(sender, tsIssueNext);
|
|
issueScrollTimer.Start();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, pbLayer))
|
|
{
|
|
if (e.Button == MouseButtons.Middle)
|
|
{
|
|
mouseHoldTimer.Tag = e.Button;
|
|
mouseHoldTimer.Start();
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventMouseUp(object sender, MouseEventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, btnNextLayer) || ReferenceEquals(sender, btnPreviousLayer))
|
|
{
|
|
layerScrollTimer.Stop();
|
|
layerScrollTimer.Interval = 500;
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, tsIssueNext) || ReferenceEquals(sender, tsIssuePrevious))
|
|
{
|
|
issueScrollTimer.Stop();
|
|
issueScrollTimer.Interval = 500;
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, pbLayer))
|
|
{
|
|
// unconditionally stop any pending mouse timer here.
|
|
mouseHoldTimer.Stop();
|
|
|
|
if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
Point location = pbLayer.PointToImage(e.Location);
|
|
if (pbLayer.SelectionMode == ImageBoxSelectionMode.Rectangle)
|
|
{
|
|
if (e.Button == MouseButtons.Left)
|
|
{
|
|
if ((ModifierKeys & Keys.Alt) != 0)
|
|
{
|
|
if (SelectObjectRoi(ROI) == 0) SelectObjectRoi(location);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (e.Button == MouseButtons.Right)
|
|
{
|
|
if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
SelectObjectRoi(location);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Shift must be pressed for any pixel edit action, middle button is ignored.
|
|
if (!btnLayerImagePixelEdit.Checked || (e.Button & MouseButtons.Middle) != 0 ||
|
|
(ModifierKeys & Keys.Shift) == 0) return;
|
|
//if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
//location = pbLayer.PointToImage(e.Location);
|
|
_lastPixelMouseLocation = Point.Empty;
|
|
|
|
// Left or Alt-Right Adds pixel, Right or Alt-Left removes pixel
|
|
DrawPixel(e.Button == MouseButtons.Left ^ (ModifierKeys & Keys.Alt) != 0, location);
|
|
//SlicerFile[ActualLayer].LayerMat = ActualLayerImage;
|
|
RefreshPixelHistory();
|
|
}
|
|
}
|
|
|
|
public bool SelectObjectRoi(Point location)
|
|
{
|
|
var point = GetTransposedPoint(location);
|
|
var brightness = ActualLayerImage.GetByte(point);
|
|
|
|
if (brightness == 0) return false;
|
|
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
|
|
{
|
|
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], point, false) >= 0)
|
|
{
|
|
var rectangle =
|
|
GetTransposedRectangle(CvInvoke.BoundingRectangle(LayerCache.LayerContours[i]));
|
|
ROI = rectangle;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public uint SelectObjectRoi(Rectangle roiRectangle)
|
|
{
|
|
if (roiRectangle.IsEmpty) return 0;
|
|
List<Rectangle> rectangles = new List<Rectangle>();
|
|
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
|
|
{
|
|
var rectangle = CvInvoke.BoundingRectangle(LayerCache.LayerContours[i]);
|
|
//roi.Intersect(rectangle);
|
|
if (roiRectangle.IntersectsWith(rectangle))
|
|
{
|
|
rectangles.Add(rectangle);
|
|
}
|
|
|
|
}
|
|
roiRectangle = rectangles.Count == 0 ? Rectangle.Empty : rectangles[0];
|
|
for (var i = 1; i < rectangles.Count; i++)
|
|
{
|
|
var rectangle = rectangles[i];
|
|
roiRectangle = Rectangle.Union(roiRectangle, rectangle);
|
|
}
|
|
|
|
ROI = GetTransposedRectangle(roiRectangle);
|
|
|
|
return (uint) rectangles.Count;
|
|
}
|
|
|
|
private void EventMouseLeave(object sender, EventArgs e)
|
|
{
|
|
// Toolstrip Buttons do not register mouseup events if the mouse is no longer over
|
|
// the button when the click is released. Cancel scroll timer if the mouse is
|
|
// moved from over top of the button.
|
|
if (ReferenceEquals(sender, tsIssueNext) || ReferenceEquals(sender, tsIssuePrevious))
|
|
{
|
|
issueScrollTimer.Stop();
|
|
issueScrollTimer.Interval = 500;
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventTimerTick(object sender, EventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, layerScrollTimer))
|
|
{
|
|
if (!btnPreviousLayer.Enabled || !btnNextLayer.Enabled)
|
|
{
|
|
layerScrollTimer.Stop();
|
|
return;
|
|
}
|
|
|
|
layerScrollTimer.Interval = 150;
|
|
ShowLayer((bool) layerScrollTimer.Tag);
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, layerZoomTimer))
|
|
{
|
|
ShowLayer();
|
|
layerZoomTimer.Stop();
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, issueScrollTimer))
|
|
{
|
|
if (!tsIssuePrevious.Enabled || !tsIssueNext.Enabled)
|
|
{
|
|
issueScrollTimer.Stop();
|
|
return;
|
|
}
|
|
|
|
issueScrollTimer.Interval = 150;
|
|
if ((bool) issueScrollTimer.Tag)
|
|
{
|
|
EventClick(tsIssueNext, null);
|
|
}
|
|
else
|
|
{
|
|
EventClick(tsIssuePrevious, null);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, mouseHoldTimer))
|
|
{
|
|
mouseHoldTimer.Stop(); // one-shot timer
|
|
|
|
if ((MouseButtons) mouseHoldTimer.Tag == MouseButtons.Middle)
|
|
{
|
|
// Reset auto-zoom level based on current zoom level and
|
|
// refresh toolstrip zoom indicator.
|
|
if (pbLayer.Zoom >= MinLockedZoomLevel)
|
|
{
|
|
LockedZoomLevel = pbLayer.Zoom;
|
|
tsLayerImageZoom.Text = $"Zoom: [ {pbLayer.Zoom / 100f}x";
|
|
tsLayerImageZoomLock.Visible = true;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
private void EventMouseClick(object sender, MouseEventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, pbLayer))
|
|
{
|
|
if ((ModifierKeys & Keys.Alt) != 0)
|
|
{
|
|
// ALT click within pbLayer performs double click action
|
|
HandleMouseDoubleClick(sender, e);
|
|
|
|
return;
|
|
}
|
|
|
|
if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
var location = pbLayer.PointToImage(e.Location);
|
|
|
|
// Check to see if the clicked location is an issue,
|
|
// and if so, select it in the ListView.
|
|
if ((ModifierKeys & Keys.Control) != 0)
|
|
SelectIssueAtPoint(location);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void EventMouseDoubleClick(object sender, MouseEventArgs e)
|
|
{
|
|
// Ignore double click if CTRL is pressed. Prevents ALT-click
|
|
// events that emulate double click from firing twice.
|
|
if ((ModifierKeys & Keys.Alt) != 0) return;
|
|
HandleMouseDoubleClick(sender, e);
|
|
}
|
|
|
|
private void HandleMouseDoubleClick(object sender, MouseEventArgs e)
|
|
{
|
|
if (ReferenceEquals(sender, pbLayer))
|
|
{
|
|
// Ignore double click events if shift is pressed. This prevents zoom
|
|
// operations from inadvertently occuring when pixels are being edited, and
|
|
// for consistency, there is no reason not to just disallow this any time
|
|
// shift is pressed regardless of whether pixel edit mode is enabled or not.
|
|
if ((ModifierKeys & Keys.Shift) != 0) return;
|
|
|
|
if ((e.Button & MouseButtons.Left) != 0)
|
|
{
|
|
if (!pbLayer.IsPointInImage(e.Location)) return;
|
|
var location = pbLayer.PointToImage(e.Location);
|
|
|
|
// Check to see if this zoom action will cross the crosshair fade threshold
|
|
// Not needed? visual artifact is not severe to make an extra showlayer call, also it acts like an "animation" removing crosshairs after zoom
|
|
/*if (tsLayerImageShowCrosshairs.Checked &&
|
|
!ReferenceEquals(Issues, null) && flvIssues.SelectedIndices.Count > 0 &&
|
|
pbLayer.Zoom <= CrosshairFadeLevel && LockedZoomLevel > CrosshairFadeLevel &&
|
|
flvIssues.SelectedObjects.Cast<LayerIssue>().Any(issue => // Find a valid candidate to update layer preview, otherwise quit
|
|
issue.LayerIndex == ActualLayer && issue.Type != LayerIssue.IssueType.EmptyLayer && issue.Type != LayerIssue.IssueType.TouchingBound))
|
|
{
|
|
// Refresh the preview without the crosshairs before zooming-in.
|
|
// Prevents zoomed-in crosshairs from breifly being displayed before
|
|
// the Layer Preview is refreshed post-zoom.
|
|
tsLayerImageShowCrosshairs.Checked = false;
|
|
ShowLayer();
|
|
tsLayerImageShowCrosshairs.Checked = true;
|
|
}*/
|
|
|
|
|
|
CenterLayerAt(location, LockedZoomLevel);
|
|
|
|
// Check to see if the clicked location is an issue, and if so, select it in the ListView.
|
|
SelectIssueAtPoint(location);
|
|
|
|
return;
|
|
}
|
|
|
|
if ((e.Button & MouseButtons.Right) != 0)
|
|
{
|
|
ZoomToFit();
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ReferenceEquals(sender, flvIssues))
|
|
{
|
|
if (!(flvIssues.SelectedObject is LayerIssue issue)) return;
|
|
// Double clicking an issue will center and zoom into the
|
|
// selected issue. Left click on an issue will zoom to fit.
|
|
if ((e.Button & MouseButtons.Left) != 0)
|
|
{
|
|
ZoomToIssue(issue);
|
|
return;
|
|
}
|
|
|
|
if ((e.Button & MouseButtons.Right) != 0)
|
|
{
|
|
ZoomToFit();
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
void DrawPixel(bool isAdd, Point location)
|
|
{
|
|
//Stopwatch sw = Stopwatch.StartNew();
|
|
//var point = pbLayer.PointToImage(location);
|
|
|
|
Point realLocation = GetTransposedPoint(location);
|
|
|
|
if ((ModifierKeys & Keys.Control) != 0)
|
|
{
|
|
var removedItems = PixelHistory.Items.RemoveAll(item =>
|
|
{
|
|
Rectangle rect = new Rectangle(item.Location, item.Size);
|
|
rect.X -= item.Size.Width / 2;
|
|
rect.Y -= item.Size.Height / 2;
|
|
return rect.Contains(realLocation);
|
|
});
|
|
if(removedItems > 0) ShowLayer();
|
|
return;
|
|
}
|
|
|
|
PixelOperation operation = null;
|
|
Bitmap bmp = pbLayer.Image as Bitmap;
|
|
|
|
if (tabControlPixelEditor.SelectedIndex == (byte) PixelOperation.PixelOperationType.Drawing)
|
|
{
|
|
LineType lineType = (LineType) cbPixelEditorDrawingLineType.SelectedItem;
|
|
PixelDrawing.BrushShapeType shapeType =
|
|
(PixelDrawing.BrushShapeType) cbPixelEditorDrawingBrushShape.SelectedIndex;
|
|
|
|
ushort brushSize = (ushort) nmPixelEditorDrawingBrushSize.Value;
|
|
short thickness = (short) (nmPixelEditorDrawingThickness.Value == 0
|
|
? 1
|
|
: nmPixelEditorDrawingThickness.Value);
|
|
|
|
uint minLayer = (uint) Math.Max(0, ActualLayer - nmPixelEditorDrawingLayersBelow.Value);
|
|
uint maxLayer = (uint) Math.Min(SlicerFile.LayerCount - 1,
|
|
ActualLayer + nmPixelEditorDrawingLayersAbove.Value);
|
|
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
|
|
{
|
|
operation = new PixelDrawing(layerIndex, realLocation, lineType,
|
|
shapeType, brushSize, thickness, isAdd);
|
|
|
|
if (PixelHistory.Contains(operation)) continue;
|
|
PixelHistory.Add(operation);
|
|
|
|
if (layerIndex == ActualLayer)
|
|
{
|
|
using (var gfx = Graphics.FromImage(bmp))
|
|
{
|
|
int shiftPos = brushSize / 2;
|
|
gfx.SmoothingMode = SmoothingMode.HighSpeed;
|
|
|
|
var color = isAdd
|
|
? (flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorAddPixelHLColor
|
|
: Settings.Default.PixelEditorAddPixelColor)
|
|
: (flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorRemovePixelHLColor
|
|
: Settings.Default.PixelEditorRemovePixelColor);
|
|
if (lineType == LineType.AntiAlias && brushSize > 1)
|
|
{
|
|
gfx.SmoothingMode = SmoothingMode.AntiAlias;
|
|
}
|
|
|
|
switch (shapeType)
|
|
{
|
|
case PixelDrawing.BrushShapeType.Rectangle:
|
|
if (thickness > 0)
|
|
gfx.DrawRectangle(new Pen(color, thickness), Math.Max(0, location.X - shiftPos),
|
|
Math.Max(0, location.Y - shiftPos),
|
|
(int) nmPixelEditorDrawingBrushSize.Value,
|
|
(int) nmPixelEditorDrawingBrushSize.Value);
|
|
else
|
|
gfx.FillRectangle(new SolidBrush(color), Math.Max(0, location.X - shiftPos),
|
|
Math.Max(0, location.Y - shiftPos),
|
|
(int) nmPixelEditorDrawingBrushSize.Value,
|
|
(int) nmPixelEditorDrawingBrushSize.Value);
|
|
break;
|
|
case PixelDrawing.BrushShapeType.Circle:
|
|
if (thickness > 0)
|
|
gfx.DrawEllipse(new Pen(color, thickness), Math.Max(0, location.X - shiftPos),
|
|
Math.Max(0, location.Y - shiftPos),
|
|
(int) nmPixelEditorDrawingBrushSize.Value,
|
|
(int) nmPixelEditorDrawingBrushSize.Value);
|
|
else
|
|
gfx.FillEllipse(new SolidBrush(color), Math.Max(0, location.X - shiftPos),
|
|
Math.Max(0, location.Y - shiftPos),
|
|
(int) nmPixelEditorDrawingBrushSize.Value,
|
|
(int) nmPixelEditorDrawingBrushSize.Value);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (tabControlPixelEditor.SelectedIndex == (byte) PixelOperation.PixelOperationType.Text)
|
|
{
|
|
if (string.IsNullOrEmpty(tbPixelEditorTextText.Text) || nmPixelEditorTextFontScale.Value < 0.2m) return;
|
|
|
|
LineType lineType = (LineType) cbPixelEditorTextLineType.SelectedItem;
|
|
FontFace fontFace = (FontFace) cbPixelEditorTextFontFace.SelectedItem;
|
|
|
|
uint minLayer = (uint) Math.Max(0, ActualLayer - nmPixelEditorTextLayersBelow.Value);
|
|
uint maxLayer = (uint) Math.Min(SlicerFile.LayerCount - 1,
|
|
ActualLayer + nmPixelEditorTextLayersAbove.Value);
|
|
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
|
|
{
|
|
operation = new PixelText(layerIndex, realLocation, lineType,
|
|
fontFace, (double) nmPixelEditorTextFontScale.Value, (ushort) nmPixelEditorTextThickness.Value,
|
|
tbPixelEditorTextText.Text, cbPixelEditorTextMirror.Checked, isAdd);
|
|
|
|
if (PixelHistory.Contains(operation)) continue;
|
|
PixelHistory.Add(operation);
|
|
}
|
|
|
|
ShowLayer();
|
|
return;
|
|
}
|
|
else if (tabControlPixelEditor.SelectedIndex == (byte) PixelOperation.PixelOperationType.Eraser)
|
|
{
|
|
if (ActualLayerImage.GetByte(realLocation) < 10) return;
|
|
uint minLayer = (uint) Math.Max(0, ActualLayer - nmPixelEditorEraserLayersBelow.Value);
|
|
uint maxLayer = (uint) Math.Min(SlicerFile.LayerCount - 1,
|
|
ActualLayer + nmPixelEditorEraserLayersAbove.Value);
|
|
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
|
|
{
|
|
operation = new PixelEraser(layerIndex, realLocation);
|
|
|
|
if (PixelHistory.Contains(operation)) continue;
|
|
PixelHistory.Add(operation);
|
|
}
|
|
|
|
ShowLayer();
|
|
return;
|
|
}
|
|
else if (tabControlPixelEditor.SelectedIndex == (byte) PixelOperation.PixelOperationType.Supports)
|
|
{
|
|
if (ActualLayer == 0) return;
|
|
operation = new PixelSupport(ActualLayer, realLocation,
|
|
(byte) nmPixelEditorSupportsTipDiameter.Value, (byte) nmPixelEditorSupportsPillarDiameter.Value,
|
|
(byte) nmPixelEditorSupportsBaseDiameter.Value);
|
|
|
|
if (PixelHistory.Contains(operation)) return;
|
|
PixelHistory.Add(operation);
|
|
|
|
SolidBrush brush = new SolidBrush(flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorSupportHLColor
|
|
: Settings.Default.PixelEditorSupportColor);
|
|
using (var gfx = Graphics.FromImage(bmp))
|
|
{
|
|
int shiftPos = (int) nmPixelEditorSupportsTipDiameter.Value / 2;
|
|
gfx.SmoothingMode = SmoothingMode.HighSpeed;
|
|
gfx.FillEllipse(brush, Math.Max(0, location.X - shiftPos), Math.Max(0, location.Y - shiftPos),
|
|
(int) nmPixelEditorSupportsTipDiameter.Value, (int) nmPixelEditorSupportsTipDiameter.Value);
|
|
}
|
|
}
|
|
else if (tabControlPixelEditor.SelectedIndex == (byte) PixelOperation.PixelOperationType.DrainHole)
|
|
{
|
|
if (ActualLayer == 0) return;
|
|
operation = new PixelDrainHole(ActualLayer, realLocation, (byte) nmPixelEditorDrainHoleDiameter.Value);
|
|
|
|
if (PixelHistory.Contains(operation)) return;
|
|
PixelHistory.Add(operation);
|
|
|
|
SolidBrush brush = new SolidBrush(flvPixelHistory.SelectedObjects.Contains(operation)
|
|
? Settings.Default.PixelEditorDrainHoleHLColor
|
|
: Settings.Default.PixelEditorDrainHoleColor);
|
|
using (var gfx = Graphics.FromImage(bmp))
|
|
{
|
|
int shiftPos = (int) nmPixelEditorDrainHoleDiameter.Value / 2;
|
|
gfx.SmoothingMode = SmoothingMode.HighSpeed;
|
|
gfx.FillEllipse(brush, Math.Max(0, location.X - shiftPos), Math.Max(0, location.Y - shiftPos),
|
|
(int) nmPixelEditorDrainHoleDiameter.Value, (int) nmPixelEditorDrainHoleDiameter.Value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new NotImplementedException("Missing pixel operation");
|
|
}
|
|
|
|
|
|
|
|
pbLayer.Invalidate();
|
|
//pbLayer.Update();
|
|
//pbLayer.Refresh();
|
|
//menuFileSave.Enabled = menuFileSaveAs.Enabled = true;
|
|
//sw.Stop();
|
|
//Debug.WriteLine(sw.ElapsedMilliseconds);
|
|
}
|
|
|
|
public void RefreshPixelHistory()
|
|
{
|
|
PixelHistory.Renumber();
|
|
flvPixelHistory.SetObjects(PixelHistory);
|
|
|
|
lbPixelHistoryOperations.Text = $"Operations: {PixelHistory.Count}";
|
|
|
|
btnPixelHistoryRemove.Enabled = flvPixelHistory.SelectedIndices.Count > 0;
|
|
btnPixelHistoryClear.Enabled =
|
|
btnPixelHistoryApply.Enabled = PixelHistory.Count > 0;
|
|
}
|
|
|
|
private void UpdateIssuesInfo()
|
|
{
|
|
if (TotalIssues == 0)
|
|
{
|
|
tsIssuePrevious.Enabled =
|
|
tsIssueCount.Enabled =
|
|
tsIssueNext.Enabled = false;
|
|
|
|
tsIssueCount.Text = "0/0";
|
|
tsIssueCount.Tag = -1;
|
|
}
|
|
else
|
|
{
|
|
int currentIssueSelected = Convert.ToInt32(tsIssueCount.Tag);
|
|
// Convert text to fixed field length in order to prevent
|
|
// prev/next buttons from moving as the number of digits in
|
|
// the display for current index changes. Without this,
|
|
// scrolling may unexpectedly stop due to the button moving
|
|
// out from underneath the mouse pointer.
|
|
var digits = Math.Floor(Math.Log10(TotalIssues) + 1);
|
|
var issueNum = currentIssueSelected + 1;
|
|
var formatString = $"D{digits}";
|
|
|
|
tsIssueCount.Enabled = true;
|
|
tsIssueCount.Text = $"{issueNum.ToString(formatString)}/{TotalIssues}";
|
|
tsIssuePrevious.Enabled = currentIssueSelected > 0;
|
|
tsIssueNext.Enabled = currentIssueSelected + 1 < TotalIssues;
|
|
}
|
|
|
|
}
|
|
|
|
private void ComputeIssues(IslandDetectionConfiguration islandConfig = null,
|
|
ResinTrapDetectionConfiguration resinTrapConfig = null,
|
|
TouchingBoundDetectionConfiguration touchingBoundConfig = null, bool emptyLayersConfig = true)
|
|
{
|
|
tabPageIssues.Tag = true;
|
|
flvIssues.ClearObjects();
|
|
UpdateIssuesInfo();
|
|
|
|
DisableGUI();
|
|
FrmLoading.SetDescription("Computing Issues");
|
|
|
|
var task = Task.Factory.StartNew(() =>
|
|
{
|
|
try
|
|
{
|
|
Issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, resinTrapConfig, touchingBoundConfig,
|
|
emptyLayersConfig, FrmLoading.RestartProgress());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(ex.Message, "Error while trying compute issues", MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
var loadingResult = FrmLoading.ShowDialog();
|
|
|
|
UpdateIssuesList();
|
|
}
|
|
|
|
public Point GetTransposedPoint(Point point, bool clockWise = true)
|
|
{
|
|
if (!btnLayerImageRotate.Checked) return point;
|
|
return clockWise
|
|
? new Point(point.Y, ActualLayerImage.Height - 1 - point.X)
|
|
: new Point(ActualLayerImage.Height - 1 - point.Y, point.X);
|
|
}
|
|
|
|
public Rectangle GetTransposedRectangle(RectangleF rectangleF, bool clockWise = true, bool ignoreLayerRotation = false) =>
|
|
GetTransposedRectangle(Rectangle.Round(rectangleF), clockWise, ignoreLayerRotation);
|
|
|
|
public Rectangle GetTransposedRectangle(Rectangle rectangle, bool clockWise = true, bool ignoreLayerRotation = false)
|
|
{
|
|
if (rectangle.IsEmpty || (!ignoreLayerRotation && !btnLayerImageRotate.Checked)) return rectangle;
|
|
return clockWise
|
|
? new Rectangle(ActualLayerImage.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, ActualLayerImage.Height - rectangle.Right, rectangle.Height, rectangle.Width); // Rotate90FlipNone: // = Rotate270FlipXY
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the bounding rectangle of the passed issue, automatically adjusting
|
|
/// the coordinates and width/height to account for whether or not the layer
|
|
/// preview image is rotated. Used to ensure images are properly zoomed or
|
|
/// centered independent of the layer preview rotation.
|
|
/// </summary>
|
|
private Rectangle GetTransposedIssueBounds(LayerIssue issue)
|
|
{
|
|
if (issue.X >= 0 && issue.Y >= 0 && (issue.BoundingRectangle.IsEmpty || issue.Size == 1) &&
|
|
btnLayerImageRotate.Checked)
|
|
return new Rectangle(ActualLayerImage.Height - 1 - issue.Y,
|
|
issue.X, 1, 1);
|
|
|
|
return GetTransposedRectangle(issue.BoundingRectangle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Centers layer view on a X,Y coordinate
|
|
/// </summary>
|
|
/// <param name="x">X coordinate</param>
|
|
/// <param name="y">X coordinate</param>
|
|
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
|
|
public void CenterLayerAt(int x, int y, int zoomLevel = 0)
|
|
{
|
|
if (zoomLevel < 0) zoomLevel = LockedZoomLevel;
|
|
if (zoomLevel > 0) pbLayer.Zoom = zoomLevel;
|
|
pbLayer.CenterAt(x, y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Centers layer view on a middle of a given rectangle
|
|
/// </summary>
|
|
/// <param name="rectangle">Rectangle holding coordinates and bounds</param>
|
|
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
|
|
/// <param name="zoomToRegion">Auto zoom to a region and ensure that region area stays all visible when possible, when true this will overwrite zoomLevel</param></param>
|
|
public void CenterLayerAt(Rectangle rectangle, int zoomLevel = 0, bool zoomToRegion = false)
|
|
{
|
|
Rectangle viewPort = Rectangle.Round(pbLayer.GetSourceImageRegion());
|
|
if (zoomToRegion ||
|
|
rectangle.Width * LockedZoomLevel / pbLayer.Zoom > viewPort.Width ||
|
|
rectangle.Height * LockedZoomLevel / pbLayer.Zoom > viewPort.Height)
|
|
{
|
|
SupressLayerZoomEvent = true;
|
|
pbLayer.ZoomToRegion(rectangle);
|
|
SupressLayerZoomEvent = false;
|
|
pbLayer.ZoomOut(true);
|
|
return;
|
|
}
|
|
|
|
CenterLayerAt(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2, zoomLevel);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Centers layer view on a middle of a given rectangle
|
|
/// </summary>
|
|
/// <param name="rectangle">Rectangle holding coordinates and bounds</param>
|
|
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
|
|
/// <param name="zoomToRegion">Auto zoom to a region and ensure that region area stays all visible when possible, when true this will overwrite zoomLevel</param></param>
|
|
public void CenterLayerAt(RectangleF rectangle, int zoomLevel = 0, bool zoomToRegion = false) =>
|
|
CenterLayerAt(Rectangle.Round(rectangle), zoomLevel, zoomToRegion);
|
|
|
|
|
|
/// <summary>
|
|
/// Centers layer view on a <see cref="Point"/>
|
|
/// </summary>
|
|
/// <param name="point">Point holding X and Y coordinates</param>
|
|
/// <param name="zoomLevel">Zoom level to set, 0 to ignore or negative value to get current locked zoom level</param>
|
|
public void CenterLayerAt(Point point, int zoomLevel = 0) => CenterLayerAt(point.X, point.Y, zoomLevel);
|
|
|
|
|
|
/// <summary>
|
|
/// Zoom the layer preview to the passed issue, or if appropriate for issue type,
|
|
/// Zoom to fit the plate or print bounds.
|
|
/// </summary>
|
|
private void ZoomToIssue(LayerIssue issue)
|
|
{
|
|
if (issue.Type == LayerIssue.IssueType.TouchingBound || issue.Type == LayerIssue.IssueType.EmptyLayer ||
|
|
(issue.X == -1 && issue.Y == -1))
|
|
{
|
|
ZoomToFit();
|
|
return;
|
|
}
|
|
|
|
if (issue.X >= 0 && issue.Y >= 0)
|
|
{
|
|
// Check to see if this zoom action will cross the crosshair fade threshold
|
|
/*if (tsLayerImageShowCrosshairs.Checked && !ReferenceEquals(Issues, null) && flvIssues.SelectedIndices.Count > 0
|
|
&& pbLayer.Zoom <= CrosshairFadeLevel && LockedZoomLevel > CrosshairFadeLevel)
|
|
{
|
|
// Refresh the preview without the crosshairs before zooming-in.
|
|
// Prevents zoomed-in crosshairs from breifly being displayed before
|
|
// the Layer Preview is refreshed post-zoom.
|
|
tsLayerImageShowCrosshairs.Checked = false;
|
|
ShowLayer();
|
|
tsLayerImageShowCrosshairs.Checked = true;
|
|
}*/
|
|
|
|
CenterLayerAt(GetTransposedIssueBounds(issue), LockedZoomLevel);
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Center the layer preview on the passed issue, or if appropriate for issue type,
|
|
/// Zoom to fit the plate or print bounds.
|
|
/// </summary>
|
|
private void CenterAtIssue(LayerIssue issue)
|
|
{
|
|
if (issue.Type == LayerIssue.IssueType.TouchingBound || issue.Type == LayerIssue.IssueType.EmptyLayer ||
|
|
(issue.X == -1 && issue.Y == -1))
|
|
{
|
|
ZoomToFit();
|
|
}
|
|
|
|
if (issue.X >= 0 && issue.Y >= 0)
|
|
{
|
|
CenterLayerAt(GetTransposedIssueBounds(issue));
|
|
}
|
|
}
|
|
|
|
private void ZoomToFit()
|
|
{
|
|
if (ReferenceEquals(SlicerFile, null)) return;
|
|
|
|
// If ALT key is pressed when ZoomToFit is performed, the configured option for
|
|
// zoom to plate vs. zoom to print bounds will be inverted.
|
|
if (Settings.Default.ZoomToFitPrintVolumeBounds ^ (ModifierKeys & Keys.Alt) != 0)
|
|
{
|
|
if (!btnLayerImageRotate.Checked)
|
|
{
|
|
pbLayer.ZoomToRegion(SlicerFile.LayerManager.BoundingRectangle);
|
|
}
|
|
else
|
|
{
|
|
pbLayer.ZoomToRegion(ActualLayerImage.Height - 1 - SlicerFile.LayerManager.BoundingRectangle.Bottom,
|
|
SlicerFile.LayerManager.BoundingRectangle.X,
|
|
SlicerFile.LayerManager.BoundingRectangle.Height,
|
|
SlicerFile.LayerManager.BoundingRectangle.Width
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pbLayer.ZoomToFit();
|
|
}
|
|
}
|
|
|
|
/// <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.
|
|
int index = -1;
|
|
foreach (LayerIssue issue in flvIssues.Objects)
|
|
{
|
|
index++;
|
|
|
|
if (issue.LayerIndex != ActualLayer) continue;
|
|
if (!GetTransposedIssueBounds(issue).Contains(location)) continue;
|
|
|
|
flvIssues.SelectedIndex = index;
|
|
flvIssues.EnsureVisible(index);
|
|
//flvIssues.Refresh();
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
public IslandDetectionConfiguration GetIslandDetectionConfiguration()
|
|
{
|
|
return new IslandDetectionConfiguration
|
|
{
|
|
Enabled = tsIssuesDetectIslands.Checked,
|
|
AllowDiagonalBonds = Settings.Default.IslandAllowDiagonalBonds,
|
|
BinaryThreshold = Settings.Default.IslandBinaryThreshold,
|
|
RequiredAreaToProcessCheck = Settings.Default.IslandRequiredAreaToProcessCheck,
|
|
RequiredPixelBrightnessToProcessCheck = Settings.Default.IslandRequiredPixelBrightnessToProcessCheck,
|
|
RequiredPixelsToSupport = Settings.Default.IslandRequiredPixelsToSupport,
|
|
RequiredPixelBrightnessToSupport = Settings.Default.IslandRequiredPixelBrightnessToSupport
|
|
};
|
|
}
|
|
|
|
public ResinTrapDetectionConfiguration GetResinTrapDetectionConfiguration()
|
|
{
|
|
return new ResinTrapDetectionConfiguration
|
|
{
|
|
Enabled = tsIssuesDetectResinTraps.Checked,
|
|
BinaryThreshold = Settings.Default.ResinTrapBinaryThreshold,
|
|
RequiredAreaToProcessCheck = Settings.Default.ResinTrapRequiredAreaToProcessCheck,
|
|
RequiredBlackPixelsToDrain = Settings.Default.ResinTrapRequiredBlackPixelsToDrain,
|
|
MaximumPixelBrightnessToDrain = Settings.Default.ResinTrapMaximumPixelBrightnessToDrain
|
|
};
|
|
}
|
|
|
|
public TouchingBoundDetectionConfiguration GetTouchingBoundsDetectionConfiguration()
|
|
{
|
|
return new TouchingBoundDetectionConfiguration
|
|
{
|
|
Enabled = tsIssuesDetectTouchingBounds.Checked,
|
|
//MaximumPixelBrightness = 100
|
|
};
|
|
}
|
|
|
|
public void AddLog(LogItem log)
|
|
{
|
|
int count = log.Index = lvLog.GetItemCount() + 1;
|
|
lvLog.AddObject(log);
|
|
lbLogOperations.Text = $"Operations: {count}";
|
|
}
|
|
|
|
public void AddLog(string description, decimal elapsedTime = 0)
|
|
{
|
|
int count = lvLog.GetItemCount() + 1;
|
|
lvLog.AddObject(new LogItem(count, description));
|
|
lbLogOperations.Text = $"Operations: {count}";
|
|
}
|
|
|
|
public void AddLogVerbose(string description, decimal elapsedTime = 0)
|
|
{
|
|
if (!IsLogVerbose) return;
|
|
AddLog(description, elapsedTime);
|
|
Debug.WriteLine(description);
|
|
}
|
|
|
|
public void EditLastLogElapsedTime(decimal elapsedTime = 0)
|
|
{
|
|
if (lvLog.GetModelObject(lvLog.GetItemCount() - 1) is LogItem log) log.ElapsedTime = elapsedTime;
|
|
}
|
|
|
|
public void DrawModifications(bool exitEditor)
|
|
{
|
|
if (PixelHistory.Count == 0)
|
|
{
|
|
if (exitEditor)
|
|
{
|
|
tabControlLeft.SelectedTab = ControlLeftLastTab;
|
|
tabControlLeft.TabPages.Remove(tabPagePixelEditor);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var result = DialogResult.None;
|
|
|
|
if (exitEditor)
|
|
{
|
|
result = MessageBox.Show(
|
|
"There are edit operations that have not been applied. " +
|
|
"Would you like to apply all operations before closing the editor?",
|
|
"Closing image editor", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
|
|
}
|
|
else
|
|
{
|
|
|
|
result = MessageBox.Show(
|
|
"Are you sure you want to apply all operations?",
|
|
"Apply image editor changes?", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
|
|
|
// For the "apply" case, We aren't exiting the editor, so map "No" to "Cancel" here
|
|
// in order to prevent pixel history from being cleared.
|
|
result = result == DialogResult.No ? DialogResult.Cancel : DialogResult.Yes;
|
|
}
|
|
|
|
if (result == DialogResult.Cancel)
|
|
{
|
|
btnLayerImagePixelEdit.Checked = true;
|
|
return;
|
|
}
|
|
|
|
if (result == DialogResult.Yes)
|
|
{
|
|
DisableGUI();
|
|
FrmLoading.SetDescription("Drawing pixels");
|
|
|
|
Task task = Task.Factory.StartNew(() =>
|
|
{
|
|
try
|
|
{
|
|
SlicerFile.LayerManager.DrawModifications(PixelHistory, FrmLoading.RestartProgress());
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"{ex.Message}", "Drawing operation failed!", MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
FrmLoading.ShowDialog();
|
|
|
|
if (Settings.Default.PartialUpdateIslandsOnEditing)
|
|
{
|
|
List<uint> whiteListLayers = new List<uint>();
|
|
foreach (var item in PixelHistory.Items)
|
|
{
|
|
if (item.OperationType != PixelOperation.PixelOperationType.Drawing &&
|
|
item.OperationType != PixelOperation.PixelOperationType.Text &&
|
|
item.OperationType != PixelOperation.PixelOperationType.Eraser &&
|
|
item.OperationType != PixelOperation.PixelOperationType.Supports) continue;
|
|
if (whiteListLayers.Contains(item.LayerIndex)) continue;
|
|
whiteListLayers.Add(item.LayerIndex);
|
|
|
|
uint nextLayer = item.LayerIndex + 1;
|
|
if (nextLayer < SlicerFile.LayerCount &&
|
|
!whiteListLayers.Contains(nextLayer))
|
|
{
|
|
whiteListLayers.Add(nextLayer);
|
|
}
|
|
}
|
|
|
|
UpdateIslands(whiteListLayers);
|
|
}
|
|
}
|
|
|
|
if (exitEditor || (Settings.Default.CloseEditOnApply && result == DialogResult.Yes))
|
|
{
|
|
btnLayerImagePixelEdit.Checked = false;
|
|
tabControlLeft.SelectedTab = ControlLeftLastTab;
|
|
tabControlLeft.TabPages.Remove(tabPagePixelEditor);
|
|
}
|
|
|
|
PixelHistory.Clear();
|
|
RefreshPixelHistory();
|
|
ShowLayer();
|
|
|
|
menuFileSave.Enabled = true;
|
|
}
|
|
|
|
private void UpdateIslands(List<uint> whiteListLayers)
|
|
{
|
|
if (whiteListLayers.Count == 0) return;
|
|
var islandConfig = GetIslandDetectionConfiguration();
|
|
var resinTrapConfig = new ResinTrapDetectionConfiguration {Enabled = false};
|
|
var touchingBoundConfig = new TouchingBoundDetectionConfiguration {Enabled = false};
|
|
islandConfig.Enabled = true;
|
|
islandConfig.WhiteListLayers = whiteListLayers;
|
|
|
|
if (ReferenceEquals(Issues, null))
|
|
{
|
|
ComputeIssues(islandConfig, resinTrapConfig, touchingBoundConfig, false);
|
|
}
|
|
else
|
|
{
|
|
DisableGUI();
|
|
FrmLoading.SetDescription("Updating Issues");
|
|
|
|
foreach (var layerIndex in islandConfig.WhiteListLayers)
|
|
{
|
|
Issues.RemoveAll(issue =>
|
|
issue.LayerIndex == layerIndex &&
|
|
issue.Type == LayerIssue.IssueType.Island); // Remove all islands for update
|
|
}
|
|
|
|
Task.Factory.StartNew(() =>
|
|
{
|
|
try
|
|
{
|
|
var issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, resinTrapConfig,
|
|
touchingBoundConfig, false,
|
|
FrmLoading.RestartProgress());
|
|
|
|
issues.RemoveAll(issue => issue.Type != LayerIssue.IssueType.Island); // Remove all non islands
|
|
Issues.AddRange(issues);
|
|
Issues = Issues.OrderBy(issue => issue.Type).ThenBy(issue => issue.LayerIndex)
|
|
.ThenBy(issue => issue.PixelsCount).ToList();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(ex.Message, "Error while trying to compute issues",
|
|
MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker) delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
FrmLoading.ShowDialog();
|
|
UpdateIssuesList();
|
|
}
|
|
}
|
|
|
|
void UpdateIssuesList()
|
|
{
|
|
flvIssues.ClearObjects();
|
|
|
|
if (!ReferenceEquals(Issues, null) && Issues.Count > 0)
|
|
{
|
|
flvIssues.SetObjects(Issues);
|
|
}
|
|
else
|
|
{
|
|
UpdateLayerTrackerHighlightIssues();
|
|
}
|
|
|
|
UpdateIssuesInfo();
|
|
ShowLayer();
|
|
}
|
|
|
|
public Dictionary<uint, uint> GetIssuesCountPerLayer()
|
|
{
|
|
if (Issues is null) return null;
|
|
Dictionary<uint, uint> layerIndexIssueCount = new Dictionary<uint, uint>();
|
|
foreach (var issue in Issues)
|
|
{
|
|
if (!layerIndexIssueCount.ContainsKey(issue.LayerIndex))
|
|
{
|
|
layerIndexIssueCount.Add(issue.LayerIndex, 1);
|
|
}
|
|
else
|
|
{
|
|
layerIndexIssueCount[issue.LayerIndex]++;
|
|
}
|
|
}
|
|
|
|
return layerIndexIssueCount;
|
|
}
|
|
|
|
void UpdateLayerTrackerHighlightIssues()
|
|
{
|
|
var issuesCountPerLayer = GetIssuesCountPerLayer();
|
|
if (issuesCountPerLayer is null)
|
|
{
|
|
pbTrackerIssues.Image = null;
|
|
return;
|
|
}
|
|
|
|
using (Mat mat = new Mat(pbTrackerIssues.Height, pbTrackerIssues.Width, DepthType.Cv8U, 3))
|
|
{
|
|
mat.SetTo(new MCvScalar(255, 255, 255));
|
|
var color = new MCvScalar(0, 0, 255);
|
|
|
|
foreach (var value in issuesCountPerLayer)
|
|
{
|
|
var tickPos = tbLayer.GetTickPos((int) value.Key);
|
|
if (tickPos == -1) continue;
|
|
int y = (pbTrackerIssues.Height - tickPos).Clamp(0, pbTrackerIssues.Height);
|
|
CvInvoke.Line(mat, new Point(0, y), new Point(mat.Width, y), color);
|
|
}
|
|
|
|
pbTrackerIssues.Image = mat.ToBitmap();
|
|
}
|
|
}
|
|
|
|
public Operation ShowRunOperation(Type type)
|
|
{
|
|
var operation = ShowOperation(type);
|
|
RunOperation(operation);
|
|
return operation;
|
|
}
|
|
|
|
public Operation ShowOperation(Type type)
|
|
{
|
|
Operation operation;
|
|
|
|
var typeBase = typeof(CtrlToolTemplate);
|
|
var classname = $"{typeBase.Namespace}.CtrlTool{type.Name.Remove(0,Operation.ClassNameLength)}";
|
|
var controlType = Type.GetType(classname);
|
|
CtrlToolWindowContent control;
|
|
|
|
bool removeContent = false;
|
|
if (controlType is null)
|
|
{
|
|
controlType = typeBase;
|
|
removeContent = true;
|
|
control = new CtrlToolTemplate(type.CreateInstance<Operation>());
|
|
}
|
|
else
|
|
{
|
|
control = controlType.CreateInstance<CtrlToolWindowContent>();
|
|
if (control is null) return null;
|
|
}
|
|
|
|
if (!control.CanRun)
|
|
{
|
|
control.Dispose();
|
|
return null;
|
|
}
|
|
|
|
if (removeContent)
|
|
{
|
|
control.Visible = false;
|
|
}
|
|
|
|
using (var frm = new FrmToolWindow(control, control.BaseOperation.PassActualLayerIndex ? (int)ActualLayer : -1))
|
|
{
|
|
if (removeContent)
|
|
{
|
|
frm.pnContent.Visible = false;
|
|
}
|
|
if (frm.IsDisposed || frm.ShowDialog() != DialogResult.OK) return null;
|
|
operation = control.BaseOperation;
|
|
}
|
|
|
|
return operation;
|
|
}
|
|
|
|
public bool RunOperation(Operation baseOperation)
|
|
{
|
|
if (baseOperation is null) return false;
|
|
|
|
switch (baseOperation)
|
|
{
|
|
case OperationEditParameters operation:
|
|
/*foreach (var modifier in operation.Modifiers.Where(modifier => modifier.HasChanged))
|
|
{
|
|
SlicerFile.SetValueFromPrintParameterModifier(modifier, modifier.NewValue);
|
|
}*/
|
|
SlicerFile.SetValuesFromPrintParametersModifiers();
|
|
RefreshInfo();
|
|
|
|
menuFileSave.Enabled = true;
|
|
|
|
return false;
|
|
case OperationRepairLayers operation:
|
|
if (ReferenceEquals(Issues, null))
|
|
{
|
|
var islandConfig = GetIslandDetectionConfiguration();
|
|
islandConfig.Enabled =
|
|
operation.RepairIslands && operation.RemoveIslandsBelowEqualPixelCount > 0;
|
|
var resinTrapConfig = GetResinTrapDetectionConfiguration();
|
|
resinTrapConfig.Enabled = operation.RepairResinTraps;
|
|
var touchingBoundConfig = new TouchingBoundDetectionConfiguration {Enabled = false};
|
|
|
|
if (islandConfig.Enabled || resinTrapConfig.Enabled)
|
|
{
|
|
ComputeIssues(islandConfig, resinTrapConfig, touchingBoundConfig,
|
|
tsIssuesDetectEmptyLayers.Checked);
|
|
}
|
|
}
|
|
|
|
operation.Issues = Issues;
|
|
|
|
break;
|
|
}
|
|
|
|
DisableGUI();
|
|
|
|
FrmLoading.SetDescription(baseOperation.ProgressTitle);
|
|
|
|
var task = Task.Factory.StartNew(() =>
|
|
{
|
|
var backup = new Layer[baseOperation.LayerRangeCount];
|
|
uint i = 0;
|
|
for (uint layerIndex = baseOperation.LayerIndexStart; layerIndex <= baseOperation.LayerIndexEnd; layerIndex++)
|
|
{
|
|
backup[i++] = SlicerFile[layerIndex].Clone();
|
|
}
|
|
|
|
try
|
|
{
|
|
switch (baseOperation)
|
|
{
|
|
// Tools
|
|
case OperationRepairLayers operation:
|
|
SlicerFile.LayerManager.RepairLayers(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationMove operation:
|
|
SlicerFile.LayerManager.Move(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationResize operation:
|
|
SlicerFile.LayerManager.Resize(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationFlip operation:
|
|
SlicerFile.LayerManager.Flip(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationRotate operation:
|
|
SlicerFile.LayerManager.Rotate(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationSolidify operation:
|
|
SlicerFile.LayerManager.Solidify(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationMorph operation:
|
|
SlicerFile.LayerManager.Morph(operation, BorderType.Default, new MCvScalar(), FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationMask operation:
|
|
SlicerFile.LayerManager.Mask(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationPixelDimming operation:
|
|
SlicerFile.LayerManager.PixelDimming(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationThreshold operation:
|
|
SlicerFile.LayerManager.ThresholdPixels(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationBlur operation:
|
|
SlicerFile.LayerManager.Blur(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
|
|
case OperationChangeResolution operation:
|
|
SlicerFile.LayerManager.ChangeResolution(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationLayerReHeight operation:
|
|
SlicerFile.LayerManager.ReHeight(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationPattern operation:
|
|
SlicerFile.LayerManager.Pattern(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
// Actions
|
|
case OperationLayerImport operation:
|
|
SlicerFile.LayerManager.Import(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationLayerClone operation:
|
|
SlicerFile.LayerManager.CloneLayer(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
case OperationLayerRemove operation:
|
|
SlicerFile.LayerManager.RemoveLayers(operation, FrmLoading.RestartProgress(operation.CanCancel));
|
|
break;
|
|
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
i = 0;
|
|
for (uint layerIndex = baseOperation.LayerIndexStart; layerIndex <= baseOperation.LayerIndexEnd; layerIndex++)
|
|
{
|
|
SlicerFile[layerIndex] = backup[i++];
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GUIExtensions.MessageBoxError($"{baseOperation.Title} Error", ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
Invoke((MethodInvoker)delegate
|
|
{
|
|
// Running on the UI thread
|
|
EnableGUI(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
var loadingResult = FrmLoading.ShowDialog();
|
|
|
|
ShowLayer();
|
|
UpdateLayerLimits();
|
|
RefreshInfo();
|
|
|
|
menuFileSave.Enabled = true;
|
|
|
|
switch (baseOperation)
|
|
{
|
|
// Tools
|
|
case OperationRepairLayers operation:
|
|
tsIssuesDetect.PerformButtonClick();
|
|
break;
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
readonly SolidBrush _pixelEditorCursorBrush = new SolidBrush(Color.FromArgb(150, 120, 255, 255));
|
|
const byte _pixelEditorCursorMinDiamater = 10;
|
|
public void UpdatePixelEditorCursor()
|
|
{
|
|
Bitmap bitmap = null;
|
|
|
|
if (tabControlPixelEditor.SelectedIndex == (byte)PixelOperation.PixelOperationType.Drawing)
|
|
{
|
|
PixelDrawing.BrushShapeType shapeType =
|
|
(PixelDrawing.BrushShapeType)cbPixelEditorDrawingBrushShape.SelectedIndex;
|
|
|
|
ushort brushSize = (ushort)nmPixelEditorDrawingBrushSize.Value;
|
|
short thickness = (short)(nmPixelEditorDrawingThickness.Value == 0
|
|
? 1
|
|
: nmPixelEditorDrawingThickness.Value);
|
|
|
|
int diameter = (brushSize * pbLayer.Zoom / 100);
|
|
if (brushSize > 1 && diameter >= _pixelEditorCursorMinDiamater)
|
|
{
|
|
bitmap = new Bitmap(diameter, diameter, PixelFormat.Format32bppArgb);
|
|
using (Graphics gr = Graphics.FromImage(bitmap))
|
|
{
|
|
gr.SmoothingMode = SmoothingMode.AntiAlias;
|
|
gr.CompositingMode = CompositingMode.SourceCopy;
|
|
|
|
switch (shapeType)
|
|
{
|
|
case PixelDrawing.BrushShapeType.Rectangle:
|
|
if (thickness >= 1)
|
|
{
|
|
gr.DrawRectangle(new Pen(_pixelEditorCursorBrush, thickness), 0, 0, diameter, diameter);
|
|
}
|
|
else
|
|
{
|
|
gr.FillRectangle(_pixelEditorCursorBrush, 0, 0, diameter, diameter);
|
|
}
|
|
break;
|
|
case PixelDrawing.BrushShapeType.Circle:
|
|
if (thickness >= 1)
|
|
{
|
|
gr.DrawEllipse(new Pen(_pixelEditorCursorBrush, thickness), 0, 0, diameter, diameter);
|
|
}
|
|
else
|
|
{
|
|
gr.FillEllipse(_pixelEditorCursorBrush, 0, 0, diameter, diameter);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*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);
|
|
}*/
|
|
else if (tabControlPixelEditor.SelectedIndex == (byte)PixelOperation.PixelOperationType.Supports || tabControlPixelEditor.SelectedIndex == (byte)PixelOperation.PixelOperationType.DrainHole)
|
|
{
|
|
var diameter = (int)(
|
|
(tabControlPixelEditor.SelectedIndex == (byte)PixelOperation.PixelOperationType.Supports ?
|
|
nmPixelEditorSupportsTipDiameter.Value : nmPixelEditorDrainHoleDiameter.Value)
|
|
* pbLayer.Zoom / 100);
|
|
|
|
if (diameter >= _pixelEditorCursorMinDiamater)
|
|
{
|
|
bitmap = new Bitmap(diameter, diameter, PixelFormat.Format32bppArgb);
|
|
using (Graphics gr = Graphics.FromImage(bitmap))
|
|
{
|
|
gr.SmoothingMode = SmoothingMode.AntiAlias;
|
|
gr.CompositingMode = CompositingMode.SourceCopy;
|
|
gr.FillEllipse(_pixelEditorCursorBrush, 0, 0, diameter, diameter);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
pbLayer.Cursor = bitmap is null ? pixelEditCursor : new Cursor(bitmap.GetHicon()) { Tag = "Custom" };
|
|
}
|
|
|
|
public bool CanGlobalHotKey
|
|
=> !(ActiveControl is TextBox
|
|
|| ActiveControl is ComboBox
|
|
|| ActiveControl is NumericUpDown
|
|
|| ActiveControl is RichTextBox
|
|
|| ActiveControl is ListView
|
|
|| ActiveControl is ObjectListView
|
|
|| ActiveControl is FastObjectListView);
|
|
|
|
|
|
}
|
|
}
|