Files
OpenNest/OpenNest/Controls/PlateView.cs
T
ajandClaude Opus 5.5 405a867a77 fix(desktop): draw planned cut-offs and issue markers on the plan preview
The proposal preview skipped the cut-off overlay: it carries cut-off parts but no
cut-off definitions, so planner cut-offs looked like thin black part outlines
instead of the orange-red dashes used everywhere else. The preview now draws each
cut-off part's own cutting moves in that style.

The same paint guard hid the validation overlay over a preview, so issue markers
for unverified plans were set but never drawn. The overlay now draws on proposals
too, with the marker's part numbers mapped to the preview's proposed order so the
right parts stay highlighted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:18:48 -04:00

1085 lines
37 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 readonly ValidationOverlay validationOverlay;
private string status;
private Plate plate;
private ActionManager actionManager;
private CutOffSettings cutOffSettings = new CutOffSettings();
private SelectionManager selection;
private CutOffHandler cutOffHandler;
private readonly ClampMenuHandler clampMenu;
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 ClampMenuHandler ClampMenu => clampMenu;
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);
validationOverlay = new ValidationOverlay(this);
Plate = new Plate(60, 120);
origin = new PointF();
parts = new List<LayoutPart>();
selection = new SelectionManager(this);
cutOffHandler = new CutOffHandler(this);
clampMenu = new ClampMenuHandler(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; }
internal bool InspectionOnly { get; set; }
internal System.Action InspectionCloseRequested { get; set; }
/// <summary>
/// Asked before a right-click changes which clamps are on the sheet; false refuses
/// (e.g. a nest run is using the clamps). Null allows.
/// </summary>
internal Func<bool> CanEditClamps { 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;
private Plate displayPlate;
private List<LayoutPart> displayParts;
private Func<bool> displayIsCurrent;
internal Plate DisplayPlate => displayPlate ?? plate;
internal List<LayoutPart> DisplayLayoutParts => displayParts ?? parts;
internal bool HasProposalDisplay => displayPlate != null;
internal void ShowProposalDisplay(Plate proposal, Func<bool> isCurrent)
{
if (!InspectionOnly)
throw new InvalidOperationException("Proposal display requires an inspection lock.");
ClearProposalDisplay();
if (proposal == null || isCurrent?.Invoke() != true)
return;
var layouts = new List<LayoutPart>();
try
{
foreach (var part in proposal.Parts)
layouts.Add(LayoutPart.Create(part, this));
displayPlate = proposal;
displayIsCurrent = isCurrent;
displayParts = layouts;
}
catch
{
foreach (var layout in layouts)
layout.ReleaseDisplayResources();
throw;
}
ClearHover();
validationOverlay.Clear();
Invalidate();
}
internal void ClearProposalDisplay()
{
var old = displayParts;
displayPlate = null;
displayParts = null;
displayIsCurrent = null;
if (old != null)
foreach (var layout in old)
layout.ReleaseDisplayResources();
validationOverlay?.Clear();
if (!IsDisposed && !Disposing)
Invalidate();
}
internal IReadOnlyList<LayoutPart> PreviewParts => HasProposalDisplay ? Array.Empty<LayoutPart>() : 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)
{
ClearProposalDisplay();
if (plate != null)
{
plate.PartAdded -= plate_PartAdded;
plate.PartRemoved -= plate_PartRemoved;
plate.PartsReordered -= plate_PartsReordered;
plate.ClampZonesChanged -= plate_ClampZonesChanged;
parts.Clear();
previewManager.Clear();
selection.Clear();
}
validationOverlay.Clear();
plate = p;
plate.PartAdded += plate_PartAdded;
plate.PartRemoved += plate_PartRemoved;
plate.PartsReordered += plate_PartsReordered;
plate.ClampZonesChanged += plate_ClampZonesChanged;
foreach (var part in plate.Parts)
parts.Add(LayoutPart.Create(part, this));
overlapOverlay.SetPlate(plate);
actionManager?.OnPlateChanged();
}
internal OverlapOverlayController OverlapOverlay => overlapOverlay;
internal ValidationOverlay ValidationOverlay => validationOverlay;
internal (float Scale, PointF Origin) CaptureViewport() => (ViewScale, origin);
internal void RestoreViewport((float Scale, PointF Origin) viewport)
{
ViewScale = viewport.Scale;
origin = viewport.Origin;
UpdateMatrix();
Invalidate();
}
internal void ShowValidationFinding(PostVerificationFinding finding) => validationOverlay.Show(finding);
internal void ClearValidationFinding() => validationOverlay.Clear();
internal void CenterValidationPoint(Vector location, Point target)
{
var current = PointWorldToControl(location);
origin.X += target.X - current.X;
origin.Y += target.Y - current.Y;
UpdateMatrix();
Invalidate();
}
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 (InspectionOnly)
return;
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 (InspectionOnly)
return;
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 (InspectionOnly)
return;
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 (InspectionOnly)
return;
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 (e.Button == MouseButtons.Left && cutOffHandler.IsDragging)
{
cutOffHandler.EndDrag();
return;
}
if (e.Button == MouseButtons.Right)
clampMenu.TryShow(e.Location, 3.0 / ViewScale);
base.OnMouseUp(e);
}
protected override void OnMouseWheel(MouseEventArgs e)
{
if (!InspectionOnly)
base.OnMouseWheel(e);
var multiplier = System.Math.Abs(e.Delta / 120);
if (!InspectionOnly && 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 (InspectionOnly)
return;
if (cutOffHandler.IsDragging && e.Button == MouseButtons.Left
&& 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)
{
if (!InspectionOnly)
base.OnMouseDoubleClick(e);
if (e.Button == MouseButtons.Middle && (InspectionOnly || SelectedParts.Count == 0))
ZoomToFit();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (InspectionOnly)
{
if (keyData == Keys.Escape && InspectionCloseRequested != null)
InspectionCloseRequested();
else if (keyData == Keys.F)
ZoomToFit();
else if (keyData is (Keys.Control | Keys.Oemplus) or (Keys.Control | Keys.OemMinus))
return base.ProcessCmdKey(ref msg, keyData); // The host's zoom shortcuts stay live.
return true;
}
// 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)
{
if (InspectionOnly)
{
if (e.KeyData == Keys.F)
ZoomToFit();
return;
}
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)
{
if (InspectionOnly)
{
if (keyData == Keys.Escape)
InspectionCloseRequested?.Invoke();
return true;
}
// 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 (displayPlate != null && displayIsCurrent?.Invoke() != true)
ClearProposalDisplay();
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.DrawClampZones(e.Graphics);
renderer.DrawRemovedClamps(e.Graphics);
renderer.DrawCutOffs(e.Graphics);
if (!HasProposalDisplay)
{
renderer.DrawActiveWorkArea(e.Graphics);
renderer.DrawDebugRemnants(e.Graphics);
DrawOverlapOverlay(e.Graphics);
}
// Plan Cutting marks findings on its proposal as well as on the live plate.
validationOverlay.Draw(e.Graphics);
base.OnPaint(e);
if (!InspectionOnly && !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();
validationOverlay.Clear();
base.OnHandleDestroyed(e);
actionManager.Cleanup();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
ClearProposalDisplay();
InspectionCloseRequested = null;
// 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;
plate.ClampZonesChanged -= plate_ClampZonesChanged;
}
clampMenu?.Dispose();
CanEditClamps = null;
overlapOverlay?.Dispose();
validationOverlay?.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
);
if (Plate.ClampZones.Count == 0)
Compactor.Settle(result, workArea, spacing);
else
Compactor.Settle(result, Plate, workArea);
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)
{
var path = parts[i].SelectionPath;
if (path?.GetBounds().Contains(graphPt) == true && 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);
}
// Clamp zones are display-only here: repaint, nothing to select or rebuild.
private void plate_ClampZonesChanged(object sender, EventArgs e) => Invalidate();
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(DisplayPlate.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(
Func<Func<CancellationToken, Expander.Result>, Expander.Result> run = null
) => selection.ExpandSelected(run);
public void RotateSelectedParts(double angle) => selection.RotateSelectedParts(angle);
protected override void UpdateMatrix()
{
ClearHover();
base.UpdateMatrix();
parts.ForEach(p => p.Update(this));
displayParts?.ForEach(p => p.Update(this));
previewManager.Update();
}
}
}