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`.
|
- **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.
|
- **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).
|
- **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.
|
- **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.
|
- **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));
|
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)
|
if (parts.Count < 3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var list = new List<Part>(parts);
|
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 lastIndex = list.Count - 1;
|
||||||
|
|
||||||
var first = list[0];
|
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
|
||||||
var last = list[lastIndex];
|
var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y;
|
||||||
|
|
||||||
var start = first.BoundingBox.Center.X;
|
var spacing = (end - start) / lastIndex;
|
||||||
var end = last.BoundingBox.Center.X;
|
|
||||||
var diff = end - start;
|
|
||||||
|
|
||||||
var spacing = diff / lastIndex;
|
for (var i = 1; i < lastIndex; ++i)
|
||||||
|
|
||||||
for (int i = 1; i < lastIndex; ++i)
|
|
||||||
{
|
{
|
||||||
var part = list[i];
|
var part = list[i];
|
||||||
var newX = start + i * spacing;
|
var cur = horizontal ? part.BoundingBox.Center.X : part.BoundingBox.Center.Y;
|
||||||
var curX = part.BoundingBox.Center.X;
|
var delta = start + i * spacing - cur;
|
||||||
|
|
||||||
part.Offset(newX - curX, 0);
|
part.Offset(horizontal ? delta : 0, horizontal ? 0 : delta);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,37 +51,7 @@ namespace OpenNest.CNC
|
|||||||
mode = Mode.Absolute;
|
mode = Mode.Absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Rotate(double angle)
|
public virtual void Rotate(double angle) => Rotate(angle, new Vector(0, 0));
|
||||||
{
|
|
||||||
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 override string ToString()
|
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);
|
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,
|
public static List<Entity> Convert(Vector center, double semiMajor, double semiMinor,
|
||||||
double rotation, double startParam, double endParam, double tolerance = 0.001)
|
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 p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0);
|
||||||
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1);
|
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 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 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 points = new List<Vector> { p0, pMid, p1 };
|
||||||
var isReversed = SumSignedAngles(arcCenter, points) < 0;
|
var isReversed = SumSignedAngles(arcCenter, points) < 0;
|
||||||
|
|
||||||
|
|||||||
@@ -7,65 +7,46 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
public static class GeometryOptimizer
|
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 item = items[i];
|
||||||
|
var candidates = findCandidates(items, item, 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 index = 0;
|
var index = 0;
|
||||||
|
|
||||||
while (index < collinearLines.Count)
|
while (index < candidates.Count)
|
||||||
{
|
{
|
||||||
Line line2 = collinearLines[index];
|
var candidate = candidates[index];
|
||||||
Line joinLine;
|
|
||||||
|
|
||||||
if (!TryJoinLines(line, line2, out joinLine))
|
if (!tryJoin(item, candidate, out var joined))
|
||||||
{
|
{
|
||||||
index++;
|
index++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
collinearLines.Remove(line2);
|
candidates.Remove(candidate);
|
||||||
lines.Remove(line2);
|
items.Remove(candidate);
|
||||||
|
|
||||||
line = joinLine;
|
item = joined;
|
||||||
index = 0;
|
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.
|
/// the arc passes through both endpoints and departs P1 in the given direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||||
List<Vector> points, Vector tangent)
|
List<Vector> points, Vector tangent) =>
|
||||||
{
|
ArcFit.FitWithStartTangent(points, 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the tangent direction at the last point of a fitted arc,
|
/// Computes the tangent direction at the last point of a fitted arc,
|
||||||
@@ -629,19 +590,8 @@ public class GeometrySimplifier
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max deviation of intermediate points (excluding endpoints) from a circle.
|
/// Max deviation of intermediate points (excluding endpoints) from a circle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius)
|
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
|
||||||
{
|
ArcFit.MaxRadialDeviation(points, cx, cy, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures the maximum distance from sampled points along the fitted arc
|
/// Measures the maximum distance from sampled points along the fitted arc
|
||||||
|
|||||||
@@ -532,9 +532,29 @@ namespace OpenNest.Geometry
|
|||||||
Line line, Line offsetLine,
|
Line line, Line offsetLine,
|
||||||
double distance, OffsetSide side, Shape offsetShape)
|
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;
|
offsetLine.StartPoint = intersection;
|
||||||
lastOffsetLine.EndPoint = intersection;
|
lastOffsetLine.EndPoint = intersection;
|
||||||
|
|||||||
@@ -523,177 +523,17 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
#endregion
|
#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)
|
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();
|
var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).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;
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList();
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList();
|
||||||
|
|
||||||
#region Find Left/Right Limits
|
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
|
||||||
|
return Box.Empty;
|
||||||
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
|
|
||||||
|
|
||||||
return new Box(lft, btm, rgt - lft, top - btm);
|
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();
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList();
|
||||||
|
|
||||||
#region Find Left/Right Limits
|
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
|
||||||
|
return Box.Empty;
|
||||||
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
|
|
||||||
|
|
||||||
var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList();
|
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;
|
return new Box(lft, btm, rgt - lft, top - btm);
|
||||||
var btm = double.MinValue;
|
}
|
||||||
|
|
||||||
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 boxBtm = box.Bottom;
|
||||||
var boxTop = box.Top;
|
var boxTop = box.Top;
|
||||||
|
|
||||||
if (boxBtm > pt.Y && boxBtm < top)
|
if (boxBtm > pt.Y && boxBtm < top)
|
||||||
top = boxBtm;
|
top = boxBtm;
|
||||||
|
|
||||||
else if (box.Top < pt.Y && boxTop > btm)
|
else if (box.Top < pt.Y && boxTop > btm)
|
||||||
btm = boxTop;
|
btm = boxTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (top == double.MaxValue)
|
if (top == double.MaxValue)
|
||||||
{
|
{
|
||||||
if (bounds.Top > pt.Y)
|
if (bounds.Top > pt.Y) top = bounds.Top;
|
||||||
top = bounds.Top;
|
else return false;
|
||||||
else return Box.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btm == double.MinValue)
|
if (btm == double.MinValue)
|
||||||
{
|
{
|
||||||
if (bounds.Bottom < pt.Y)
|
if (bounds.Bottom < pt.Y) btm = bounds.Bottom;
|
||||||
btm = bounds.Bottom;
|
else return false;
|
||||||
else return Box.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#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(
|
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||||
List<Vector> points, Vector tangent)
|
List<Vector> points, Vector tangent) =>
|
||||||
{
|
ArcFit.FitWithStartTangent(points, tangent);
|
||||||
if (points.Count < 3)
|
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
|
||||||
|
|
||||||
var p1 = points[0];
|
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
|
||||||
var pn = points[^1];
|
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
|
||||||
|
|
||||||
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 SumSignedAngles(Vector center, List<Vector> points)
|
private static double SumSignedAngles(Vector center, List<Vector> points)
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-46
@@ -171,56 +171,18 @@ namespace OpenNest
|
|||||||
if (perimeter1 == null || perimeter2 == null)
|
if (perimeter1 == null || perimeter2 == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
perimeter1.Offset(Location);
|
var polygon1 = perimeter1.ToPolygon();
|
||||||
perimeter2.Offset(part.Location);
|
var polygon2 = perimeter2.ToPolygon();
|
||||||
|
|
||||||
if (!perimeter1.Intersects(perimeter2, out var rawPts))
|
if (polygon1 == null || polygon2 == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Exclude intersection points that coincide with vertices of BOTH
|
polygon1.Offset(Location);
|
||||||
// perimeters — these are touch points (shared corners/endpoints),
|
polygon2.Offset(part.Location);
|
||||||
// not actual crossings where one shape enters the other's interior.
|
|
||||||
var verts1 = CollectVertices(perimeter1);
|
|
||||||
var verts2 = CollectVertices(perimeter2);
|
|
||||||
|
|
||||||
foreach (var pt in rawPts)
|
var result = Geometry.Collision.Check(polygon1, polygon2);
|
||||||
{
|
pts = result.IntersectionPoints.ToList();
|
||||||
if (IsNearAnyVertex(pt, verts1) && IsNearAnyVertex(pt, verts2))
|
return result.Overlaps;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Left
|
public double Left
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace OpenNest
|
|||||||
var profile = new ShapeProfile(
|
var profile = new ShapeProfile(
|
||||||
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
var totalSpacing = spacing + chordTolerance;
|
var totalSpacing = spacing;
|
||||||
|
|
||||||
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
chordTolerance, part.Location);
|
chordTolerance, part.Location);
|
||||||
@@ -63,7 +63,7 @@ namespace OpenNest
|
|||||||
var profile = new ShapeProfile(
|
var profile = new ShapeProfile(
|
||||||
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
var totalSpacing = spacing + chordTolerance;
|
var totalSpacing = spacing;
|
||||||
|
|
||||||
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
chordTolerance, part.Location, facingDirection);
|
chordTolerance, part.Location, facingDirection);
|
||||||
@@ -97,7 +97,7 @@ namespace OpenNest
|
|||||||
var profile = new ShapeProfile(
|
var profile = new ShapeProfile(
|
||||||
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
var totalSpacing = spacing + chordTolerance;
|
var totalSpacing = spacing;
|
||||||
|
|
||||||
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
chordTolerance, part.Location, facingDirection);
|
chordTolerance, part.Location, facingDirection);
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ namespace OpenNest.CirclePacking
|
|||||||
throw new NotImplementedException();
|
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;
|
var bin = Bin.Clone() as Bin;
|
||||||
|
|
||||||
@@ -35,65 +39,36 @@ namespace OpenNest.CirclePacking
|
|||||||
bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
|
||||||
bin.Top - item.BoundingBox.Top + 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)
|
if (count == 0)
|
||||||
return bin;
|
return bin;
|
||||||
|
|
||||||
var xoffset = (bin.Width - item.Diameter) / (count - 1);
|
var primaryOffset = (primarySize - item.Diameter) / (count - 1);
|
||||||
var yoffset = Trigonometry.Height(xoffset * 0.5, item.Diameter);
|
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;
|
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);
|
bin.Items.Add(addedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
row++;
|
stripe++;
|
||||||
}
|
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bin;
|
return bin;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
// Multi-part group: linear pattern fill only.
|
// Multi-part group: linear pattern fill only.
|
||||||
PhaseResults.Clear();
|
PhaseResults.Clear();
|
||||||
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
var engine = new FillLinear(workArea, Plate.PartSpacing) { Label = "GroupPattern" };
|
||||||
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
||||||
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea, Comparer);
|
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea, Comparer);
|
||||||
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
|
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
using OpenNest.Engine.Strategies;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Fill
|
namespace OpenNest.Engine.Fill
|
||||||
@@ -33,7 +33,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (pair == null)
|
if (pair == null)
|
||||||
return new List<Part>();
|
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)
|
if (column.Count == 0)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
// --- Step 1: Pair Construction ---
|
// --- 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 part1 = Part.CreateAtOrigin(drawing, rotationAngle);
|
||||||
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
|
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
|
||||||
@@ -111,46 +111,40 @@ namespace OpenNest.Engine.Fill
|
|||||||
part2.UpdateBounds();
|
part2.UpdateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-anchor pair to work area origin.
|
var pair = AnchorToWorkArea(part1, part2);
|
||||||
var pairBbox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
if (pair == null)
|
||||||
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)
|
|
||||||
return 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) ---
|
// --- 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.
|
// Find geometry-aware copy distance for the pair vertically.
|
||||||
var boundary1 = new PartBoundary(part1, halfSpacing);
|
var boundary1 = new PartBoundary(pair.Part1, halfSpacing);
|
||||||
var boundary2 = new PartBoundary(part2, halfSpacing);
|
var boundary2 = new PartBoundary(pair.Part2, halfSpacing);
|
||||||
|
|
||||||
// Compute vertical copy distance using bounding boxes as starting point,
|
// Compute vertical copy distance using bounding boxes as starting point,
|
||||||
// then slide down to find true geometry distance.
|
// then slide down to find true geometry distance.
|
||||||
var pairHeight = pairBbox.Length;
|
var pairHeight = pair.Bbox.Length;
|
||||||
var testOffset = new Vector(0, pairHeight);
|
var testOffset = new Vector(0, pairHeight);
|
||||||
|
|
||||||
// Create test parts for slide distance measurement.
|
// Create test parts for slide distance measurement.
|
||||||
var testPart1 = part1.CloneAtOffset(testOffset);
|
var testPart1 = pair.Part1.CloneAtOffset(testOffset);
|
||||||
var testPart2 = part2.CloneAtOffset(testOffset);
|
var testPart2 = pair.Part2.CloneAtOffset(testOffset);
|
||||||
|
|
||||||
// Find minimum distance from test pair sliding down toward original pair.
|
// Find minimum distance from test pair sliding down toward original pair.
|
||||||
var copyDistance = FindVerticalCopyDistance(
|
var copyDistance = FindVerticalCopyDistance(
|
||||||
part1, part2, testPart1, testPart2,
|
pair.Part1, pair.Part2, testPart1, testPart2,
|
||||||
boundary1, boundary2, pairHeight);
|
boundary1, boundary2, pairHeight);
|
||||||
|
|
||||||
if (copyDistance <= 0)
|
if (copyDistance <= 0)
|
||||||
@@ -159,13 +153,13 @@ namespace OpenNest.Engine.Fill
|
|||||||
var count = 1;
|
var count = 1;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var nextBottom = pairBbox.Bottom + copyDistance * count;
|
var nextBottom = pair.Bbox.Bottom + copyDistance * count;
|
||||||
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
|
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var offset = new Vector(0, copyDistance * count);
|
var offset = new Vector(0, copyDistance * count);
|
||||||
column.Add(part1.CloneAtOffset(offset));
|
column.Add(pair.Part1.CloneAtOffset(offset));
|
||||||
column.Add(part2.CloneAtOffset(offset));
|
column.Add(pair.Part2.CloneAtOffset(offset));
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,23 +173,20 @@ namespace OpenNest.Engine.Fill
|
|||||||
double pairHeight)
|
double pairHeight)
|
||||||
{
|
{
|
||||||
// Check all 4 combinations: test parts sliding down toward original parts.
|
// 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;
|
var minSlide = double.MaxValue;
|
||||||
|
foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs)
|
||||||
// Test1 -> Orig1
|
{
|
||||||
var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down);
|
var d = SlideDistance(moving, movingLoc, stationary, stationaryLoc, PushDirection.Down);
|
||||||
if (d < minSlide) minSlide = d;
|
|
||||||
|
|
||||||
// Test1 -> Orig2
|
|
||||||
d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down);
|
|
||||||
if (d < minSlide) minSlide = d;
|
|
||||||
|
|
||||||
// Test2 -> Orig1
|
|
||||||
d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down);
|
|
||||||
if (d < minSlide) minSlide = d;
|
|
||||||
|
|
||||||
// Test2 -> Orig2
|
|
||||||
d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down);
|
|
||||||
if (d < minSlide) minSlide = d;
|
if (d < minSlide) minSlide = d;
|
||||||
|
}
|
||||||
|
|
||||||
if (minSlide >= double.MaxValue || minSlide < 0)
|
if (minSlide >= double.MaxValue || minSlide < 0)
|
||||||
return pairHeight + partSpacing;
|
return pairHeight + partSpacing;
|
||||||
@@ -225,12 +216,9 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
// --- Step 3: Iterative Adjustment ---
|
// --- Step 3: Iterative Adjustment ---
|
||||||
|
|
||||||
private List<Part> AdjustColumn(
|
private List<Part> AdjustColumn(PartPair pair, List<Part> column, CancellationToken token)
|
||||||
(Part part1, Part part2, Box pairBbox) pair,
|
|
||||||
List<Part> column,
|
|
||||||
CancellationToken token)
|
|
||||||
{
|
{
|
||||||
var originalPairWidth = pair.pairBbox.Width;
|
var originalPairWidth = pair.Bbox.Width;
|
||||||
|
|
||||||
for (var iteration = 0; iteration < MaxIterations; iteration++)
|
for (var iteration = 0; iteration < MaxIterations; iteration++)
|
||||||
{
|
{
|
||||||
@@ -261,7 +249,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (adjusted == null)
|
if (adjusted == null)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox);
|
var newColumn = BuildColumn(adjusted.Value);
|
||||||
if (newColumn.Count == 0)
|
if (newColumn.Count == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -272,9 +260,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Part part1, Part part2, Box pairBbox)? TryAdjustPair(
|
private PartPair? TryAdjustPair(PartPair pair, double adjustment, double originalPairWidth)
|
||||||
(Part part1, Part part2, Box pairBbox) pair,
|
|
||||||
double adjustment, double originalPairWidth)
|
|
||||||
{
|
{
|
||||||
// Try shifting part2 up first.
|
// Try shifting part2 up first.
|
||||||
var result = TryShiftDirection(pair, adjustment, originalPairWidth);
|
var result = TryShiftDirection(pair, adjustment, originalPairWidth);
|
||||||
@@ -286,13 +272,11 @@ namespace OpenNest.Engine.Fill
|
|||||||
return TryShiftDirection(pair, -adjustment, originalPairWidth);
|
return TryShiftDirection(pair, -adjustment, originalPairWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Part part1, Part part2, Box pairBbox)? TryShiftDirection(
|
private PartPair? TryShiftDirection(PartPair pair, double verticalShift, double originalPairWidth)
|
||||||
(Part part1, Part part2, Box pairBbox) pair,
|
|
||||||
double verticalShift, double originalPairWidth)
|
|
||||||
{
|
{
|
||||||
// Clone parts so we don't mutate the originals.
|
// Clone parts so we don't mutate the originals.
|
||||||
var p1 = (Part)pair.part1.Clone();
|
var p1 = (Part)pair.Part1.Clone();
|
||||||
var p2 = (Part)pair.part2.Clone();
|
var p2 = (Part)pair.Part2.Clone();
|
||||||
|
|
||||||
// Separate: shift part2 right so bounding boxes don't touch.
|
// Separate: shift part2 right so bounding boxes don't touch.
|
||||||
p2.Offset(partSpacing, 0);
|
p2.Offset(partSpacing, 0);
|
||||||
@@ -308,20 +292,12 @@ namespace OpenNest.Engine.Fill
|
|||||||
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
|
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
|
||||||
|
|
||||||
// Check if the pair got wider.
|
// 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)
|
if (newBbox.Width > originalPairWidth + Tolerance.Epsilon)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Re-anchor to work area origin.
|
return AnchorToWorkArea(p1, p2);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Step 4: Horizontal Repetition ---
|
// --- Step 4: Horizontal Repetition ---
|
||||||
@@ -331,110 +307,35 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (column.Count == 0)
|
if (column.Count == 0)
|
||||||
return column;
|
return column;
|
||||||
|
|
||||||
var columnBbox = ((IEnumerable<IBoundable>)column).GetBoundingBox();
|
var pattern = new Pattern();
|
||||||
var columnWidth = columnBbox.Width;
|
pattern.Parts.AddRange(column);
|
||||||
|
pattern.UpdateBounds();
|
||||||
|
|
||||||
// Create a test column shifted right by columnWidth + spacing.
|
var linear = new FillLinear(workArea, partSpacing);
|
||||||
var testOffset = columnWidth + partSpacing;
|
return linear.Fill(pattern, NestDirection.Horizontal);
|
||||||
var testColumn = new List<Part>(column.Count);
|
}
|
||||||
foreach (var part in column)
|
|
||||||
testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0)));
|
|
||||||
|
|
||||||
// Compact the test column left against the original column.
|
// --- Helpers ---
|
||||||
var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left);
|
|
||||||
|
|
||||||
// Derive the true copy distance from where the test column ended up.
|
private PartPair? AnchorToWorkArea(Part part1, Part part2)
|
||||||
var testBbox = ((IEnumerable<IBoundable>)testColumn).GetBoundingBox();
|
|
||||||
var copyDistance = testBbox.Left - columnBbox.Left;
|
|
||||||
|
|
||||||
if (copyDistance <= Tolerance.Epsilon)
|
|
||||||
copyDistance = columnWidth + partSpacing;
|
|
||||||
|
|
||||||
// Safety: if the compacted test column overlaps the original column,
|
|
||||||
// fall back to bbox-based spacing.
|
|
||||||
var probe = new List<Part>(column);
|
|
||||||
probe.AddRange(testColumn.Where(IsWithinWorkArea));
|
|
||||||
if (HasOverlappingParts(probe))
|
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"[FillExtents] Compacted column overlaps, falling back to bbox spacing");
|
var bbox = PairBbox(part1, part2);
|
||||||
copyDistance = columnWidth + partSpacing;
|
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.
|
bbox = PairBbox(part1, part2);
|
||||||
testColumn.Clear();
|
return new PartPair(part1, part2, bbox);
|
||||||
foreach (var part in column)
|
|
||||||
testColumn.Add(part.CloneAtOffset(new Vector(copyDistance, 0)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})");
|
private static Box PairBbox(Part part1, Part part2) =>
|
||||||
|
((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
|
||||||
|
|
||||||
// Build all columns.
|
private static bool HasOverlappingParts(List<Part> parts) =>
|
||||||
var result = new List<Part>(column);
|
FillHelpers.HasOverlappingParts(parts);
|
||||||
|
|
||||||
// Add the test column we already computed as column 2.
|
private readonly record struct PartPair(Part Part1, Part Part2, Box Bbox);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Fill
|
namespace OpenNest.Engine.Fill
|
||||||
@@ -19,6 +20,11 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
public double HalfSpacing => PartSpacing / 2;
|
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)
|
private static Vector MakeOffset(NestDirection direction, double distance)
|
||||||
{
|
{
|
||||||
return direction == NestDirection.Horizontal
|
return direction == NestDirection.Horizontal
|
||||||
@@ -323,7 +329,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
return result;
|
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++)
|
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)
|
var overlapY = System.Math.Min(b1.Top, b2.Top)
|
||||||
- System.Math.Max(b1.Bottom, b2.Bottom);
|
- 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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overlapA = -1;
|
||||||
|
overlapB = -1;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,10 +399,9 @@ namespace OpenNest.Engine.Fill
|
|||||||
var row = new List<Part>(pattern.Parts);
|
var row = new List<Part>(pattern.Parts);
|
||||||
row.AddRange(TilePattern(pattern, direction, boundaries));
|
row.AddRange(TilePattern(pattern, direction, boundaries));
|
||||||
|
|
||||||
// Safety: if geometry-aware spacing produced overlapping parts,
|
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a1, out var b1))
|
||||||
// fall back to bbox-based spacing for this axis.
|
|
||||||
if (pattern.Parts.Count > 1 && HasOverlappingParts(row))
|
|
||||||
{
|
{
|
||||||
|
LogOverlap("Step1-Primary", direction, pattern, row, a1, b1);
|
||||||
row = new List<Part>(pattern.Parts);
|
row = new List<Part>(pattern.Parts);
|
||||||
row.AddRange(TilePatternBbox(pattern, direction));
|
row.AddRange(TilePatternBbox(pattern, direction));
|
||||||
}
|
}
|
||||||
@@ -397,8 +411,9 @@ namespace OpenNest.Engine.Fill
|
|||||||
{
|
{
|
||||||
row.AddRange(TilePattern(pattern, perpAxis, boundaries));
|
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 = new List<Part>(pattern.Parts);
|
||||||
row.AddRange(TilePatternBbox(pattern, perpAxis));
|
row.AddRange(TilePatternBbox(pattern, perpAxis));
|
||||||
}
|
}
|
||||||
@@ -415,9 +430,45 @@ namespace OpenNest.Engine.Fill
|
|||||||
var gridResult = new List<Part>(rowPattern.Parts);
|
var gridResult = new List<Part>(rowPattern.Parts);
|
||||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
|
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;
|
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>
|
/// <summary>
|
||||||
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
|
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -167,134 +167,84 @@ namespace OpenNest.Engine.Fill
|
|||||||
/// Sorts pair columns by height (shortest first on the left) to create
|
/// Sorts pair columns by height (shortest first on the left) to create
|
||||||
/// a staircase profile that maximizes usable remnant area.
|
/// a staircase profile that maximizes usable remnant area.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static void SortColumnsByHeight(List<Part> parts, double spacing)
|
internal static void SortColumnsByHeight(List<Part> parts, double spacing) =>
|
||||||
{
|
SortStrips(parts, spacing,
|
||||||
if (parts == null || parts.Count <= 1)
|
primaryEdge: b => b.Left, extentEdge: b => b.Right,
|
||||||
return;
|
sortMetric: MaxTop, stripMin: MinLeft, stripMax: MaxRight,
|
||||||
|
makeOffset: d => new Vector(d, 0));
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sorts pair rows by width (narrowest first on the bottom) to create
|
/// Sorts pair rows by width (narrowest first on the bottom) to create
|
||||||
/// a staircase profile on the right side that maximizes usable remnant area.
|
/// a staircase profile on the right side that maximizes usable remnant area.
|
||||||
/// </summary>
|
/// </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)
|
if (parts == null || parts.Count <= 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Sort parts by Bottom edge for grouping.
|
parts.Sort((a, b) => primaryEdge(a.BoundingBox).CompareTo(primaryEdge(b.BoundingBox)));
|
||||||
parts.Sort((a, b) => a.BoundingBox.Bottom.CompareTo(b.BoundingBox.Bottom));
|
|
||||||
|
|
||||||
// Group parts into rows by Y overlap.
|
var strips = new List<List<Part>>();
|
||||||
var rows = new List<List<Part>>();
|
var strip = new List<Part> { parts[0] };
|
||||||
var row = new List<Part> { parts[0] };
|
var stripExtent = extentEdge(parts[0].BoundingBox);
|
||||||
var rowTop = parts[0].BoundingBox.Top;
|
|
||||||
|
|
||||||
for (var i = 1; i < parts.Count; i++)
|
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);
|
strips.Add(strip);
|
||||||
row = new List<Part> { parts[i] };
|
strip = new List<Part> { parts[i] };
|
||||||
rowTop = parts[i].BoundingBox.Top;
|
stripExtent = extentEdge(parts[i].BoundingBox);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
row.Add(parts[i]);
|
strip.Add(parts[i]);
|
||||||
if (parts[i].BoundingBox.Top > rowTop)
|
var extent = extentEdge(parts[i].BoundingBox);
|
||||||
rowTop = parts[i].BoundingBox.Top;
|
if (extent > stripExtent)
|
||||||
|
stripExtent = extent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows.Add(row);
|
strips.Add(strip);
|
||||||
|
|
||||||
if (rows.Count <= 1)
|
if (strips.Count <= 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Measure inter-row gap from original layout.
|
var gap = stripMin(strips[1]) - stripMax(strips[0]);
|
||||||
var gap = MinBottom(rows[1]) - MaxTop(rows[0]);
|
|
||||||
|
|
||||||
// Sort rows by width ascending (narrowest first).
|
strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b)));
|
||||||
rows.Sort((a, b) => MaxRight(a).CompareTo(MaxRight(b)));
|
|
||||||
|
|
||||||
// Reposition rows bottom-to-top.
|
var pos = primaryEdge(parts[0].BoundingBox);
|
||||||
var y = parts[0].BoundingBox.Bottom; // parts already sorted by Bottom
|
|
||||||
|
|
||||||
foreach (var r in rows)
|
foreach (var s in strips)
|
||||||
{
|
{
|
||||||
var rowBottom = MinBottom(r);
|
var delta = pos - stripMin(s);
|
||||||
var dy = y - rowBottom;
|
|
||||||
|
|
||||||
if (System.Math.Abs(dy) > OpenNest.Math.Tolerance.Epsilon)
|
if (System.Math.Abs(delta) > OpenNest.Math.Tolerance.Epsilon)
|
||||||
{
|
{
|
||||||
var offset = new Vector(0, dy);
|
var offset = makeOffset(delta);
|
||||||
foreach (var part in r)
|
foreach (var part in s)
|
||||||
part.Offset(offset);
|
part.Offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
y = MaxTop(r) + gap;
|
pos = stripMax(s) + gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild the parts list in row order.
|
|
||||||
parts.Clear();
|
parts.Clear();
|
||||||
foreach (var r in rows)
|
foreach (var s in strips)
|
||||||
parts.AddRange(r);
|
parts.AddRange(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double MaxTop(List<Part> col)
|
private static double MaxTop(List<Part> col)
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (pattern.Parts.Count == 0)
|
if (pattern.Parts.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var engine = new FillLinear(workArea, partSpacing);
|
var engine = new FillLinear(workArea, partSpacing) { Label = "Pairs" };
|
||||||
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||||
{
|
{
|
||||||
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
|
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
|
||||||
@@ -321,7 +321,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filler = new FillLinear(remnantBox, partSpacing);
|
var filler = new FillLinear(remnantBox, partSpacing) { Label = "Pairs-Remnant" };
|
||||||
List<Part> parts = null;
|
List<Part> parts = null;
|
||||||
|
|
||||||
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ public class StripeFiller
|
|||||||
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
|
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var stripeEngine = new FillLinear(stripeBox, spacing);
|
var stripeEngine = new FillLinear(stripeBox, spacing) { Label = "Stripe" };
|
||||||
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
||||||
|
|
||||||
if (stripeParts == null || stripeParts.Count == 0)
|
if (stripeParts == null || stripeParts.Count == 0)
|
||||||
@@ -136,7 +136,7 @@ public class StripeFiller
|
|||||||
stripePattern.Parts.AddRange(stripeParts);
|
stripePattern.Parts.AddRange(stripeParts);
|
||||||
stripePattern.UpdateBounds();
|
stripePattern.UpdateBounds();
|
||||||
|
|
||||||
var gridEngine = new FillLinear(workArea, spacing);
|
var gridEngine = new FillLinear(workArea, spacing) { Label = "Stripe-Grid" };
|
||||||
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
||||||
|
|
||||||
if (gridParts == null || gridParts.Count == 0)
|
if (gridParts == null || gridParts.Count == 0)
|
||||||
@@ -244,7 +244,7 @@ public class StripeFiller
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filler = new FillLinear(remnantBox, spacing);
|
var filler = new FillLinear(remnantBox, spacing) { Label = "Stripe-Remnant" };
|
||||||
List<Part> best = null;
|
List<Part> best = null;
|
||||||
|
|
||||||
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
||||||
@@ -396,7 +396,7 @@ public class StripeFiller
|
|||||||
var stripeBox = axis == NestDirection.Horizontal
|
var stripeBox = axis == NestDirection.Horizontal
|
||||||
? new Box(0, 0, sheetSpan, perpDim)
|
? new Box(0, 0, sheetSpan, perpDim)
|
||||||
: new Box(0, 0, perpDim, sheetSpan);
|
: 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 filled = engine.Fill(rotated, axis);
|
||||||
var n = filled?.Count ?? 0;
|
var n = filled?.Count ?? 0;
|
||||||
|
|
||||||
@@ -481,33 +481,6 @@ public class StripeFiller
|
|||||||
return axis == NestDirection.Horizontal ? box.Width : box.Length;
|
return axis == NestDirection.Horizontal ? box.Width : box.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static bool HasOverlappingParts(List<Part> parts) =>
|
||||||
/// Checks if any pair of parts geometrically overlap. Uses bounding box
|
FillHelpers.HasOverlappingParts(parts);
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using OpenNest.Engine;
|
|||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Engine.Strategies;
|
using OpenNest.Engine.Strategies;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -275,8 +276,12 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var box2 = parts[j].BoundingBox;
|
var box2 = parts[j].BoundingBox;
|
||||||
|
|
||||||
if (box1.Right < box2.Left || box2.Right < box1.Left ||
|
var overlapX = System.Math.Min(box1.Right, box2.Right)
|
||||||
box1.Top < box2.Bottom || box2.Top < box1.Bottom)
|
- 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;
|
continue;
|
||||||
|
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
|
|||||||
@@ -36,52 +36,44 @@ namespace OpenNest.RectanglePacking
|
|||||||
throw new NotImplementedException();
|
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;
|
var bin = Bin.Clone() as Bin;
|
||||||
|
|
||||||
int normalColumns = 0;
|
var primarySize = horizontal ? item.Width : item.Length;
|
||||||
int rotateColumns = 0;
|
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;
|
return bin;
|
||||||
|
|
||||||
var normalRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Length);
|
var normalSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / secondarySize);
|
||||||
var rotateRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Width);
|
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;
|
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();
|
item.Rotate();
|
||||||
|
|
||||||
bin.Items.AddRange(FillGrid(item, rotateRows, rotateColumns, int.MaxValue));
|
bin.Items.AddRange(FillGrid(item, rotateRows, rotateCols, 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));
|
|
||||||
|
|
||||||
return bin;
|
return bin;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,22 +20,10 @@ namespace OpenNest.Engine.Strategies
|
|||||||
|
|
||||||
var angles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
var angles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||||
|
|
||||||
List<Part> best = null;
|
return FillHelpers.BestOverAngles(context, angles,
|
||||||
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
angle => filler.Fill(context.Item.Drawing, angle,
|
||||||
|
context.PlateNumber, context.Token, context.Progress),
|
||||||
foreach (var angle in angles)
|
NestPhase.Extents, "Extents");
|
||||||
{
|
|
||||||
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>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,5 +109,78 @@ namespace OpenNest.Engine.Strategies
|
|||||||
var fallback = fillFunc(other);
|
var fallback = fillFunc(other);
|
||||||
return fallback ?? new List<Part>();
|
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 workArea = context.WorkArea;
|
||||||
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
||||||
var preferred = context.Policy?.PreferredDirection;
|
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 engine = new FillLinear(workArea, context.Plate.PartSpacing) { Label = "Linear" };
|
||||||
|
|
||||||
var angle = angles[ai];
|
|
||||||
var engine = new FillLinear(workArea, context.Plate.PartSpacing);
|
|
||||||
|
|
||||||
var result = FillHelpers.FillWithDirectionPreference(
|
var result = FillHelpers.FillWithDirectionPreference(
|
||||||
dir => engine.Fill(context.Item.Drawing, angle, dir),
|
dir => engine.Fill(context.Item.Drawing, angle, dir),
|
||||||
preferred, comparer, workArea);
|
preferred, comparer, workArea);
|
||||||
|
|
||||||
var angleDeg = Angle.ToDegrees(angle);
|
|
||||||
|
|
||||||
if (result != null && result.Count > 0)
|
if (result != null && result.Count > 0)
|
||||||
{
|
{
|
||||||
context.AngleResults.Add(new AngleResult
|
context.AngleResults.Add(new AngleResult
|
||||||
{
|
{
|
||||||
AngleDeg = angleDeg,
|
AngleDeg = Angle.ToDegrees(angle),
|
||||||
Direction = preferred ?? NestDirection.Horizontal,
|
Direction = preferred ?? NestDirection.Horizontal,
|
||||||
PartCount = result.Count
|
PartCount = result.Count
|
||||||
});
|
});
|
||||||
|
|
||||||
if (best == null || comparer.IsBetter(result, best, workArea))
|
|
||||||
best = result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NestEngineBase.ReportProgress(context.Progress, new ProgressReport
|
return result;
|
||||||
{
|
},
|
||||||
Phase = NestPhase.Linear,
|
NestPhase.Linear, "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>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 current = (Arc)result[i];
|
||||||
var next = (Arc)result[i + 1];
|
var next = (Arc)result[i + 1];
|
||||||
var gap = current.EndPoint().DistanceTo(next.StartPoint());
|
var gap = current.EndPoint().DistanceTo(next.StartPoint());
|
||||||
Assert.True(gap < 0.001,
|
Assert.True(gap < 1e-6,
|
||||||
$"Gap of {gap:F6} between arc {i} and arc {i + 1}");
|
$"Gap of {gap:E4} between arc {i} and arc {i + 1}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastArc = (Arc)result[^1];
|
var lastArc = (Arc)result[^1];
|
||||||
var firstArc = (Arc)result[0];
|
var firstArc = (Arc)result[0];
|
||||||
var closingGap = lastArc.EndPoint().DistanceTo(firstArc.StartPoint());
|
var closingGap = lastArc.EndPoint().DistanceTo(firstArc.StartPoint());
|
||||||
Assert.True(closingGap < 0.001,
|
Assert.True(closingGap < 1e-6,
|
||||||
$"Closing gap of {closingGap:F6}");
|
$"Closing gap of {closingGap:E4}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -107,4 +107,5 @@ public class StrategyOverlapTests
|
|||||||
|
|
||||||
Assert.Empty(failures);
|
Assert.Empty(failures);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using OpenNest.Controls;
|
using OpenNest.Controls;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
namespace OpenNest.Actions
|
namespace OpenNest.Actions
|
||||||
{
|
{
|
||||||
@@ -11,6 +13,19 @@ namespace OpenNest.Actions
|
|||||||
this.plateView = plateView;
|
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 bool SurvivesPlateChange => false;
|
||||||
|
|
||||||
public virtual void OnPlateChanged() { }
|
public virtual void OnPlateChanged() { }
|
||||||
|
|||||||
@@ -212,36 +212,7 @@ namespace OpenNest.Actions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectangleF GetRectangle()
|
private RectangleF GetRectangle() => GetRectangle(Point1, Point2);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SelectionType SelectionType
|
public SelectionType SelectionType
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -166,36 +166,7 @@ namespace OpenNest.Actions
|
|||||||
plateView.Refresh();
|
plateView.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectangleF GetRectangle()
|
private RectangleF GetRectangle() => GetRectangle(Point1, Point2);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Status
|
public enum Status
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
var bendLinksPanel = new FlowLayoutPanel
|
var bendLinksPanel = new FlowLayoutPanel
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Bottom,
|
Dock = DockStyle.Top,
|
||||||
Height = 20,
|
Height = 20,
|
||||||
FlowDirection = FlowDirection.LeftToRight,
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
WrapContents = false
|
WrapContents = false
|
||||||
|
|||||||
@@ -1243,15 +1243,24 @@ namespace OpenNest.Controls
|
|||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
var progressForm = new NestProgressForm(cts, showPlateRow: false);
|
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 =>
|
var progress = new Progress<NestProgress>(p =>
|
||||||
{
|
{
|
||||||
progressForm.UpdateProgress(p);
|
progressForm.UpdateProgress(p);
|
||||||
|
|
||||||
if (p.IsOverallBest)
|
if (p.IsOverallBest)
|
||||||
SetStationaryParts(p.BestParts);
|
progressForm.UpdatePreview(p.BestParts);
|
||||||
else
|
|
||||||
SetActiveParts(p.BestParts);
|
|
||||||
|
|
||||||
|
SetActiveParts(p.BestParts);
|
||||||
ActiveWorkArea = p.ActiveWorkArea;
|
ActiveWorkArea = p.ActiveWorkArea;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -905,15 +905,24 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var progressForm = new NestProgressForm(nestingCts, showPlateRow: true);
|
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 =>
|
var progress = new Progress<NestProgress>(p =>
|
||||||
{
|
{
|
||||||
progressForm.UpdateProgress(p);
|
progressForm.UpdateProgress(p);
|
||||||
|
|
||||||
if (p.IsOverallBest)
|
if (p.IsOverallBest)
|
||||||
activeForm.PlateView.SetStationaryParts(p.BestParts);
|
progressForm.UpdatePreview(p.BestParts);
|
||||||
else
|
|
||||||
activeForm.PlateView.SetActiveParts(p.BestParts);
|
|
||||||
|
|
||||||
|
activeForm.PlateView.SetActiveParts(p.BestParts);
|
||||||
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -939,8 +948,20 @@ namespace OpenNest.Forms
|
|||||||
: activeForm.PlateView.Plate;
|
: activeForm.PlateView.Plate;
|
||||||
|
|
||||||
if (plate != activeForm.PlateView.Plate)
|
if (plate != activeForm.PlateView.Plate)
|
||||||
|
{
|
||||||
activeForm.LoadLastPlate();
|
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 anyPlaced = false;
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
|||||||
+70
-22
@@ -41,6 +41,14 @@ namespace OpenNest.Forms
|
|||||||
buttonPanel = new System.Windows.Forms.FlowLayoutPanel();
|
buttonPanel = new System.Windows.Forms.FlowLayoutPanel();
|
||||||
stopButton = new System.Windows.Forms.Button();
|
stopButton = new System.Windows.Forms.Button();
|
||||||
acceptButton = 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();
|
resultsPanel.SuspendLayout();
|
||||||
resultsTable.SuspendLayout();
|
resultsTable.SuspendLayout();
|
||||||
densityPanel.SuspendLayout();
|
densityPanel.SuspendLayout();
|
||||||
@@ -49,6 +57,40 @@ namespace OpenNest.Forms
|
|||||||
buttonPanel.SuspendLayout();
|
buttonPanel.SuspendLayout();
|
||||||
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
|
||||||
//
|
//
|
||||||
phaseStepper.ActivePhase = null;
|
phaseStepper.ActivePhase = null;
|
||||||
@@ -56,7 +98,7 @@ namespace OpenNest.Forms
|
|||||||
phaseStepper.IsComplete = false;
|
phaseStepper.IsComplete = false;
|
||||||
phaseStepper.Location = new System.Drawing.Point(0, 0);
|
phaseStepper.Location = new System.Drawing.Point(0, 0);
|
||||||
phaseStepper.Name = "phaseStepper";
|
phaseStepper.Name = "phaseStepper";
|
||||||
phaseStepper.Size = new System.Drawing.Size(450, 60);
|
phaseStepper.Size = new System.Drawing.Size(266, 60);
|
||||||
phaseStepper.TabIndex = 0;
|
phaseStepper.TabIndex = 0;
|
||||||
//
|
//
|
||||||
// resultsPanel
|
// resultsPanel
|
||||||
@@ -69,7 +111,7 @@ namespace OpenNest.Forms
|
|||||||
resultsPanel.Margin = new System.Windows.Forms.Padding(10, 4, 10, 4);
|
resultsPanel.Margin = new System.Windows.Forms.Padding(10, 4, 10, 4);
|
||||||
resultsPanel.Name = "resultsPanel";
|
resultsPanel.Name = "resultsPanel";
|
||||||
resultsPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10);
|
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;
|
resultsPanel.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// resultsTable
|
// 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.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;
|
resultsTable.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// partsLabel
|
// partsLabel
|
||||||
@@ -115,7 +157,7 @@ namespace OpenNest.Forms
|
|||||||
partsValue.Name = "partsValue";
|
partsValue.Name = "partsValue";
|
||||||
partsValue.Size = new System.Drawing.Size(13, 15);
|
partsValue.Size = new System.Drawing.Size(13, 15);
|
||||||
partsValue.TabIndex = 1;
|
partsValue.TabIndex = 1;
|
||||||
partsValue.Text = "�";
|
partsValue.Text = "\u2014";
|
||||||
//
|
//
|
||||||
// densityLabel
|
// densityLabel
|
||||||
//
|
//
|
||||||
@@ -137,7 +179,7 @@ namespace OpenNest.Forms
|
|||||||
densityPanel.Location = new System.Drawing.Point(90, 23);
|
densityPanel.Location = new System.Drawing.Point(90, 23);
|
||||||
densityPanel.Margin = new System.Windows.Forms.Padding(0);
|
densityPanel.Margin = new System.Windows.Forms.Padding(0);
|
||||||
densityPanel.Name = "densityPanel";
|
densityPanel.Name = "densityPanel";
|
||||||
densityPanel.Size = new System.Drawing.Size(262, 21);
|
densityPanel.Size = new System.Drawing.Size(148, 21);
|
||||||
densityPanel.TabIndex = 3;
|
densityPanel.TabIndex = 3;
|
||||||
densityPanel.WrapContents = false;
|
densityPanel.WrapContents = false;
|
||||||
//
|
//
|
||||||
@@ -150,14 +192,14 @@ namespace OpenNest.Forms
|
|||||||
densityValue.Name = "densityValue";
|
densityValue.Name = "densityValue";
|
||||||
densityValue.Size = new System.Drawing.Size(13, 15);
|
densityValue.Size = new System.Drawing.Size(13, 15);
|
||||||
densityValue.TabIndex = 0;
|
densityValue.TabIndex = 0;
|
||||||
densityValue.Text = "�";
|
densityValue.Text = "\u2014";
|
||||||
//
|
//
|
||||||
// densityBar
|
// densityBar
|
||||||
//
|
//
|
||||||
densityBar.Location = new System.Drawing.Point(21, 5);
|
densityBar.Location = new System.Drawing.Point(21, 5);
|
||||||
densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0);
|
densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0);
|
||||||
densityBar.Name = "densityBar";
|
densityBar.Name = "densityBar";
|
||||||
densityBar.Size = new System.Drawing.Size(241, 8);
|
densityBar.Size = new System.Drawing.Size(127, 8);
|
||||||
densityBar.TabIndex = 1;
|
densityBar.TabIndex = 1;
|
||||||
densityBar.Value = 0D;
|
densityBar.Value = 0D;
|
||||||
//
|
//
|
||||||
@@ -182,7 +224,7 @@ namespace OpenNest.Forms
|
|||||||
nestedAreaValue.Name = "nestedAreaValue";
|
nestedAreaValue.Name = "nestedAreaValue";
|
||||||
nestedAreaValue.Size = new System.Drawing.Size(13, 15);
|
nestedAreaValue.Size = new System.Drawing.Size(13, 15);
|
||||||
nestedAreaValue.TabIndex = 5;
|
nestedAreaValue.TabIndex = 5;
|
||||||
nestedAreaValue.Text = "�";
|
nestedAreaValue.Text = "\u2014";
|
||||||
//
|
//
|
||||||
// resultsHeader
|
// resultsHeader
|
||||||
//
|
//
|
||||||
@@ -206,7 +248,7 @@ namespace OpenNest.Forms
|
|||||||
statusPanel.Location = new System.Drawing.Point(0, 180);
|
statusPanel.Location = new System.Drawing.Point(0, 180);
|
||||||
statusPanel.Name = "statusPanel";
|
statusPanel.Name = "statusPanel";
|
||||||
statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10);
|
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;
|
statusPanel.TabIndex = 2;
|
||||||
//
|
//
|
||||||
// statusTable
|
// 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.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;
|
statusTable.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// plateLabel
|
// plateLabel
|
||||||
@@ -252,7 +294,7 @@ namespace OpenNest.Forms
|
|||||||
plateValue.Name = "plateValue";
|
plateValue.Name = "plateValue";
|
||||||
plateValue.Size = new System.Drawing.Size(13, 15);
|
plateValue.Size = new System.Drawing.Size(13, 15);
|
||||||
plateValue.TabIndex = 1;
|
plateValue.TabIndex = 1;
|
||||||
plateValue.Text = "�";
|
plateValue.Text = "\u2014";
|
||||||
//
|
//
|
||||||
// elapsedLabel
|
// elapsedLabel
|
||||||
//
|
//
|
||||||
@@ -298,7 +340,7 @@ namespace OpenNest.Forms
|
|||||||
descriptionValue.Name = "descriptionValue";
|
descriptionValue.Name = "descriptionValue";
|
||||||
descriptionValue.Size = new System.Drawing.Size(20, 17);
|
descriptionValue.Size = new System.Drawing.Size(20, 17);
|
||||||
descriptionValue.TabIndex = 5;
|
descriptionValue.TabIndex = 5;
|
||||||
descriptionValue.Text = "�";
|
descriptionValue.Text = "\u2014";
|
||||||
//
|
//
|
||||||
// statusHeader
|
// statusHeader
|
||||||
//
|
//
|
||||||
@@ -323,14 +365,14 @@ namespace OpenNest.Forms
|
|||||||
buttonPanel.Location = new System.Drawing.Point(0, 295);
|
buttonPanel.Location = new System.Drawing.Point(0, 295);
|
||||||
buttonPanel.Name = "buttonPanel";
|
buttonPanel.Name = "buttonPanel";
|
||||||
buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6);
|
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;
|
buttonPanel.TabIndex = 3;
|
||||||
//
|
//
|
||||||
// stopButton
|
// stopButton
|
||||||
//
|
//
|
||||||
stopButton.Enabled = false;
|
stopButton.Enabled = false;
|
||||||
stopButton.Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
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.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3);
|
||||||
stopButton.Name = "stopButton";
|
stopButton.Name = "stopButton";
|
||||||
stopButton.Size = new System.Drawing.Size(93, 27);
|
stopButton.Size = new System.Drawing.Size(93, 27);
|
||||||
@@ -343,7 +385,7 @@ namespace OpenNest.Forms
|
|||||||
//
|
//
|
||||||
acceptButton.Enabled = false;
|
acceptButton.Enabled = false;
|
||||||
acceptButton.Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
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.Margin = new System.Windows.Forms.Padding(6, 3, 0, 3);
|
||||||
acceptButton.Name = "acceptButton";
|
acceptButton.Name = "acceptButton";
|
||||||
acceptButton.Size = new System.Drawing.Size(93, 27);
|
acceptButton.Size = new System.Drawing.Size(93, 27);
|
||||||
@@ -356,18 +398,22 @@ namespace OpenNest.Forms
|
|||||||
//
|
//
|
||||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
ClientSize = new System.Drawing.Size(450, 345);
|
ClientSize = new System.Drawing.Size(750, 420);
|
||||||
Controls.Add(buttonPanel);
|
Controls.Add(splitContainer);
|
||||||
Controls.Add(statusPanel);
|
FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
|
||||||
Controls.Add(resultsPanel);
|
|
||||||
Controls.Add(phaseStepper);
|
|
||||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
|
MinimumSize = new System.Drawing.Size(550, 380);
|
||||||
Name = "NestProgressForm";
|
Name = "NestProgressForm";
|
||||||
ShowInTaskbar = false;
|
ShowInTaskbar = false;
|
||||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
Text = "Nesting Progress";
|
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.ResumeLayout(false);
|
||||||
resultsPanel.PerformLayout();
|
resultsPanel.PerformLayout();
|
||||||
resultsTable.ResumeLayout(false);
|
resultsTable.ResumeLayout(false);
|
||||||
@@ -380,7 +426,6 @@ namespace OpenNest.Forms
|
|||||||
statusTable.PerformLayout();
|
statusTable.PerformLayout();
|
||||||
buttonPanel.ResumeLayout(false);
|
buttonPanel.ResumeLayout(false);
|
||||||
ResumeLayout(false);
|
ResumeLayout(false);
|
||||||
PerformLayout();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -409,5 +454,8 @@ namespace OpenNest.Forms
|
|||||||
private System.Windows.Forms.FlowLayoutPanel buttonPanel;
|
private System.Windows.Forms.FlowLayoutPanel buttonPanel;
|
||||||
private System.Windows.Forms.Button acceptButton;
|
private System.Windows.Forms.Button acceptButton;
|
||||||
private System.Windows.Forms.Button stopButton;
|
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 bool Accepted { get; private set; }
|
||||||
|
|
||||||
|
public Plate PreviewPlate
|
||||||
|
{
|
||||||
|
get => previewPlateView.Plate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
previewPlateView.Plate = value;
|
||||||
|
previewPlateView.ZoomToFit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true)
|
public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true)
|
||||||
{
|
{
|
||||||
this.cts = cts;
|
this.cts = cts;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
previewPlateView.AllowSelect = false;
|
||||||
|
|
||||||
if (!showPlateRow)
|
if (!showPlateRow)
|
||||||
{
|
{
|
||||||
plateLabel.Visible = false;
|
plateLabel.Visible = false;
|
||||||
@@ -59,8 +71,10 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
|
|
||||||
phaseStepper.ActivePhase = progress.Phase;
|
phaseStepper.ActivePhase = progress.Phase;
|
||||||
|
|
||||||
SetValueWithFlash(plateValue, progress.PlateNumber.ToString());
|
SetValueWithFlash(plateValue, progress.PlateNumber.ToString());
|
||||||
|
|
||||||
|
if (progress.IsOverallBest)
|
||||||
|
{
|
||||||
SetValueWithFlash(partsValue, progress.BestPartCount.ToString());
|
SetValueWithFlash(partsValue, progress.BestPartCount.ToString());
|
||||||
|
|
||||||
var densityText = progress.BestDensity.ToString("P1");
|
var densityText = progress.BestDensity.ToString("P1");
|
||||||
@@ -70,12 +84,27 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
SetValueWithFlash(nestedAreaValue,
|
SetValueWithFlash(nestedAreaValue,
|
||||||
$"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)");
|
$"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)");
|
||||||
|
}
|
||||||
|
|
||||||
descriptionValue.Text = !string.IsNullOrEmpty(progress.Description)
|
descriptionValue.Text = !string.IsNullOrEmpty(progress.Description)
|
||||||
? progress.Description
|
? progress.Description
|
||||||
: progress.Phase.DisplayName();
|
: 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()
|
public void ShowCompleted()
|
||||||
{
|
{
|
||||||
if (IsDisposed || !IsHandleCreated)
|
if (IsDisposed || !IsHandleCreated)
|
||||||
@@ -135,6 +164,8 @@ namespace OpenNest.Forms
|
|||||||
if (!cts.IsCancellationRequested)
|
if (!cts.IsCancellationRequested)
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
|
|
||||||
|
previewPlateView.Dispose();
|
||||||
|
|
||||||
base.OnFormClosing(e);
|
base.OnFormClosing(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ namespace OpenNest.Forms
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var workArea = new Box(0, 0, plateSize.Length, plateSize.Width);
|
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);
|
var hParts = filler.Fill(pattern, NestDirection.Horizontal);
|
||||||
foreach (var part in hParts)
|
foreach (var part in hParts)
|
||||||
@@ -228,7 +228,7 @@ namespace OpenNest.Forms
|
|||||||
hLabel.Text = $"Horizontal — {hParts.Count} parts";
|
hLabel.Text = $"Horizontal — {hParts.Count} parts";
|
||||||
hPreview.ZoomToFit();
|
hPreview.ZoomToFit();
|
||||||
|
|
||||||
var vFiller = new FillLinear(workArea, PartSpacing);
|
var vFiller = new FillLinear(workArea, PartSpacing) { Label = "PatternTile-V" };
|
||||||
var vParts = vFiller.Fill(pattern, NestDirection.Vertical);
|
var vParts = vFiller.Fill(pattern, NestDirection.Vertical);
|
||||||
foreach (var part in vParts)
|
foreach (var part in vParts)
|
||||||
vPreview.Plate.Parts.Add(part);
|
vPreview.Plate.Parts.Add(part);
|
||||||
@@ -328,7 +328,7 @@ namespace OpenNest.Forms
|
|||||||
if (pattern == null)
|
if (pattern == null)
|
||||||
return;
|
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);
|
var tiledParts = filler.Fill(pattern, applyDirection);
|
||||||
|
|
||||||
Result = new PatternTileResult
|
Result = new PatternTileResult
|
||||||
|
|||||||
Reference in New Issue
Block a user