25 Commits
Author SHA1 Message Date
ajandClaude Opus 4.6 036f723876 fix: update PlateView fill path and sync stats with preview
- Route best-result updates to progress form preview in
  PlateView.FillWithProgress (Ctrl+F path) — was still using
  the old SetStationaryParts approach
- Only update results stats (parts, density, area) when
  IsOverallBest so they match the preview display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:56:41 -04:00
ajandClaude Opus 4.6 21a5d3b026 feat: route best-result updates to progress form preview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:59:51 -04:00
ajandClaude Opus 4.6 0607c6c7c5 feat: add UpdatePreview method and PreviewPlate property
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:58:47 -04:00
ajandClaude Opus 4.6 c8cfeb3c6b feat: add SplitContainer and PlateView to NestProgressForm layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:57:12 -04:00
ajandClaude Opus 4.6 d4f424f274 refactor: simplify FillExtents with PartPair record and FillLinear delegation
Replace verbose value tuple with named PartPair record struct, extract
AnchorToWorkArea/PairBbox helpers to eliminate duplication, and delegate
RepeatColumns to FillLinear.Fill which already handles geometry-aware
column tiling with overlap fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:16:16 -04:00
ajandClaude Opus 4.6 028b1fabfc fix: move bend line action links above list for more vertical space
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:32:53 -04:00
aj a7c2fcffe6 test: add edge case tests for Collision; update CLAUDE.md 2026-03-29 09:43:31 -04:00
aj b834813889 refactor: delegate Part.Intersects to Collision.Check 2026-03-29 09:41:40 -04:00
ajandClaude Sonnet 4.6 4fa6100722 test: add hole subtraction and batch collision tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 09:40:28 -04:00
ajandClaude Opus 4.6 8f2fbee02c feat: add Collision static class with Sutherland-Hodgman clipping and tests
Polygon-polygon collision detection using convex decomposition (ear-clipping
triangulation) followed by Sutherland-Hodgman clipping on each triangle pair.
Handles overlapping, non-overlapping, edge-touching, containment, and concave
polygons. Includes hole subtraction support for future use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:35:41 -04:00
ajandClaude Opus 4.6 230a11d32e feat: add CollisionResult data class for polygon collision detection
Immutable result type that holds overlap flag, overlap regions (as polygons),
intersection points, and computed overlap area.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:32:51 -04:00
ajandClaude Opus 4.6 953429dae9 fix: add overlap safety check and diagnostics to FillGrid Step 2
FillGrid had no overlap check after perpendicular tiling of the row
pattern (Step 2), unlike Step 1 which had one. When geometry-aware
FindPatternCopyDistance underestimated row spacing, overlapping parts
were returned unchecked.

Changes:
- Make FillLinear.HasOverlappingParts shape-aware (bbox pre-filter +
  Part.Intersects) instead of bbox-only, preventing false positives on
  interlocking pairs while catching real overlaps
- Add missing overlap safety check after Step 2 perpendicular tiling
  with bbox fallback
- Add diagnostic Debug.WriteLine logging when overlap fallback triggers,
  including engine label, step, direction, work area, spacing, pattern
  details, and overlapping part locations/rotations for reproduction
- Add FillLinear.Label property set at all callsites for log traceability
- Refactor LinearFillStrategy and ExtentsFillStrategy to use shared
  FillHelpers.BestOverAngles helper for angle-sweep logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:08:38 -04:00
ajandClaude Opus 4.6 1c2b569ff4 fix: eliminate endpoint gaps in EllipseConverter arc output
EllipseConverter computed arc radius from start point only, causing
~0.0009 unit gaps between consecutive arcs. Use circumcircle of
(start, mid, end) points so both endpoints lie exactly on the arc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:37:01 -04:00
ajandClaude Opus 4.6 048b10a1e9 refactor: deduplicate BestFitHorizontal and BestFitVertical
Extract shared BestFitAxis helper parameterized by orientation,
eliminating 23-line duplicate in rectangle packing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:23:08 -04:00
ajandClaude Opus 4.6 3022982f6d refactor: consolidate HasOverlappingParts into FillHelpers
StripeFiller and FillExtents had identical 24-line overlap detection
methods; move to FillHelpers and delegate from both callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:22:00 -04:00
ajandClaude Opus 4.6 cc85493a0c refactor: deduplicate EvenlyDistribute horizontal and vertical
Extract shared EvenlyDistribute helper parameterized by axis,
eliminating 27-line duplicate between the two methods.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:20:01 -04:00
ajandClaude Opus 4.6 3da287cdc0 refactor: extract generic MergePass from GeometryOptimizer
The arc and line Optimize methods had identical merge-loop structure;
extract a generic MergePass helper with type-specific delegates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:19:04 -04:00
ajandClaude Opus 4.6 d1a701a7f7 refactor: move shared GetRectangle to Action base class
Both ActionSelect and ActionZoomWindow had identical 29-line
GetRectangle methods; consolidate into the common base class.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:16:00 -04:00
ajandClaude Opus 4.6 17f786c9e8 refactor: delegate Program.Rotate(angle) to Rotate(angle, origin)
The parameterless rotation is equivalent to rotating around (0,0),
so delegate to the origin overload to eliminate 30-line duplicate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:14:51 -04:00
ajandClaude Opus 4.6 b7a8e2662c refactor: deduplicate FillHorizontal and FillVertical in FillEndOdd
Extract shared FillAxis helper parameterized by orientation,
eliminating 34-line duplicate between horizontal and vertical fills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:13:00 -04:00
ajandClaude Opus 4.6 912a47c5e8 refactor: extract shared ArcFit utilities from SplineConverter and GeometrySimplifier
Move identical FitWithStartTangent and MaxRadialDeviation methods
to a shared ArcFit class, eliminating 40-line duplicate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:11:47 -04:00
ajandClaude Opus 4.6 a85213a524 refactor: deduplicate SortColumnsByHeight and SortRowsByWidth
Extract shared SortStrips helper parameterized by axis selectors,
eliminating 61-line near-duplicate between column and row sorting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:05:54 -04:00
ajandClaude Opus 4.6 fb696aaf58 refactor: remove dead code and deduplicate SpatialQuery
Remove 4 unused ClosestDistance methods and extract shared
FindVerticalLimits/FindHorizontalLimits helpers from the
GetLargestBox methods, eliminating 6 duplicate code groups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:03:27 -04:00
ajandClaude Opus 4.6 d854a1f5d2 fix: use arc joins at convex corners in offset geometry
Convex corners were being miter-joined (lines extended to a point)
because IntersectsUnbounded always finds an intersection for non-parallel
lines. Now checks the cross product of original line directions to detect
convex corners and inserts an arc instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:53:20 -04:00
ajandClaude Opus 4.6 abc707f1d9 fix: allow line-on-line contact and remove extra spacing gap
- Part.Intersects: filter intersection points at a vertex of either
  shape (was both), so edge-touching parts are not flagged as overlapping
- NestEngineBase.HasOverlaps: use epsilon-based bounding box pre-filter
  consistent with FillExtents and Plate.HasOverlappingParts
- PartGeometry.GetOffsetPartLines: remove extra chordTolerance added to
  spacing offset — was causing 0.002" gap beyond the intended part spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:36:35 -04:00
