mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 13:51:06 -04:00
perf(desktop): reuse cached part paths when zooming and dragging
Each zoom step rebuilt every part's display paths from its CNC program, and each drag step rebuilt the moved parts, which dominates for parts with hundreds of holes. LayoutPart now keeps its paths in the drawing's program frame and only maps them to the view on a zoom or move, rebuilding when IsDirty is set or the program, rotation or manual lead-ins change. Also caches the label StringFormat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
548522775f
commit
c27188c722
6 files changed
+226
-39
No files matched your search
@@ -0,0 +1,96 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Geometry;
|
||||
using Program = OpenNest.CNC.Program;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A zoom or a move maps a part's cached program-frame paths to the view instead of rebuilding
|
||||
/// them; the result must match a part built from scratch in the same state.
|
||||
/// </summary>
|
||||
public class LayoutPartViewCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void ZoomMoveAndRotateMatchAFreshBuild() => StaTestThread.Run(() =>
|
||||
{
|
||||
var part = new Part(new Drawing("Holes", PlateWithHoles()), new Vector(5, 7));
|
||||
using var view = new PlateView(ColorSchemeRegistry.Get("Workshop"));
|
||||
view.Matrix = new Matrix(2, 0, 0, -2, 0, 0);
|
||||
var layout = LayoutPart.Create(part, view);
|
||||
AssertMatchesFreshBuild(layout, view);
|
||||
|
||||
view.Matrix = new Matrix(3.5f, 0, 0, -3.5f, 0, 0);
|
||||
layout.UpdateView(view);
|
||||
AssertMatchesFreshBuild(layout, view);
|
||||
|
||||
layout.Offset(12.25, -3.5);
|
||||
Assert.True(layout.IsDirty);
|
||||
layout.UpdateView(view);
|
||||
Assert.False(layout.IsDirty);
|
||||
AssertMatchesFreshBuild(layout, view);
|
||||
|
||||
// A rotation of the base part, then a move through the layout, must not reuse stale paths.
|
||||
part.Rotate(OpenNest.Math.Angle.ToRadians(30), part.BoundingBox.Center);
|
||||
layout.Offset(1, 1);
|
||||
layout.UpdateView(view);
|
||||
AssertMatchesFreshBuild(layout, view);
|
||||
}, TimeSpan.FromMinutes(1), "Layout part view cache test timed out.");
|
||||
|
||||
[Fact]
|
||||
public void DirtyRebuildsAfterInPlaceProgramEdit() => StaTestThread.Run(() =>
|
||||
{
|
||||
var part = new Part(new Drawing("Edited", PlateWithHoles()));
|
||||
using var view = new PlateView(ColorSchemeRegistry.Get("Workshop"));
|
||||
view.Matrix = new Matrix(2, 0, 0, -2, 0, 0);
|
||||
var layout = LayoutPart.Create(part, view);
|
||||
var before = layout.Path.PointCount;
|
||||
|
||||
part.Program.MoveTo(5, 25);
|
||||
part.Program.LineTo(10, 25);
|
||||
layout.IsDirty = true;
|
||||
layout.UpdateView(view);
|
||||
|
||||
Assert.True(layout.Path.PointCount > before);
|
||||
AssertMatchesFreshBuild(layout, view);
|
||||
}, TimeSpan.FromMinutes(1), "Layout part rebuild test timed out.");
|
||||
|
||||
private static void AssertMatchesFreshBuild(LayoutPart layout, PlateView view)
|
||||
{
|
||||
var fresh = LayoutPart.Create(layout.BasePart, view);
|
||||
Assert.Equal(fresh.Path.PathTypes, layout.Path.PathTypes);
|
||||
var expected = fresh.Path.PathPoints;
|
||||
var actual = layout.Path.PathPoints;
|
||||
Assert.Equal(expected.Length, actual.Length);
|
||||
for (var i = 0; i < expected.Length; i++)
|
||||
{
|
||||
Assert.Equal(expected[i].X, actual[i].X, 2);
|
||||
Assert.Equal(expected[i].Y, actual[i].Y, 2);
|
||||
}
|
||||
Assert.Equal(fresh.DisplayBounds.X, layout.DisplayBounds.X, 2);
|
||||
Assert.Equal(fresh.DisplayBounds.Y, layout.DisplayBounds.Y, 2);
|
||||
Assert.Equal(fresh.DisplayBounds.Width, layout.DisplayBounds.Width, 2);
|
||||
Assert.Equal(fresh.DisplayBounds.Height, layout.DisplayBounds.Height, 2);
|
||||
fresh.ReleaseDisplayResources();
|
||||
}
|
||||
|
||||
private static Program PlateWithHoles()
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(40, 0);
|
||||
program.LineTo(40, 30);
|
||||
program.LineTo(0, 30);
|
||||
program.LineTo(0, 0);
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var center = new Vector(5 + i * 6, 15);
|
||||
var start = new Vector(center.X + 2, center.Y);
|
||||
program.MoveTo(start);
|
||||
program.ArcTo(start, center, RotationType.CCW);
|
||||
}
|
||||
return program;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ namespace OpenNest.Actions
|
||||
{
|
||||
parts.ForEach(p =>
|
||||
{
|
||||
p.Update(plateView);
|
||||
p.UpdateView(plateView);
|
||||
p.Draw(e.Graphics);
|
||||
});
|
||||
}
|
||||
@@ -97,7 +97,7 @@ namespace OpenNest.Actions
|
||||
parts.ForEach(p =>
|
||||
{
|
||||
if (p.IsDirty)
|
||||
p.Update(plateView);
|
||||
p.UpdateView(plateView);
|
||||
|
||||
p.Draw(e.Graphics);
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace OpenNest.Controls
|
||||
var part = layoutParts[i];
|
||||
|
||||
if (part.IsDirty)
|
||||
part.Update(view);
|
||||
part.UpdateView(view);
|
||||
|
||||
if (!part.DisplayBounds.IntersectsWith(viewBounds))
|
||||
continue;
|
||||
@@ -142,7 +142,7 @@ namespace OpenNest.Controls
|
||||
var part = previewParts[i];
|
||||
|
||||
if (part.IsDirty)
|
||||
part.Update(view);
|
||||
part.UpdateView(view);
|
||||
|
||||
var path = part.Path;
|
||||
if (!part.DisplayBounds.IntersectsWith(viewBounds))
|
||||
@@ -554,7 +554,7 @@ namespace OpenNest.Controls
|
||||
var layoutPart = layoutParts[i];
|
||||
|
||||
if (layoutPart.IsDirty)
|
||||
layoutPart.Update(view);
|
||||
layoutPart.UpdateView(view);
|
||||
|
||||
layoutPart.UpdateOffset(view.DisplayPlate.PartSpacing, view.OffsetTolerance, view.Matrix);
|
||||
|
||||
|
||||
@@ -1119,9 +1119,10 @@ namespace OpenNest.Controls
|
||||
{
|
||||
ClearHover();
|
||||
base.UpdateMatrix();
|
||||
parts.ForEach(p => p.Update(this));
|
||||
displayParts?.ForEach(p => p.Update(this));
|
||||
previewManager.Update();
|
||||
// Geometry is unchanged: map the cached paths to the new scale, not rebuild them.
|
||||
parts.ForEach(p => p.UpdateView(this));
|
||||
displayParts?.ForEach(p => p.UpdateView(this));
|
||||
previewManager.UpdateView();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,11 @@ namespace OpenNest.Controls
|
||||
activeParts.ForEach(p => p.Update(view));
|
||||
}
|
||||
|
||||
public void UpdateView()
|
||||
{
|
||||
activeParts.ForEach(p => p.UpdateView(view));
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
activeParts.Clear();
|
||||
|
||||
+116
-31
@@ -20,6 +20,7 @@ namespace OpenNest
|
||||
[ThreadStatic] private static Pen selectedPen;
|
||||
[ThreadStatic] private static Brush selectedBrush;
|
||||
[ThreadStatic] private static Pen leadInPen;
|
||||
[ThreadStatic] private static StringFormat labelFormat;
|
||||
|
||||
private Color color;
|
||||
private Color? colorOverride;
|
||||
@@ -35,6 +36,20 @@ namespace OpenNest
|
||||
private Vector? _labelPoint;
|
||||
private PointF _labelScreenPoint;
|
||||
|
||||
// Paths in the drawing's program frame, before placement. A zoom or a move only maps
|
||||
// these to the screen again; rebuilding them from the program dominates for parts with
|
||||
// hundreds of holes. They are rebuilt when the program, its rotation or its lead-ins change.
|
||||
private GraphicsPath localCut;
|
||||
private GraphicsPath localLead;
|
||||
private GraphicsPath localEtch;
|
||||
private GraphicsPath localFill;
|
||||
private GraphicsPath localDisplay;
|
||||
private CNC.Program localProgram;
|
||||
private double localRotation;
|
||||
private bool localManualLeadIns;
|
||||
private bool needsRebuild;
|
||||
private bool moved;
|
||||
|
||||
public readonly Part BasePart;
|
||||
|
||||
private LayoutPart(Part part)
|
||||
@@ -47,7 +62,16 @@ namespace OpenNest
|
||||
SetDisplayColor(part.BaseDrawing.Color);
|
||||
}
|
||||
|
||||
internal bool IsDirty { get; set; }
|
||||
/// <summary>Setting it rebuilds the paths from the program on the next update.</summary>
|
||||
internal bool IsDirty
|
||||
{
|
||||
get => needsRebuild || moved;
|
||||
set
|
||||
{
|
||||
needsRebuild = value;
|
||||
moved = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSelected { get; set; }
|
||||
|
||||
@@ -119,18 +143,13 @@ namespace OpenNest
|
||||
{
|
||||
Draw(g, muted);
|
||||
|
||||
using var sf = new StringFormat
|
||||
{
|
||||
Alignment = StringAlignment.Center,
|
||||
LineAlignment = StringAlignment.Center,
|
||||
};
|
||||
g.DrawString(
|
||||
id,
|
||||
ProgramIdFont,
|
||||
muted ? Brushes.DimGray : Brushes.Black,
|
||||
_labelScreenPoint.X,
|
||||
_labelScreenPoint.Y,
|
||||
sf
|
||||
LabelFormat
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,6 +171,22 @@ namespace OpenNest
|
||||
DisplayPath = null;
|
||||
OffsetPath?.Dispose();
|
||||
OffsetPath = null;
|
||||
ReleaseLocalPaths();
|
||||
}
|
||||
|
||||
private void ReleaseLocalPaths()
|
||||
{
|
||||
localCut?.Dispose();
|
||||
localCut = null;
|
||||
localLead?.Dispose();
|
||||
localLead = null;
|
||||
localEtch?.Dispose();
|
||||
localEtch = null;
|
||||
localFill?.Dispose();
|
||||
localFill = null;
|
||||
localDisplay?.Dispose();
|
||||
localDisplay = null;
|
||||
localProgram = null;
|
||||
}
|
||||
|
||||
public GraphicsPath OffsetPath { get; private set; }
|
||||
@@ -188,7 +223,7 @@ namespace OpenNest
|
||||
|
||||
// Display-only entities are kept in the drawing's program frame before rotation, so they
|
||||
// follow the part by its cumulative rotation about the program origin, then its location.
|
||||
private GraphicsPath BuildDisplayPath(Matrix matrix)
|
||||
private GraphicsPath BuildDisplayPath()
|
||||
{
|
||||
var entities = EntityRoles.DisplayGeometry(BasePart.BaseDrawing);
|
||||
if (entities.Count == 0)
|
||||
@@ -198,11 +233,9 @@ namespace OpenNest
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
entity.Rotate(BasePart.Rotation);
|
||||
entity.Offset(BasePart.Location);
|
||||
using var figure = new Shape { Entities = new List<Entity> { entity } }.GetGraphicsPath();
|
||||
path.AddPath(figure, false);
|
||||
}
|
||||
path.Transform(matrix);
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -237,33 +270,79 @@ namespace OpenNest
|
||||
return PolyLabel.Find(outer, holes, PolyLabel.ThicknessPrecision(outer, holes));
|
||||
}
|
||||
|
||||
/// <summary>Rebuilds the part's paths from its program and maps them to the view.</summary>
|
||||
public void Update(DrawControl plateView)
|
||||
{
|
||||
BuildLocalPaths();
|
||||
ApplyView(plateView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the part to the view, rebuilding its paths only when <see cref="IsDirty"/> was set
|
||||
/// or its program, rotation or lead-ins changed. Use after a zoom or a move.
|
||||
/// </summary>
|
||||
internal void UpdateView(DrawControl plateView)
|
||||
{
|
||||
if (needsRebuild
|
||||
|| localCut == null
|
||||
|| !ReferenceEquals(localProgram, BasePart.Program)
|
||||
|| localRotation != BasePart.Rotation
|
||||
|| localManualLeadIns != BasePart.HasManualLeadIns)
|
||||
BuildLocalPaths();
|
||||
|
||||
ApplyView(plateView);
|
||||
}
|
||||
|
||||
private void BuildLocalPaths()
|
||||
{
|
||||
ReleaseLocalPaths();
|
||||
var program = BasePart.Program;
|
||||
program.GetDisplayPaths(Vector.Zero,
|
||||
out var cutPath, out var leadPath, out var etchPath, out var hasOpenCuts);
|
||||
localCut = cutPath;
|
||||
localEtch = etchPath;
|
||||
// Only programs with break cuts or open (tabbed) cuts need a fill apart from the outline path.
|
||||
if (hasOpenCuts || HasBreakCuts(program))
|
||||
localFill = program.GetFillPath(Vector.Zero);
|
||||
localLead = BasePart.HasManualLeadIns ? leadPath : null;
|
||||
if (!BasePart.HasManualLeadIns)
|
||||
leadPath.Dispose();
|
||||
localDisplay = BuildDisplayPath();
|
||||
|
||||
localProgram = program;
|
||||
localRotation = BasePart.Rotation;
|
||||
localManualLeadIns = BasePart.HasManualLeadIns;
|
||||
needsRebuild = false;
|
||||
}
|
||||
|
||||
private static GraphicsPath Transformed(GraphicsPath path, Matrix matrix)
|
||||
{
|
||||
if (path == null)
|
||||
return null;
|
||||
var result = (GraphicsPath)path.Clone();
|
||||
result.Transform(matrix);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ApplyView(DrawControl plateView)
|
||||
{
|
||||
colorScheme = (plateView as PlateView)?.ColorScheme ?? ColorScheme.Default;
|
||||
RefreshColors();
|
||||
BasePart.Program.GetDisplayPaths(BasePart.Location,
|
||||
out var cutPath, out var leadPath, out var etchPath, out var hasOpenCuts);
|
||||
cutPath.Transform(plateView.Matrix);
|
||||
leadPath.Transform(plateView.Matrix);
|
||||
etchPath.Transform(plateView.Matrix);
|
||||
|
||||
// Place the program-frame paths, then map them to the screen.
|
||||
using var matrix = plateView.Matrix.Clone();
|
||||
matrix.Translate((float)BasePart.Location.X, (float)BasePart.Location.Y);
|
||||
|
||||
Path?.Dispose();
|
||||
LeadInPath?.Dispose();
|
||||
EtchPath?.Dispose();
|
||||
FillPath?.Dispose();
|
||||
FillPath = null;
|
||||
// Only programs with break cuts or open (tabbed) cuts need a fill apart from the outline path.
|
||||
if (hasOpenCuts || HasBreakCuts(BasePart.Program))
|
||||
{
|
||||
FillPath = BasePart.Program.GetFillPath(BasePart.Location);
|
||||
FillPath.Transform(plateView.Matrix);
|
||||
}
|
||||
Path = cutPath;
|
||||
EtchPath = etchPath;
|
||||
DisplayPath?.Dispose();
|
||||
DisplayPath = BuildDisplayPath(plateView.Matrix);
|
||||
LeadInPath = BasePart.HasManualLeadIns ? leadPath : null;
|
||||
if (!BasePart.HasManualLeadIns)
|
||||
leadPath.Dispose();
|
||||
var cutPath = Path = Transformed(localCut, matrix);
|
||||
var etchPath = EtchPath = Transformed(localEtch, matrix);
|
||||
FillPath = Transformed(localFill, matrix);
|
||||
LeadInPath = Transformed(localLead, matrix);
|
||||
DisplayPath = Transformed(localDisplay, matrix);
|
||||
|
||||
var bounds = cutPath.GetBounds();
|
||||
if (etchPath.PointCount > 0)
|
||||
@@ -398,6 +477,12 @@ namespace OpenNest
|
||||
|
||||
private static Pen LeadInPen => leadInPen ??= new Pen(Color.OrangeRed, 1.5f);
|
||||
|
||||
private static StringFormat LabelFormat => labelFormat ??= new StringFormat
|
||||
{
|
||||
Alignment = StringAlignment.Center,
|
||||
LineAlignment = StringAlignment.Center,
|
||||
};
|
||||
|
||||
private static Brush SelectedBrush
|
||||
{
|
||||
get
|
||||
@@ -433,7 +518,7 @@ namespace OpenNest
|
||||
set
|
||||
{
|
||||
BasePart.Location = value;
|
||||
IsDirty = true;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,13 +542,13 @@ namespace OpenNest
|
||||
public void Offset(double x, double y)
|
||||
{
|
||||
BasePart.Offset(x, y);
|
||||
IsDirty = true;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
public void Offset(Vector voffset)
|
||||
{
|
||||
BasePart.Offset(voffset);
|
||||
IsDirty = true;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
public Box BoundingBox
|
||||
|
||||
Reference in new issue
Block a user