From 0573cb2f6d74938646e47e353a5613fe29f37fc8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 7 Mar 2026 10:51:21 -0500 Subject: [PATCH] feat: fill open area and optimize pattern rotation via convex hull ActionClone.Fill() now computes the largest open rectangle from the cursor position (trying both vertical and horizontal) and passes it to the engine, so fills no longer overlap existing parts. Pattern fills try all convex hull edge angles to find the rotation that maximizes part count. Co-Authored-By: Claude Opus 4.6 --- OpenNest.Engine/NestEngine.cs | 140 +++++++++++++++++++++++++++++--- OpenNest/Actions/ActionClone.cs | 40 ++++++++- 2 files changed, 166 insertions(+), 14 deletions(-) diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 9d2e9c9..f4dead9 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -62,22 +62,28 @@ namespace OpenNest var workArea = Plate.WorkArea(); var engine = new FillLinear(workArea, Plate.PartSpacing); + var angles = FindHullEdgeAngles(groupParts); + var best = FillPattern(engine, groupParts, angles); - // Build a pattern from the group of parts. - var pattern = new Pattern(); - foreach (var part in groupParts) - { - var clone = (Part)part.Clone(); - clone.UpdateBounds(); - pattern.Parts.Add(clone); - } - pattern.UpdateBounds(); + if (best == null || best.Count == 0) + return false; + + Plate.Parts.AddRange(best); + return true; + } + + public bool Fill(NestItem item, Box workArea) + { + var bestRotation = FindBestRotation(item); + + var engine = new FillLinear(workArea, Plate.PartSpacing); - // Try 4 configurations: 2 axes x 2 orientations (horizontal/vertical). var configs = new[] { - engine.Fill(pattern, NestDirection.Horizontal), - engine.Fill(pattern, NestDirection.Vertical) + engine.Fill(item.Drawing, bestRotation, NestDirection.Horizontal), + engine.Fill(item.Drawing, bestRotation, NestDirection.Vertical), + engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Horizontal), + engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Vertical) }; List best = null; @@ -88,6 +94,25 @@ namespace OpenNest best = config; } + if (best == null || best.Count == 0) + return false; + + if (item.Quantity > 0 && best.Count > item.Quantity) + best = best.Take(item.Quantity).ToList(); + + Plate.Parts.AddRange(best); + return true; + } + + public bool Fill(List groupParts, Box workArea) + { + if (groupParts == null || groupParts.Count == 0) + return false; + + var engine = new FillLinear(workArea, Plate.PartSpacing); + var angles = FindHullEdgeAngles(groupParts); + var best = FillPattern(engine, groupParts, angles); + if (best == null || best.Count == 0) return false; @@ -185,6 +210,97 @@ namespace OpenNest return parts.Count > 0; } + private List FindHullEdgeAngles(List parts) + { + var points = new List(); + + foreach (var part in parts) + { + var entities = ConvertProgram.ToGeometry(part.Program) + .Where(e => e.Layer != SpecialLayers.Rapid); + + var shapes = Helper.GetShapes(entities); + + foreach (var shape in shapes) + { + var polygon = shape.ToPolygonWithTolerance(0.1); + + foreach (var vertex in polygon.Vertices) + points.Add(vertex + part.Location); + } + } + + if (points.Count < 3) + return new List { 0 }; + + var hull = ConvexHull.Compute(points); + var vertices = hull.Vertices; + var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count; + + var angles = new List { 0 }; + + for (var i = 0; i < n; i++) + { + var next = (i + 1) % n; + var dx = vertices[next].X - vertices[i].X; + var dy = vertices[next].Y - vertices[i].Y; + + if (dx * dx + dy * dy < Tolerance.Epsilon) + continue; + + var angle = -System.Math.Atan2(dy, dx); + + if (!angles.Any(a => a.IsEqualTo(angle))) + angles.Add(angle); + } + + return angles; + } + + private Pattern BuildRotatedPattern(List groupParts, double angle) + { + var pattern = new Pattern(); + var center = ((IEnumerable)groupParts).GetBoundingBox().Center; + + foreach (var part in groupParts) + { + var clone = (Part)part.Clone(); + clone.UpdateBounds(); + + if (!angle.IsEqualTo(0)) + clone.Rotate(angle, center); + + pattern.Parts.Add(clone); + } + + pattern.UpdateBounds(); + return pattern; + } + + private List FillPattern(FillLinear engine, List groupParts, List angles) + { + List best = null; + + foreach (var angle in angles) + { + var pattern = BuildRotatedPattern(groupParts, angle); + + if (pattern.Parts.Count == 0) + continue; + + var h = engine.Fill(pattern, NestDirection.Horizontal); + var v = engine.Fill(pattern, NestDirection.Vertical); + + if (best == null || h.Count > best.Count) + best = h; + + if (best == null || v.Count > best.Count) + best = v; + } + + return best; + } + private double FindBestRotation(NestItem item) { var entities = ConvertProgram.ToGeometry(item.Drawing.Program) diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index 534b013..5879048 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -106,6 +106,17 @@ namespace OpenNest.Actions plateView.SelectedParts.AddRange(parts); } + public override void ConnectEvents() + { + plateView.KeyDown += plateView_KeyDown; + plateView.MouseMove += plateView_MouseMove; + plateView.MouseDown += plateView_MouseDown; + plateView.Paint += plateView_Paint; + + plateView.SelectedParts.Clear(); + plateView.SelectedParts.AddRange(parts); + } + public override void DisconnectEvents() { plateView.KeyDown -= plateView_KeyDown; @@ -159,9 +170,34 @@ namespace OpenNest.Actions private void Fill() { - var engine = new NestEngine(plateView.Plate); + var plate = plateView.Plate; + var engine = new NestEngine(plate); var groupParts = parts.Select(p => p.BasePart).ToList(); - engine.Fill(groupParts); + + var bounds = plate.WorkArea(); + + if (plate.Parts.Count == 0) + { + engine.Fill(groupParts); + return; + } + + var boxes = new List(); + foreach (var part in plate.Parts) + boxes.Add(part.BoundingBox.Offset(plate.PartSpacing)); + + var pt = plateView.CurrentPoint; + var vertical = Helper.GetLargestBoxVertically(pt, bounds, boxes); + var horizontal = Helper.GetLargestBoxHorizontally(pt, bounds, boxes); + + var bestArea = vertical; + if (horizontal.Area() > vertical.Area()) + bestArea = horizontal; + + if (bestArea == Box.Empty) + return; + + engine.Fill(groupParts, bestArea); } } }