mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 17:54:15 -04:00
Brings in the Plate menu reorganization, Database-mode drag-and-drop open, the SavedNestsForm look fixes and the Irregular block-fill work budget fix. Conflicts: - MainForm.Designer.cs: keep the branch's Plate menu order (Arrange submenu, View in CAD last) and add master's Plan Cutting item ahead of the lead-in commands. - SavedNestsForm.cs: keep master's browser rewrite (details, preview, columns) and apply the branch's intent to it: open maximized with MaximizedBounds clear of the taskbar, the lighter grid palette on all three grids, and B/KB/MB/GB file sizes.
934 lines
31 KiB
C#
934 lines
31 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using OpenNest.Actions;
|
|
using OpenNest.Collections;
|
|
using OpenNest.Diagnostics;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Engine.Fill;
|
|
using OpenNest.Engine.Jobs.Placement;
|
|
using OpenNest.Forms;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
using Timer = System.Timers.Timer;
|
|
|
|
namespace OpenNest.Controls
|
|
{
|
|
public class PlateView : DrawControl
|
|
{
|
|
private readonly Timer redrawTimer;
|
|
private readonly OverlapOverlayController overlapOverlay;
|
|
|
|
private string status;
|
|
private Plate plate;
|
|
private ActionManager actionManager;
|
|
private CutOffSettings cutOffSettings = new CutOffSettings();
|
|
private SelectionManager selection;
|
|
private CutOffHandler cutOffHandler;
|
|
private PreviewManager previewManager;
|
|
protected List<LayoutPart> parts;
|
|
private Point middleMouseDownPoint;
|
|
private Box activeWorkArea;
|
|
private List<Box> debugRemnants;
|
|
private PlateRenderer renderer;
|
|
private LayoutPart hoveredPart;
|
|
private Point hoverPoint;
|
|
private bool showTooltip;
|
|
private bool hoverPending;
|
|
private Timer hoverTimer;
|
|
|
|
public Box ActiveWorkArea
|
|
{
|
|
get => activeWorkArea;
|
|
set
|
|
{
|
|
activeWorkArea = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
public List<Box> DebugRemnants
|
|
{
|
|
get => debugRemnants;
|
|
set
|
|
{
|
|
debugRemnants = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
public List<int> DebugRemnantPriorities { get; set; }
|
|
|
|
public List<LayoutPart> SelectedParts => selection.SelectedParts;
|
|
public ReadOnlyCollection<LayoutPart> Parts => new ReadOnlyCollection<LayoutPart>(parts);
|
|
|
|
internal SelectionManager Selection => selection;
|
|
internal CutOffHandler CutOffs => cutOffHandler;
|
|
internal ActionManager Actions => actionManager;
|
|
internal PreviewManager Previews => previewManager;
|
|
|
|
public event EventHandler<ItemAddedEventArgs<Part>> PartAdded;
|
|
public event EventHandler<ItemRemovedEventArgs<Part>> PartRemoved;
|
|
|
|
/// <summary>
|
|
/// Raised after the plate's parts were reordered in place, for example by an applied cutting
|
|
/// plan that also installed new programs. The view has already redrawn its parts in the new order.
|
|
/// </summary>
|
|
public event EventHandler PartsReordered;
|
|
public event EventHandler StatusChanged;
|
|
|
|
/// <summary>
|
|
/// Raised when files are dropped on the plate and something is listening (see
|
|
/// <see cref="OnDragEnter"/>); callers decide which paths to act on.
|
|
/// </summary>
|
|
public event EventHandler<string[]> FilesDropped;
|
|
|
|
public event EventHandler SelectionChanged
|
|
{
|
|
add => selection.SelectionChanged += value;
|
|
remove => selection.SelectionChanged -= value;
|
|
}
|
|
|
|
public PlateView()
|
|
: this(ColorScheme.Default) { }
|
|
|
|
public PlateView(ColorScheme colorScheme)
|
|
{
|
|
overlapOverlay = new OverlapOverlayController(this);
|
|
Plate = new Plate(60, 120);
|
|
origin = new PointF();
|
|
parts = new List<LayoutPart>();
|
|
selection = new SelectionManager(this);
|
|
cutOffHandler = new CutOffHandler(this);
|
|
previewManager = new PreviewManager(this);
|
|
|
|
redrawTimer = new Timer()
|
|
{
|
|
AutoReset = false,
|
|
Enabled = true,
|
|
Interval = 50,
|
|
};
|
|
redrawTimer.Elapsed += redrawTimer_Elapsed;
|
|
|
|
hoverTimer = new Timer() { AutoReset = false, Interval = 1000 };
|
|
hoverTimer.Elapsed += hoverTimer_Elapsed;
|
|
|
|
SetStyle(
|
|
ControlStyles.AllPaintingInWmPaint
|
|
| ControlStyles.OptimizedDoubleBuffer
|
|
| ControlStyles.UserPaint,
|
|
true
|
|
);
|
|
|
|
ViewScale = 1.0f;
|
|
RotateIncrementAngle = 10;
|
|
OffsetIncrementDistance = 10;
|
|
ColorScheme = colorScheme;
|
|
BackColor = colorScheme.BackgroundColor;
|
|
Cursor = Cursors.Cross;
|
|
AllowPan = true;
|
|
AllowSelect = true;
|
|
AllowZoom = true;
|
|
AllowDrop = true;
|
|
DrawOrigin = true;
|
|
DrawRapid = false;
|
|
DrawBounds = true;
|
|
DrawOffset = false;
|
|
FillParts = true;
|
|
renderer = new PlateRenderer(this);
|
|
actionManager = new ActionManager(this);
|
|
actionManager.SetAction(typeof(ActionSelect));
|
|
|
|
UpdateMatrix();
|
|
}
|
|
|
|
public ColorScheme ColorScheme { get; set; }
|
|
|
|
public bool AllowZoom { get; set; }
|
|
|
|
public bool AllowSelect { get; set; }
|
|
|
|
public bool AllowPan { get; set; }
|
|
|
|
public bool DrawOrigin { get; set; }
|
|
|
|
public bool DrawRapid { get; set; }
|
|
|
|
public bool DrawPiercePoints { get; set; }
|
|
|
|
public bool DrawBounds { get; set; }
|
|
|
|
public bool DrawOffset { get; set; }
|
|
|
|
public bool DrawCutDirection { get; set; }
|
|
|
|
public bool ShowBendLines { get; set; }
|
|
|
|
public double OffsetTolerance { get; set; } = 0.001;
|
|
|
|
public bool FillParts { get; set; }
|
|
|
|
internal List<LayoutPart> LayoutParts => parts;
|
|
|
|
internal IReadOnlyList<LayoutPart> PreviewParts => previewManager.PreviewParts;
|
|
internal Brush PreviewBrush => previewManager.PreviewBrush;
|
|
internal Pen PreviewPen => previewManager.PreviewPen;
|
|
|
|
internal RectangleF GetViewBounds() => new RectangleF(-origin.X, -origin.Y, Width, Height);
|
|
|
|
internal PlateRenderer Renderer => renderer;
|
|
|
|
public CutOffSettings CutOffSettings
|
|
{
|
|
get => cutOffSettings;
|
|
set
|
|
{
|
|
cutOffSettings = value;
|
|
Plate?.RegenerateCutOffs(value);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
public double RotateIncrementAngle { get; set; }
|
|
|
|
public double OffsetIncrementDistance { get; set; }
|
|
|
|
public Plate Plate
|
|
{
|
|
get { return plate; }
|
|
set { SetPlate(value); }
|
|
}
|
|
|
|
private void SetPlate(Plate p)
|
|
{
|
|
if (plate != null)
|
|
{
|
|
plate.PartAdded -= plate_PartAdded;
|
|
plate.PartRemoved -= plate_PartRemoved;
|
|
plate.PartsReordered -= plate_PartsReordered;
|
|
parts.Clear();
|
|
previewManager.Clear();
|
|
selection.Clear();
|
|
}
|
|
|
|
plate = p;
|
|
plate.PartAdded += plate_PartAdded;
|
|
plate.PartRemoved += plate_PartRemoved;
|
|
plate.PartsReordered += plate_PartsReordered;
|
|
|
|
foreach (var part in plate.Parts)
|
|
parts.Add(LayoutPart.Create(part, this));
|
|
|
|
overlapOverlay.SetPlate(plate);
|
|
actionManager?.OnPlateChanged();
|
|
}
|
|
|
|
internal OverlapOverlayController OverlapOverlay => overlapOverlay;
|
|
public OverlapCheckStatus OverlapStatus => overlapOverlay.Status;
|
|
public PlateOverlapReport OverlapReport => overlapOverlay.Report;
|
|
public bool IsOverlapCheckRunning => overlapOverlay.IsRunning;
|
|
public OverlapDisplayMode OverlapDisplay
|
|
{
|
|
get => overlapOverlay.DisplayMode;
|
|
set => overlapOverlay.DisplayMode = value;
|
|
}
|
|
public event EventHandler OverlapStateChanged
|
|
{
|
|
add => overlapOverlay.StateChanged += value;
|
|
remove => overlapOverlay.StateChanged -= value;
|
|
}
|
|
public Task CheckOverlapsAsync(Units units) => overlapOverlay.CheckAsync(units);
|
|
public void CancelOverlapCheck() => overlapOverlay.Cancel();
|
|
/// <summary>
|
|
/// Call before any editor that may mutate clean drawing programs in place: marks the
|
|
/// report out of date and drops cached drawing material.
|
|
/// </summary>
|
|
public void InvalidateOverlapCheck() => overlapOverlay.InvalidateGeometry();
|
|
|
|
/// <summary>
|
|
/// Recheck overlaps automatically once layout edits have been quiet for
|
|
/// <see cref="OverlapAutoCheckDelay"/> ms. Null restores manual-only checks.
|
|
/// </summary>
|
|
public void SetOverlapAutoCheck(Func<Units> units) => overlapOverlay.SetAutoCheck(units);
|
|
|
|
public int OverlapAutoCheckDelay
|
|
{
|
|
get => overlapOverlay.AutoCheckDelay;
|
|
set => overlapOverlay.AutoCheckDelay = value;
|
|
}
|
|
|
|
public string Status
|
|
{
|
|
get { return status; }
|
|
set
|
|
{
|
|
status = value;
|
|
|
|
if (StatusChanged != null)
|
|
StatusChanged.Invoke(this, new EventArgs());
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseEnter(EventArgs e)
|
|
{
|
|
base.OnMouseEnter(e);
|
|
}
|
|
|
|
protected override void OnMouseLeave(EventArgs e)
|
|
{
|
|
ClearHover();
|
|
base.OnMouseLeave(e);
|
|
}
|
|
|
|
private void ClearHover()
|
|
{
|
|
overlapOverlay?.ClearHover();
|
|
hoverTimer?.Stop();
|
|
hoverPending = false;
|
|
hoveredPart = null;
|
|
showTooltip = false;
|
|
Invalidate();
|
|
}
|
|
|
|
protected override void OnResize(EventArgs e)
|
|
{
|
|
ClearHover();
|
|
base.OnResize(e);
|
|
}
|
|
|
|
protected override void OnDpiChangedAfterParent(EventArgs e)
|
|
{
|
|
ClearHover();
|
|
base.OnDpiChangedAfterParent(e);
|
|
}
|
|
|
|
protected override void OnFontChanged(EventArgs e)
|
|
{
|
|
ClearHover();
|
|
base.OnFontChanged(e);
|
|
}
|
|
|
|
protected override void OnDragEnter(DragEventArgs drgevent)
|
|
{
|
|
if (drgevent.Data.GetData(typeof(Drawing)) != null)
|
|
drgevent.Effect = DragDropEffects.Copy;
|
|
else if (FilesDropped != null && drgevent.Data.GetDataPresent(DataFormats.FileDrop))
|
|
drgevent.Effect = DragDropEffects.Copy;
|
|
}
|
|
|
|
protected override void OnDragDrop(DragEventArgs drgevent)
|
|
{
|
|
if (drgevent.Data.GetDataPresent(DataFormats.FileDrop))
|
|
{
|
|
FilesDropped?.Invoke(this, (string[])drgevent.Data.GetData(DataFormats.FileDrop));
|
|
return;
|
|
}
|
|
|
|
var dwg = drgevent.Data.GetData(typeof(Drawing)) as Drawing;
|
|
|
|
if (dwg == null)
|
|
return;
|
|
|
|
var pt1 = PointToClient(new Point(drgevent.X, drgevent.Y));
|
|
var pt2 = PointControlToWorld(pt1);
|
|
|
|
AddPartFromDrawing(dwg, pt2);
|
|
}
|
|
|
|
protected override void OnMouseDown(MouseEventArgs e)
|
|
{
|
|
ClearHover();
|
|
if (!Focused)
|
|
Focus();
|
|
|
|
if (e.Button == MouseButtons.Middle)
|
|
middleMouseDownPoint = e.Location;
|
|
|
|
if (e.Button == MouseButtons.Left && actionManager.CurrentAction is ActionSelect)
|
|
{
|
|
var hitCutOff = cutOffHandler.TryStartDrag(CurrentPoint, 5.0 / ViewScale);
|
|
if (hitCutOff != null)
|
|
{
|
|
selection.DeselectParts();
|
|
selection.SelectedCutOffs.Clear();
|
|
selection.SelectedCutOffs.Add(hitCutOff);
|
|
Invalidate();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
selection.DeselectCutOffs();
|
|
}
|
|
}
|
|
|
|
base.OnMouseDown(e);
|
|
}
|
|
|
|
protected override void OnMouseUp(MouseEventArgs e)
|
|
{
|
|
if (e.Button == MouseButtons.Middle && SelectedParts.Count > 0)
|
|
{
|
|
var dx = e.X - middleMouseDownPoint.X;
|
|
var dy = e.Y - middleMouseDownPoint.Y;
|
|
|
|
if (dx * dx + dy * dy < 25)
|
|
{
|
|
selection.RotateSelectedParts(Angle.ToRadians(90));
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
|
{
|
|
cutOffHandler.EndDrag();
|
|
return;
|
|
}
|
|
|
|
base.OnMouseUp(e);
|
|
}
|
|
|
|
protected override void OnMouseWheel(MouseEventArgs e)
|
|
{
|
|
base.OnMouseWheel(e);
|
|
|
|
var multiplier = System.Math.Abs(e.Delta / 120);
|
|
|
|
if (SelectedParts.Count > 0 && ((ModifierKeys & Keys.Shift) == Keys.Shift))
|
|
{
|
|
var increment =
|
|
(ModifierKeys & Keys.Control) == Keys.Control
|
|
? RotateIncrementAngle * 0.1
|
|
: RotateIncrementAngle;
|
|
|
|
var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier);
|
|
|
|
selection.RotateSelectedParts(angle);
|
|
}
|
|
else
|
|
{
|
|
if (AllowZoom)
|
|
{
|
|
if (e.Delta > 0)
|
|
ZoomToControlPoint(
|
|
e.Location,
|
|
(float)System.Math.Pow(ZoomInFactor, multiplier)
|
|
);
|
|
else
|
|
ZoomToControlPoint(
|
|
e.Location,
|
|
(float)System.Math.Pow(ZoomOutFactor, multiplier)
|
|
);
|
|
}
|
|
}
|
|
|
|
Invalidate();
|
|
}
|
|
|
|
protected override void OnMouseMove(MouseEventArgs e)
|
|
{
|
|
if (e.Button != MouseButtons.None)
|
|
ClearHover();
|
|
if (e.Button == MouseButtons.Middle)
|
|
{
|
|
if (AllowPan)
|
|
{
|
|
var diffx = e.X - lastPoint.X;
|
|
var diffy = e.Y - lastPoint.Y;
|
|
|
|
origin.X += diffx;
|
|
origin.Y += diffy;
|
|
|
|
Invalidate();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LastPoint = CurrentPoint;
|
|
CurrentPoint = PointControlToWorld(e.Location);
|
|
}
|
|
|
|
lastPoint = e.Location;
|
|
|
|
if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
|
|
{
|
|
cutOffHandler.UpdateDrag(CurrentPoint, selection.SelectedCutOffs[0]);
|
|
return;
|
|
}
|
|
|
|
if (e.Button == MouseButtons.None && actionManager.CurrentAction is ActionSelect)
|
|
{
|
|
hoverPoint = e.Location;
|
|
hoverPending = true;
|
|
showTooltip = false;
|
|
hoverTimer.Stop();
|
|
hoverTimer.Start();
|
|
|
|
if (hoveredPart != null)
|
|
Invalidate();
|
|
overlapOverlay.UpdateHover(e.Location);
|
|
}
|
|
else
|
|
{
|
|
ClearHover();
|
|
}
|
|
|
|
base.OnMouseMove(e);
|
|
}
|
|
|
|
protected override void OnMouseDoubleClick(MouseEventArgs e)
|
|
{
|
|
base.OnMouseDoubleClick(e);
|
|
|
|
if (e.Button == MouseButtons.Middle && SelectedParts.Count == 0)
|
|
ZoomToFit();
|
|
}
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
// Audited: DrawControl/PlateView, Action key handlers, EditNestForm,
|
|
// MainForm.ProcessCmdKey and menu shortcuts have no plain PgUp/PgDn
|
|
// binding. BestFitViewerForm owns these only in its separate dialog.
|
|
// No modifier keys, clicks, or wheel events are consumed by hover paging.
|
|
if ((keyData == Keys.PageUp || keyData == Keys.PageDown)
|
|
&& overlapOverlay.TryPageHover(keyData == Keys.PageUp ? -1 : 1))
|
|
return true;
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
}
|
|
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|
{
|
|
switch (e.KeyCode)
|
|
{
|
|
case Keys.Delete:
|
|
selection.DeleteSelected();
|
|
break;
|
|
|
|
case Keys.F:
|
|
if ((ModifierKeys & Keys.Control) == 0)
|
|
ZoomToFit();
|
|
else
|
|
base.OnKeyDown(e);
|
|
break;
|
|
|
|
default:
|
|
base.OnKeyDown(e);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void ProcessEscapeKey() => actionManager.ProcessEscapeKey();
|
|
|
|
public void EndAction() => actionManager.EndAction();
|
|
|
|
protected override bool ProcessDialogKey(Keys keyData)
|
|
{
|
|
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
|
// All other keys can be handled in OnKeyDown method.
|
|
|
|
switch (keyData)
|
|
{
|
|
case Keys.Escape:
|
|
ProcessEscapeKey();
|
|
break;
|
|
|
|
case Keys.Left:
|
|
SelectedParts.ForEach(part => part.Offset(-OffsetIncrementDistance, 0));
|
|
Invalidate();
|
|
break;
|
|
|
|
case Keys.X:
|
|
case Keys.Shift | Keys.Left:
|
|
selection.PushSelected(PushDirection.Left);
|
|
break;
|
|
|
|
case Keys.Shift | Keys.X:
|
|
case Keys.Shift | Keys.Right:
|
|
selection.PushSelected(PushDirection.Right);
|
|
break;
|
|
|
|
case Keys.Shift | Keys.Y:
|
|
case Keys.Shift | Keys.Up:
|
|
selection.PushSelected(PushDirection.Up);
|
|
break;
|
|
|
|
case Keys.Y:
|
|
case Keys.Shift | Keys.Down:
|
|
selection.PushSelected(PushDirection.Down);
|
|
break;
|
|
|
|
case Keys.Right:
|
|
SelectedParts.ForEach(part => part.Offset(OffsetIncrementDistance, 0));
|
|
Invalidate();
|
|
break;
|
|
|
|
case Keys.Up:
|
|
SelectedParts.ForEach(part => part.Offset(0, OffsetIncrementDistance));
|
|
Invalidate();
|
|
break;
|
|
|
|
case Keys.Down:
|
|
SelectedParts.ForEach(part => part.Offset(0, -OffsetIncrementDistance));
|
|
Invalidate();
|
|
break;
|
|
}
|
|
|
|
return base.ProcessDialogKey(keyData);
|
|
}
|
|
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
{
|
|
if (BackColor != ColorScheme.BackgroundColor)
|
|
BackColor = ColorScheme.BackgroundColor;
|
|
|
|
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
|
|
|
|
if (DrawOrigin)
|
|
{
|
|
e.Graphics.DrawLine(ColorScheme.OriginPen, origin.X, 0, origin.X, Height);
|
|
e.Graphics.DrawLine(ColorScheme.OriginPen, 0, origin.Y, Width, origin.Y);
|
|
}
|
|
|
|
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
|
|
|
renderer.DrawPlate(e.Graphics);
|
|
renderer.DrawParts(e.Graphics);
|
|
renderer.DrawCutOffs(e.Graphics);
|
|
renderer.DrawActiveWorkArea(e.Graphics);
|
|
renderer.DrawDebugRemnants(e.Graphics);
|
|
DrawOverlapOverlay(e.Graphics);
|
|
|
|
base.OnPaint(e);
|
|
|
|
if (!overlapOverlay.DrawHover(e.Graphics) && hoveredPart != null && showTooltip)
|
|
{
|
|
e.Graphics.ResetTransform();
|
|
var text = hoveredPart.BasePart.BaseDrawing.Name;
|
|
var size = e.Graphics.MeasureString(text, Font);
|
|
var x = hoverPoint.X + 16f;
|
|
var y = hoverPoint.Y - size.Height - 6f;
|
|
|
|
if (x + size.Width + 8 > ClientSize.Width)
|
|
x = hoverPoint.X - size.Width - 8;
|
|
if (y < 0)
|
|
y = hoverPoint.Y + 20;
|
|
|
|
var rect = new RectangleF(x, y, size.Width + 6, size.Height + 4);
|
|
using (var bgBrush = new SolidBrush(Color.FromArgb(230, Color.White)))
|
|
e.Graphics.FillRectangle(bgBrush, rect);
|
|
e.Graphics.DrawRectangle(Pens.DimGray, rect.X, rect.Y, rect.Width, rect.Height);
|
|
e.Graphics.DrawString(text, Font, Brushes.Black, x + 3, y + 2);
|
|
}
|
|
}
|
|
|
|
private void DrawOverlapOverlay(Graphics graphics) => overlapOverlay.Draw(graphics);
|
|
|
|
protected override void OnHandleDestroyed(EventArgs e)
|
|
{
|
|
ClearHover();
|
|
overlapOverlay.ReleaseHandle();
|
|
base.OnHandleDestroyed(e);
|
|
actionManager.Cleanup();
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
// A plate can outlive its view; stop it calling back into a disposed one.
|
|
if (plate != null)
|
|
{
|
|
plate.PartAdded -= plate_PartAdded;
|
|
plate.PartRemoved -= plate_PartRemoved;
|
|
plate.PartsReordered -= plate_PartsReordered;
|
|
}
|
|
overlapOverlay?.Dispose();
|
|
hoverTimer?.Dispose();
|
|
hoverTimer = null;
|
|
redrawTimer?.Dispose();
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
public override void Refresh()
|
|
{
|
|
parts.ForEach(p => p.Update(this));
|
|
previewManager.Update();
|
|
Invalidate();
|
|
}
|
|
|
|
public CutOff GetCutOffAtPoint(Vector point, double tolerance) =>
|
|
cutOffHandler.GetCutOffAtPoint(point, tolerance);
|
|
|
|
public LayoutPart GetPartAtControlPoint(Point pt) => selection.GetPartAtControlPoint(pt);
|
|
|
|
public LayoutPart GetPartAtGraphPoint(PointF pt) => selection.GetPartAtGraphPoint(pt);
|
|
|
|
public LayoutPart GetPartAtPoint(Vector pt) => selection.GetPartAtPoint(pt);
|
|
|
|
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType) =>
|
|
selection.GetPartsFromWindow(rect, selectionType);
|
|
|
|
public void SetAction(Type type) => actionManager.SetAction(type);
|
|
|
|
public void SetAction(Type type, params object[] args) =>
|
|
actionManager.SetAction(type, args);
|
|
|
|
public void AlignSelected(AlignType alignType) => selection.AlignSelected(alignType);
|
|
|
|
public void AlignSelected(AlignType alignType, LayoutPart fixedPart) =>
|
|
selection.AlignSelected(alignType, fixedPart);
|
|
|
|
public void AddPartFromDrawing(Drawing dwg, Vector location)
|
|
{
|
|
var part = new Part(dwg, location);
|
|
|
|
part.Offset(
|
|
part.Location.X - part.BoundingBox.Center.X,
|
|
part.Location.Y - part.BoundingBox.Center.Y
|
|
);
|
|
|
|
Plate.Parts.Add(part);
|
|
}
|
|
|
|
public void SetActiveParts(List<Part> parts) => previewManager.SetActiveParts(parts);
|
|
|
|
public void ClearPreviewParts() => previewManager.ClearPreviewParts();
|
|
|
|
public void AcceptPreviewParts(List<Part> parts) =>
|
|
previewManager.AcceptPreviewParts(parts);
|
|
|
|
// UI-thread state: closing the progress window only requests cancellation.
|
|
public bool IsFillInProgress { get; private set; }
|
|
|
|
public async void FillWithProgress(List<Part> groupParts, Box workArea)
|
|
{
|
|
if (IsFillInProgress)
|
|
return;
|
|
|
|
IsFillInProgress = true;
|
|
var operationActive = true;
|
|
try
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
using var cts = new CancellationTokenSource();
|
|
using var progressForm = new NestProgressForm(cts, showPlateRow: false);
|
|
try
|
|
{
|
|
var progress = new Progress<NestProgress>(p =>
|
|
{
|
|
// This callback can outlive its operation, even into the next fill.
|
|
if (!operationActive || IsDisposed || progressForm.IsDisposed)
|
|
return;
|
|
|
|
progressForm.UpdateProgress(p);
|
|
|
|
if (p.IsOverallBest)
|
|
SetActiveParts(p.BestParts);
|
|
|
|
ActiveWorkArea = p.ActiveWorkArea;
|
|
});
|
|
|
|
progressForm.Show(FindForm());
|
|
var strategy = EngineSelection.FillStrategy;
|
|
var spacing = Plate.PartSpacing;
|
|
var parts = await Task.Run(() =>
|
|
{
|
|
var result = PlateFillService.FillGroup(
|
|
strategy,
|
|
Plate,
|
|
groupParts,
|
|
workArea,
|
|
progress,
|
|
cts.Token
|
|
);
|
|
Compactor.Settle(result, workArea, spacing);
|
|
return result;
|
|
});
|
|
operationActive = false;
|
|
|
|
if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
|
|
{
|
|
SetActiveParts(parts);
|
|
AcceptPreviewParts(parts);
|
|
|
|
if (Plate.CutOffs.Count > 0)
|
|
Plate.RegenerateCutOffs(cutOffSettings);
|
|
|
|
sw.Stop();
|
|
Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms";
|
|
}
|
|
else
|
|
{
|
|
ClearPreviewParts();
|
|
}
|
|
|
|
progressForm.ShowCompleted();
|
|
}
|
|
finally
|
|
{
|
|
operationActive = false;
|
|
ActiveWorkArea = null;
|
|
progressForm.Close();
|
|
Focus();
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
ClearPreviewParts();
|
|
}
|
|
finally
|
|
{
|
|
operationActive = false;
|
|
IsFillInProgress = false;
|
|
}
|
|
}
|
|
|
|
public void RemoveSelectedParts() => selection.RemoveSelectedParts();
|
|
|
|
private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
|
{
|
|
if (IsDisposed || !IsHandleCreated)
|
|
return;
|
|
BeginInvoke(new System.Action(Invalidate));
|
|
}
|
|
|
|
private void hoverTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
|
{
|
|
if (IsDisposed || !IsHandleCreated)
|
|
return;
|
|
BeginInvoke(new System.Action(HoverCheck));
|
|
}
|
|
|
|
private void HoverCheck()
|
|
{
|
|
if (!hoverPending || IsDisposed || Disposing)
|
|
return;
|
|
hoverPending = false;
|
|
var graphPt = PointControlToGraph(hoverPoint);
|
|
LayoutPart hitPart = null;
|
|
|
|
for (var i = parts.Count - 1; i >= 0; --i)
|
|
{
|
|
if (parts[i].Path.GetBounds().Contains(graphPt) && parts[i].Path.IsVisible(graphPt))
|
|
{
|
|
hitPart = parts[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
hoveredPart = hitPart;
|
|
showTooltip = hitPart != null;
|
|
|
|
if (showTooltip)
|
|
Invalidate();
|
|
}
|
|
|
|
private void plate_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
|
{
|
|
if (PartAdded != null)
|
|
PartAdded.Invoke(this, e);
|
|
|
|
parts.Insert(e.Index, LayoutPart.Create(e.Item, this));
|
|
redrawTimer.Start();
|
|
}
|
|
|
|
private void plate_PartRemoved(object sender, ItemRemovedEventArgs<Part> e)
|
|
{
|
|
if (PartRemoved != null)
|
|
PartRemoved.Invoke(this, e);
|
|
|
|
parts.RemoveAll(p => p.BasePart == e.Item);
|
|
}
|
|
|
|
private void plate_PartsReordered(object sender, EventArgs e)
|
|
{
|
|
// Part numbers are drawn from the layout order, and an applied cutting plan can also
|
|
// replace programs, so follow the plate's order and rebuild every part's graphics.
|
|
// A reorder keeps repeated references, so reuse each existing layout once.
|
|
var layouts = new Dictionary<Part, Queue<LayoutPart>>(ReferenceEqualityComparer.Instance);
|
|
foreach (var layout in parts)
|
|
{
|
|
if (!layouts.TryGetValue(layout.BasePart, out var queue))
|
|
layouts.Add(layout.BasePart, queue = new Queue<LayoutPart>());
|
|
queue.Enqueue(layout);
|
|
}
|
|
var reordered = plate.Parts
|
|
.Select(part => layouts.TryGetValue(part, out var queue) && queue.Count > 0
|
|
? queue.Dequeue()
|
|
: LayoutPart.Create(part, this))
|
|
.ToList();
|
|
parts.Clear();
|
|
parts.AddRange(reordered);
|
|
foreach (var layout in parts)
|
|
layout.IsDirty = true;
|
|
|
|
PartsReordered?.Invoke(this, EventArgs.Empty);
|
|
Invalidate();
|
|
}
|
|
|
|
public void DeselectAll() => selection.DeselectAll();
|
|
|
|
public void SelectAll() => selection.SelectAll();
|
|
|
|
public void NotifySelectionChanged() => selection.NotifySelectionChanged();
|
|
|
|
public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true)
|
|
{
|
|
base.ZoomToPoint(pt, zoomFactor, false);
|
|
|
|
if (redraw)
|
|
Invalidate();
|
|
}
|
|
|
|
public override void ZoomToArea(
|
|
double x,
|
|
double y,
|
|
double width,
|
|
double height,
|
|
bool redraw = true
|
|
)
|
|
{
|
|
base.ZoomToArea(x, y, width, height, false);
|
|
|
|
if (redraw)
|
|
Invalidate();
|
|
}
|
|
|
|
public virtual void ZoomToFit(bool redraw = true)
|
|
{
|
|
ZoomToArea(plate.BoundingBox(true), redraw);
|
|
}
|
|
|
|
public virtual void ZoomToSelected(bool redraw = true)
|
|
{
|
|
ZoomToArea(SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox(), redraw);
|
|
}
|
|
|
|
public virtual void ZoomToPlate(bool redraw = true)
|
|
{
|
|
ZoomToArea(plate.BoundingBox(false), redraw);
|
|
}
|
|
|
|
public void PushSelected(PushDirection direction) => selection.PushSelected(direction);
|
|
|
|
public bool ExpandSelected() => selection.ExpandSelected();
|
|
|
|
public void RotateSelectedParts(double angle) => selection.RotateSelectedParts(angle);
|
|
|
|
protected override void UpdateMatrix()
|
|
{
|
|
ClearHover();
|
|
base.UpdateMatrix();
|
|
parts.ForEach(p => p.Update(this));
|
|
previewManager.Update();
|
|
}
|
|
}
|
|
}
|