Compare commits
25
Commits
61b917c398
...
036f723876
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
036f723876 | ||
|
|
21a5d3b026 | ||
|
|
0607c6c7c5 | ||
|
|
c8cfeb3c6b | ||
|
|
d4f424f274 | ||
|
|
028b1fabfc | ||
|
|
a7c2fcffe6 | ||
|
|
b834813889 | ||
|
|
4fa6100722 | ||
|
|
8f2fbee02c | ||
|
|
230a11d32e | ||
|
|
953429dae9 | ||
|
|
1c2b569ff4 | ||
|
|
048b10a1e9 | ||
|
|
3022982f6d | ||
|
|
cc85493a0c | ||
|
|
3da287cdc0 | ||
|
|
d1a701a7f7 | ||
|
|
17f786c9e8 | ||
|
|
b7a8e2662c | ||
|
|
912a47c5e8 | ||
|
|
a85213a524 | ||
|
|
fb696aaf58 | ||
|
|
d854a1f5d2 | ||
|
|
abc707f1d9 |
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
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)));
|
||||
var linear = new FillLinear(workArea, partSpacing);
|
||||
return linear.Fill(pattern, NestDirection.Horizontal);
|
||||
}
|
||||
|
||||
// Compact the test column left against the original column.
|
||||
var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
|
||||
// --- Helpers ---
|
||||
|
||||
// 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))
|
||||
private PartPair? AnchorToWorkArea(Part part1, Part part2)
|
||||
{
|
||||
Debug.WriteLine($"[FillExtents] Compacted column overlaps, falling back to bbox spacing");
|
||||
copyDistance = columnWidth + partSpacing;
|
||||
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();
|
||||
|
||||
// Rebuild test column at safe distance.
|
||||
testColumn.Clear();
|
||||
foreach (var part in column)
|
||||
testColumn.Add(part.CloneAtOffset(new Vector(copyDistance, 0)));
|
||||
bbox = PairBbox(part1, part2);
|
||||
return new PartPair(part1, part2, bbox);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
|
||||
private static Box PairBbox(Part part1, Part part2) =>
|
||||
((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||
|
||||
// Build all columns.
|
||||
var result = new List<Part>(column);
|
||||
private static bool HasOverlappingParts(List<Part> parts) =>
|
||||
FillHelpers.HasOverlappingParts(parts);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private bool IsWithinWorkArea(Part part)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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 readonly record struct PartPair(Part Part1, Part Part2, Box Bbox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
return FillHelpers.BestOverAngles(context, angles,
|
||||
angle =>
|
||||
{
|
||||
context.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var angle = angles[ai];
|
||||
var engine = new FillLinear(workArea, context.Plate.PartSpacing);
|
||||
|
||||
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);
|
||||
|
||||
var angleDeg = Angle.ToDegrees(angle);
|
||||
|
||||
if (result != null && result.Count > 0)
|
||||
{
|
||||
context.AngleResults.Add(new AngleResult
|
||||
{
|
||||
AngleDeg = angleDeg,
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -107,4 +107,5 @@ public class StrategyOverlapTests
|
||||
|
||||
Assert.Empty(failures);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() { }
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace OpenNest.Controls
|
||||
|
||||
var bendLinksPanel = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
Dock = DockStyle.Top,
|
||||
Height = 20,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
WrapContents = false
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+70
-22
@@ -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();
|
||||
@@ -49,6 +57,40 @@ namespace OpenNest.Forms
|
||||
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;
|
||||
@@ -56,7 +98,7 @@ namespace OpenNest.Forms
|
||||
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
|
||||
@@ -69,7 +111,7 @@ 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
|
||||
@@ -91,7 +133,7 @@ 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
|
||||
@@ -115,7 +157,7 @@ namespace OpenNest.Forms
|
||||
partsValue.Name = "partsValue";
|
||||
partsValue.Size = new System.Drawing.Size(13, 15);
|
||||
partsValue.TabIndex = 1;
|
||||
partsValue.Text = "�";
|
||||
partsValue.Text = "\u2014";
|
||||
//
|
||||
// densityLabel
|
||||
//
|
||||
@@ -137,7 +179,7 @@ namespace OpenNest.Forms
|
||||
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;
|
||||
//
|
||||
@@ -150,14 +192,14 @@ 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;
|
||||
//
|
||||
@@ -182,7 +224,7 @@ namespace OpenNest.Forms
|
||||
nestedAreaValue.Name = "nestedAreaValue";
|
||||
nestedAreaValue.Size = new System.Drawing.Size(13, 15);
|
||||
nestedAreaValue.TabIndex = 5;
|
||||
nestedAreaValue.Text = "�";
|
||||
nestedAreaValue.Text = "\u2014";
|
||||
//
|
||||
// resultsHeader
|
||||
//
|
||||
@@ -206,7 +248,7 @@ 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
|
||||
@@ -228,7 +270,7 @@ 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
|
||||
@@ -252,7 +294,7 @@ namespace OpenNest.Forms
|
||||
plateValue.Name = "plateValue";
|
||||
plateValue.Size = new System.Drawing.Size(13, 15);
|
||||
plateValue.TabIndex = 1;
|
||||
plateValue.Text = "�";
|
||||
plateValue.Text = "\u2014";
|
||||
//
|
||||
// elapsedLabel
|
||||
//
|
||||
@@ -298,7 +340,7 @@ namespace OpenNest.Forms
|
||||
descriptionValue.Name = "descriptionValue";
|
||||
descriptionValue.Size = new System.Drawing.Size(20, 17);
|
||||
descriptionValue.TabIndex = 5;
|
||||
descriptionValue.Text = "�";
|
||||
descriptionValue.Text = "\u2014";
|
||||
//
|
||||
// statusHeader
|
||||
//
|
||||
@@ -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);
|
||||
@@ -343,7 +385,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
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);
|
||||
@@ -356,18 +398,22 @@ namespace OpenNest.Forms
|
||||
//
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +71,10 @@ namespace OpenNest.Forms
|
||||
}
|
||||
|
||||
phaseStepper.ActivePhase = progress.Phase;
|
||||
|
||||
SetValueWithFlash(plateValue, progress.PlateNumber.ToString());
|
||||
|
||||
if (progress.IsOverallBest)
|
||||
{
|
||||
SetValueWithFlash(partsValue, progress.BestPartCount.ToString());
|
||||
|
||||
var densityText = progress.BestDensity.ToString("P1");
|
||||
@@ -70,12 +84,27 @@ namespace OpenNest.Forms
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user