38 changed files with 1359 additions and 1076 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ Domain model, geometry, and CNC primitives organized into namespaces:
- **Root** (`namespace OpenNest`): Domain model — `Nest``Plate[]``Part[]``Drawing``Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`). Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning.
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, and `RotatingCalipers`.
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
+17 -42
View File
@@ -125,61 +125,36 @@ namespace OpenNest
parts.ForEach(part => Bottom(fixedPart, part));
}
public static void EvenlyDistributeHorizontally(List<Part> parts)
public static void EvenlyDistributeHorizontally(List<Part> parts) =>
EvenlyDistribute(parts, horizontal: true);
public static void EvenlyDistributeVertically(List<Part> parts) =>
EvenlyDistribute(parts, horizontal: false);
private static void EvenlyDistribute(List<Part> parts, bool horizontal)
{
if (parts.Count < 3)
return;
var list = new List<Part>(parts);
list.Sort((p1, p2) => p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X));
list.Sort((p1, p2) => horizontal
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y));
var lastIndex = list.Count - 1;
var first = list[0];
var last = list[lastIndex];
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y;
var start = first.BoundingBox.Center.X;
var end = last.BoundingBox.Center.X;
var diff = end - start;
var spacing = (end - start) / lastIndex;
var spacing = diff / lastIndex;
for (int i = 1; i < lastIndex; ++i)
for (var i = 1; i < lastIndex; ++i)
{
var part = list[i];
var newX = start + i * spacing;
var curX = part.BoundingBox.Center.X;
var cur = horizontal ? part.BoundingBox.Center.X : part.BoundingBox.Center.Y;
var delta = start + i * spacing - cur;
part.Offset(newX - curX, 0);
}
}
public static void EvenlyDistributeVertically(List<Part> parts)
{
if (parts.Count < 3)
return;
var list = new List<Part>(parts);
list.Sort((p1, p2) => p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y));
var lastIndex = list.Count - 1;
var first = list[0];
var last = list[lastIndex];
var start = first.BoundingBox.Center.Y;
var end = last.BoundingBox.Center.Y;
var diff = end - start;
var spacing = diff / lastIndex;
for (int i = 1; i < lastIndex; ++i)
{
var part = list[i];
var newX = start + i * spacing;
var curX = part.BoundingBox.Center.Y;
part.Offset(0, newX - curX);
part.Offset(horizontal ? delta : 0, horizontal ? 0 : delta);
}
}
}
+1 -31
View File
@@ -51,37 +51,7 @@ namespace OpenNest.CNC
mode = Mode.Absolute;
}
public virtual void Rotate(double angle)
{
var mode = Mode;
SetModeAbs();
for (int i = 0; i < Codes.Count; ++i)
{
var code = Codes[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
subpgm.Program.Rotate(angle);
}
if (code is Motion == false)
continue;
var code2 = (Motion)code;
code2.Rotate(angle);
}
if (mode == Mode.Incremental)
SetModeInc();
Rotation = Angle.NormalizeRad(Rotation + angle);
}
public virtual void Rotate(double angle) => Rotate(angle, new Vector(0, 0));
public override string ToString()
{
+76
View File
@@ -0,0 +1,76 @@
using System.Collections.Generic;
namespace OpenNest.Geometry
{
/// <summary>
/// Shared arc-fitting utilities used by SplineConverter and GeometrySimplifier.
/// </summary>
internal static class ArcFit
{
/// <summary>
/// Fits a circular arc constrained to be tangent to the given direction at the
/// first point. The center lies at the intersection of the normal at P1 (perpendicular
/// to the tangent) and the perpendicular bisector of the chord P1->Pn, guaranteeing
/// the arc passes through both endpoints and departs P1 in the given direction.
/// </summary>
internal static (Vector center, double radius, double deviation) FitWithStartTangent(
List<Vector> points, Vector tangent)
{
if (points.Count < 3)
return (Vector.Invalid, 0, double.MaxValue);
var p1 = points[0];
var pn = points[^1];
var mx = (p1.X + pn.X) / 2;
var my = (p1.Y + pn.Y) / 2;
var dx = pn.X - p1.X;
var dy = pn.Y - p1.Y;
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
if (chordLen < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var bx = -dy / chordLen;
var by = dx / chordLen;
var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y);
if (tLen < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var nx = -tangent.Y / tLen;
var ny = tangent.X / tLen;
var det = nx * by - ny * bx;
if (System.Math.Abs(det) < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var s = ((mx - p1.X) * by - (my - p1.Y) * bx) / det;
var cx = p1.X + s * nx;
var cy = p1.Y + s * ny;
var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
if (radius < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius));
}
/// <summary>
/// Computes the maximum radial deviation of interior points from a circle.
/// </summary>
internal static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius)
{
var maxDev = 0.0;
for (var i = 1; i < points.Count - 1; i++)
{
var px = points[i].X - cx;
var py = points[i].Y - cy;
var dist = System.Math.Sqrt(px * px + py * py);
var dev = System.Math.Abs(dist - radius);
if (dev > maxDev) maxDev = dev;
}
return maxDev;
}
}
}
+330
View File
@@ -0,0 +1,330 @@
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Geometry
{
public static class Collision
{
public static CollisionResult Check(Polygon a, Polygon b,
List<Polygon> holesA = null, List<Polygon> holesB = null)
{
// Step 1: Bounding box pre-filter
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
return CollisionResult.None;
// Step 2: Quick intersection test for crossing points
var intersectionPoints = FindCrossingPoints(a, b);
// Step 3: Convex decomposition
var trisA = TriangulateWithBounds(a);
var trisB = TriangulateWithBounds(b);
// Step 4: Clip all triangle pairs
var regions = new List<Polygon>();
foreach (var triA in trisA)
{
foreach (var triB in trisB)
{
if (!BoundingBoxesOverlap(triA.BoundingBox, triB.BoundingBox))
continue;
var clipped = ClipConvex(triA, triB);
if (clipped != null)
regions.Add(clipped);
}
}
// Step 5: Hole subtraction
if (regions.Count > 0)
regions = SubtractHoles(regions, holesA, holesB);
if (regions.Count == 0)
return new CollisionResult(false, regions, intersectionPoints);
// Step 6: Build result
return new CollisionResult(true, regions, intersectionPoints);
}
public static bool HasOverlap(Polygon a, Polygon b,
List<Polygon> holesA = null, List<Polygon> holesB = null)
{
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
return false;
// Full check is needed: crossing points alone miss containment cases
// (one polygon entirely inside another has zero edge crossings).
return Check(a, b, holesA, holesB).Overlaps;
}
public static List<CollisionResult> CheckAll(List<Polygon> polygons,
List<List<Polygon>> holes = null)
{
var results = new List<CollisionResult>();
for (var i = 0; i < polygons.Count; i++)
{
for (var j = i + 1; j < polygons.Count; j++)
{
var holesA = holes != null && i < holes.Count ? holes[i] : null;
var holesB = holes != null && j < holes.Count ? holes[j] : null;
var result = Check(polygons[i], polygons[j], holesA, holesB);
if (result.Overlaps)
results.Add(result);
}
}
return results;
}
public static bool HasAnyOverlap(List<Polygon> polygons,
List<List<Polygon>> holes = null)
{
for (var i = 0; i < polygons.Count; i++)
{
for (var j = i + 1; j < polygons.Count; j++)
{
var holesA = holes != null && i < holes.Count ? holes[i] : null;
var holesB = holes != null && j < holes.Count ? holes[j] : null;
if (HasOverlap(polygons[i], polygons[j], holesA, holesB))
return true;
}
}
return false;
}
private static bool BoundingBoxesOverlap(Box a, Box b)
{
var overlapX = System.Math.Min(a.Right, b.Right)
- System.Math.Max(a.Left, b.Left);
var overlapY = System.Math.Min(a.Top, b.Top)
- System.Math.Max(a.Bottom, b.Bottom);
return overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon;
}
private static List<Vector> FindCrossingPoints(Polygon a, Polygon b)
{
if (!Intersect.Intersects(a, b, out var rawPts))
return new List<Vector>();
// Filter boundary contacts (vertex touches)
var vertsA = CollectVertices(a);
var vertsB = CollectVertices(b);
var filtered = new List<Vector>();
foreach (var pt in rawPts)
{
if (IsNearAnyVertex(pt, vertsA) || IsNearAnyVertex(pt, vertsB))
continue;
filtered.Add(pt);
}
return filtered;
}
private static List<Vector> CollectVertices(Polygon polygon)
{
var verts = new List<Vector>(polygon.Vertices.Count);
foreach (var v in polygon.Vertices)
verts.Add(v);
return verts;
}
private static bool IsNearAnyVertex(Vector pt, List<Vector> vertices)
{
foreach (var v in vertices)
{
if (pt.X.IsEqualTo(v.X) && pt.Y.IsEqualTo(v.Y))
return true;
}
return false;
}
/// <summary>
/// Triangulates a polygon and ensures each triangle has its bounding box updated.
/// </summary>
private static List<Polygon> TriangulateWithBounds(Polygon polygon)
{
var tris = ConvexDecomposition.Triangulate(polygon);
foreach (var tri in tris)
tri.UpdateBounds();
return tris;
}
/// <summary>
/// Sutherland-Hodgman polygon clipping. Clips subject against each edge
/// of clip. Both must be convex. Returns null if no overlap.
/// </summary>
private static Polygon ClipConvex(Polygon subject, Polygon clip)
{
var output = new List<Vector>(subject.Vertices);
// Remove closing vertex if present
if (output.Count > 1 && output[0].X == output[output.Count - 1].X
&& output[0].Y == output[output.Count - 1].Y)
output.RemoveAt(output.Count - 1);
var clipVerts = new List<Vector>(clip.Vertices);
if (clipVerts.Count > 1 && clipVerts[0].X == clipVerts[clipVerts.Count - 1].X
&& clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y)
clipVerts.RemoveAt(clipVerts.Count - 1);
for (var i = 0; i < clipVerts.Count; i++)
{
if (output.Count == 0)
return null;
var edgeStart = clipVerts[i];
var edgeEnd = clipVerts[(i + 1) % clipVerts.Count];
var input = output;
output = new List<Vector>();
for (var j = 0; j < input.Count; j++)
{
var current = input[j];
var next = input[(j + 1) % input.Count];
var currentInside = Cross(edgeStart, edgeEnd, current) >= -Tolerance.Epsilon;
var nextInside = Cross(edgeStart, edgeEnd, next) >= -Tolerance.Epsilon;
if (currentInside)
{
output.Add(current);
if (!nextInside)
{
var ix = LineIntersection(edgeStart, edgeEnd, current, next);
if (ix.IsValid())
output.Add(ix);
}
}
else if (nextInside)
{
var ix = LineIntersection(edgeStart, edgeEnd, current, next);
if (ix.IsValid())
output.Add(ix);
}
}
}
if (output.Count < 3)
return null;
var result = new Polygon();
result.Vertices.AddRange(output);
result.Close();
result.UpdateBounds();
// Reject degenerate slivers
if (result.Area() < Tolerance.Epsilon)
return null;
return result;
}
/// <summary>
/// Cross product of vectors (edgeStart->edgeEnd) and (edgeStart->point).
/// Positive = point is left of edge (inside for CCW polygon).
/// </summary>
private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point)
{
return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y)
- (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
}
/// <summary>
/// Intersection of lines (a1->a2) and (b1->b2). Returns Vector.Invalid if parallel.
/// </summary>
private static Vector LineIntersection(Vector a1, Vector a2, Vector b1, Vector b2)
{
var d1x = a2.X - a1.X;
var d1y = a2.Y - a1.Y;
var d2x = b2.X - b1.X;
var d2y = b2.Y - b1.Y;
var cross = d1x * d2y - d1y * d2x;
if (System.Math.Abs(cross) < Tolerance.Epsilon)
return Vector.Invalid;
var t = ((b1.X - a1.X) * d2y - (b1.Y - a1.Y) * d2x) / cross;
return new Vector(a1.X + t * d1x, a1.Y + t * d1y);
}
/// <summary>
/// Subtracts holes from overlap regions.
/// </summary>
private static List<Polygon> SubtractHoles(List<Polygon> regions,
List<Polygon> holesA, List<Polygon> holesB)
{
var allHoles = new List<Polygon>();
if (holesA != null) allHoles.AddRange(holesA);
if (holesB != null) allHoles.AddRange(holesB);
if (allHoles.Count == 0)
return regions;
foreach (var hole in allHoles)
{
var holeTris = TriangulateWithBounds(hole);
var surviving = new List<Polygon>();
foreach (var region in regions)
{
var pieces = SubtractTriangles(region, holeTris);
surviving.AddRange(pieces);
}
regions = surviving;
if (regions.Count == 0)
break;
}
return regions;
}
/// <summary>
/// Subtracts hole triangles from a region. Conservative: partial overlaps
/// keep the full piece triangle (acceptable for visual shading).
/// </summary>
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
{
var current = new List<Polygon> { region };
foreach (var holeTri in holeTris)
{
if (!BoundingBoxesOverlap(region.BoundingBox, holeTri.BoundingBox))
continue;
var next = new List<Polygon>();
foreach (var piece in current)
{
var pieceTris = TriangulateWithBounds(piece);
foreach (var pieceTri in pieceTris)
{
var inside = ClipConvex(pieceTri, holeTri);
if (inside == null)
{
// No overlap with hole - keep
next.Add(pieceTri);
}
else if (inside.Area() < pieceTri.Area() - Tolerance.Epsilon)
{
// Partial overlap - keep the piece (conservative)
next.Add(pieceTri);
}
// else: fully inside hole - discard
}
}
current = next;
}
return current;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Geometry
{
public class CollisionResult
{
public static readonly CollisionResult None = new(false, new List<Polygon>(), new List<Vector>());
public CollisionResult(bool overlaps, List<Polygon> overlapRegions, List<Vector> intersectionPoints)
{
Overlaps = overlaps;
OverlapRegions = overlapRegions;
IntersectionPoints = intersectionPoints;
OverlapArea = overlapRegions.Sum(r => r.Area());
}
public bool Overlaps { get; }
public IReadOnlyList<Polygon> OverlapRegions { get; }
public IReadOnlyList<Vector> IntersectionPoints { get; }
public double OverlapArea { get; }
}
}
+29 -1
View File
@@ -64,6 +64,25 @@ namespace OpenNest.Geometry
return new Vector(p1.X + s * n1.X, p1.Y + s * n1.Y);
}
internal static Vector Circumcenter(Vector a, Vector b, Vector c)
{
var ax = a.X - c.X;
var ay = a.Y - c.Y;
var bx = b.X - c.X;
var by = b.Y - c.Y;
var D = 2.0 * (ax * by - ay * bx);
if (System.Math.Abs(D) < 1e-10)
return Vector.Invalid;
var a2 = ax * ax + ay * ay;
var b2 = bx * bx + by * by;
var ux = (by * a2 - ay * b2) / D;
var uy = (ax * b2 - bx * a2) / D;
return new Vector(ux + c.X, uy + c.Y);
}
public static List<Entity> Convert(Vector center, double semiMajor, double semiMinor,
double rotation, double startParam, double endParam, double tolerance = 0.001)
{
@@ -185,11 +204,20 @@ namespace OpenNest.Geometry
{
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0);
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1);
var pMid = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (t0 + t1) / 2);
// Use circumcircle of (p0, pMid, p1) so the arc passes through both
// endpoints exactly, eliminating gaps between adjacent arcs.
var cc = Circumcenter(p0, pMid, p1);
if (cc.IsValid())
{
arcCenter = cc;
radius = p0.DistanceTo(cc);
}
var startAngle = System.Math.Atan2(p0.Y - arcCenter.Y, p0.X - arcCenter.X);
var endAngle = System.Math.Atan2(p1.Y - arcCenter.Y, p1.X - arcCenter.X);
var pMid = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (t0 + t1) / 2);
var points = new List<Vector> { p0, pMid, p1 };
var isReversed = SumSignedAngles(arcCenter, points) < 0;
+25 -44
View File
@@ -7,65 +7,46 @@ namespace OpenNest.Geometry
{
public static class GeometryOptimizer
{
public static void Optimize(IList<Arc> arcs)
public static void Optimize(IList<Arc> arcs) =>
MergePass(arcs,
(list, item, i) => list.GetCoradialArs(item, i),
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined));
public static void Optimize(IList<Line> lines) =>
MergePass(lines,
(list, item, i) => list.GetCollinearLines(item, i),
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined));
private delegate bool TryJoin<T>(T a, T b, out T joined);
private static void MergePass<T>(IList<T> items,
Func<IList<T>, T, int, List<T>> findCandidates,
TryJoin<T> tryJoin) where T : class
{
for (int i = 0; i < arcs.Count; ++i)
for (var i = 0; i < items.Count; ++i)
{
var arc = arcs[i];
var coradialArcs = arcs.GetCoradialArs(arc, i);
int index = 0;
while (index < coradialArcs.Count)
{
Arc arc2 = coradialArcs[index];
Arc joinArc;
if (!TryJoinArcs(arc, arc2, out joinArc))
{
index++;
continue;
}
coradialArcs.Remove(arc2);
arcs.Remove(arc2);
arc = joinArc;
index = 0;
}
arcs[i] = arc;
}
}
public static void Optimize(IList<Line> lines)
{
for (int i = 0; i < lines.Count; ++i)
{
var line = lines[i];
var collinearLines = lines.GetCollinearLines(line, i);
var item = items[i];
var candidates = findCandidates(items, item, i);
var index = 0;
while (index < collinearLines.Count)
while (index < candidates.Count)
{
Line line2 = collinearLines[index];
Line joinLine;
var candidate = candidates[index];
if (!TryJoinLines(line, line2, out joinLine))
if (!tryJoin(item, candidate, out var joined))
{
index++;
continue;
}
collinearLines.Remove(line2);
lines.Remove(line2);
candidates.Remove(candidate);
items.Remove(candidate);
line = joinLine;
item = joined;
index = 0;
}
lines[i] = line;
items[i] = item;
}
}
+4 -54
View File
@@ -437,47 +437,8 @@ public class GeometrySimplifier
/// the arc passes through both endpoints and departs P1 in the given direction.
/// </summary>
private static (Vector center, double radius, double deviation) FitWithStartTangent(
List<Vector> points, Vector tangent)
{
if (points.Count < 3)
return (Vector.Invalid, 0, double.MaxValue);
var p1 = points[0];
var pn = points[^1];
var mx = (p1.X + pn.X) / 2;
var my = (p1.Y + pn.Y) / 2;
var dx = pn.X - p1.X;
var dy = pn.Y - p1.Y;
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
if (chordLen < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var bx = -dy / chordLen;
var by = dx / chordLen;
var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y);
if (tLen < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var nx = -tangent.Y / tLen;
var ny = tangent.X / tLen;
var det = nx * by - ny * bx;
if (System.Math.Abs(det) < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var t = ((mx - p1.X) * by - (my - p1.Y) * bx) / det;
var cx = p1.X + t * nx;
var cy = p1.Y + t * ny;
var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
if (radius < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius));
}
List<Vector> points, Vector tangent) =>
ArcFit.FitWithStartTangent(points, tangent);
/// <summary>
/// Computes the tangent direction at the last point of a fitted arc,
@@ -629,19 +590,8 @@ public class GeometrySimplifier
/// <summary>
/// Max deviation of intermediate points (excluding endpoints) from a circle.
/// </summary>
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius)
{
var maxDev = 0.0;
for (var i = 1; i < points.Count - 1; i++)
{
var px = points[i].X - cx;
var py = points[i].Y - cy;
var dist = System.Math.Sqrt(px * px + py * py);
var dev = System.Math.Abs(dist - radius);
if (dev > maxDev) maxDev = dev;
}
return maxDev;
}
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
/// <summary>
/// Measures the maximum distance from sampled points along the fitted arc
+22 -2
View File
@@ -532,9 +532,29 @@ namespace OpenNest.Geometry
Line line, Line offsetLine,
double distance, OffsetSide side, Shape offsetShape)
{
Vector intersection;
// Determine if this is a convex corner using the cross product of
// the original line directions. Convex corners need an arc; concave
// corners use the line intersection (miter join).
var d1 = lastLine.EndPoint - lastLine.StartPoint;
var d2 = line.EndPoint - line.StartPoint;
var cross = d1.X * d2.Y - d1.Y * d2.X;
if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out intersection))
var isConvex = (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) ||
(side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
if (isConvex)
{
var arc = new Arc(
line.StartPoint,
distance,
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
line.StartPoint.AngleTo(offsetLine.StartPoint),
side == OffsetSide.Left
);
offsetShape.Entities.Add(arc);
}
else if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection))
{
offsetLine.StartPoint = intersection;
lastOffsetLine.EndPoint = intersection;
+51 -209
View File
@@ -523,177 +523,17 @@ namespace OpenNest.Geometry
#endregion
public static double ClosestDistanceLeft(Box box, List<Box> boxes)
{
var closestDistance = double.MaxValue;
for (int i = 0; i < boxes.Count; i++)
{
var compareBox = boxes[i];
RelativePosition pos;
if (!box.IsHorizontalTo(compareBox, out pos))
continue;
if (pos != RelativePosition.Right)
continue;
var distance = box.Left - compareBox.Right;
if (distance < closestDistance)
closestDistance = distance;
}
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
}
public static double ClosestDistanceRight(Box box, List<Box> boxes)
{
var closestDistance = double.MaxValue;
for (int i = 0; i < boxes.Count; i++)
{
var compareBox = boxes[i];
RelativePosition pos;
if (!box.IsHorizontalTo(compareBox, out pos))
continue;
if (pos != RelativePosition.Left)
continue;
var distance = compareBox.Left - box.Right;
if (distance < closestDistance)
closestDistance = distance;
}
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
}
public static double ClosestDistanceUp(Box box, List<Box> boxes)
{
var closestDistance = double.MaxValue;
for (int i = 0; i < boxes.Count; i++)
{
var compareBox = boxes[i];
RelativePosition pos;
if (!box.IsVerticalTo(compareBox, out pos))
continue;
if (pos != RelativePosition.Bottom)
continue;
var distance = compareBox.Bottom - box.Top;
if (distance < closestDistance)
closestDistance = distance;
}
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
}
public static double ClosestDistanceDown(Box box, List<Box> boxes)
{
var closestDistance = double.MaxValue;
for (int i = 0; i < boxes.Count; i++)
{
var compareBox = boxes[i];
RelativePosition pos;
if (!box.IsVerticalTo(compareBox, out pos))
continue;
if (pos != RelativePosition.Top)
continue;
var distance = box.Bottom - compareBox.Top;
if (distance < closestDistance)
closestDistance = distance;
}
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
}
public static Box GetLargestBoxVertically(Vector pt, Box bounds, IEnumerable<Box> boxes)
{
var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).ToList();
#region Find Top/Bottom Limits
var top = double.MaxValue;
var btm = double.MinValue;
foreach (var box in verticalBoxes)
{
var boxBtm = box.Bottom;
var boxTop = box.Top;
if (boxBtm > pt.Y && boxBtm < top)
top = boxBtm;
else if (box.Top < pt.Y && boxTop > btm)
btm = boxTop;
}
if (top == double.MaxValue)
{
if (bounds.Top > pt.Y)
top = bounds.Top;
else return Box.Empty;
}
if (btm == double.MinValue)
{
if (bounds.Bottom < pt.Y)
btm = bounds.Bottom;
else return Box.Empty;
}
#endregion
if (!FindVerticalLimits(pt, bounds, verticalBoxes, out var top, out var btm))
return Box.Empty;
var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList();
#region Find Left/Right Limits
var lft = double.MinValue;
var rgt = double.MaxValue;
foreach (var box in horizontalBoxes)
{
var boxLft = box.Left;
var boxRgt = box.Right;
if (boxLft > pt.X && boxLft < rgt)
rgt = boxLft;
else if (boxRgt < pt.X && boxRgt > lft)
lft = boxRgt;
}
if (rgt == double.MaxValue)
{
if (bounds.Right > pt.X)
rgt = bounds.Right;
else return Box.Empty;
}
if (lft == double.MinValue)
{
if (bounds.Left < pt.X)
lft = bounds.Left;
else return Box.Empty;
}
#endregion
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
return Box.Empty;
return new Box(lft, btm, rgt - lft, top - btm);
}
@@ -702,75 +542,77 @@ namespace OpenNest.Geometry
{
var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList();
#region Find Left/Right Limits
var lft = double.MinValue;
var rgt = double.MaxValue;
foreach (var box in horizontalBoxes)
{
var boxLft = box.Left;
var boxRgt = box.Right;
if (boxLft > pt.X && boxLft < rgt)
rgt = boxLft;
else if (boxRgt < pt.X && boxRgt > lft)
lft = boxRgt;
}
if (rgt == double.MaxValue)
{
if (bounds.Right > pt.X)
rgt = bounds.Right;
else return Box.Empty;
}
if (lft == double.MinValue)
{
if (bounds.Left < pt.X)
lft = bounds.Left;
else return Box.Empty;
}
#endregion
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
return Box.Empty;
var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList();
#region Find Top/Bottom Limits
if (!FindVerticalLimits(pt, bounds, verticalBoxes, out var top, out var btm))
return Box.Empty;
var top = double.MaxValue;
var btm = double.MinValue;
return new Box(lft, btm, rgt - lft, top - btm);
}
foreach (var box in verticalBoxes)
private static bool FindVerticalLimits(Vector pt, Box bounds, List<Box> boxes, out double top, out double btm)
{
top = double.MaxValue;
btm = double.MinValue;
foreach (var box in boxes)
{
var boxBtm = box.Bottom;
var boxTop = box.Top;
if (boxBtm > pt.Y && boxBtm < top)
top = boxBtm;
else if (box.Top < pt.Y && boxTop > btm)
btm = boxTop;
}
if (top == double.MaxValue)
{
if (bounds.Top > pt.Y)
top = bounds.Top;
else return Box.Empty;
if (bounds.Top > pt.Y) top = bounds.Top;
else return false;
}
if (btm == double.MinValue)
{
if (bounds.Bottom < pt.Y)
btm = bounds.Bottom;
else return Box.Empty;
if (bounds.Bottom < pt.Y) btm = bounds.Bottom;
else return false;
}
#endregion
return true;
}
return new Box(lft, btm, rgt - lft, top - btm);
private static bool FindHorizontalLimits(Vector pt, Box bounds, List<Box> boxes, out double lft, out double rgt)
{
lft = double.MinValue;
rgt = double.MaxValue;
foreach (var box in boxes)
{
var boxLft = box.Left;
var boxRgt = box.Right;
if (boxLft > pt.X && boxLft < rgt)
rgt = boxLft;
else if (boxRgt < pt.X && boxRgt > lft)
lft = boxRgt;
}
if (rgt == double.MaxValue)
{
if (bounds.Right > pt.X) rgt = bounds.Right;
else return false;
}
if (lft == double.MinValue)
{
if (bounds.Left < pt.X) lft = bounds.Left;
else return false;
}
return true;
}
}
}
+4 -54
View File
@@ -131,61 +131,11 @@ namespace OpenNest.Geometry
}
private static (Vector center, double radius, double deviation) FitWithStartTangent(
List<Vector> points, Vector tangent)
{
if (points.Count < 3)
return (Vector.Invalid, 0, double.MaxValue);
List<Vector> points, Vector tangent) =>
ArcFit.FitWithStartTangent(points, tangent);
var p1 = points[0];
var pn = points[^1];
var mx = (p1.X + pn.X) / 2;
var my = (p1.Y + pn.Y) / 2;
var dx = pn.X - p1.X;
var dy = pn.Y - p1.Y;
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
if (chordLen < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var bx = -dy / chordLen;
var by = dx / chordLen;
var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y);
if (tLen < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var nx = -tangent.Y / tLen;
var ny = tangent.X / tLen;
var det = nx * by - ny * bx;
if (System.Math.Abs(det) < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
var s = ((mx - p1.X) * by - (my - p1.Y) * bx) / det;
var cx = p1.X + s * nx;
var cy = p1.Y + s * ny;
var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
if (radius < 1e-10)
return (Vector.Invalid, 0, double.MaxValue);
return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius));
}
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius)
{
var maxDev = 0.0;
for (var i = 1; i < points.Count - 1; i++)
{
var px = points[i].X - cx;
var py = points[i].Y - cy;
var dist = System.Math.Sqrt(px * px + py * py);
var dev = System.Math.Abs(dist - radius);
if (dev > maxDev) maxDev = dev;
}
return maxDev;
}
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
private static double SumSignedAngles(Vector center, List<Vector> points)
{
+8 -46
View File
@@ -171,56 +171,18 @@ namespace OpenNest
if (perimeter1 == null || perimeter2 == null)
return false;
perimeter1.Offset(Location);
perimeter2.Offset(part.Location);
var polygon1 = perimeter1.ToPolygon();
var polygon2 = perimeter2.ToPolygon();
if (!perimeter1.Intersects(perimeter2, out var rawPts))
if (polygon1 == null || polygon2 == null)
return false;
// Exclude intersection points that coincide with vertices of BOTH
// perimeters — these are touch points (shared corners/endpoints),
// not actual crossings where one shape enters the other's interior.
var verts1 = CollectVertices(perimeter1);
var verts2 = CollectVertices(perimeter2);
polygon1.Offset(Location);
polygon2.Offset(part.Location);
foreach (var pt in rawPts)
{
if (IsNearAnyVertex(pt, verts1) && IsNearAnyVertex(pt, verts2))
continue;
pts.Add(pt);
}
return pts.Count > 0;
}
private static List<Vector> CollectVertices(Geometry.Shape shape)
{
var verts = new List<Vector>();
foreach (var entity in shape.Entities)
{
switch (entity)
{
case Geometry.Line line:
verts.Add(line.StartPoint);
verts.Add(line.EndPoint);
break;
case Geometry.Arc arc:
verts.Add(arc.StartPoint());
verts.Add(arc.EndPoint());
break;
}
}
return verts;
}
private static bool IsNearAnyVertex(Vector pt, List<Vector> vertices)
{
foreach (var v in vertices)
{
if (pt.X.IsEqualTo(v.X) && pt.Y.IsEqualTo(v.Y))
return true;
}
return false;
var result = Geometry.Collision.Check(polygon1, polygon2);
pts = result.IntersectionPoints.ToList();
return result.Overlaps;
}
public double Left
+3 -3
View File
@@ -45,7 +45,7 @@ namespace OpenNest
var profile = new ShapeProfile(
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
var lines = new List<Line>();
var totalSpacing = spacing + chordTolerance;
var totalSpacing = spacing;
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
chordTolerance, part.Location);
@@ -63,7 +63,7 @@ namespace OpenNest
var profile = new ShapeProfile(
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
var lines = new List<Line>();
var totalSpacing = spacing + chordTolerance;
var totalSpacing = spacing;
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
chordTolerance, part.Location, facingDirection);
@@ -97,7 +97,7 @@ namespace OpenNest
var profile = new ShapeProfile(
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
var lines = new List<Line>();
var totalSpacing = spacing + chordTolerance;
var totalSpacing = spacing;
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
chordTolerance, part.Location, facingDirection);
+22 -47
View File
@@ -27,7 +27,11 @@ namespace OpenNest.CirclePacking
throw new NotImplementedException();
}
private Bin FillHorizontal(Item item)
private Bin FillHorizontal(Item item) => FillAxis(item, horizontal: true);
private Bin FillVertical(Item item) => FillAxis(item, horizontal: false);
private Bin FillAxis(Item item, bool horizontal)
{
var bin = Bin.Clone() as Bin;
@@ -35,65 +39,36 @@ namespace OpenNest.CirclePacking
bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
var count = System.Math.Floor((bin.Width + Tolerance.Epsilon) / item.Diameter);
var primarySize = horizontal ? bin.Width : bin.Length;
var count = System.Math.Floor((primarySize + Tolerance.Epsilon) / item.Diameter);
if (count == 0)
return bin;
var xoffset = (bin.Width - item.Diameter) / (count - 1);
var yoffset = Trigonometry.Height(xoffset * 0.5, item.Diameter);
var primaryOffset = (primarySize - item.Diameter) / (count - 1);
var secondaryOffset = horizontal
? Trigonometry.Height(primaryOffset * 0.5, item.Diameter)
: Trigonometry.Base(primaryOffset * 0.5, item.Diameter);
int row = 0;
var outerStart = horizontal ? bin.Y : bin.X;
var outerMax = horizontal ? max.Y : max.X;
var innerStart = horizontal ? bin.X : bin.Y;
var innerMax = horizontal ? max.X : max.Y;
for (var y = bin.Y; y <= max.Y; y += yoffset)
var stripe = 0;
for (var outer = outerStart; outer <= outerMax; outer += secondaryOffset)
{
var x = row.IsOdd() ? bin.X + xoffset * 0.5 : bin.X;
var inner = stripe.IsOdd() ? innerStart + primaryOffset * 0.5 : innerStart;
for (; x <= max.X; x += xoffset)
for (; inner <= innerMax; inner += primaryOffset)
{
var addedItem = item.Clone() as Item;
addedItem.Center = new Vector(x, y);
addedItem.Center = horizontal ? new Vector(inner, outer) : new Vector(outer, inner);
bin.Items.Add(addedItem);
}
row++;
}
return bin;
}
private Bin FillVertical(Item item)
{
var bin = Bin.Clone() as Bin;
var max = new Vector(
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
var count = System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Diameter);
if (count == 0)
return bin;
var yoffset = (bin.Length - item.Diameter) / (count - 1);
var xoffset = Trigonometry.Base(yoffset * 0.5, item.Diameter);
int column = 0;
for (var x = bin.X; x <= max.X; x += xoffset)
{
var y = column.IsOdd() ? bin.Y + yoffset * 0.5 : bin.Y;
for (; y <= max.Y; y += yoffset)
{
var addedItem = item.Clone() as Item;
addedItem.Center = new Vector(x, y);
bin.Items.Add(addedItem);
}
column++;
stripe++;
}
return bin;
+1 -1
View File
@@ -94,7 +94,7 @@ namespace OpenNest
// Multi-part group: linear pattern fill only.
PhaseResults.Clear();
var engine = new FillLinear(workArea, Plate.PartSpacing);
var engine = new FillLinear(workArea, Plate.PartSpacing) { Label = "GroupPattern" };
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea, Comparer);
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
+66 -165
View File
@@ -1,9 +1,9 @@
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace OpenNest.Engine.Fill
@@ -33,7 +33,7 @@ namespace OpenNest.Engine.Fill
if (pair == null)
return new List<Part>();
var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox);
var column = BuildColumn(pair.Value);
if (column.Count == 0)
return new List<Part>();
@@ -81,7 +81,7 @@ namespace OpenNest.Engine.Fill
// --- Step 1: Pair Construction ---
private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle)
private PartPair? BuildPair(Drawing drawing, double rotationAngle)
{
var part1 = Part.CreateAtOrigin(drawing, rotationAngle);
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
@@ -111,46 +111,40 @@ namespace OpenNest.Engine.Fill
part2.UpdateBounds();
}
// Re-anchor pair to work area origin.
var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom);
part1.Offset(anchor);
part2.Offset(anchor);
part1.UpdateBounds();
part2.UpdateBounds();
pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
// Verify pair fits in work area.
if (pairBbox.Width > workArea.Width + Tolerance.Epsilon ||
pairBbox.Length > workArea.Length + Tolerance.Epsilon)
var pair = AnchorToWorkArea(part1, part2);
if (pair == null)
return null;
return (part1, part2, pairBbox);
// Verify pair fits in work area.
if (pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon ||
pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon)
return null;
return pair;
}
// --- Step 2: Build Column (tile vertically) ---
private List<Part> BuildColumn(Part part1, Part part2, Box pairBbox)
private List<Part> BuildColumn(PartPair pair)
{
var column = new List<Part> { (Part)part1.Clone(), (Part)part2.Clone() };
var column = new List<Part> { (Part)pair.Part1.Clone(), (Part)pair.Part2.Clone() };
// Find geometry-aware copy distance for the pair vertically.
var boundary1 = new PartBoundary(part1, halfSpacing);
var boundary2 = new PartBoundary(part2, halfSpacing);
var boundary1 = new PartBoundary(pair.Part1, halfSpacing);
var boundary2 = new PartBoundary(pair.Part2, halfSpacing);
// Compute vertical copy distance using bounding boxes as starting point,
// then slide down to find true geometry distance.
var pairHeight = pairBbox.Length;
var pairHeight = pair.Bbox.Length;
var testOffset = new Vector(0, pairHeight);
// Create test parts for slide distance measurement.
var testPart1 = part1.CloneAtOffset(testOffset);
var testPart2 = part2.CloneAtOffset(testOffset);
var testPart1 = pair.Part1.CloneAtOffset(testOffset);
var testPart2 = pair.Part2.CloneAtOffset(testOffset);
// Find minimum distance from test pair sliding down toward original pair.
var copyDistance = FindVerticalCopyDistance(
part1, part2, testPart1, testPart2,
pair.Part1, pair.Part2, testPart1, testPart2,
boundary1, boundary2, pairHeight);
if (copyDistance <= 0)
@@ -159,13 +153,13 @@ namespace OpenNest.Engine.Fill
var count = 1;
while (true)
{
var nextBottom = pairBbox.Bottom + copyDistance * count;
var nextBottom = pair.Bbox.Bottom + copyDistance * count;
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
break;
var offset = new Vector(0, copyDistance * count);
column.Add(part1.CloneAtOffset(offset));
column.Add(part2.CloneAtOffset(offset));
column.Add(pair.Part1.CloneAtOffset(offset));
column.Add(pair.Part2.CloneAtOffset(offset));
count++;
}
@@ -179,23 +173,20 @@ namespace OpenNest.Engine.Fill
double pairHeight)
{
// Check all 4 combinations: test parts sliding down toward original parts.
var slidePairs = new[]
{
(moving: boundary1, movingLoc: testPart1.Location, stationary: boundary1, stationaryLoc: origPart1.Location),
(moving: boundary1, movingLoc: testPart1.Location, stationary: boundary2, stationaryLoc: origPart2.Location),
(moving: boundary2, movingLoc: testPart2.Location, stationary: boundary1, stationaryLoc: origPart1.Location),
(moving: boundary2, movingLoc: testPart2.Location, stationary: boundary2, stationaryLoc: origPart2.Location),
};
var minSlide = double.MaxValue;
// Test1 -> Orig1
var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
// Test1 -> Orig2
d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
// Test2 -> Orig1
d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
// Test2 -> Orig2
d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down);
if (d < minSlide) minSlide = d;
foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs)
{
var d = SlideDistance(moving, movingLoc, stationary, stationaryLoc, PushDirection.Down);
if (d < minSlide) minSlide = d;
}
if (minSlide >= double.MaxValue || minSlide < 0)
return pairHeight + partSpacing;
@@ -225,12 +216,9 @@ namespace OpenNest.Engine.Fill
// --- Step 3: Iterative Adjustment ---
private List<Part> AdjustColumn(
(Part part1, Part part2, Box pairBbox) pair,
List<Part> column,
CancellationToken token)
private List<Part> AdjustColumn(PartPair pair, List<Part> column, CancellationToken token)
{
var originalPairWidth = pair.pairBbox.Width;
var originalPairWidth = pair.Bbox.Width;
for (var iteration = 0; iteration < MaxIterations; iteration++)
{
@@ -261,7 +249,7 @@ namespace OpenNest.Engine.Fill
if (adjusted == null)
break;
var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox);
var newColumn = BuildColumn(adjusted.Value);
if (newColumn.Count == 0)
break;
@@ -272,9 +260,7 @@ namespace OpenNest.Engine.Fill
return column;
}
private (Part part1, Part part2, Box pairBbox)? TryAdjustPair(
(Part part1, Part part2, Box pairBbox) pair,
double adjustment, double originalPairWidth)
private PartPair? TryAdjustPair(PartPair pair, double adjustment, double originalPairWidth)
{
// Try shifting part2 up first.
var result = TryShiftDirection(pair, adjustment, originalPairWidth);
@@ -286,13 +272,11 @@ namespace OpenNest.Engine.Fill
return TryShiftDirection(pair, -adjustment, originalPairWidth);
}
private (Part part1, Part part2, Box pairBbox)? TryShiftDirection(
(Part part1, Part part2, Box pairBbox) pair,
double verticalShift, double originalPairWidth)
private PartPair? TryShiftDirection(PartPair pair, double verticalShift, double originalPairWidth)
{
// Clone parts so we don't mutate the originals.
var p1 = (Part)pair.part1.Clone();
var p2 = (Part)pair.part2.Clone();
var p1 = (Part)pair.Part1.Clone();
var p2 = (Part)pair.Part2.Clone();
// Separate: shift part2 right so bounding boxes don't touch.
p2.Offset(partSpacing, 0);
@@ -308,20 +292,12 @@ namespace OpenNest.Engine.Fill
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
// Check if the pair got wider.
var newBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
var newBbox = PairBbox(p1, p2);
if (newBbox.Width > originalPairWidth + Tolerance.Epsilon)
return null;
// Re-anchor to work area origin.
var anchor = new Vector(workArea.X - newBbox.Left, workArea.Y - newBbox.Bottom);
p1.Offset(anchor);
p2.Offset(anchor);
p1.UpdateBounds();
p2.UpdateBounds();
newBbox = ((IEnumerable<IBoundable>)new IBoundable[] { p1, p2 }).GetBoundingBox();
return (p1, p2, newBbox);
return AnchorToWorkArea(p1, p2);
}
// --- Step 4: Horizontal Repetition ---
@@ -331,110 +307,35 @@ namespace OpenNest.Engine.Fill
if (column.Count == 0)
return column;
var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox();
var columnWidth = columnBbox.Width;
var pattern = new Pattern();
pattern.Parts.AddRange(column);
pattern.UpdateBounds();
// Create a test column shifted right by columnWidth + spacing.
var testOffset = columnWidth + partSpacing;
var testColumn = new List<Part>(column.Count);
foreach (var part in column)
testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0)));
// Compact the test column left against the original column.
var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
// Derive the true copy distance from where the test column ended up.
var testBbox = ((IEnumerable<IBoundable>)testColumn).GetBoundingBox();
var copyDistance = testBbox.Left - columnBbox.Left;
if (copyDistance <= Tolerance.Epsilon)
copyDistance = columnWidth + partSpacing;
// Safety: if the compacted test column overlaps the original column,
// fall back to bbox-based spacing.
var probe = new List<Part>(column);
probe.AddRange(testColumn.Where(IsWithinWorkArea));
if (HasOverlappingParts(probe))
{
Debug.WriteLine($"[FillExtents] Compacted column overlaps, falling back to bbox spacing");
copyDistance = columnWidth + partSpacing;
// Rebuild test column at safe distance.
testColumn.Clear();
foreach (var part in column)
testColumn.Add(part.CloneAtOffset(new Vector(copyDistance, 0)));
}
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
// Build all columns.
var result = new List<Part>(column);
// Add the test column we already computed as column 2.
foreach (var part in testColumn)
{
if (IsWithinWorkArea(part))
result.Add(part);
}
// Tile additional columns at the copy distance.
var colIndex = 2;
while (!token.IsCancellationRequested)
{
var offset = new Vector(copyDistance * colIndex, 0);
var anyFit = false;
foreach (var part in column)
{
var clone = part.CloneAtOffset(offset);
if (IsWithinWorkArea(clone))
{
result.Add(clone);
anyFit = true;
}
}
if (!anyFit)
break;
colIndex++;
}
return result;
var linear = new FillLinear(workArea, partSpacing);
return linear.Fill(pattern, NestDirection.Horizontal);
}
private bool IsWithinWorkArea(Part part)
// --- Helpers ---
private PartPair? AnchorToWorkArea(Part part1, Part part2)
{
return part.BoundingBox.Right <= workArea.Right + Tolerance.Epsilon &&
part.BoundingBox.Top <= workArea.Top + Tolerance.Epsilon &&
part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon &&
part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon;
var bbox = PairBbox(part1, part2);
var anchor = new Vector(workArea.X - bbox.Left, workArea.Y - bbox.Bottom);
part1.Offset(anchor);
part2.Offset(anchor);
part1.UpdateBounds();
part2.UpdateBounds();
bbox = PairBbox(part1, part2);
return new PartPair(part1, part2, bbox);
}
private static bool HasOverlappingParts(List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
private static Box PairBbox(Part part1, Part part2) =>
((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
private static bool HasOverlappingParts(List<Part> parts) =>
FillHelpers.HasOverlappingParts(parts);
var overlapX = System.Math.Min(b1.Right, b2.Right)
- System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
return true;
}
}
return false;
}
private readonly record struct PartPair(Part Part1, Part Part2, Box Bbox);
}
}
+57 -6
View File
@@ -1,6 +1,7 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace OpenNest.Engine.Fill
@@ -19,6 +20,11 @@ namespace OpenNest.Engine.Fill
public double HalfSpacing => PartSpacing / 2;
/// <summary>
/// Diagnostic label set by callers to identify the engine/context in overlap logs.
/// </summary>
public string Label { get; set; }
private static Vector MakeOffset(NestDirection direction, double distance)
{
return direction == NestDirection.Horizontal
@@ -323,7 +329,7 @@ namespace OpenNest.Engine.Fill
return result;
}
private static bool HasOverlappingParts(List<Part> parts)
private static bool HasOverlappingParts(List<Part> parts, out int overlapA, out int overlapB)
{
for (var i = 0; i < parts.Count; i++)
{
@@ -338,11 +344,20 @@ namespace OpenNest.Engine.Fill
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon)
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
{
overlapA = i;
overlapB = j;
return true;
}
}
}
overlapA = -1;
overlapB = -1;
return false;
}
@@ -384,10 +399,9 @@ namespace OpenNest.Engine.Fill
var row = new List<Part>(pattern.Parts);
row.AddRange(TilePattern(pattern, direction, boundaries));
// Safety: if geometry-aware spacing produced overlapping parts,
// fall back to bbox-based spacing for this axis.
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
{
LogOverlap("Step1-Primary", direction, pattern, row, a1, b1);
row = new List<Part>(pattern.Parts);
row.AddRange(TilePatternBbox(pattern, direction));
}
@@ -397,8 +411,9 @@ namespace OpenNest.Engine.Fill
{
row.AddRange(TilePattern(pattern, perpAxis, boundaries));
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
{
LogOverlap("Step1-PerpOnly", perpAxis, pattern, row, a2, b2);
row = new List<Part>(pattern.Parts);
row.AddRange(TilePatternBbox(pattern, perpAxis));
}
@@ -415,9 +430,45 @@ namespace OpenNest.Engine.Fill
var gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
if (HasOverlappingParts(gridResult, out var a3, out var b3))
{
LogOverlap("Step2-Perp", perpAxis, rowPattern, gridResult, a3, b3);
gridResult = new List<Part>(rowPattern.Parts);
gridResult.AddRange(TilePatternBbox(rowPattern, perpAxis));
}
return gridResult;
}
private void LogOverlap(string step, NestDirection tilingDir,
Pattern pattern, List<Part> parts, int idxA, int idxB)
{
var pa = parts[idxA];
var pb = parts[idxB];
var ba = pa.BoundingBox;
var bb = pb.BoundingBox;
Debug.WriteLine($"[FillLinear] OVERLAP FALLBACK ({Label ?? "unknown"})");
Debug.WriteLine($" Step: {step}, TilingDir: {tilingDir}");
Debug.WriteLine($" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}");
Debug.WriteLine($" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}");
Debug.WriteLine($" Total parts after tiling: {parts.Count}");
Debug.WriteLine($" Overlapping pair [{idxA}] vs [{idxB}]:");
Debug.WriteLine($" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° " +
$"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})");
Debug.WriteLine($" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° " +
$"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})");
// Log all pattern seed parts for reproduction
Debug.WriteLine($" Pattern seed parts:");
for (var i = 0; i < pattern.Parts.Count; i++)
{
var p = pattern.Parts[i];
Debug.WriteLine($" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° " +
$"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}");
}
}
/// <summary>
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
/// </summary>
+44 -94
View File
@@ -167,134 +167,84 @@ namespace OpenNest.Engine.Fill
/// Sorts pair columns by height (shortest first on the left) to create
/// a staircase profile that maximizes usable remnant area.
/// </summary>
internal static void SortColumnsByHeight(List<Part> parts, double spacing)
{
if (parts == null || parts.Count <= 1)
return;
// Sort parts by Left edge for grouping.
parts.Sort((a, b) => a.BoundingBox.Left.CompareTo(b.BoundingBox.Left));
// Group parts into columns by X overlap.
var columns = new List<List<Part>>();
var column = new List<Part> { parts[0] };
var columnRight = parts[0].BoundingBox.Right;
for (var i = 1; i < parts.Count; i++)
{
if (parts[i].BoundingBox.Left > columnRight + spacing / 2)
{
columns.Add(column);
column = new List<Part> { parts[i] };
columnRight = parts[i].BoundingBox.Right;
}
else
{
column.Add(parts[i]);
if (parts[i].BoundingBox.Right > columnRight)
columnRight = parts[i].BoundingBox.Right;
}
}
columns.Add(column);
if (columns.Count <= 1)
return;
// Measure inter-column gap from original layout.
var gap = MinLeft(columns[1]) - MaxRight(columns[0]);
// Sort columns by height ascending (shortest first).
columns.Sort((a, b) => MaxTop(a).CompareTo(MaxTop(b)));
// Reposition columns left-to-right.
var x = parts[0].BoundingBox.Left; // parts already sorted by Left
foreach (var col in columns)
{
var colLeft = MinLeft(col);
var dx = x - colLeft;
if (System.Math.Abs(dx) > OpenNest.Math.Tolerance.Epsilon)
{
var offset = new Vector(dx, 0);
foreach (var part in col)
part.Offset(offset);
}
x = MaxRight(col) + gap;
}
// Rebuild the parts list in column order.
parts.Clear();
foreach (var col in columns)
parts.AddRange(col);
}
internal static void SortColumnsByHeight(List<Part> parts, double spacing) =>
SortStrips(parts, spacing,
primaryEdge: b => b.Left, extentEdge: b => b.Right,
sortMetric: MaxTop, stripMin: MinLeft, stripMax: MaxRight,
makeOffset: d => new Vector(d, 0));
/// <summary>
/// Sorts pair rows by width (narrowest first on the bottom) to create
/// a staircase profile on the right side that maximizes usable remnant area.
/// </summary>
internal static void SortRowsByWidth(List<Part> parts, double spacing)
internal static void SortRowsByWidth(List<Part> parts, double spacing) =>
SortStrips(parts, spacing,
primaryEdge: b => b.Bottom, extentEdge: b => b.Top,
sortMetric: MaxRight, stripMin: MinBottom, stripMax: MaxTop,
makeOffset: d => new Vector(0, d));
private static void SortStrips(
List<Part> parts, double spacing,
Func<Box, double> primaryEdge,
Func<Box, double> extentEdge,
Func<List<Part>, double> sortMetric,
Func<List<Part>, double> stripMin,
Func<List<Part>, double> stripMax,
Func<double, Vector> makeOffset)
{
if (parts == null || parts.Count <= 1)
return;
// Sort parts by Bottom edge for grouping.
parts.Sort((a, b) => a.BoundingBox.Bottom.CompareTo(b.BoundingBox.Bottom));
parts.Sort((a, b) => primaryEdge(a.BoundingBox).CompareTo(primaryEdge(b.BoundingBox)));
// Group parts into rows by Y overlap.
var rows = new List<List<Part>>();
var row = new List<Part> { parts[0] };
var rowTop = parts[0].BoundingBox.Top;
var strips = new List<List<Part>>();
var strip = new List<Part> { parts[0] };
var stripExtent = extentEdge(parts[0].BoundingBox);
for (var i = 1; i < parts.Count; i++)
{
if (parts[i].BoundingBox.Bottom > rowTop + spacing / 2)
if (primaryEdge(parts[i].BoundingBox) > stripExtent + spacing / 2)
{
rows.Add(row);
row = new List<Part> { parts[i] };
rowTop = parts[i].BoundingBox.Top;
strips.Add(strip);
strip = new List<Part> { parts[i] };
stripExtent = extentEdge(parts[i].BoundingBox);
}
else
{
row.Add(parts[i]);
if (parts[i].BoundingBox.Top > rowTop)
rowTop = parts[i].BoundingBox.Top;
strip.Add(parts[i]);
var extent = extentEdge(parts[i].BoundingBox);
if (extent > stripExtent)
stripExtent = extent;
}
}
rows.Add(row);
strips.Add(strip);
if (rows.Count <= 1)
if (strips.Count <= 1)
return;
// Measure inter-row gap from original layout.
var gap = MinBottom(rows[1]) - MaxTop(rows[0]);
var gap = stripMin(strips[1]) - stripMax(strips[0]);
// Sort rows by width ascending (narrowest first).
rows.Sort((a, b) => MaxRight(a).CompareTo(MaxRight(b)));
strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b)));
// Reposition rows bottom-to-top.
var y = parts[0].BoundingBox.Bottom; // parts already sorted by Bottom
var pos = primaryEdge(parts[0].BoundingBox);
foreach (var r in rows)
foreach (var s in strips)
{
var rowBottom = MinBottom(r);
var dy = y - rowBottom;
var delta = pos - stripMin(s);
if (System.Math.Abs(dy) > OpenNest.Math.Tolerance.Epsilon)
if (System.Math.Abs(delta) > OpenNest.Math.Tolerance.Epsilon)
{
var offset = new Vector(0, dy);
foreach (var part in r)
var offset = makeOffset(delta);
foreach (var part in s)
part.Offset(offset);
}
y = MaxTop(r) + gap;
pos = stripMax(s) + gap;
}
// Rebuild the parts list in row order.
parts.Clear();
foreach (var r in rows)
parts.AddRange(r);
foreach (var s in strips)
parts.AddRange(s);
}
private static double MaxTop(List<Part> col)
+2 -2
View File
@@ -195,7 +195,7 @@ namespace OpenNest.Engine.Fill
if (pattern.Parts.Count == 0)
continue;
var engine = new FillLinear(workArea, partSpacing);
var engine = new FillLinear(workArea, partSpacing) { Label = "Pairs" };
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
{
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
@@ -321,7 +321,7 @@ namespace OpenNest.Engine.Fill
return cachedResult;
}
var filler = new FillLinear(remnantBox, partSpacing);
var filler = new FillLinear(remnantBox, partSpacing) { Label = "Pairs-Remnant" };
List<Part> parts = null;
foreach (var angle in new[] { 0.0, Angle.HalfPI })
+6 -33
View File
@@ -121,7 +121,7 @@ public class StripeFiller
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
return null;
var stripeEngine = new FillLinear(stripeBox, spacing);
var stripeEngine = new FillLinear(stripeBox, spacing) { Label = "Stripe" };
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
if (stripeParts == null || stripeParts.Count == 0)
@@ -136,7 +136,7 @@ public class StripeFiller
stripePattern.Parts.AddRange(stripeParts);
stripePattern.UpdateBounds();
var gridEngine = new FillLinear(workArea, spacing);
var gridEngine = new FillLinear(workArea, spacing) { Label = "Stripe-Grid" };
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
if (gridParts == null || gridParts.Count == 0)
@@ -244,7 +244,7 @@ public class StripeFiller
return cachedResult;
}
var filler = new FillLinear(remnantBox, spacing);
var filler = new FillLinear(remnantBox, spacing) { Label = "Stripe-Remnant" };
List<Part> best = null;
foreach (var angle in new[] { 0.0, Angle.HalfPI })
@@ -396,7 +396,7 @@ public class StripeFiller
var stripeBox = axis == NestDirection.Horizontal
? new Box(0, 0, sheetSpan, perpDim)
: new Box(0, 0, perpDim, sheetSpan);
var engine = new FillLinear(stripeBox, spacing);
var engine = new FillLinear(stripeBox, spacing) { Label = "Stripe-EstimateRow" };
var filled = engine.Fill(rotated, axis);
var n = filled?.Count ?? 0;
@@ -481,33 +481,6 @@ public class StripeFiller
return axis == NestDirection.Horizontal ? box.Width : box.Length;
}
/// <summary>
/// Checks if any pair of parts geometrically overlap. Uses bounding box
/// pre-filtering for performance, then falls back to shape intersection.
/// </summary>
private static bool HasOverlappingParts(List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right)
- System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
return true;
}
}
return false;
}
private static bool HasOverlappingParts(List<Part> parts) =>
FillHelpers.HasOverlappingParts(parts);
}
+7 -2
View File
@@ -2,6 +2,7 @@ using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -275,8 +276,12 @@ namespace OpenNest
{
var box2 = parts[j].BoundingBox;
if (box1.Right < box2.Left || box2.Right < box1.Left ||
box1.Top < box2.Bottom || box2.Top < box1.Bottom)
var overlapX = System.Math.Min(box1.Right, box2.Right)
- System.Math.Max(box1.Left, box2.Left);
var overlapY = System.Math.Min(box1.Top, box2.Top)
- System.Math.Max(box1.Bottom, box2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
List<Vector> pts;
+26 -34
View File
@@ -36,52 +36,44 @@ namespace OpenNest.RectanglePacking
throw new NotImplementedException();
}
private Bin BestFitHorizontal(Item item)
private Bin BestFitHorizontal(Item item) => BestFitAxis(item, horizontal: true);
private Bin BestFitVertical(Item item) => BestFitAxis(item, horizontal: false);
private Bin BestFitAxis(Item item, bool horizontal)
{
var bin = Bin.Clone() as Bin;
int normalColumns = 0;
int rotateColumns = 0;
var primarySize = horizontal ? item.Width : item.Length;
var secondarySize = horizontal ? item.Length : item.Width;
var binPrimary = horizontal ? bin.Width : Bin.Length;
var binSecondary = horizontal ? bin.Length : Bin.Width;
if (!BestCombination.FindFrom2(item.Width, item.Length, bin.Width, out normalColumns, out rotateColumns))
if (!BestCombination.FindFrom2(primarySize, secondarySize, binPrimary, out var normalPrimary, out var rotatePrimary))
return bin;
var normalRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Length);
var rotateRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Width);
var normalSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / secondarySize);
var rotateSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / primarySize);
var (normalRows, normalCols) = horizontal
? (normalSecondary, normalPrimary)
: (normalPrimary, normalSecondary);
var (rotateRows, rotateCols) = horizontal
? (rotateSecondary, rotatePrimary)
: (rotatePrimary, rotateSecondary);
item.Location = bin.Location;
bin.Items.AddRange(FillGrid(item, normalRows, normalColumns, int.MaxValue));
bin.Items.AddRange(FillGrid(item, normalRows, normalCols, int.MaxValue));
if (horizontal)
item.Location.X += item.Width * normalPrimary;
else
item.Location.Y += item.Length * normalPrimary;
item.Location.X += item.Width * normalColumns;
item.Rotate();
bin.Items.AddRange(FillGrid(item, rotateRows, rotateColumns, int.MaxValue));
return bin;
}
private Bin BestFitVertical(Item item)
{
var bin = Bin.Clone() as Bin;
int normalRows = 0;
int rotateRows = 0;
if (!BestCombination.FindFrom2(item.Length, item.Width, Bin.Length, out normalRows, out rotateRows))
return bin;
var normalColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
var rotateColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Length);
item.Location = bin.Location;
bin.Items.AddRange(FillGrid(item, normalRows, normalColumns, int.MaxValue));
item.Location.Y += item.Length * normalRows;
item.Rotate();
bin.Items.AddRange(FillGrid(item, rotateRows, rotateColumns, int.MaxValue));
bin.Items.AddRange(FillGrid(item, rotateRows, rotateCols, int.MaxValue));
return bin;
}
@@ -20,22 +20,10 @@ namespace OpenNest.Engine.Strategies
var angles = new[] { bestRotation, bestRotation + Angle.HalfPI };
List<Part> best = null;
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
foreach (var angle in angles)
{
context.Token.ThrowIfCancellationRequested();
var result = filler.Fill(context.Item.Drawing, angle,
context.PlateNumber, context.Token, context.Progress);
if (result != null && result.Count > 0)
{
if (best == null || comparer.IsBetter(result, best, context.WorkArea))
best = result;
}
}
return best ?? new List<Part>();
return FillHelpers.BestOverAngles(context, angles,
angle => filler.Fill(context.Item.Drawing, angle,
context.PlateNumber, context.Token, context.Progress),
NestPhase.Extents, "Extents");
}
}
}
+73
View File
@@ -109,5 +109,78 @@ namespace OpenNest.Engine.Strategies
var fallback = fillFunc(other);
return fallback ?? new List<Part>();
}
/// <summary>
/// Sweeps a list of angles, calling fillAtAngle for each, and returns
/// the best result according to the context's comparer. Handles
/// cancellation and progress reporting.
/// </summary>
public static List<Part> BestOverAngles(
FillContext context,
IReadOnlyList<double> angles,
Func<double, List<Part>> fillAtAngle,
NestPhase phase,
string phaseLabel)
{
var workArea = context.WorkArea;
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
List<Part> best = null;
for (var i = 0; i < angles.Count; i++)
{
context.Token.ThrowIfCancellationRequested();
var angle = angles[i];
var result = fillAtAngle(angle);
var angleDeg = Angle.ToDegrees(angle);
if (result != null && result.Count > 0)
{
if (best == null || comparer.IsBetter(result, best, workArea))
best = result;
}
NestEngineBase.ReportProgress(context.Progress, new ProgressReport
{
Phase = phase,
PlateNumber = context.PlateNumber,
Parts = best,
WorkArea = workArea,
Description = $"{phaseLabel}: {i + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts",
});
}
return best ?? new List<Part>();
}
/// <summary>
/// Checks if any pair of parts geometrically overlap. Uses bounding box
/// pre-filtering for performance, then falls back to shape intersection.
/// </summary>
internal static bool HasOverlappingParts(List<Part> parts)
{
for (var i = 0; i < parts.Count; i++)
{
var b1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right)
- System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top)
- System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon)
continue;
if (parts[i].Intersects(parts[j], out _))
return true;
}
}
return false;
}
}
}
@@ -19,45 +19,28 @@ namespace OpenNest.Engine.Strategies
var workArea = context.WorkArea;
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
var preferred = context.Policy?.PreferredDirection;
List<Part> best = null;
for (var ai = 0; ai < angles.Count; ai++)
{
context.Token.ThrowIfCancellationRequested();
var angle = angles[ai];
var engine = new FillLinear(workArea, context.Plate.PartSpacing);
var result = FillHelpers.FillWithDirectionPreference(
dir => engine.Fill(context.Item.Drawing, angle, dir),
preferred, comparer, workArea);
var angleDeg = Angle.ToDegrees(angle);
if (result != null && result.Count > 0)
return FillHelpers.BestOverAngles(context, angles,
angle =>
{
context.AngleResults.Add(new AngleResult
var engine = new FillLinear(workArea, context.Plate.PartSpacing) { Label = "Linear" };
var result = FillHelpers.FillWithDirectionPreference(
dir => engine.Fill(context.Item.Drawing, angle, dir),
preferred, comparer, workArea);
if (result != null && result.Count > 0)
{
AngleDeg = angleDeg,
Direction = preferred ?? NestDirection.Horizontal,
PartCount = result.Count
});
context.AngleResults.Add(new AngleResult
{
AngleDeg = Angle.ToDegrees(angle),
Direction = preferred ?? NestDirection.Horizontal,
PartCount = result.Count
});
}
if (best == null || comparer.IsBetter(result, best, workArea))
best = result;
}
NestEngineBase.ReportProgress(context.Progress, new ProgressReport
{
Phase = NestPhase.Linear,
PlateNumber = context.PlateNumber,
Parts = best,
WorkArea = workArea,
Description = $"Linear: {ai + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts",
});
}
return best ?? new List<Part>();
return result;
},
NestPhase.Linear, "Linear");
}
}
}
+218
View File
@@ -0,0 +1,218 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Tests;
public class CollisionTests
{
/// Two unit squares overlapping by 0.5 in X.
/// Square A: (0,0)-(1,1), Square B: (0.5,0)-(1.5,1)
/// Expected overlap: (0.5,0)-(1,1), area = 0.5
[Fact]
public void Check_OverlappingSquares_ReturnsOverlapRegion()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(0.5, 0, 1.5, 1);
var result = Collision.Check(a, b);
Assert.True(result.Overlaps);
Assert.True(result.OverlapArea > 0.49 && result.OverlapArea < 0.51);
Assert.NotEmpty(result.OverlapRegions);
}
/// Two squares that don't touch at all.
[Fact]
public void Check_NonOverlappingSquares_ReturnsNone()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(5, 5, 6, 6);
var result = Collision.Check(a, b);
Assert.False(result.Overlaps);
Assert.Empty(result.OverlapRegions);
Assert.Equal(0, result.OverlapArea);
}
/// Two squares sharing an edge (touching but not overlapping).
[Fact]
public void Check_EdgeTouchingSquares_ReturnsNone()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(1, 0, 2, 1);
var result = Collision.Check(a, b);
Assert.False(result.Overlaps);
}
/// One square fully inside another. Inner: (0.25,0.25)-(0.75,0.75), area = 0.25
[Fact]
public void Check_ContainedSquare_ReturnsInnerArea()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(0.25, 0.25, 0.75, 0.75);
var result = Collision.Check(a, b);
Assert.True(result.Overlaps);
Assert.True(result.OverlapArea > 0.24 && result.OverlapArea < 0.26);
}
/// L-shaped concave polygon overlapping a square.
[Fact]
public void Check_ConcavePolygonOverlap_ReturnsOverlap()
{
// L-shape: 2x2 with a 1x1 notch cut from top-right
var lShape = new Polygon();
lShape.Vertices.Add(new Vector(0, 0));
lShape.Vertices.Add(new Vector(2, 0));
lShape.Vertices.Add(new Vector(2, 1));
lShape.Vertices.Add(new Vector(1, 1));
lShape.Vertices.Add(new Vector(1, 2));
lShape.Vertices.Add(new Vector(0, 2));
lShape.Close();
lShape.UpdateBounds();
// Square overlapping the notch area and bottom-right
var square = MakeSquare(1.5, 0, 2.5, 1.5);
var result = Collision.Check(lShape, square);
Assert.True(result.Overlaps);
// Overlap is 0.5 x 1.0 = 0.5 (the part of the square inside the L bottom-right)
Assert.True(result.OverlapArea > 0.49 && result.OverlapArea < 0.51);
}
/// <summary>
/// Square A has a hole. Square B overlaps only the hole area.
/// This should NOT be a collision — B fits inside A's cutout.
/// </summary>
[Fact]
public void Check_OverlapInsideHole_ReturnsNone()
{
var a = MakeSquare(0, 0, 4, 4);
var holeA = new List<Polygon> { MakeSquare(1, 1, 3, 3) };
// B fits entirely inside the hole
var b = MakeSquare(1.5, 1.5, 2.5, 2.5);
var result = Collision.Check(a, b, holesA: holeA);
Assert.False(result.Overlaps);
}
/// <summary>
/// Square A has a hole. Square B partially overlaps the hole and
/// partially overlaps solid material. Should still be a collision.
/// </summary>
[Fact]
public void Check_PartialOverlapWithHole_StillOverlaps()
{
var a = MakeSquare(0, 0, 4, 4);
var holeA = new List<Polygon> { MakeSquare(1, 1, 3, 3) };
// B extends beyond the hole into solid material
var b = MakeSquare(2, 2, 5, 5);
var result = Collision.Check(a, b, holesA: holeA);
// Hole subtraction uses a conservative approach (keeps partial overlaps),
// so we only verify that a collision is still detected for solid material.
Assert.True(result.Overlaps);
}
/// <summary>
/// HasOverlap with holes returns false when overlap is inside cutout.
/// </summary>
[Fact]
public void HasOverlap_InsideHole_ReturnsFalse()
{
var a = MakeSquare(0, 0, 4, 4);
var holeA = new List<Polygon> { MakeSquare(1, 1, 3, 3) };
var b = MakeSquare(1.5, 1.5, 2.5, 2.5);
Assert.False(Collision.HasOverlap(a, b, holesA: holeA));
}
[Fact]
public void CheckAll_MultiplePolygons_FindsAllOverlaps()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(0.5, 0, 1.5, 1); // overlaps A
var c = MakeSquare(5, 5, 6, 6); // overlaps nobody
var results = Collision.CheckAll(new List<Polygon> { a, b, c });
Assert.Single(results);
Assert.True(results[0].Overlaps);
}
[Fact]
public void CheckAll_NoOverlaps_ReturnsEmpty()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(3, 3, 4, 4);
var results = Collision.CheckAll(new List<Polygon> { a, b });
Assert.Empty(results);
}
[Fact]
public void HasAnyOverlap_WithOverlap_ReturnsTrue()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(0.5, 0, 1.5, 1);
Assert.True(Collision.HasAnyOverlap(new List<Polygon> { a, b }));
}
[Fact]
public void HasAnyOverlap_NoOverlap_ReturnsFalse()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(3, 3, 4, 4);
Assert.False(Collision.HasAnyOverlap(new List<Polygon> { a, b }));
}
[Fact]
public void Check_IdenticalSquares_FullOverlap()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(0, 0, 1, 1);
var result = Collision.Check(a, b);
Assert.True(result.Overlaps);
Assert.True(result.OverlapArea > 0.99 && result.OverlapArea < 1.01);
}
[Fact]
public void HasAnyOverlap_SinglePolygon_ReturnsFalse()
{
var a = MakeSquare(0, 0, 1, 1);
Assert.False(Collision.HasAnyOverlap(new List<Polygon> { a }));
}
[Fact]
public void HasAnyOverlap_EmptyList_ReturnsFalse()
{
Assert.False(Collision.HasAnyOverlap(new List<Polygon>()));
}
private static Polygon MakeSquare(double left, double bottom, double right, double top)
{
var p = new Polygon();
p.Vertices.Add(new Vector(left, bottom));
p.Vertices.Add(new Vector(right, bottom));
p.Vertices.Add(new Vector(right, top));
p.Vertices.Add(new Vector(left, top));
p.Close();
p.UpdateBounds();
return p;
}
}
+4 -4
View File
@@ -172,15 +172,15 @@ public class EllipseConverterTests
var current = (Arc)result[i];
var next = (Arc)result[i + 1];
var gap = current.EndPoint().DistanceTo(next.StartPoint());
Assert.True(gap < 0.001,
$"Gap of {gap:F6} between arc {i} and arc {i + 1}");
Assert.True(gap < 1e-6,
$"Gap of {gap:E4} between arc {i} and arc {i + 1}");
}
var lastArc = (Arc)result[^1];
var firstArc = (Arc)result[0];
var closingGap = lastArc.EndPoint().DistanceTo(firstArc.StartPoint());
Assert.True(closingGap < 0.001,
$"Closing gap of {closingGap:F6}");
Assert.True(closingGap < 1e-6,
$"Closing gap of {closingGap:E4}");
}
[Fact]
+1
View File
@@ -107,4 +107,5 @@ public class StrategyOverlapTests
Assert.Empty(failures);
}
}
+15
View File
@@ -1,4 +1,6 @@
using OpenNest.Controls;
using OpenNest.Geometry;
using System.Drawing;
namespace OpenNest.Actions
{
@@ -11,6 +13,19 @@ namespace OpenNest.Actions
this.plateView = plateView;
}
protected RectangleF GetRectangle(Vector worldPt1, Vector worldPt2)
{
var pt1 = plateView.PointWorldToGraph(worldPt1);
var pt2 = plateView.PointWorldToGraph(worldPt2);
var x = pt1.X < pt2.X ? pt1.X : pt2.X;
var y = pt1.Y < pt2.Y ? pt1.Y : pt2.Y;
var w = System.Math.Abs(pt2.X - pt1.X);
var h = System.Math.Abs(pt2.Y - pt1.Y);
return new RectangleF(x, y, w, h);
}
public virtual bool SurvivesPlateChange => false;
public virtual void OnPlateChanged() { }
+1 -30
View File
@@ -212,36 +212,7 @@ namespace OpenNest.Actions
}
}
private RectangleF GetRectangle()
{
var rect = new RectangleF();
var pt1 = plateView.PointWorldToGraph(Point1);
var pt2 = plateView.PointWorldToGraph(Point2);
if (pt1.X < pt2.X)
{
rect.X = pt1.X;
rect.Width = pt2.X - pt1.X;
}
else
{
rect.X = pt2.X;
rect.Width = pt1.X - pt2.X;
}
if (pt1.Y < pt2.Y)
{
rect.Y = pt1.Y;
rect.Height = pt2.Y - pt1.Y;
}
else
{
rect.Y = pt2.Y;
rect.Height = pt1.Y - pt2.Y;
}
return rect;
}
private RectangleF GetRectangle() => GetRectangle(Point1, Point2);
public SelectionType SelectionType
{
+1 -30
View File
@@ -166,36 +166,7 @@ namespace OpenNest.Actions
plateView.Refresh();
}
private RectangleF GetRectangle()
{
var rect = new RectangleF();
var pt1 = plateView.PointWorldToGraph(Point1);
var pt2 = plateView.PointWorldToGraph(Point2);
if (pt1.X < pt2.X)
{
rect.X = pt1.X;
rect.Width = pt2.X - pt1.X;
}
else
{
rect.X = pt2.X;
rect.Width = pt1.X - pt2.X;
}
if (pt1.Y < pt2.Y)
{
rect.Y = pt1.Y;
rect.Height = pt2.Y - pt1.Y;
}
else
{
rect.Y = pt2.Y;
rect.Height = pt1.Y - pt2.Y;
}
return rect;
}
private RectangleF GetRectangle() => GetRectangle(Point1, Point2);
public enum Status
{
+1 -1
View File
@@ -74,7 +74,7 @@ namespace OpenNest.Controls
var bendLinksPanel = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
Dock = DockStyle.Top,
Height = 20,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false
+12 -3
View File
@@ -1243,15 +1243,24 @@ namespace OpenNest.Controls
var cts = new CancellationTokenSource();
var progressForm = new NestProgressForm(cts, showPlateRow: false);
var previewPlate = new Plate(Plate.Size)
{
Quadrant = Plate.Quadrant,
PartSpacing = Plate.PartSpacing,
Thickness = Plate.Thickness,
Material = Plate.Material,
};
previewPlate.EdgeSpacing = Plate.EdgeSpacing;
progressForm.PreviewPlate = previewPlate;
var progress = new Progress<NestProgress>(p =>
{
progressForm.UpdateProgress(p);
if (p.IsOverallBest)
SetStationaryParts(p.BestParts);
else
SetActiveParts(p.BestParts);
progressForm.UpdatePreview(p.BestParts);
SetActiveParts(p.BestParts);
ActiveWorkArea = p.ActiveWorkArea;
});
+24 -3
View File
@@ -905,15 +905,24 @@ namespace OpenNest.Forms
var progressForm = new NestProgressForm(nestingCts, showPlateRow: true);
var previewPlate = new Plate(activeForm.PlateView.Plate.Size)
{
Quadrant = activeForm.PlateView.Plate.Quadrant,
PartSpacing = activeForm.PlateView.Plate.PartSpacing,
Thickness = activeForm.PlateView.Plate.Thickness,
Material = activeForm.PlateView.Plate.Material,
};
previewPlate.EdgeSpacing = activeForm.PlateView.Plate.EdgeSpacing;
progressForm.PreviewPlate = previewPlate;
var progress = new Progress<NestProgress>(p =>
{
progressForm.UpdateProgress(p);
if (p.IsOverallBest)
activeForm.PlateView.SetStationaryParts(p.BestParts);
else
activeForm.PlateView.SetActiveParts(p.BestParts);
progressForm.UpdatePreview(p.BestParts);
activeForm.PlateView.SetActiveParts(p.BestParts);
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
});
@@ -939,8 +948,20 @@ namespace OpenNest.Forms
: activeForm.PlateView.Plate;
if (plate != activeForm.PlateView.Plate)
{
activeForm.LoadLastPlate();
var newPreviewPlate = new Plate(plate.Size)
{
Quadrant = plate.Quadrant,
PartSpacing = plate.PartSpacing,
Thickness = plate.Thickness,
Material = plate.Material,
};
newPreviewPlate.EdgeSpacing = plate.EdgeSpacing;
progressForm.PreviewPlate = newPreviewPlate;
}
var anyPlaced = false;
var engine = NestEngineRegistry.Create(plate);
+120 -72
View File
@@ -41,6 +41,14 @@ namespace OpenNest.Forms
buttonPanel = new System.Windows.Forms.FlowLayoutPanel();
stopButton = new System.Windows.Forms.Button();
acceptButton = new System.Windows.Forms.Button();
splitContainer = new System.Windows.Forms.SplitContainer();
statsPanel = new System.Windows.Forms.Panel();
previewPlateView = new OpenNest.Controls.PlateView();
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
splitContainer.Panel1.SuspendLayout();
splitContainer.Panel2.SuspendLayout();
splitContainer.SuspendLayout();
statsPanel.SuspendLayout();
resultsPanel.SuspendLayout();
resultsTable.SuspendLayout();
densityPanel.SuspendLayout();
@@ -48,19 +56,53 @@ namespace OpenNest.Forms
statusTable.SuspendLayout();
buttonPanel.SuspendLayout();
SuspendLayout();
//
//
// splitContainer
//
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
splitContainer.Location = new System.Drawing.Point(0, 0);
splitContainer.Name = "splitContainer";
splitContainer.Panel1.Controls.Add(previewPlateView);
splitContainer.Panel2.Controls.Add(statsPanel);
splitContainer.Size = new System.Drawing.Size(750, 420);
splitContainer.SplitterDistance = 480;
splitContainer.TabIndex = 0;
//
// previewPlateView
//
previewPlateView.AllowDrop = false;
previewPlateView.Dock = System.Windows.Forms.DockStyle.Fill;
previewPlateView.Location = new System.Drawing.Point(0, 0);
previewPlateView.Name = "previewPlateView";
previewPlateView.Size = new System.Drawing.Size(480, 420);
previewPlateView.TabIndex = 0;
//
// statsPanel
//
statsPanel.AutoScroll = true;
statsPanel.Controls.Add(buttonPanel);
statsPanel.Controls.Add(statusPanel);
statsPanel.Controls.Add(resultsPanel);
statsPanel.Controls.Add(phaseStepper);
statsPanel.Dock = System.Windows.Forms.DockStyle.Fill;
statsPanel.Location = new System.Drawing.Point(0, 0);
statsPanel.Name = "statsPanel";
statsPanel.Size = new System.Drawing.Size(266, 420);
statsPanel.TabIndex = 0;
//
// phaseStepper
//
//
phaseStepper.ActivePhase = null;
phaseStepper.Dock = System.Windows.Forms.DockStyle.Top;
phaseStepper.IsComplete = false;
phaseStepper.Location = new System.Drawing.Point(0, 0);
phaseStepper.Name = "phaseStepper";
phaseStepper.Size = new System.Drawing.Size(450, 60);
phaseStepper.Size = new System.Drawing.Size(266, 60);
phaseStepper.TabIndex = 0;
//
//
// resultsPanel
//
//
resultsPanel.BackColor = System.Drawing.Color.White;
resultsPanel.Controls.Add(resultsTable);
resultsPanel.Controls.Add(resultsHeader);
@@ -69,11 +111,11 @@ namespace OpenNest.Forms
resultsPanel.Margin = new System.Windows.Forms.Padding(10, 4, 10, 4);
resultsPanel.Name = "resultsPanel";
resultsPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10);
resultsPanel.Size = new System.Drawing.Size(450, 120);
resultsPanel.Size = new System.Drawing.Size(266, 120);
resultsPanel.TabIndex = 1;
//
//
// resultsTable
//
//
resultsTable.AutoSize = true;
resultsTable.ColumnCount = 2;
resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 90F));
@@ -91,11 +133,11 @@ namespace OpenNest.Forms
resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
resultsTable.Size = new System.Drawing.Size(422, 69);
resultsTable.Size = new System.Drawing.Size(238, 69);
resultsTable.TabIndex = 1;
//
//
// partsLabel
//
//
partsLabel.AutoSize = true;
partsLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
partsLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
@@ -105,9 +147,9 @@ namespace OpenNest.Forms
partsLabel.Size = new System.Drawing.Size(43, 17);
partsLabel.TabIndex = 0;
partsLabel.Text = "Parts:";
//
//
// partsValue
//
//
partsValue.AutoSize = true;
partsValue.Font = new System.Drawing.Font("Consolas", 9.75F);
partsValue.Location = new System.Drawing.Point(90, 3);
@@ -115,10 +157,10 @@ namespace OpenNest.Forms
partsValue.Name = "partsValue";
partsValue.Size = new System.Drawing.Size(13, 15);
partsValue.TabIndex = 1;
partsValue.Text = "";
//
partsValue.Text = "\u2014";
//
// densityLabel
//
//
densityLabel.AutoSize = true;
densityLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
densityLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
@@ -128,21 +170,21 @@ namespace OpenNest.Forms
densityLabel.Size = new System.Drawing.Size(59, 17);
densityLabel.TabIndex = 2;
densityLabel.Text = "Density:";
//
//
// densityPanel
//
//
densityPanel.AutoSize = true;
densityPanel.Controls.Add(densityValue);
densityPanel.Controls.Add(densityBar);
densityPanel.Location = new System.Drawing.Point(90, 23);
densityPanel.Margin = new System.Windows.Forms.Padding(0);
densityPanel.Name = "densityPanel";
densityPanel.Size = new System.Drawing.Size(262, 21);
densityPanel.Size = new System.Drawing.Size(148, 21);
densityPanel.TabIndex = 3;
densityPanel.WrapContents = false;
//
//
// densityValue
//
//
densityValue.AutoSize = true;
densityValue.Font = new System.Drawing.Font("Consolas", 9.75F);
densityValue.Location = new System.Drawing.Point(0, 3);
@@ -150,19 +192,19 @@ namespace OpenNest.Forms
densityValue.Name = "densityValue";
densityValue.Size = new System.Drawing.Size(13, 15);
densityValue.TabIndex = 0;
densityValue.Text = "";
//
densityValue.Text = "\u2014";
//
// densityBar
//
//
densityBar.Location = new System.Drawing.Point(21, 5);
densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0);
densityBar.Name = "densityBar";
densityBar.Size = new System.Drawing.Size(241, 8);
densityBar.Size = new System.Drawing.Size(127, 8);
densityBar.TabIndex = 1;
densityBar.Value = 0D;
//
//
// nestedAreaLabel
//
//
nestedAreaLabel.AutoSize = true;
nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
nestedAreaLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
@@ -172,9 +214,9 @@ namespace OpenNest.Forms
nestedAreaLabel.Size = new System.Drawing.Size(55, 17);
nestedAreaLabel.TabIndex = 4;
nestedAreaLabel.Text = "Nested:";
//
//
// nestedAreaValue
//
//
nestedAreaValue.AutoSize = true;
nestedAreaValue.Font = new System.Drawing.Font("Consolas", 9.75F);
nestedAreaValue.Location = new System.Drawing.Point(90, 49);
@@ -182,10 +224,10 @@ namespace OpenNest.Forms
nestedAreaValue.Name = "nestedAreaValue";
nestedAreaValue.Size = new System.Drawing.Size(13, 15);
nestedAreaValue.TabIndex = 5;
nestedAreaValue.Text = "";
//
nestedAreaValue.Text = "\u2014";
//
// resultsHeader
//
//
resultsHeader.AutoSize = true;
resultsHeader.Dock = System.Windows.Forms.DockStyle.Top;
resultsHeader.Font = new System.Drawing.Font("Segoe UI", 10.5F, System.Drawing.FontStyle.Bold);
@@ -196,9 +238,9 @@ namespace OpenNest.Forms
resultsHeader.Size = new System.Drawing.Size(65, 23);
resultsHeader.TabIndex = 0;
resultsHeader.Text = "RESULTS";
//
//
// statusPanel
//
//
statusPanel.BackColor = System.Drawing.Color.White;
statusPanel.Controls.Add(statusTable);
statusPanel.Controls.Add(statusHeader);
@@ -206,11 +248,11 @@ namespace OpenNest.Forms
statusPanel.Location = new System.Drawing.Point(0, 180);
statusPanel.Name = "statusPanel";
statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10);
statusPanel.Size = new System.Drawing.Size(450, 115);
statusPanel.Size = new System.Drawing.Size(266, 115);
statusPanel.TabIndex = 2;
//
//
// statusTable
//
//
statusTable.AutoSize = true;
statusTable.ColumnCount = 2;
statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 90F));
@@ -228,11 +270,11 @@ namespace OpenNest.Forms
statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusTable.Size = new System.Drawing.Size(422, 69);
statusTable.Size = new System.Drawing.Size(238, 69);
statusTable.TabIndex = 1;
//
//
// plateLabel
//
//
plateLabel.AutoSize = true;
plateLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
plateLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
@@ -242,9 +284,9 @@ namespace OpenNest.Forms
plateLabel.Size = new System.Drawing.Size(43, 17);
plateLabel.TabIndex = 0;
plateLabel.Text = "Plate:";
//
//
// plateValue
//
//
plateValue.AutoSize = true;
plateValue.Font = new System.Drawing.Font("Consolas", 9.75F);
plateValue.Location = new System.Drawing.Point(90, 3);
@@ -252,10 +294,10 @@ namespace OpenNest.Forms
plateValue.Name = "plateValue";
plateValue.Size = new System.Drawing.Size(13, 15);
plateValue.TabIndex = 1;
plateValue.Text = "";
//
plateValue.Text = "\u2014";
//
// elapsedLabel
//
//
elapsedLabel.AutoSize = true;
elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
elapsedLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
@@ -265,9 +307,9 @@ namespace OpenNest.Forms
elapsedLabel.Size = new System.Drawing.Size(59, 17);
elapsedLabel.TabIndex = 2;
elapsedLabel.Text = "Elapsed:";
//
//
// elapsedValue
//
//
elapsedValue.AutoSize = true;
elapsedValue.Font = new System.Drawing.Font("Consolas", 9.75F);
elapsedValue.Location = new System.Drawing.Point(90, 26);
@@ -276,9 +318,9 @@ namespace OpenNest.Forms
elapsedValue.Size = new System.Drawing.Size(35, 15);
elapsedValue.TabIndex = 3;
elapsedValue.Text = "0:00";
//
//
// descriptionLabel
//
//
descriptionLabel.AutoSize = true;
descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
descriptionLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51);
@@ -288,9 +330,9 @@ namespace OpenNest.Forms
descriptionLabel.Size = new System.Drawing.Size(49, 17);
descriptionLabel.TabIndex = 4;
descriptionLabel.Text = "Detail:";
//
//
// descriptionValue
//
//
descriptionValue.AutoSize = true;
descriptionValue.Font = new System.Drawing.Font("Segoe UI", 9.75F);
descriptionValue.Location = new System.Drawing.Point(90, 49);
@@ -298,10 +340,10 @@ namespace OpenNest.Forms
descriptionValue.Name = "descriptionValue";
descriptionValue.Size = new System.Drawing.Size(20, 17);
descriptionValue.TabIndex = 5;
descriptionValue.Text = "";
//
descriptionValue.Text = "\u2014";
//
// statusHeader
//
//
statusHeader.AutoSize = true;
statusHeader.Dock = System.Windows.Forms.DockStyle.Top;
statusHeader.Font = new System.Drawing.Font("Segoe UI", 10.5F, System.Drawing.FontStyle.Bold);
@@ -312,9 +354,9 @@ namespace OpenNest.Forms
statusHeader.Size = new System.Drawing.Size(59, 23);
statusHeader.TabIndex = 0;
statusHeader.Text = "STATUS";
//
//
// buttonPanel
//
//
buttonPanel.AutoSize = true;
buttonPanel.Controls.Add(stopButton);
buttonPanel.Controls.Add(acceptButton);
@@ -323,14 +365,14 @@ namespace OpenNest.Forms
buttonPanel.Location = new System.Drawing.Point(0, 295);
buttonPanel.Name = "buttonPanel";
buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6);
buttonPanel.Size = new System.Drawing.Size(450, 45);
buttonPanel.Size = new System.Drawing.Size(266, 45);
buttonPanel.TabIndex = 3;
//
//
// stopButton
//
//
stopButton.Enabled = false;
stopButton.Font = new System.Drawing.Font("Segoe UI", 9.75F);
stopButton.Location = new System.Drawing.Point(339, 9);
stopButton.Location = new System.Drawing.Point(155, 9);
stopButton.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
stopButton.Name = "stopButton";
stopButton.Size = new System.Drawing.Size(93, 27);
@@ -338,12 +380,12 @@ namespace OpenNest.Forms
stopButton.Text = "Stop";
stopButton.UseVisualStyleBackColor = true;
stopButton.Click += StopButton_Click;
//
//
// acceptButton
//
//
acceptButton.Enabled = false;
acceptButton.Font = new System.Drawing.Font("Segoe UI", 9.75F);
acceptButton.Location = new System.Drawing.Point(246, 9);
acceptButton.Location = new System.Drawing.Point(56, 9);
acceptButton.Margin = new System.Windows.Forms.Padding(6, 3, 0, 3);
acceptButton.Name = "acceptButton";
acceptButton.Size = new System.Drawing.Size(93, 27);
@@ -351,23 +393,27 @@ namespace OpenNest.Forms
acceptButton.Text = "Accept";
acceptButton.UseVisualStyleBackColor = true;
acceptButton.Click += AcceptButton_Click;
//
//
// NestProgressForm
//
//
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(450, 345);
Controls.Add(buttonPanel);
Controls.Add(statusPanel);
Controls.Add(resultsPanel);
Controls.Add(phaseStepper);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
ClientSize = new System.Drawing.Size(750, 420);
Controls.Add(splitContainer);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
MaximizeBox = false;
MinimizeBox = false;
MinimumSize = new System.Drawing.Size(550, 380);
Name = "NestProgressForm";
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Nesting Progress";
splitContainer.Panel1.ResumeLayout(false);
splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
splitContainer.ResumeLayout(false);
statsPanel.ResumeLayout(false);
statsPanel.PerformLayout();
resultsPanel.ResumeLayout(false);
resultsPanel.PerformLayout();
resultsTable.ResumeLayout(false);
@@ -380,7 +426,6 @@ namespace OpenNest.Forms
statusTable.PerformLayout();
buttonPanel.ResumeLayout(false);
ResumeLayout(false);
PerformLayout();
}
#endregion
@@ -409,5 +454,8 @@ namespace OpenNest.Forms
private System.Windows.Forms.FlowLayoutPanel buttonPanel;
private System.Windows.Forms.Button acceptButton;
private System.Windows.Forms.Button stopButton;
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.Panel statsPanel;
private Controls.PlateView previewPlateView;
}
}
+39 -8
View File
@@ -27,11 +27,23 @@ namespace OpenNest.Forms
public bool Accepted { get; private set; }
public Plate PreviewPlate
{
get => previewPlateView.Plate;
set
{
previewPlateView.Plate = value;
previewPlateView.ZoomToFit();
}
}
public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true)
{
this.cts = cts;
InitializeComponent();
previewPlateView.AllowSelect = false;
if (!showPlateRow)
{
plateLabel.Visible = false;
@@ -59,23 +71,40 @@ namespace OpenNest.Forms
}
phaseStepper.ActivePhase = progress.Phase;
SetValueWithFlash(plateValue, progress.PlateNumber.ToString());
SetValueWithFlash(partsValue, progress.BestPartCount.ToString());
var densityText = progress.BestDensity.ToString("P1");
var densityFlashColor = GetDensityColor(progress.BestDensity);
SetValueWithFlash(densityValue, densityText, densityFlashColor);
densityBar.Value = progress.BestDensity;
if (progress.IsOverallBest)
{
SetValueWithFlash(partsValue, progress.BestPartCount.ToString());
SetValueWithFlash(nestedAreaValue,
$"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)");
var densityText = progress.BestDensity.ToString("P1");
var densityFlashColor = GetDensityColor(progress.BestDensity);
SetValueWithFlash(densityValue, densityText, densityFlashColor);
densityBar.Value = progress.BestDensity;
SetValueWithFlash(nestedAreaValue,
$"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)");
}
descriptionValue.Text = !string.IsNullOrEmpty(progress.Description)
? progress.Description
: progress.Phase.DisplayName();
}
public void UpdatePreview(List<Part> bestParts)
{
if (IsDisposed || !IsHandleCreated)
return;
var plate = previewPlateView.Plate;
plate.Parts.Clear();
foreach (var part in bestParts)
plate.Parts.Add((Part)part.Clone());
previewPlateView.ZoomToFit();
}
public void ShowCompleted()
{
if (IsDisposed || !IsHandleCreated)
@@ -135,6 +164,8 @@ namespace OpenNest.Forms
if (!cts.IsCancellationRequested)
cts.Cancel();
previewPlateView.Dispose();
base.OnFormClosing(e);
}
+3 -3
View File
@@ -220,7 +220,7 @@ namespace OpenNest.Forms
return;
var workArea = new Box(0, 0, plateSize.Length, plateSize.Width);
var filler = new FillLinear(workArea, PartSpacing);
var filler = new FillLinear(workArea, PartSpacing) { Label = "PatternTile-H" };
var hParts = filler.Fill(pattern, NestDirection.Horizontal);
foreach (var part in hParts)
@@ -228,7 +228,7 @@ namespace OpenNest.Forms
hLabel.Text = $"Horizontal — {hParts.Count} parts";
hPreview.ZoomToFit();
var vFiller = new FillLinear(workArea, PartSpacing);
var vFiller = new FillLinear(workArea, PartSpacing) { Label = "PatternTile-V" };
var vParts = vFiller.Fill(pattern, NestDirection.Vertical);
foreach (var part in vParts)
vPreview.Plate.Parts.Add(part);
@@ -328,7 +328,7 @@ namespace OpenNest.Forms
if (pattern == null)
return;
var filler = new FillLinear(new Box(0, 0, plateSize.Length, plateSize.Width), PartSpacing);
var filler = new FillLinear(new Box(0, 0, plateSize.Length, plateSize.Width), PartSpacing) { Label = "PatternTile-Apply" };
var tiledParts = filler.Fill(pattern, applyDirection);
Result = new PatternTileResult