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:
@@ -26,9 +26,9 @@ namespace OpenNest
|
||||
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);
|
||||
Drawing.Program = program;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ namespace OpenNest
|
||||
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);
|
||||
|
||||
@@ -66,26 +66,10 @@ namespace OpenNest
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
var bb = part.BoundingBox;
|
||||
double partStart, partEnd, partMin, partMax;
|
||||
|
||||
if (Axis == CutOffAxis.Vertical)
|
||||
{
|
||||
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));
|
||||
Entity perimeter = null;
|
||||
cache?.TryGetValue(part, out perimeter);
|
||||
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
|
||||
exclusions.AddRange(partExclusions);
|
||||
}
|
||||
|
||||
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
|
||||
@@ -120,6 +104,75 @@ namespace OpenNest
|
||||
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)
|
||||
{
|
||||
var program = new Program();
|
||||
|
||||
@@ -249,9 +249,8 @@ namespace OpenNest.Geometry
|
||||
|
||||
foreach (var geo in shape.Entities)
|
||||
{
|
||||
List<Vector> pts3;
|
||||
geo.Intersects(line, out pts3);
|
||||
pts.AddRange(pts3);
|
||||
if (geo.Intersects(line, out var pts3))
|
||||
pts.AddRange(pts3);
|
||||
}
|
||||
|
||||
return pts.Count > 0;
|
||||
|
||||
@@ -86,7 +86,7 @@ public class CutOffGeometryTests
|
||||
part.Location = new Vector(0, 0);
|
||||
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.
|
||||
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
|
||||
@@ -125,7 +125,7 @@ public class CutOffGeometryTests
|
||||
part.Location = new Vector(0, 0);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cache = CutOff.BuildPerimeterCache(plate);
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||
|
||||
@@ -157,7 +157,7 @@ public class CutOffGeometryTests
|
||||
part.Location = new Vector(10, 10);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var cache = CutOff.BuildPerimeterCache(plate);
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
||||
cutoff.Regenerate(plate, ZeroClearance, cache);
|
||||
|
||||
@@ -202,7 +202,7 @@ public class CutOffGeometryTests
|
||||
part.Location = new Vector(0, 0);
|
||||
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.
|
||||
var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
|
||||
@@ -233,7 +233,7 @@ public class CutOffGeometryTests
|
||||
plate.Parts.Add(part);
|
||||
|
||||
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);
|
||||
cutoff.Regenerate(plate, settings, cache);
|
||||
|
||||
@@ -243,32 +243,8 @@ public class CutOffGeometryTests
|
||||
}
|
||||
|
||||
[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();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
@@ -277,9 +253,12 @@ public class CutOffGeometryTests
|
||||
var plate = new Plate(100, 100);
|
||||
plate.Parts.Add(new Part(new Drawing("open", pgm)));
|
||||
|
||||
var cache = CutOff.BuildPerimeterCache(plate);
|
||||
var cache = Plate.BuildPerimeterCache(plate);
|
||||
Assert.Single(cache);
|
||||
Assert.Null(cache[0]);
|
||||
|
||||
var perimeter = cache[plate.Parts[0]];
|
||||
Assert.NotNull(perimeter);
|
||||
Assert.IsType<Polygon>(perimeter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
@@ -13,11 +14,18 @@ namespace OpenNest.Actions
|
||||
{
|
||||
private CutOff previewCutOff;
|
||||
private CutOffSettings settings;
|
||||
private CutOffAxis lockedAxis = CutOffAxis.Vertical;
|
||||
private Dictionary<Part, Entity> perimeterCache;
|
||||
private readonly Timer debounceTimer;
|
||||
private bool regeneratePending;
|
||||
|
||||
public ActionCutOff(PlateView plateView)
|
||||
: base(plateView)
|
||||
{
|
||||
settings = plateView.CutOffSettings;
|
||||
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||
debounceTimer = new Timer { Interval = 16 };
|
||||
debounceTimer.Tick += OnDebounce;
|
||||
|
||||
plateView.MouseMove += OnMouseMove;
|
||||
plateView.MouseDown += OnMouseDown;
|
||||
@@ -27,6 +35,8 @@ namespace OpenNest.Actions
|
||||
|
||||
public override void ConnectEvents()
|
||||
{
|
||||
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||
|
||||
plateView.MouseMove += OnMouseMove;
|
||||
plateView.MouseDown += OnMouseDown;
|
||||
plateView.KeyDown += OnKeyDown;
|
||||
@@ -35,12 +45,14 @@ namespace OpenNest.Actions
|
||||
|
||||
public override void DisconnectEvents()
|
||||
{
|
||||
debounceTimer.Stop();
|
||||
plateView.MouseMove -= OnMouseMove;
|
||||
plateView.MouseDown -= OnMouseDown;
|
||||
plateView.KeyDown -= OnKeyDown;
|
||||
plateView.Paint -= OnPaint;
|
||||
|
||||
previewCutOff = null;
|
||||
perimeterCache = null;
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
@@ -50,11 +62,21 @@ namespace OpenNest.Actions
|
||||
|
||||
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var pt = plateView.CurrentPoint;
|
||||
var axis = DetectAxis(pt);
|
||||
regeneratePending = true;
|
||||
debounceTimer.Start();
|
||||
}
|
||||
|
||||
previewCutOff = new CutOff(pt, axis);
|
||||
previewCutOff.Regenerate(plateView.Plate, settings);
|
||||
private void OnDebounce(object sender, System.EventArgs e)
|
||||
{
|
||||
debounceTimer.Stop();
|
||||
|
||||
if (!regeneratePending)
|
||||
return;
|
||||
|
||||
regeneratePending = false;
|
||||
var pt = plateView.CurrentPoint;
|
||||
previewCutOff = new CutOff(pt, lockedAxis);
|
||||
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
@@ -64,18 +86,33 @@ namespace OpenNest.Actions
|
||||
return;
|
||||
|
||||
var pt = plateView.CurrentPoint;
|
||||
var axis = DetectAxis(pt);
|
||||
var cutoff = new CutOff(pt, axis);
|
||||
var cutoff = new CutOff(pt, lockedAxis);
|
||||
|
||||
plateView.Plate.CutOffs.Add(cutoff);
|
||||
plateView.Plate.RegenerateCutOffs(settings);
|
||||
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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