diff --git a/OpenNest.Core/CutOff.cs b/OpenNest.Core/CutOff.cs index f900711..ee3344b 100644 --- a/OpenNest.Core/CutOff.cs +++ b/OpenNest.Core/CutOff.cs @@ -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 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 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(); diff --git a/OpenNest.Core/Geometry/Intersect.cs b/OpenNest.Core/Geometry/Intersect.cs index bdbcb1c..39acd7a 100644 --- a/OpenNest.Core/Geometry/Intersect.cs +++ b/OpenNest.Core/Geometry/Intersect.cs @@ -249,9 +249,8 @@ namespace OpenNest.Geometry foreach (var geo in shape.Entities) { - List pts3; - geo.Intersects(line, out pts3); - pts.AddRange(pts3); + if (geo.Intersects(line, out var pts3)) + pts.AddRange(pts3); } return pts.Count > 0; diff --git a/OpenNest.Tests/CutOffGeometryTests.cs b/OpenNest.Tests/CutOffGeometryTests.cs index a16a1e9..3e20d3a 100644 --- a/OpenNest.Tests/CutOffGeometryTests.cs +++ b/OpenNest.Tests/CutOffGeometryTests.cs @@ -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(perimeter); } [Fact] diff --git a/OpenNest/Actions/ActionCutOff.cs b/OpenNest/Actions/ActionCutOff.cs index 0365b26..ac8b91a 100644 --- a/OpenNest/Actions/ActionCutOff.cs +++ b/OpenNest/Actions/ActionCutOff.cs @@ -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 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; - } } }