/*
* GNU AFFERO GENERAL PUBLIC LICENSE
* Version 3, 19 November 2007
* Copyright (C) 2007 Free Software Foundation, Inc.
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/
using System;
using System.Collections;
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 OperationThreshold(), Resources.th_16x16),
new OperationMenuItem(new OperationArithmetic(), Resources.square_root_16x16),
new OperationMenuItem(new OperationMask(), Resources.mask_16x16),
new OperationMenuItem(new OperationPixelDimming(), Resources.pixel_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),
new OperationMenuItem(new OperationCalculator(), Resources.calculator_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 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.
///
/// Returns the zoom level at which the crosshairs will fade and no longer be displayed
///
public int CrosshairFadeLevel => ZoomLevels[Settings.Default.CrosshairFadeLevel + ZoomLevelSkipCount];
///
/// Returns the zoom level that will be used for autozoom actions
///
private int LockedZoomLevel { get; set; } = 1200;
///
/// Minimum Zoom level to which autozoom can be locked.
///
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 bool CanSave
{
get => menuFileSave.Enabled;
set => menuFileSave.Enabled = value;
}
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;
tsIssuesDetectOverhangs.Checked = Settings.Default.ComputeOverhangs;
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.ToString());
}
}
#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);
if (SlicerFile is null && Settings.Default.LoadDemoFileOnStartup)
{
ProcessFile(About.DemoFile);
}
}
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;
CanSave = 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(BindingFlags.Public | BindingFlags.Instance))
{
if (property.Name.Equals("Item")) continue;
var value = property.GetValue(config);
switch (value)
{
case null:
continue;
case IList list:
tw.WriteLine($"{property.Name} = {list.Count}");
break;
default:
tw.WriteLine($"{property.Name} = {value}");
break;
}
}
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(BindingFlags.Public | BindingFlags.Instance))
{
if (property.Name.Equals("Item")) continue;
var value = property.GetValue(config);
switch (value)
{
case null:
continue;
case IList list:
sb.AppendLine($"{property.Name} = {list.Count}");
break;
default:
sb.AppendLine($"{property.Name} = {value}");
break;
}
}
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> processIssues = new Dictionary>();
List layersRemove = new List();
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();
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 task = Task.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();
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.ToString(), "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();
// Update GUI
List removeSelectedObjects = new List();
foreach (LayerIssue issue in flvIssues.SelectedObjects)
{
//if (!issue.HaveValidPoint) continue;
if (issue.Type != LayerIssue.IssueType.Island &&
issue.Type != LayerIssue.IssueType.ResinTrap &&
issue.Type != LayerIssue.IssueType.EmptyLayer) continue;
Issues.Remove(issue);
removeSelectedObjects.Add(issue);
if (issue.Type == LayerIssue.IssueType.Island)
{
var nextLayer = issue.Layer.Index + 1;
if (nextLayer >= SlicerFile.LayerCount) continue;
if (whiteListLayers.Contains(nextLayer)) continue;
whiteListLayers.Add(nextLayer);
}
}
flvIssues.RemoveObjects(removeSelectedObjects);
if (layersRemove.Count > 0)
{
UpdateLayerLimits();
RefreshInfo();
}
if (Settings.Default.PartialUpdateIslandsOnEditing)
{
UpdateIslandsOverhangs(whiteListLayers);
}
//ShowLayer(); // It will call latter so its a extra call
UpdateIssuesInfo();
CanSave = 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(),
GetOverhangDetectionConfiguration(),
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 task = Task.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}",
"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().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
///
/// Closes file and clear UI
///
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)
{
if (!File.Exists(fileName)) return;
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 task = Task.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.ToString(), "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++;
CanSave = 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 items = new List();
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}";
}
///
/// Draw a crosshair around a rectangle
///
///
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);
}
///
/// Reshow current layer
///
void ShowLayer() => ShowLayer(Math.Min(ActualLayer, SlicerFile.LastLayerIndex));
void ShowLayer(bool direction)
{
if (direction)
{
if (ActualLayer < SlicerFile.LayerCount - 1)
{
ShowLayer(ActualLayer + 1);
}
return;
}
if (ActualLayer > 0)
{
ShowLayer(ActualLayer - 1);
}
}
///
/// Shows a layer number
///
/// Layer number
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();
//var imageBgrSpan = ActualLayerImageBgr.GetPixelSpan();
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();
//var nextSpan = nextImage.GetPixelSpan();
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 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.Overhang:
color = selectedIssues.Contains(issue)
? Settings.Default.OverhangHLColor
: Settings.Default.OverhangColor;
if (btnLayerImageShowCrosshairs.Checked &&
!Settings.Default.CrosshairShowOnlyOnSelectedIssues &&
pbLayer.Zoom <= CrosshairFadeLevel)
{
DrawCrosshair(issue.BoundingRectangle);
}
break;
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;
}
if(color.IsEmpty) continue;
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)) return;
// This event repeats for as long as the key is pressed, so if we've
// already set the cursor from a previous key down event, just return.
if (!ReferenceEquals(pbLayer.Cursor.Tag, null) || pbLayer.Cursor == pixelEditCursor || pbLayer.Cursor == Cursors.Cross
|| pbLayer.Cursor == Cursors.Hand || pbLayer.SelectionMode == ImageBoxSelectionMode.Rectangle) return;
// Pixel Edit is active, Shift is down, and the cursor is over the image region.
if (pbLayer.ClientRectangle.Contains(pbLayer.PointToClient(MousePosition)))
{
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 rectangles = new List();
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().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();
//CanSavemenuFileSaveAs.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,
OverhangDetectionConfiguration overhangConfig = 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, overhangConfig, resinTrapConfig, touchingBoundConfig,
emptyLayersConfig, null, FrmLoading.RestartProgress());
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "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
}
///
/// 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.
///
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);
}
///
/// Centers layer view on a X,Y coordinate
///
/// X coordinate
/// X coordinate
/// Zoom level to set, 0 to ignore or negative value to get current locked zoom level
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);
}
///
/// Centers layer view on a middle of a given rectangle
///
/// Rectangle holding coordinates and bounds
/// Zoom level to set, 0 to ignore or negative value to get current locked zoom level
/// Auto zoom to a region and ensure that region area stays all visible when possible, when true this will overwrite zoomLevel
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);
}
///
/// Centers layer view on a middle of a given rectangle
///
/// Rectangle holding coordinates and bounds
/// Zoom level to set, 0 to ignore or negative value to get current locked zoom level
/// Auto zoom to a region and ensure that region area stays all visible when possible, when true this will overwrite zoomLevel
public void CenterLayerAt(RectangleF rectangle, int zoomLevel = 0, bool zoomToRegion = false) =>
CenterLayerAt(Rectangle.Round(rectangle), zoomLevel, zoomToRegion);
///
/// Centers layer view on a
///
/// Point holding X and Y coordinates
/// Zoom level to set, 0 to ignore or negative value to get current locked zoom level
public void CenterLayerAt(Point point, int zoomLevel = 0) => CenterLayerAt(point.X, point.Y, zoomLevel);
///
/// Zoom the layer preview to the passed issue, or if appropriate for issue type,
/// Zoom to fit the plate or print bounds.
///
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);
}
}
///
/// Center the layer preview on the passed issue, or if appropriate for issue type,
/// Zoom to fit the plate or print bounds.
///
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();
}
}
///
/// If there is an issue under the point location passed, that issue will be selected and
/// scrolled into view on the IssueList.
///
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,
RequiredPixelsToSupportMultiplier = Settings.Default.IslandRequiredPixelsToSupportMultiplier,
RequiredPixelsToSupport = Settings.Default.IslandRequiredPixelsToSupport,
RequiredPixelBrightnessToSupport = Settings.Default.IslandRequiredPixelBrightnessToSupport
};
}
public OverhangDetectionConfiguration GetOverhangDetectionConfiguration()
{
return new OverhangDetectionConfiguration
{
Enabled = tsIssuesDetectOverhangs.Checked,
IndependentFromIslands = Settings.Default.OverhangIndependentFromIslands,
ErodeIterations = Settings.Default.OverhangErodeIterations,
};
}
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.Items, FrmLoading.RestartProgress());
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "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 whiteListLayers = new List();
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);
}
}
UpdateIslandsOverhangs(whiteListLayers);
}
}
if (exitEditor || (Settings.Default.CloseEditOnApply && result == DialogResult.Yes))
{
btnLayerImagePixelEdit.Checked = false;
tabControlLeft.SelectedTab = ControlLeftLastTab;
tabControlLeft.TabPages.Remove(tabPagePixelEditor);
}
PixelHistory.Clear();
RefreshPixelHistory();
ShowLayer();
CanSave = true;
}
private void UpdateIslandsOverhangs(List whiteListLayers)
{
if (whiteListLayers.Count == 0) return;
var islandConfig = GetIslandDetectionConfiguration();
var overhangConfig = GetOverhangDetectionConfiguration();
var resinTrapConfig = new ResinTrapDetectionConfiguration {Enabled = false};
var touchingBoundConfig = new TouchingBoundDetectionConfiguration {Enabled = false};
islandConfig.Enabled = true;
islandConfig.WhiteListLayers = whiteListLayers;
overhangConfig.Enabled = true;
overhangConfig.WhiteListLayers = whiteListLayers;
if (Issues is null)
{
ComputeIssues(islandConfig, overhangConfig, 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 || issue.Type == LayerIssue.IssueType.Overhang)); // Remove all islands and overhangs for update
}
Task.Factory.StartNew(() =>
{
try
{
var issues = SlicerFile.LayerManager.GetAllIssues(islandConfig, overhangConfig, resinTrapConfig,
touchingBoundConfig, false, null,
FrmLoading.RestartProgress());
issues.RemoveAll(issue => issue.Type != LayerIssue.IssueType.Island && issue.Type != LayerIssue.IssueType.Overhang); // 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.ToString(), "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 GetIssuesCountPerLayer()
{
if (Issues is null) return null;
Dictionary layerIndexIssueCount = new Dictionary();
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());
}
else
{
control = controlType.CreateInstance();
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();
CanSave = true;
return false;
case OperationRepairLayers operation:
if (Issues is null)
{
var islandConfig = GetIslandDetectionConfiguration();
islandConfig.Enabled = operation.RepairIslands && operation.RemoveIslandsBelowEqualPixelCount > 0;
var overhangConfig = new OverhangDetectionConfiguration {Enabled = false};
var resinTrapConfig = GetResinTrapDetectionConfiguration();
resinTrapConfig.Enabled = operation.RepairResinTraps;
var touchingBoundConfig = new TouchingBoundDetectionConfiguration {Enabled = false};
if (islandConfig.Enabled || resinTrapConfig.Enabled)
{
ComputeIssues(
islandConfig,
overhangConfig,
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();
}*/
var backup = SlicerFile.LayerManager.Clone();
try
{
switch (baseOperation)
{
// Tools
case OperationRepairLayers operation:
operation.IslandDetectionConfig = GetIslandDetectionConfiguration();
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 OperationThreshold operation:
SlicerFile.LayerManager.ThresholdPixels(operation, FrmLoading.RestartProgress(operation.CanCancel));
break;
case OperationArithmetic operation:
SlicerFile.LayerManager.Arithmetic(operation, 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 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++];
}*/
SlicerFile.LayerManager = backup;
}
catch (Exception ex)
{
GUIExtensions.MessageBoxError($"{baseOperation.Title} Error", ex.ToString());
}
finally
{
Invoke((MethodInvoker)delegate
{
// Running on the UI thread
EnableGUI(true);
});
}
});
var loadingResult = FrmLoading.ShowDialog();
ShowLayer();
UpdateLayerLimits();
RefreshInfo();
CanSave = 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);
var cursorSize = thickness > 1 ? brushSize + thickness : brushSize;
int diameter = (int) (cursorSize * pbLayer.ZoomFactor);
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, (int)(thickness * 2 * pbLayer.ZoomFactor)), 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, (int)(thickness * 2 * pbLayer.ZoomFactor)), 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);
}
}