refactor: CutOff uses Dictionary<Part, Entity> instead of index-based list
Replace CutOff.BuildPerimeterCache (List<Shape>) with Plate.BuildPerimeterCache (Dictionary<Part, Entity>) throughout. Consolidate two Regenerate overloads into a single method with optional cache parameter. Fix Shape intersection bug where non-intersecting entities added spurious Vector.Zero points to results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+76
-23
@@ -26,9 +26,9 @@ namespace OpenNest
|
|||||||
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Regenerate(Plate plate, CutOffSettings settings)
|
public void Regenerate(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache = null)
|
||||||
{
|
{
|
||||||
var segments = ComputeSegments(plate, settings);
|
var segments = ComputeSegments(plate, settings, cache);
|
||||||
var program = BuildProgram(segments, settings);
|
var program = BuildProgram(segments, settings);
|
||||||
Drawing.Program = program;
|
Drawing.Program = program;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ namespace OpenNest
|
|||||||
return $"CutOff-{axisChar}-{coord:F2}";
|
return $"CutOff-{axisChar}-{coord:F2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings)
|
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache)
|
||||||
{
|
{
|
||||||
var bounds = plate.BoundingBox(includeParts: false);
|
var bounds = plate.BoundingBox(includeParts: false);
|
||||||
|
|
||||||
@@ -66,26 +66,10 @@ namespace OpenNest
|
|||||||
if (part.BaseDrawing.IsCutOff)
|
if (part.BaseDrawing.IsCutOff)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var bb = part.BoundingBox;
|
Entity perimeter = null;
|
||||||
double partStart, partEnd, partMin, partMax;
|
cache?.TryGetValue(part, out perimeter);
|
||||||
|
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
|
||||||
if (Axis == CutOffAxis.Vertical)
|
exclusions.AddRange(partExclusions);
|
||||||
{
|
|
||||||
partMin = bb.X - settings.PartClearance;
|
|
||||||
partMax = bb.X + bb.Width + settings.PartClearance;
|
|
||||||
partStart = bb.Y - settings.PartClearance;
|
|
||||||
partEnd = bb.Y + bb.Length + settings.PartClearance;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
partMin = bb.Y - settings.PartClearance;
|
|
||||||
partMax = bb.Y + bb.Length + settings.PartClearance;
|
|
||||||
partStart = bb.X - settings.PartClearance;
|
|
||||||
partEnd = bb.X + bb.Width + settings.PartClearance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cutPosition >= partMin && cutPosition <= partMax)
|
|
||||||
exclusions.Add((partStart, partEnd));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
|
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
|
||||||
@@ -120,6 +104,75 @@ namespace OpenNest
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<(double Start, double End)> GetPartExclusions(
|
||||||
|
Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||||
|
{
|
||||||
|
var bb = part.BoundingBox;
|
||||||
|
double partMin, partMax, partStart, partEnd;
|
||||||
|
|
||||||
|
if (Axis == CutOffAxis.Vertical)
|
||||||
|
{
|
||||||
|
partMin = bb.X - clearance;
|
||||||
|
partMax = bb.X + bb.Width + clearance;
|
||||||
|
partStart = bb.Y - clearance;
|
||||||
|
partEnd = bb.Y + bb.Length + clearance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
partMin = bb.Y - clearance;
|
||||||
|
partMax = bb.Y + bb.Length + clearance;
|
||||||
|
partStart = bb.X - clearance;
|
||||||
|
partEnd = bb.X + bb.Width + clearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cutPosition < partMin || cutPosition > partMax)
|
||||||
|
return new List<(double Start, double End)>();
|
||||||
|
|
||||||
|
if (perimeter != null)
|
||||||
|
{
|
||||||
|
var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance);
|
||||||
|
if (perimeterExclusions != null)
|
||||||
|
return perimeterExclusions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<(double Start, double End)> { (partStart, partEnd) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(double Start, double End)> IntersectPerimeter(
|
||||||
|
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||||
|
{
|
||||||
|
Vector p1, p2;
|
||||||
|
if (Axis == CutOffAxis.Vertical)
|
||||||
|
{
|
||||||
|
p1 = new Vector(cutPosition, lineStart);
|
||||||
|
p2 = new Vector(cutPosition, lineEnd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
p1 = new Vector(lineStart, cutPosition);
|
||||||
|
p2 = new Vector(lineEnd, cutPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cutLine = new Line(p1, p2);
|
||||||
|
|
||||||
|
if (!perimeter.Intersects(cutLine, out var pts) || pts.Count < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var coords = pts
|
||||||
|
.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
|
||||||
|
.OrderBy(c => c)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (coords.Count % 2 != 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var result = new List<(double Start, double End)>();
|
||||||
|
for (var i = 0; i < coords.Count; i += 2)
|
||||||
|
result.Add((coords[i] - clearance, coords[i + 1] + clearance));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
|
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
|
||||||
{
|
{
|
||||||
var program = new Program();
|
var program = new Program();
|
||||||
|
|||||||
@@ -249,8 +249,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
foreach (var geo in shape.Entities)
|
foreach (var geo in shape.Entities)
|
||||||
{
|
{
|
||||||
List<Vector> pts3;
|
if (geo.Intersects(line, out var pts3))
|
||||||
geo.Intersects(line, out pts3);
|
|
||||||
pts.AddRange(pts3);
|
pts.AddRange(pts3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public class CutOffGeometryTests
|
|||||||
part.Location = new Vector(0, 0);
|
part.Location = new Vector(0, 0);
|
||||||
plate.Parts.Add(part);
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
var cache = Plate.BuildPerimeterCache(plate);
|
||||||
|
|
||||||
// Cut at X=2: inside the BB but near the edge of the circle.
|
// Cut at X=2: inside the BB but near the edge of the circle.
|
||||||
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
|
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
|
||||||
@@ -125,7 +125,7 @@ public class CutOffGeometryTests
|
|||||||
part.Location = new Vector(0, 0);
|
part.Location = new Vector(0, 0);
|
||||||
plate.Parts.Add(part);
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
var cache = Plate.BuildPerimeterCache(plate);
|
||||||
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
|
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
|
||||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ public class CutOffGeometryTests
|
|||||||
part.Location = new Vector(10, 10);
|
part.Location = new Vector(10, 10);
|
||||||
plate.Parts.Add(part);
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
var cache = Plate.BuildPerimeterCache(plate);
|
||||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ public class CutOffGeometryTests
|
|||||||
part.Location = new Vector(0, 0);
|
part.Location = new Vector(0, 0);
|
||||||
plate.Parts.Add(part);
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
var cache = Plate.BuildPerimeterCache(plate);
|
||||||
|
|
||||||
// Horizontal cut at Y=2: near the edge of the circle.
|
// Horizontal cut at Y=2: near the edge of the circle.
|
||||||
var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
|
var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
|
||||||
@@ -233,7 +233,7 @@ public class CutOffGeometryTests
|
|||||||
plate.Parts.Add(part);
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
var settings = new CutOffSettings { PartClearance = 5.0 };
|
var settings = new CutOffSettings { PartClearance = 5.0 };
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
var cache = Plate.BuildPerimeterCache(plate);
|
||||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||||
cutoff.Regenerate(plate, settings, cache);
|
cutoff.Regenerate(plate, settings, cache);
|
||||||
|
|
||||||
@@ -243,32 +243,8 @@ public class CutOffGeometryTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildPerimeterCache_ReturnsOneEntryPerPart()
|
public void BuildPerimeterCache_OpenContourGetsConvexHull()
|
||||||
{
|
{
|
||||||
var plate = new Plate(100, 100);
|
|
||||||
plate.Parts.Add(new Part(new Drawing("a", MakeSquare(10))));
|
|
||||||
plate.Parts.Add(new Part(new Drawing("b", MakeCircle(5))));
|
|
||||||
plate.Parts.Add(new Part(new Drawing("c", MakeDiamond(8))));
|
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
|
||||||
Assert.Equal(3, cache.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildPerimeterCache_SkipsCutOffParts()
|
|
||||||
{
|
|
||||||
var plate = new Plate(100, 100);
|
|
||||||
plate.Parts.Add(new Part(new Drawing("real", MakeSquare(10))));
|
|
||||||
plate.Parts.Add(new Part(new Drawing("cutoff", new Program()) { IsCutOff = true }));
|
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
|
||||||
Assert.Single(cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void BuildPerimeterCache_NullForOpenContour()
|
|
||||||
{
|
|
||||||
// Open contour: line that doesn't close
|
|
||||||
var pgm = new Program();
|
var pgm = new Program();
|
||||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||||
@@ -277,9 +253,12 @@ public class CutOffGeometryTests
|
|||||||
var plate = new Plate(100, 100);
|
var plate = new Plate(100, 100);
|
||||||
plate.Parts.Add(new Part(new Drawing("open", pgm)));
|
plate.Parts.Add(new Part(new Drawing("open", pgm)));
|
||||||
|
|
||||||
var cache = CutOff.BuildPerimeterCache(plate);
|
var cache = Plate.BuildPerimeterCache(plate);
|
||||||
Assert.Single(cache);
|
Assert.Single(cache);
|
||||||
Assert.Null(cache[0]);
|
|
||||||
|
var perimeter = cache[plate.Parts[0]];
|
||||||
|
Assert.NotNull(perimeter);
|
||||||
|
Assert.IsType<Polygon>(perimeter);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Controls;
|
using OpenNest.Controls;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
@@ -13,11 +14,18 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
private CutOff previewCutOff;
|
private CutOff previewCutOff;
|
||||||
private CutOffSettings settings;
|
private CutOffSettings settings;
|
||||||
|
private CutOffAxis lockedAxis = CutOffAxis.Vertical;
|
||||||
|
private Dictionary<Part, Entity> perimeterCache;
|
||||||
|
private readonly Timer debounceTimer;
|
||||||
|
private bool regeneratePending;
|
||||||
|
|
||||||
public ActionCutOff(PlateView plateView)
|
public ActionCutOff(PlateView plateView)
|
||||||
: base(plateView)
|
: base(plateView)
|
||||||
{
|
{
|
||||||
settings = plateView.CutOffSettings;
|
settings = plateView.CutOffSettings;
|
||||||
|
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||||
|
debounceTimer = new Timer { Interval = 16 };
|
||||||
|
debounceTimer.Tick += OnDebounce;
|
||||||
|
|
||||||
plateView.MouseMove += OnMouseMove;
|
plateView.MouseMove += OnMouseMove;
|
||||||
plateView.MouseDown += OnMouseDown;
|
plateView.MouseDown += OnMouseDown;
|
||||||
@@ -27,6 +35,8 @@ namespace OpenNest.Actions
|
|||||||
|
|
||||||
public override void ConnectEvents()
|
public override void ConnectEvents()
|
||||||
{
|
{
|
||||||
|
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||||
|
|
||||||
plateView.MouseMove += OnMouseMove;
|
plateView.MouseMove += OnMouseMove;
|
||||||
plateView.MouseDown += OnMouseDown;
|
plateView.MouseDown += OnMouseDown;
|
||||||
plateView.KeyDown += OnKeyDown;
|
plateView.KeyDown += OnKeyDown;
|
||||||
@@ -35,12 +45,14 @@ namespace OpenNest.Actions
|
|||||||
|
|
||||||
public override void DisconnectEvents()
|
public override void DisconnectEvents()
|
||||||
{
|
{
|
||||||
|
debounceTimer.Stop();
|
||||||
plateView.MouseMove -= OnMouseMove;
|
plateView.MouseMove -= OnMouseMove;
|
||||||
plateView.MouseDown -= OnMouseDown;
|
plateView.MouseDown -= OnMouseDown;
|
||||||
plateView.KeyDown -= OnKeyDown;
|
plateView.KeyDown -= OnKeyDown;
|
||||||
plateView.Paint -= OnPaint;
|
plateView.Paint -= OnPaint;
|
||||||
|
|
||||||
previewCutOff = null;
|
previewCutOff = null;
|
||||||
|
perimeterCache = null;
|
||||||
plateView.Invalidate();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,11 +62,21 @@ namespace OpenNest.Actions
|
|||||||
|
|
||||||
private void OnMouseMove(object sender, MouseEventArgs e)
|
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
var pt = plateView.CurrentPoint;
|
regeneratePending = true;
|
||||||
var axis = DetectAxis(pt);
|
debounceTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
previewCutOff = new CutOff(pt, axis);
|
private void OnDebounce(object sender, System.EventArgs e)
|
||||||
previewCutOff.Regenerate(plateView.Plate, settings);
|
{
|
||||||
|
debounceTimer.Stop();
|
||||||
|
|
||||||
|
if (!regeneratePending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
regeneratePending = false;
|
||||||
|
var pt = plateView.CurrentPoint;
|
||||||
|
previewCutOff = new CutOff(pt, lockedAxis);
|
||||||
|
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
||||||
plateView.Invalidate();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,19 +86,34 @@ namespace OpenNest.Actions
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var pt = plateView.CurrentPoint;
|
var pt = plateView.CurrentPoint;
|
||||||
var axis = DetectAxis(pt);
|
var cutoff = new CutOff(pt, lockedAxis);
|
||||||
var cutoff = new CutOff(pt, axis);
|
|
||||||
|
|
||||||
plateView.Plate.CutOffs.Add(cutoff);
|
plateView.Plate.CutOffs.Add(cutoff);
|
||||||
plateView.Plate.RegenerateCutOffs(settings);
|
plateView.Plate.RegenerateCutOffs(settings);
|
||||||
|
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||||
plateView.Invalidate();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.KeyCode == Keys.Escape)
|
if (e.KeyCode == Keys.Space)
|
||||||
|
{
|
||||||
|
lockedAxis = lockedAxis == CutOffAxis.Vertical
|
||||||
|
? CutOffAxis.Horizontal
|
||||||
|
: CutOffAxis.Vertical;
|
||||||
|
|
||||||
|
if (previewCutOff != null)
|
||||||
|
{
|
||||||
|
previewCutOff = new CutOff(plateView.CurrentPoint, lockedAxis);
|
||||||
|
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
||||||
|
plateView.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.KeyCode == Keys.Escape)
|
||||||
|
{
|
||||||
plateView.SetAction(typeof(ActionSelect));
|
plateView.SetAction(typeof(ActionSelect));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnPaint(object sender, PaintEventArgs e)
|
private void OnPaint(object sender, PaintEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -103,20 +140,5 @@ namespace OpenNest.Actions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CutOffAxis DetectAxis(Vector pt)
|
|
||||||
{
|
|
||||||
var bounds = plateView.Plate.BoundingBox(includeParts: false);
|
|
||||||
var distToLeftRight = System.Math.Min(
|
|
||||||
System.Math.Abs(pt.X - bounds.X),
|
|
||||||
System.Math.Abs(pt.X - (bounds.X + bounds.Width)));
|
|
||||||
var distToTopBottom = System.Math.Min(
|
|
||||||
System.Math.Abs(pt.Y - bounds.Y),
|
|
||||||
System.Math.Abs(pt.Y - (bounds.Y + bounds.Length)));
|
|
||||||
|
|
||||||
return distToTopBottom < distToLeftRight
|
|
||||||
? CutOffAxis.Horizontal
|
|
||||||
: CutOffAxis.Vertical;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user