Merge branch 'chore/csharpier-sweep'

# Conflicts:
#	OpenNest.Core/Geometry/ArcFit.cs
#	OpenNest.Core/Geometry/GeometrySimplifier.cs
#	OpenNest.Posts.GravographIS/GravographISWriter.cs
#	OpenNest.Posts.GravographIS/NestPolylineExtractor.cs
This commit is contained in:
aj
2026-09-20 16:53:52 -04:00
473 changed files with 16171 additions and 7660 deletions
+38 -30
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -12,16 +12,18 @@ namespace OpenNest.Geometry
private Vector center;
private bool reversed;
public Arc()
{
}
public Arc() { }
public Arc(double x, double y, double r, double a1, double a2, bool reversed = false)
: this(new Vector(x, y), r, a1, a2, reversed)
{
}
: this(new Vector(x, y), r, a1, a2, reversed) { }
public Arc(Vector center, double radius, double startAngle, double endAngle, bool reversed = false)
public Arc(
Vector center,
double radius,
double startAngle,
double endAngle,
bool reversed = false
)
{
this.center = center;
this.radius = radius;
@@ -93,8 +95,7 @@ namespace OpenNest.Geometry
}
}
public bool IsFullCircle() =>
SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
public bool IsFullCircle() => SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
/// <summary>
/// Angle in radians between start and end angles.
@@ -130,10 +131,7 @@ namespace OpenNest.Geometry
public RotationType Rotation
{
get { return IsReversed ? RotationType.CW : RotationType.CCW; }
set
{
IsReversed = (value == RotationType.CW);
}
set { IsReversed = (value == RotationType.CW); }
}
/// <summary>
@@ -144,7 +142,8 @@ namespace OpenNest.Geometry
{
return new Vector(
Center.X + Radius * System.Math.Cos(StartAngle),
Center.Y + Radius * System.Math.Sin(StartAngle));
Center.Y + Radius * System.Math.Sin(StartAngle)
);
}
/// <summary>
@@ -155,7 +154,8 @@ namespace OpenNest.Geometry
{
return new Vector(
Center.X + Radius * System.Math.Cos(EndAngle),
Center.Y + Radius * System.Math.Sin(EndAngle));
Center.Y + Radius * System.Math.Sin(EndAngle)
);
}
/// <summary>
@@ -166,7 +166,8 @@ namespace OpenNest.Geometry
var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
return new Vector(
Center.X + Radius * System.Math.Cos(midAngle),
Center.Y + Radius * System.Math.Sin(midAngle));
Center.Y + Radius * System.Math.Sin(midAngle)
);
}
/// <summary>
@@ -231,7 +232,10 @@ namespace OpenNest.Geometry
return 1;
var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius);
return System.Math.Max(1, (int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle));
return System.Math.Max(
1,
(int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle)
);
}
/// <summary>
@@ -242,21 +246,23 @@ namespace OpenNest.Geometry
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{
var points = new List<Vector>();
var stepAngle = reversed
? -SweepAngle() / segments
: SweepAngle() / segments;
var stepAngle = reversed ? -SweepAngle() / segments : SweepAngle() / segments;
var r = circumscribe && segments > 0
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
: Radius;
var r =
circumscribe && segments > 0
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
: Radius;
for (int i = 0; i <= segments; ++i)
{
var angle = stepAngle * i + StartAngle;
points.Add(new Vector(
System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y));
points.Add(
new Vector(
System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y
)
);
}
return points;
@@ -470,7 +476,8 @@ namespace OpenNest.Geometry
{
return new Vector(
System.Math.Cos(angle) * Radius + Center.X,
System.Math.Sin(angle) * Radius + Center.Y);
System.Math.Sin(angle) * Radius + Center.Y
);
}
else
{
@@ -500,7 +507,8 @@ namespace OpenNest.Geometry
/// <returns></returns>
public override bool Intersects(Arc arc, out List<Vector> pts)
{
return Intersect.Intersects(this, arc, out pts); ;
return Intersect.Intersects(this, arc, out pts);
;
}
/// <summary>
+24 -12
View File
@@ -17,10 +17,14 @@ namespace OpenNest.Geometry
foreach (var box in boxes)
{
if (box.Left < minX) minX = box.Left;
if (box.Right > maxX) maxX = box.Right;
if (box.Bottom < minY) minY = box.Bottom;
if (box.Top > maxY) maxY = box.Top;
if (box.Left < minX)
minX = box.Left;
if (box.Right > maxX)
maxX = box.Right;
if (box.Bottom < minY)
minY = box.Bottom;
if (box.Top > maxY)
maxY = box.Top;
}
return new Box(minX, minY, maxX - minX, maxY - minY);
@@ -41,11 +45,15 @@ namespace OpenNest.Geometry
{
var vertex = pts[i];
if (vertex.X < minX) minX = vertex.X;
else if (vertex.X > maxX) maxX = vertex.X;
if (vertex.X < minX)
minX = vertex.X;
else if (vertex.X > maxX)
maxX = vertex.X;
if (vertex.Y < minY) minY = vertex.Y;
else if (vertex.Y > maxY) maxY = vertex.Y;
if (vertex.Y < minY)
minY = vertex.Y;
else if (vertex.Y > maxY)
maxY = vertex.Y;
}
return new Box(minX, minY, maxX - minX, maxY - minY);
@@ -65,10 +73,14 @@ namespace OpenNest.Geometry
foreach (var box in items)
{
if (box.Left < left) left = box.Left;
if (box.Right > right) right = box.Right;
if (box.Bottom < bottom) bottom = box.Bottom;
if (box.Top > top) top = box.Top;
if (box.Left < left)
left = box.Left;
if (box.Right > right)
right = box.Right;
if (box.Bottom < bottom)
bottom = box.Bottom;
if (box.Top > top)
top = box.Top;
}
return new Box(left, bottom, right - left, top - bottom);
+21 -13
View File
@@ -8,9 +8,7 @@ namespace OpenNest.Geometry
public static readonly Box Empty = new Box();
public Box()
: this(0, 0, 0, 0)
{
}
: this(0, 0, 0, 0) { }
public Box(double x, double y, double w, double h)
{
@@ -117,10 +115,14 @@ namespace OpenNest.Geometry
public bool Intersects(Box box)
{
if (Left >= box.Right) return false;
if (Right <= box.Left) return false;
if (Top <= box.Bottom) return false;
if (Bottom >= box.Top) return false;
if (Left >= box.Right)
return false;
if (Right <= box.Left)
return false;
if (Top <= box.Bottom)
return false;
if (Bottom >= box.Top)
return false;
return true;
}
@@ -146,18 +148,24 @@ namespace OpenNest.Geometry
public bool Contains(Box box)
{
if (box.Top > Top) return false;
if (box.Left < Left) return false;
if (box.Right > Right) return false;
if (box.Bottom < Bottom) return false;
if (box.Top > Top)
return false;
if (box.Left < Left)
return false;
if (box.Right > Right)
return false;
if (box.Bottom < Bottom)
return false;
return true;
}
public bool Contains(Vector pt)
{
return pt.X >= Left - Tolerance.Epsilon && pt.X <= Right + Tolerance.Epsilon
&& pt.Y >= Bottom - Tolerance.Epsilon && pt.Y <= Top + Tolerance.Epsilon;
return pt.X >= Left - Tolerance.Epsilon
&& pt.X <= Right + Tolerance.Epsilon
&& pt.Y >= Bottom - Tolerance.Epsilon
&& pt.Y <= Top + Tolerance.Epsilon;
}
public bool IsHorizontalTo(Box box)
+25 -29
View File
@@ -1,5 +1,5 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -8,14 +8,10 @@ namespace OpenNest.Geometry
private Vector center;
private double radius;
public Circle()
{
}
public Circle() { }
public Circle(double x, double y, double radius)
: this(new Vector(x, y), radius)
{
}
: this(new Vector(x, y), radius) { }
public Circle(Vector center, double radius)
{
@@ -137,21 +133,22 @@ namespace OpenNest.Geometry
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{
var points = new List<Vector>();
var stepAngle = Rotation == RotationType.CW
? -Angle.TwoPI / segments
: Angle.TwoPI / segments;
var stepAngle =
Rotation == RotationType.CW ? -Angle.TwoPI / segments : Angle.TwoPI / segments;
var r = circumscribe && segments > 0
? Radius / System.Math.Cos(stepAngle / 2.0)
: Radius;
var r =
circumscribe && segments > 0 ? Radius / System.Math.Cos(stepAngle / 2.0) : Radius;
for (int i = 0; i <= segments; ++i)
{
var angle = stepAngle * i;
points.Add(new Vector(
System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y));
points.Add(
new Vector(
System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y
)
);
}
return points;
@@ -278,11 +275,9 @@ namespace OpenNest.Geometry
{
if (side == OffsetSide.Left && Rotation == RotationType.CCW)
{
return Radius <= distance ? null : new Circle(center, Radius - distance)
{
Layer = Layer,
Rotation = Rotation
};
return Radius <= distance
? null
: new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation };
}
else
{
@@ -294,11 +289,9 @@ namespace OpenNest.Geometry
{
if (ContainsPoint(pt))
{
return Radius <= distance ? null : new Circle(center, Radius - distance)
{
Layer = Layer,
Rotation = Rotation
};
return Radius <= distance
? null
: new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation };
}
else
{
@@ -317,7 +310,8 @@ namespace OpenNest.Geometry
return new Vector(
System.Math.Cos(angle) * Radius + Center.X,
System.Math.Sin(angle) * Radius + Center.Y);
System.Math.Sin(angle) * Radius + Center.Y
);
}
/// <summary>
@@ -350,7 +344,9 @@ namespace OpenNest.Geometry
public override bool Intersects(Circle circle)
{
var dist = Center.DistanceTo(circle.Center);
return (dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius));
return (
dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius)
);
}
/// <summary>
+65 -31
View File
@@ -1,12 +1,16 @@
using OpenNest.Math;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
public static class Collision
{
public static CollisionResult Check(Polygon a, Polygon b,
List<Polygon> holesA = null, List<Polygon> holesB = null)
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))
@@ -46,8 +50,12 @@ namespace OpenNest.Geometry
return new CollisionResult(true, regions, intersectionPoints);
}
public static bool HasOverlap(Polygon a, Polygon b,
List<Polygon> holesA = null, List<Polygon> holesB = null)
public static bool HasOverlap(
Polygon a,
Polygon b,
List<Polygon> holesA = null,
List<Polygon> holesB = null
)
{
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
return false;
@@ -57,8 +65,10 @@ namespace OpenNest.Geometry
return Check(a, b, holesA, holesB).Overlaps;
}
public static List<CollisionResult> CheckAll(List<Polygon> polygons,
List<List<Polygon>> holes = null)
public static List<CollisionResult> CheckAll(
List<Polygon> polygons,
List<List<Polygon>> holes = null
)
{
var results = new List<CollisionResult>();
@@ -78,8 +88,7 @@ namespace OpenNest.Geometry
return results;
}
public static bool HasAnyOverlap(List<Polygon> polygons,
List<List<Polygon>> holes = null)
public static bool HasAnyOverlap(List<Polygon> polygons, List<List<Polygon>> holes = null)
{
for (var i = 0; i < polygons.Count; i++)
{
@@ -98,10 +107,8 @@ namespace OpenNest.Geometry
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);
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;
}
@@ -164,13 +171,19 @@ namespace OpenNest.Geometry
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)
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)
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++)
@@ -231,7 +244,7 @@ namespace OpenNest.Geometry
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);
- (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
}
/// <summary>
@@ -255,12 +268,17 @@ namespace OpenNest.Geometry
/// <summary>
/// Subtracts holes from overlap regions.
/// </summary>
private static List<Polygon> SubtractHoles(List<Polygon> regions,
List<Polygon> holesA, List<Polygon> holesB)
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 (holesA != null)
allHoles.AddRange(holesA);
if (holesB != null)
allHoles.AddRange(holesB);
if (allHoles.Count == 0)
return regions;
@@ -313,9 +331,16 @@ namespace OpenNest.Geometry
var holeCount = holeTri.IsClosed() ? holeVerts.Count - 1 : holeVerts.Count;
var survived = false;
for (var i = 0; i < holeCount; i++)
survived |= AddIfPositiveArea(next,
ClipOutsideHalfSpace(pieceTri, holeVerts[i], holeVerts[(i + 1) % holeCount]));
if (!survived) continue; // piece lies entirely within the hole
survived |= AddIfPositiveArea(
next,
ClipOutsideHalfSpace(
pieceTri,
holeVerts[i],
holeVerts[(i + 1) % holeCount]
)
);
if (!survived)
continue; // piece lies entirely within the hole
}
}
@@ -329,7 +354,11 @@ namespace OpenNest.Geometry
/// Sutherland-Hodgman clip of a convex polygon to the strict outside of the
/// infinite line edgeStart->edgeEnd of a CCW hole edge (Cross &lt; -Epsilon).
/// </summary>
private static List<Vector> ClipOutsideHalfSpace(Polygon piece, Vector edgeStart, Vector edgeEnd)
private static List<Vector> ClipOutsideHalfSpace(
Polygon piece,
Vector edgeStart,
Vector edgeEnd
)
{
var verts = piece.Vertices;
var count = piece.IsClosed() ? verts.Count - 1 : verts.Count;
@@ -340,22 +369,27 @@ namespace OpenNest.Geometry
var next = verts[(i + 1) % count];
var currentInside = Cross(edgeStart, edgeEnd, current) >= -Tolerance.Epsilon;
var nextInside = Cross(edgeStart, edgeEnd, next) >= -Tolerance.Epsilon;
if (!currentInside) kept.Add(current);
if (currentInside == nextInside) continue;
if (!currentInside)
kept.Add(current);
if (currentInside == nextInside)
continue;
var intersection = LineIntersection(edgeStart, edgeEnd, current, next);
if (intersection.IsValid()) kept.Add(intersection);
if (intersection.IsValid())
kept.Add(intersection);
}
return kept;
}
private static bool AddIfPositiveArea(List<Polygon> polygons, List<Vector> vertices)
{
if (vertices.Count < 3) return false;
if (vertices.Count < 3)
return false;
var polygon = new Polygon();
polygon.Vertices.AddRange(vertices);
polygon.Close();
polygon.UpdateBounds();
if (polygon.Area() <= Tolerance.Epsilon) return false;
if (polygon.Area() <= Tolerance.Epsilon)
return false;
polygons.Add(polygon);
return true;
}
+10 -2
View File
@@ -5,9 +5,17 @@ namespace OpenNest.Geometry
{
public class CollisionResult
{
public static readonly CollisionResult None = new(false, new List<Polygon>(), new List<Vector>());
public static readonly CollisionResult None = new(
false,
new List<Polygon>(),
new List<Vector>()
);
public CollisionResult(bool overlaps, List<Polygon> overlapRegions, List<Vector> intersectionPoints)
public CollisionResult(
bool overlaps,
List<Polygon> overlapRegions,
List<Vector> intersectionPoints
)
{
Overlaps = overlaps;
OverlapRegions = overlapRegions;
+13 -4
View File
@@ -19,8 +19,11 @@ namespace OpenNest.Geometry
var verts = new List<Vector>(polygon.Vertices);
// Remove closing vertex if polygon is closed.
if (verts.Count > 1 && verts[0].X == verts[verts.Count - 1].X
&& verts[0].Y == verts[verts.Count - 1].Y)
if (
verts.Count > 1
&& verts[0].X == verts[verts.Count - 1].X
&& verts[0].Y == verts[verts.Count - 1].Y
)
verts.RemoveAt(verts.Count - 1);
if (verts.Count < 3)
@@ -84,8 +87,14 @@ namespace OpenNest.Geometry
/// Tests whether the vertex at curr forms an ear (a convex vertex whose
/// triangle contains no other polygon vertices).
/// </summary>
private static bool IsEar(Vector prev, Vector curr, Vector next,
List<Vector> verts, List<int> indices, int n)
private static bool IsEar(
Vector prev,
Vector curr,
Vector next,
List<Vector> verts,
List<int> indices,
int n
)
{
// Must be convex (CCW turn).
if (Cross(prev, curr, next) <= 0)
+8 -2
View File
@@ -20,7 +20,10 @@ namespace OpenNest.Geometry
foreach (var p in sorted)
{
while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0)
while (
lower.Count >= 2
&& Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0
)
lower.RemoveAt(lower.Count - 1);
lower.Add(p);
@@ -32,7 +35,10 @@ namespace OpenNest.Geometry
{
var p = sorted[i];
while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0)
while (
upper.Count >= 2
&& Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0
)
upper.RemoveAt(upper.Count - 1);
upper.Add(p);
+140 -39
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -9,7 +9,13 @@ namespace OpenNest.Geometry
private const int MaxSubdivisionDepth = 12;
private const int DeviationSamples = 20;
internal static Vector EvaluatePoint(double semiMajor, double semiMinor, double rotation, Vector center, double t)
internal static Vector EvaluatePoint(
double semiMajor,
double semiMinor,
double rotation,
Vector center,
double t
)
{
var x = semiMajor * System.Math.Cos(t);
var y = semiMinor * System.Math.Sin(t);
@@ -17,12 +23,15 @@ namespace OpenNest.Geometry
var cos = System.Math.Cos(rotation);
var sin = System.Math.Sin(rotation);
return new Vector(
center.X + x * cos - y * sin,
center.Y + x * sin + y * cos);
return new Vector(center.X + x * cos - y * sin, center.Y + x * sin + y * cos);
}
internal static Vector EvaluateTangent(double semiMajor, double semiMinor, double rotation, double t)
internal static Vector EvaluateTangent(
double semiMajor,
double semiMinor,
double rotation,
double t
)
{
var tx = -semiMajor * System.Math.Sin(t);
var ty = semiMinor * System.Math.Cos(t);
@@ -30,12 +39,15 @@ namespace OpenNest.Geometry
var cos = System.Math.Cos(rotation);
var sin = System.Math.Sin(rotation);
return new Vector(
tx * cos - ty * sin,
tx * sin + ty * cos);
return new Vector(tx * cos - ty * sin, tx * sin + ty * cos);
}
internal static Vector EvaluateNormal(double semiMajor, double semiMinor, double rotation, double t)
internal static Vector EvaluateNormal(
double semiMajor,
double semiMinor,
double rotation,
double t
)
{
// Inward normal: perpendicular to tangent, pointing toward center of curvature.
// In local coords: N(t) = (-b*cos(t), -a*sin(t))
@@ -45,9 +57,7 @@ namespace OpenNest.Geometry
var cos = System.Math.Cos(rotation);
var sin = System.Math.Sin(rotation);
return new Vector(
nx * cos - ny * sin,
nx * sin + ny * cos);
return new Vector(nx * cos - ny * sin, nx * sin + ny * cos);
}
internal static Vector IntersectNormals(Vector p1, Vector n1, Vector p2, Vector n2)
@@ -83,11 +93,21 @@ namespace OpenNest.Geometry
return new Vector(ux + c.X, uy + c.Y);
}
public static List<Entity> Convert(Vector center, double semiMajor, double semiMinor,
double rotation, double startParam, double endParam, double tolerance = 0.001)
public static List<Entity> Convert(
Vector center,
double semiMajor,
double semiMinor,
double rotation,
double startParam,
double endParam,
double tolerance = 0.001
)
{
if (tolerance <= 0)
throw new ArgumentOutOfRangeException(nameof(tolerance), "Tolerance must be positive.");
throw new ArgumentOutOfRangeException(
nameof(tolerance),
"Tolerance must be positive."
);
if (semiMajor <= 0 || semiMinor <= 0)
throw new ArgumentOutOfRangeException("Semi-axis lengths must be positive.");
@@ -102,14 +122,28 @@ namespace OpenNest.Geometry
var entities = new List<Entity>();
for (var i = 0; i < splits.Count - 1; i++)
FitSegment(center, semiMajor, semiMinor, rotation,
splits[i], splits[i + 1], tolerance, entities, 0);
FitSegment(
center,
semiMajor,
semiMinor,
rotation,
splits[i],
splits[i + 1],
tolerance,
entities,
0
);
return entities;
}
private static List<Entity> ConvertCircle(Vector center, double radius,
double rotation, double startParam, double endParam)
private static List<Entity> ConvertCircle(
Vector center,
double radius,
double rotation,
double startParam,
double endParam
)
{
var sweep = endParam - startParam;
var isFull = System.Math.Abs(sweep - Angle.TwoPI) < 0.01;
@@ -123,7 +157,7 @@ namespace OpenNest.Geometry
return new List<Entity>
{
new Arc(center, radius, startAngle1, midAngle, false),
new Arc(center, radius, midAngle, endAngle2, false)
new Arc(center, radius, midAngle, endAngle2, false),
};
}
@@ -136,7 +170,8 @@ namespace OpenNest.Geometry
{
var splits = new List<double> { startParam };
var firstQuadrant = System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2);
var firstQuadrant =
System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2);
for (var q = firstQuadrant; q < endParam; q += System.Math.PI / 2)
{
if (q > startParam + 1e-10 && q < endParam - 1e-10)
@@ -147,8 +182,17 @@ namespace OpenNest.Geometry
return splits;
}
private static void FitSegment(Vector center, double semiMajor, double semiMinor,
double rotation, double t0, double t1, double tolerance, List<Entity> results, int depth)
private static void FitSegment(
Vector center,
double semiMajor,
double semiMinor,
double rotation,
double t0,
double t1,
double tolerance,
List<Entity> results,
int depth
)
{
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t0);
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t1);
@@ -168,12 +212,29 @@ namespace OpenNest.Geometry
}
var radius = p0.DistanceTo(arcCenter);
var maxDev = MeasureDeviation(center, semiMajor, semiMinor, rotation,
t0, t1, arcCenter, radius);
var maxDev = MeasureDeviation(
center,
semiMajor,
semiMinor,
rotation,
t0,
t1,
arcCenter,
radius
);
if (maxDev <= tolerance)
{
var arc = CreateArc(arcCenter, radius, center, semiMajor, semiMinor, rotation, t0, t1);
var arc = CreateArc(
arcCenter,
radius,
center,
semiMajor,
semiMinor,
rotation,
t0,
t1
);
if (arc.SweepAngle() < Tolerance.Epsilon)
results.Add(new Line(p0, p1));
else
@@ -182,13 +243,41 @@ namespace OpenNest.Geometry
else
{
var tMid = (t0 + t1) / 2.0;
FitSegment(center, semiMajor, semiMinor, rotation, t0, tMid, tolerance, results, depth + 1);
FitSegment(center, semiMajor, semiMinor, rotation, tMid, t1, tolerance, results, depth + 1);
FitSegment(
center,
semiMajor,
semiMinor,
rotation,
t0,
tMid,
tolerance,
results,
depth + 1
);
FitSegment(
center,
semiMajor,
semiMinor,
rotation,
tMid,
t1,
tolerance,
results,
depth + 1
);
}
}
private static double MeasureDeviation(Vector center, double semiMajor, double semiMinor,
double rotation, double t0, double t1, Vector arcCenter, double radius)
private static double MeasureDeviation(
Vector center,
double semiMajor,
double semiMinor,
double rotation,
double t0,
double t1,
Vector arcCenter,
double radius
)
{
var maxDev = 0.0;
for (var i = 1; i <= DeviationSamples; i++)
@@ -197,14 +286,22 @@ namespace OpenNest.Geometry
var p = EvaluatePoint(semiMajor, semiMinor, rotation, center, t);
var dist = p.DistanceTo(arcCenter);
var dev = System.Math.Abs(dist - radius);
if (dev > maxDev) maxDev = dev;
if (dev > maxDev)
maxDev = dev;
}
return maxDev;
}
private static Arc CreateArc(Vector arcCenter, double radius,
Vector ellipseCenter, double semiMajor, double semiMinor, double rotation,
double t0, double t1)
private static Arc CreateArc(
Vector arcCenter,
double radius,
Vector ellipseCenter,
double semiMajor,
double semiMinor,
double rotation,
double t0,
double t1
)
{
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0);
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1);
@@ -225,8 +322,10 @@ namespace OpenNest.Geometry
var points = new List<Vector> { p0, pMid, p1 };
var isReversed = SumSignedAngles(arcCenter, points) < 0;
if (startAngle < 0) startAngle += Angle.TwoPI;
if (endAngle < 0) endAngle += Angle.TwoPI;
if (startAngle < 0)
startAngle += Angle.TwoPI;
if (endAngle < 0)
endAngle += Angle.TwoPI;
return new Arc(arcCenter, radius, startAngle, endAngle, isReversed);
}
@@ -239,8 +338,10 @@ namespace OpenNest.Geometry
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
var da = a2 - a1;
while (da > System.Math.PI) da -= Angle.TwoPI;
while (da < -System.Math.PI) da += Angle.TwoPI;
while (da > System.Math.PI)
da -= Angle.TwoPI;
while (da < -System.Math.PI)
da += Angle.TwoPI;
total += da;
}
return total;
+7 -3
View File
@@ -1,7 +1,7 @@
using OpenNest.Math;
using System;
using System;
using System.Collections.Generic;
using System.Drawing;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -331,7 +331,11 @@ namespace OpenNest.Geometry
return points;
}
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
public static BoundingRectangleResult FindBestRotation(
this List<Entity> entities,
double startAngle = 0,
double endAngle = Angle.TwoPI
)
{
// Check for Shape entity first (recursive case returns early)
foreach (var entity in entities)
+2 -3
View File
@@ -1,5 +1,4 @@

namespace OpenNest.Geometry
namespace OpenNest.Geometry
{
public enum EntityType
{
@@ -7,6 +6,6 @@ namespace OpenNest.Geometry
Circle,
Line,
Shape,
Polygon
Polygon,
}
}
+88 -47
View File
@@ -1,21 +1,25 @@
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenNest.Math;
namespace OpenNest.Geometry
{
public static class GeometryOptimizer
{
public static void Optimize(IList<Arc> arcs) =>
MergePass(arcs,
MergePass(
arcs,
(list, item, i) => list.GetCoradialArs(item, i),
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined));
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined)
);
public static void Optimize(IList<Line> lines) =>
MergePass(lines,
MergePass(
lines,
(list, item, i) => list.GetCollinearLines(item, i),
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined));
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined)
);
public static void Deduplicate(IList<Circle> circles)
{
@@ -23,8 +27,10 @@ namespace OpenNest.Geometry
{
for (var j = i - 1; j >= 0; j--)
{
if (circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon
&& circles[i].Radius.IsEqualTo(circles[j].Radius))
if (
circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon
&& circles[i].Radius.IsEqualTo(circles[j].Radius)
)
{
circles.RemoveAt(i);
break;
@@ -39,9 +45,11 @@ namespace OpenNest.Geometry
{
for (var j = arcs.Count - 1; j >= 0; j--)
{
if (arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon
if (
arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon
&& arcs[j].Radius.IsEqualTo(circles[i].Radius)
&& arcs[j].IsFullCircle())
&& arcs[j].IsFullCircle()
)
{
arcs.RemoveAt(j);
}
@@ -51,9 +59,12 @@ namespace OpenNest.Geometry
private delegate bool TryJoin<T>(T a, T b, out T joined);
private static void MergePass<T>(IList<T> items,
private static void MergePass<T>(
IList<T> items,
Func<IList<T>, T, int, List<T>> findCandidates,
TryJoin<T> tryJoin) where T : class
TryJoin<T> tryJoin
)
where T : class
{
for (var i = 0; i < items.Count; ++i)
{
@@ -117,10 +128,14 @@ namespace OpenNest.Geometry
if (!onPoint)
{
if (t1 < b2 - Tolerance.Epsilon) return false;
if (b1 > t2 + Tolerance.Epsilon) return false;
if (l1 > r2 + Tolerance.Epsilon) return false;
if (r1 < l2 - Tolerance.Epsilon) return false;
if (t1 < b2 - Tolerance.Epsilon)
return false;
if (b1 > t2 + Tolerance.Epsilon)
return false;
if (l1 > r2 + Tolerance.Epsilon)
return false;
if (r1 < l2 - Tolerance.Epsilon)
return false;
}
var l = l1 < l2 ? l1 : l2;
@@ -129,9 +144,17 @@ namespace OpenNest.Geometry
var b = b1 < b2 ? b1 : b2;
if (!line1.IsVertical() && line1.Slope() < 0)
lineOut = new Line(new Vector(l, t), new Vector(r, b)) { Layer = line1.Layer, Color = line1.Color };
lineOut = new Line(new Vector(l, t), new Vector(r, b))
{
Layer = line1.Layer,
Color = line1.Color,
};
else
lineOut = new Line(new Vector(l, b), new Vector(r, t)) { Layer = line1.Layer, Color = line1.Color };
lineOut = new Line(new Vector(l, b), new Vector(r, t))
{
Layer = line1.Layer,
Color = line1.Color,
};
return true;
}
@@ -177,33 +200,47 @@ namespace OpenNest.Geometry
if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
return false;
if (startAngle < 0) startAngle += Angle.TwoPI;
if (endAngle < 0) endAngle += Angle.TwoPI;
if (startAngle < 0)
startAngle += Angle.TwoPI;
if (endAngle < 0)
endAngle += Angle.TwoPI;
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color };
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle)
{
Layer = arc1.Layer,
Color = arc1.Color,
};
return true;
}
private static List<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex)
private static List<Line> GetCollinearLines(
this IList<Line> lines,
Line line,
int startIndex
)
{
var collinearLines = new List<Line>();
Parallel.For(startIndex, lines.Count, index =>
{
var compareLine = lines[index];
if (Object.ReferenceEquals(line, compareLine))
return;
if (!line.IsCollinearTo(compareLine))
return;
lock (collinearLines)
Parallel.For(
startIndex,
lines.Count,
index =>
{
collinearLines.Add(compareLine);
var compareLine = lines[index];
if (Object.ReferenceEquals(line, compareLine))
return;
if (!line.IsCollinearTo(compareLine))
return;
lock (collinearLines)
{
collinearLines.Add(compareLine);
}
}
});
);
return collinearLines;
}
@@ -212,21 +249,25 @@ namespace OpenNest.Geometry
{
var coradialArcs = new List<Arc>();
Parallel.For(startIndex, arcs.Count, index =>
{
var compareArc = arcs[index];
if (Object.ReferenceEquals(arc, compareArc))
return;
if (!arc.IsCoradialTo(compareArc))
return;
lock (coradialArcs)
Parallel.For(
startIndex,
arcs.Count,
index =>
{
coradialArcs.Add(compareArc);
var compareArc = arcs[index];
if (Object.ReferenceEquals(arc, compareArc))
return;
if (!arc.IsCoradialTo(compareArc))
return;
lock (coradialArcs)
{
coradialArcs.Add(compareArc);
}
}
});
);
return coradialArcs;
}
+1 -2
View File
@@ -1,5 +1,4 @@

namespace OpenNest.Geometry
namespace OpenNest.Geometry
{
public interface IBoundable
{
+8 -4
View File
@@ -28,10 +28,14 @@ namespace OpenNest.Geometry
for (var i = 1; i < verts.Count; i++)
{
if (verts[i].X < minX) minX = verts[i].X;
if (verts[i].X > maxX) maxX = verts[i].X;
if (verts[i].Y < minY) minY = verts[i].Y;
if (verts[i].Y > maxY) maxY = verts[i].Y;
if (verts[i].X < minX)
minX = verts[i].X;
if (verts[i].X > maxX)
maxX = verts[i].X;
if (verts[i].Y < minY)
minY = verts[i].Y;
if (verts[i].Y > maxY)
maxY = verts[i].Y;
}
// The IFP is the work area shrunk inward by the part's extent in each direction.
+60 -31
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -18,8 +18,19 @@ namespace OpenNest.Geometry
}
pts = pts.Where(pt =>
Angle.IsBetweenRad(arc1.Center.AngleTo(pt), arc1.StartAngle, arc1.EndAngle, arc1.IsReversed) &&
Angle.IsBetweenRad(arc2.Center.AngleTo(pt), arc2.StartAngle, arc2.EndAngle, arc2.IsReversed))
Angle.IsBetweenRad(
arc1.Center.AngleTo(pt),
arc1.StartAngle,
arc1.EndAngle,
arc1.IsReversed
)
&& Angle.IsBetweenRad(
arc2.Center.AngleTo(pt),
arc2.StartAngle,
arc2.EndAngle,
arc2.IsReversed
)
)
.ToList();
return pts.Count > 0;
@@ -35,11 +46,15 @@ namespace OpenNest.Geometry
return false;
}
pts = pts.Where(pt => Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed)).ToList();
pts = pts.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0;
}
@@ -54,11 +69,15 @@ namespace OpenNest.Geometry
return false;
}
pts = pts.Where(pt => Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed)).ToList();
pts = pts.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0;
}
@@ -74,11 +93,15 @@ namespace OpenNest.Geometry
pts2.AddRange(pts3);
}
pts = pts2.Where(pt => Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed)).ToList();
pts = pts2.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0;
}
@@ -95,11 +118,15 @@ namespace OpenNest.Geometry
pts2.AddRange(pts3);
}
pts = pts2.Where(pt => Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed)).ToList();
pts = pts2.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt),
arc.StartAngle,
arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0;
}
@@ -123,20 +150,22 @@ namespace OpenNest.Geometry
}
var d = circle2.Center - circle1.Center;
var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance);
var a =
(
circle1.Radius * circle1.Radius
- circle2.Radius * circle2.Radius
+ distance * distance
) / (2.0 * distance);
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
var pt = new Vector(
circle1.Center.X + (a * d.X) / distance,
circle1.Center.Y + (a * d.Y) / distance);
circle1.Center.Y + (a * d.Y) / distance
);
var i1 = new Vector(
pt.X + (h * d.Y) / distance,
pt.Y - (h * d.X) / distance);
var i1 = new Vector(pt.X + (h * d.Y) / distance, pt.Y - (h * d.X) / distance);
var i2 = new Vector(
pt.X - (h * d.Y) / distance,
pt.Y + (h * d.X) / distance);
var i2 = new Vector(pt.X - (h * d.Y) / distance, pt.Y + (h * d.X) / distance);
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
+1 -1
View File
@@ -7,7 +7,7 @@ namespace OpenNest.Geometry
public static readonly Layer Default = new Layer("0")
{
Color = Color.White,
IsVisible = true
IsVisible = true,
};
public Layer(string name)
+12 -18
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -9,14 +9,10 @@ namespace OpenNest.Geometry
internal Vector pt1;
internal Vector pt2;
public Line()
{
}
public Line() { }
public Line(double x1, double y1, double x2, double y2)
: this(new Vector(x1, y1), new Vector(x2, y2))
{
}
: this(new Vector(x1, y1), new Vector(x2, y2)) { }
public Line(Vector startPoint, Vector endPoint)
{
@@ -83,9 +79,7 @@ namespace OpenNest.Geometry
return EndPoint;
else
{
return new Vector(
StartPoint.X + param * diff2.X,
StartPoint.Y + param * diff2.Y);
return new Vector(StartPoint.X + param * diff2.X, StartPoint.Y + param * diff2.Y);
}
}
@@ -372,7 +366,7 @@ namespace OpenNest.Geometry
/// <summary>
/// Updates the bounding box.
/// </summary>
public override sealed void UpdateBounds()
public sealed override void UpdateBounds()
{
if (StartPoint.X < EndPoint.X)
{
@@ -429,13 +423,13 @@ namespace OpenNest.Geometry
/// <returns>A tuple of (first, second) sub-lines.</returns>
public (Line first, Line second) SplitAt(Vector point)
{
var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon
? null
: new Line(StartPoint, point);
var first =
point.DistanceTo(StartPoint) < Tolerance.Epsilon
? null
: new Line(StartPoint, point);
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon
? null
: new Line(point, EndPoint);
var second =
point.DistanceTo(EndPoint) < Tolerance.Epsilon ? null : new Line(point, EndPoint);
return (first, second);
}
+17 -11
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Clipper2Lib;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Geometry
{
@@ -99,14 +99,15 @@ namespace OpenNest.Geometry
var startB = FindBottomLeft(b);
var result = new Polygon();
// The starting point of the Minkowski sum A + B is the sum of the
// starting points of A and B. For NFP = A + (-B), this is
// The starting point of the Minkowski sum A + B is the sum of the
// starting points of A and B. For NFP = A + (-B), this is
// startA + startReflectedB.
var current = new Vector(
a.Vertices[startA].X + b.Vertices[startB].X,
a.Vertices[startA].Y + b.Vertices[startB].Y);
a.Vertices[startA].Y + b.Vertices[startB].Y
);
result.Vertices.Add(current);
var ia = 0;
@@ -132,10 +133,12 @@ namespace OpenNest.Geometry
else
{
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
if (angleA < 0) angleA += Angle.TwoPI;
if (angleA < 0)
angleA += Angle.TwoPI;
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
if (angleB < 0) angleB += Angle.TwoPI;
if (angleB < 0)
angleB += Angle.TwoPI;
if (angleA < angleB)
{
@@ -149,7 +152,8 @@ namespace OpenNest.Geometry
{
edge = new Vector(
orderedA[ia].X + orderedB[ib].X,
orderedA[ia].Y + orderedB[ib].Y);
orderedA[ia].Y + orderedB[ib].Y
);
ia++;
ib++;
}
@@ -203,8 +207,10 @@ namespace OpenNest.Geometry
for (var i = 1; i < n; i++)
{
if (verts[i].Y < verts[best].Y ||
(verts[i].Y == verts[best].Y && verts[i].X < verts[best].X))
if (
verts[i].Y < verts[best].Y
|| (verts[i].Y == verts[best].Y && verts[i].X < verts[best].X)
)
best = i;
}
+22 -11
View File
@@ -4,12 +4,14 @@ namespace OpenNest.Geometry
{
public static class PolyLabel
{
public static Vector Find(Polygon outer, IList<Polygon> holes = null, double precision = 0.5)
public static Vector Find(
Polygon outer,
IList<Polygon> holes = null,
double precision = 0.5
)
{
if (outer.Vertices.Count < 3)
return outer.Vertices.Count > 0
? outer.Vertices[0]
: new Vector();
return outer.Vertices.Count > 0 ? outer.Vertices[0] : new Vector();
var minX = double.MaxValue;
var minY = double.MaxValue;
@@ -19,10 +21,14 @@ namespace OpenNest.Geometry
for (var i = 0; i < outer.Vertices.Count; i++)
{
var v = outer.Vertices[i];
if (v.X < minX) minX = v.X;
if (v.Y < minY) minY = v.Y;
if (v.X > maxX) maxX = v.X;
if (v.Y > maxY) maxY = v.Y;
if (v.X < minX)
minX = v.X;
if (v.Y < minY)
minY = v.Y;
if (v.X > maxX)
maxX = v.X;
if (v.Y > maxY)
maxY = v.Y;
}
var width = maxX - minX;
@@ -37,8 +43,8 @@ namespace OpenNest.Geometry
var queue = new List<Cell>();
for (var x = minX; x < maxX; x += cellSize)
for (var y = minY; y < maxY; y += cellSize)
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes));
for (var y = minY; y < maxY; y += cellSize)
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes));
queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist));
@@ -194,7 +200,12 @@ namespace OpenNest.Geometry
}
}
private static double PointToAllEdgesDist(double x, double y, Polygon outer, IList<Polygon> holes)
private static double PointToAllEdgesDist(
double x,
double y,
Polygon outer,
IList<Polygon> holes
)
{
var minDist = PointToPolygonDist(x, y, outer);
+64 -21
View File
@@ -1,7 +1,7 @@
using OpenNest.Math;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -107,7 +107,9 @@ namespace OpenNest.Geometry
public RotationType RotationDirection()
{
if (Vertices.Count < 3)
throw new Exception("Not enough points to determine direction. Must have at least 3 points.");
throw new Exception(
"Not enough points to determine direction. Must have at least 3 points."
);
return CalculateArea() > 0 ? RotationType.CCW : RotationType.CW;
}
@@ -309,11 +311,15 @@ namespace OpenNest.Geometry
{
var vertex = Vertices[i];
if (vertex.X < minX) minX = vertex.X;
else if (vertex.X > maxX) maxX = vertex.X;
if (vertex.X < minX)
minX = vertex.X;
else if (vertex.X > maxX)
maxX = vertex.X;
if (vertex.Y < minY) minY = vertex.Y;
else if (vertex.Y > maxY) maxY = vertex.Y;
if (vertex.Y < minY)
minY = vertex.Y;
else if (vertex.Y > maxY)
maxY = vertex.Y;
}
boundingBox.X = minX;
@@ -354,10 +360,19 @@ namespace OpenNest.Geometry
{
var prev = (i - 1 + count) % count;
var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
var a1 = new Vector(
Vertices[prev].X + normals[prev].X,
Vertices[prev].Y + normals[prev].Y
);
var a2 = new Vector(
Vertices[i].X + normals[prev].X,
Vertices[i].Y + normals[prev].Y
);
var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);
var b2 = new Vector(
Vertices[(i + 1) % count].X + normals[i].X,
Vertices[(i + 1) % count].Y + normals[i].Y
);
var edgeA = new Line(a1, a2);
var edgeB = new Line(b1, b2);
@@ -365,7 +380,9 @@ namespace OpenNest.Geometry
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
result.Vertices.Add(pt);
else
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
result.Vertices.Add(
new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y)
);
}
result.Close();
@@ -379,8 +396,10 @@ namespace OpenNest.Geometry
var left = OffsetEntity(distance, OffsetSide.Left);
var right = OffsetEntity(distance, OffsetSide.Right);
if (left == null) return right;
if (right == null) return left;
if (left == null)
return right;
if (right == null)
return left;
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
@@ -581,13 +600,25 @@ namespace OpenNest.Geometry
var bj = edgeBounds[j];
// Prune with bounding box check.
if (bi.maxX < bj.minX || bj.maxX < bi.minX ||
bi.maxY < bj.minY || bj.maxY < bi.minY)
if (
bi.maxX < bj.minX
|| bj.maxX < bi.minX
|| bi.maxY < bj.minY
|| bj.maxY < bi.minY
)
{
continue;
}
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
if (
SegmentsIntersect(
Vertices[i],
Vertices[i + 1],
Vertices[j],
Vertices[j + 1],
out pt
)
)
{
edgeI = i;
edgeJ = j;
@@ -620,7 +651,13 @@ namespace OpenNest.Geometry
return areaA >= areaB ? loopA : loopB;
}
private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt)
private static bool SegmentsIntersect(
Vector a1,
Vector a2,
Vector b1,
Vector b2,
out Vector pt
)
{
var da = a2 - a1;
var db = b2 - b1;
@@ -636,8 +673,12 @@ namespace OpenNest.Geometry
var t = (dc.X * db.Y - dc.Y * db.X) / cross;
var u = (dc.X * da.Y - dc.Y * da.X) / cross;
if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon &&
u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon)
if (
t > Tolerance.Epsilon
&& t < 1.0 - Tolerance.Epsilon
&& u > Tolerance.Epsilon
&& u < 1.0 - Tolerance.Epsilon
)
{
pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y);
return true;
@@ -701,8 +742,10 @@ namespace OpenNest.Geometry
var vi = Vertices[i];
var vj = Vertices[j];
if ((vi.Y > pt.Y) != (vj.Y > pt.Y) &&
pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X)
if (
(vi.Y > pt.Y) != (vj.Y > pt.Y)
&& pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X
)
{
inside = !inside;
}
+35 -15
View File
@@ -1,5 +1,5 @@
using OpenNest.Math;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -71,18 +71,24 @@ namespace OpenNest.Geometry
var vy = ux;
// Project all hull vertices onto edge direction (u) and perpendicular (v)
double minU = double.MaxValue, maxU = double.MinValue;
double minV = double.MaxValue, maxV = double.MinValue;
double minU = double.MaxValue,
maxU = double.MinValue;
double minV = double.MaxValue,
maxV = double.MinValue;
for (int j = 0; j < n; j++)
{
var projU = vertices[j].X * ux + vertices[j].Y * uy;
var projV = vertices[j].X * vx + vertices[j].Y * vy;
if (projU < minU) minU = projU;
if (projU > maxU) maxU = projU;
if (projV < minV) minV = projV;
if (projV > maxV) maxV = projV;
if (projU < minU)
minU = projU;
if (projU > maxU)
maxU = projU;
if (projV < minV)
minV = projV;
if (projV > maxV)
maxV = projV;
}
var width = maxU - minU;
@@ -99,7 +105,11 @@ namespace OpenNest.Geometry
return best ?? new BoundingRectangleResult(0, 0, 0);
}
public static BoundingRectangleResult MinimumBoundingRectangle(Polygon hull, double startAngle, double endAngle)
public static BoundingRectangleResult MinimumBoundingRectangle(
Polygon hull,
double startAngle,
double endAngle
)
{
var vertices = hull.Vertices;
int n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
@@ -153,23 +163,33 @@ namespace OpenNest.Geometry
return best ?? new BoundingRectangleResult(startAngle, 0, 0);
}
private static BoundingRectangleResult EvaluateAtAngle(IList<Vector> vertices, int n, double angle)
private static BoundingRectangleResult EvaluateAtAngle(
IList<Vector> vertices,
int n,
double angle
)
{
var cos = System.Math.Cos(angle);
var sin = System.Math.Sin(angle);
double minU = double.MaxValue, maxU = double.MinValue;
double minV = double.MaxValue, maxV = double.MinValue;
double minU = double.MaxValue,
maxU = double.MinValue;
double minV = double.MaxValue,
maxV = double.MinValue;
for (int j = 0; j < n; j++)
{
var projU = vertices[j].X * cos + vertices[j].Y * sin;
var projV = -vertices[j].X * sin + vertices[j].Y * cos;
if (projU < minU) minU = projU;
if (projU > maxU) maxU = projU;
if (projV < minV) minV = projV;
if (projV > maxV) maxV = projV;
if (projU < minU)
minU = projU;
if (projU > maxU)
maxU = projU;
if (projV < minV)
minV = projV;
if (projV > maxV)
maxV = projV;
}
var width = maxU - minU;
+95 -50
View File
@@ -282,11 +282,7 @@ namespace OpenNest.Geometry
case EntityType.Line:
var line = (Line)entity;
polygon.Vertices.AddRange(new[]
{
line.StartPoint,
line.EndPoint
});
polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint });
break;
case EntityType.Circle:
@@ -320,21 +316,21 @@ namespace OpenNest.Geometry
{
case EntityType.Arc:
var arc = (Arc)entity;
polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe));
polygon.Vertices.AddRange(
arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe)
);
break;
case EntityType.Line:
var line = (Line)entity;
polygon.Vertices.AddRange(new[]
{
line.StartPoint,
line.EndPoint
});
polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint });
break;
case EntityType.Circle:
var circle = (Circle)entity;
polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe));
polygon.Vertices.AddRange(
circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe)
);
break;
default:
@@ -462,9 +458,7 @@ namespace OpenNest.Geometry
/// </summary>
public override void UpdateBounds()
{
boundingBox = Entities.Select(geo => geo.BoundingBox)
.ToList()
.GetBoundingBox();
boundingBox = Entities.Select(geo => geo.BoundingBox).ToList().GetBoundingBox();
}
public override Entity OffsetEntity(double distance, OffsetSide side)
@@ -493,22 +487,27 @@ namespace OpenNest.Geometry
switch (entity.Type)
{
case EntityType.Line:
{
var line = (Line)entity;
var offsetLine = (Line)offsetEntity;
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
{
var line = (Line)entity;
var offsetLine = (Line)offsetEntity;
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
{
JoinOffsetLines(
(Line)lastEntity, (Line)lastOffsetEntity,
line, offsetLine,
distance, side, offsetShape);
}
offsetShape.Entities.Add(offsetLine);
break;
JoinOffsetLines(
(Line)lastEntity,
(Line)lastOffsetEntity,
line,
offsetLine,
distance,
side,
offsetShape
);
}
offsetShape.Entities.Add(offsetLine);
break;
}
default:
offsetShape.Entities.Add(offsetEntity);
break;
@@ -519,27 +518,42 @@ namespace OpenNest.Geometry
}
// Close the shape: join last offset entity back to first
if (lastOffsetEntity != null && firstOffsetEntity != null
if (
lastOffsetEntity != null
&& firstOffsetEntity != null
&& lastOffsetEntity != firstOffsetEntity
&& lastOffsetEntity.Type == EntityType.Line
&& firstOffsetEntity.Type == EntityType.Line)
&& firstOffsetEntity.Type == EntityType.Line
)
{
JoinOffsetLines(
(Line)lastEntity, (Line)lastOffsetEntity,
(Line)firstEntity, (Line)firstOffsetEntity,
distance, side, offsetShape);
(Line)lastEntity,
(Line)lastOffsetEntity,
(Line)firstEntity,
(Line)firstOffsetEntity,
distance,
side,
offsetShape
);
}
foreach (var cutout in definedShape.Cutouts)
offsetShape.Entities.AddRange(((Shape)cutout.OffsetEntity(distance, side)).Entities);
offsetShape.Entities.AddRange(
((Shape)cutout.OffsetEntity(distance, side)).Entities
);
return offsetShape;
}
private static void JoinOffsetLines(
Line lastLine, Line lastOffsetLine,
Line line, Line offsetLine,
double distance, OffsetSide side, Shape offsetShape)
Line lastLine,
Line lastOffsetLine,
Line line,
Line offsetLine,
double distance,
OffsetSide side,
Shape offsetShape
)
{
// Determine if this is a convex corner using the cross product of
// the original line directions. Convex corners need an arc; concave
@@ -548,8 +562,9 @@ namespace OpenNest.Geometry
var d2 = line.EndPoint - line.StartPoint;
var cross = d1.X * d2.Y - d1.Y * d2.X;
var isConvex = (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) ||
(side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
var isConvex =
(side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon)
|| (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
if (isConvex)
{
@@ -559,11 +574,13 @@ namespace OpenNest.Geometry
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))
else if (
Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)
)
{
offsetLine.StartPoint = intersection;
lastOffsetLine.EndPoint = intersection;
@@ -576,7 +593,7 @@ namespace OpenNest.Geometry
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
line.StartPoint.AngleTo(offsetLine.StartPoint),
side == OffsetSide.Left
);
);
offsetShape.Entities.Add(arc);
}
@@ -596,8 +613,11 @@ namespace OpenNest.Geometry
{
var poly = ToPolygon();
if (poly == null || poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CW)
if (
poly == null
|| poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CW
)
return OffsetEntity(distance, OffsetSide.Left) as Shape;
// Shape is CCW — reverse to CW so Left offset goes outward.
@@ -611,10 +631,21 @@ namespace OpenNest.Geometry
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
break;
case Arc a:
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
copy.Entities.Add(
new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed)
{
Layer = a.Layer,
}
);
break;
case Circle c:
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW });
copy.Entities.Add(
new Circle(c.Center, c.Radius)
{
Layer = c.Layer,
Rotation = RotationType.CW,
}
);
break;
}
}
@@ -631,8 +662,11 @@ namespace OpenNest.Geometry
{
var poly = ToPolygon();
if (poly == null || poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CCW)
if (
poly == null
|| poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CCW
)
return OffsetEntity(distance, OffsetSide.Left) as Shape;
// Create a reversed copy to avoid mutating shared entity objects.
@@ -646,10 +680,21 @@ namespace OpenNest.Geometry
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
break;
case Arc a:
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
copy.Entities.Add(
new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed)
{
Layer = a.Layer,
}
);
break;
case Circle c:
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW });
copy.Entities.Add(
new Circle(c.Center, c.Radius)
{
Layer = c.Layer,
Rotation = RotationType.CCW,
}
);
break;
}
}
+10 -3
View File
@@ -1,13 +1,16 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
public static class ShapeBuilder
{
public static List<Shape> GetShapes(IEnumerable<Entity> entities, double? weldTolerance = null)
public static List<Shape> GetShapes(
IEnumerable<Entity> entities,
double? weldTolerance = null
)
{
var lines = new List<Line>();
var arcs = new List<Arc>();
@@ -141,7 +144,11 @@ namespace OpenNest.Geometry
private static void AddToGroup(
List<List<(Entity entity, bool isStart, Vector point)>> groups,
Entity entity, bool isStart, Vector point, double tolerance)
Entity entity,
bool isStart,
Vector point,
double tolerance
)
{
foreach (var group in groups)
{
+1 -2
View File
@@ -84,8 +84,7 @@ namespace OpenNest.Geometry
{
var poly = shape.ToPolygon();
if (poly != null && poly.Vertices.Count >= 3
&& poly.RotationDirection() != desired)
if (poly != null && poly.Vertices.Count >= 3 && poly.RotationDirection() != desired)
{
shape.Reverse();
}
+2 -1
View File
@@ -44,6 +44,7 @@ namespace OpenNest.Geometry
public override string ToString() => $"{Width} x {Length}";
public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
public string ToString(int decimalPlaces) =>
$"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
}
}
+408 -157
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -13,57 +13,72 @@ namespace OpenNest.Geometry
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
{
return RayEdgeDistance(
vertex.X, vertex.Y,
edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y,
direction);
vertex.X,
vertex.Y,
edge.pt1.X,
edge.pt1.Y,
edge.pt2.X,
edge.pt2.Y,
direction
);
}
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
private static double RayEdgeDistance(
double vx, double vy,
double p1x, double p1y, double p2x, double p2y,
PushDirection direction)
double vx,
double vy,
double p1x,
double p1y,
double p2x,
double p2y,
PushDirection direction
)
{
switch (direction)
{
case PushDirection.Left:
case PushDirection.Right:
{
var dy = p2y - p1y;
if (System.Math.Abs(dy) < Tolerance.Epsilon)
return double.MaxValue;
var t = (vy - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
{
var dy = p2y - p1y;
if (System.Math.Abs(dy) < Tolerance.Epsilon)
return double.MaxValue;
}
var t = (vy - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon)
return dist;
if (dist >= -Tolerance.Epsilon)
return 0;
return double.MaxValue;
}
case PushDirection.Down:
case PushDirection.Up:
{
var dx = p2x - p1x;
if (System.Math.Abs(dx) < Tolerance.Epsilon)
return double.MaxValue;
var t = (vx - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
{
var dx = p2x - p1x;
if (System.Math.Abs(dx) < Tolerance.Epsilon)
return double.MaxValue;
}
var t = (vx - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon)
return dist;
if (dist >= -Tolerance.Epsilon)
return 0;
return double.MaxValue;
}
default:
return double.MaxValue;
@@ -75,11 +90,18 @@ namespace OpenNest.Geometry
/// Returns double.MaxValue if the ray does not hit the segment.
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayEdgeDistance(
double vx, double vy,
double p1x, double p1y, double p2x, double p2y,
double dirX, double dirY)
double vx,
double vy,
double p1x,
double p1y,
double p2x,
double p2y,
double dirX,
double dirY
)
{
var ex = p2x - p1x;
var ey = p2y - p1y;
@@ -99,8 +121,10 @@ namespace OpenNest.Geometry
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
if (t > Tolerance.Epsilon) return t;
if (t >= -Tolerance.Epsilon) return 0;
if (t > Tolerance.Epsilon)
return t;
if (t >= -Tolerance.Epsilon)
return 0;
return double.MaxValue;
}
@@ -109,12 +133,19 @@ namespace OpenNest.Geometry
/// Returns false if no real intersection exists.
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
private static bool SolveRayCircle(
double vx, double vy,
double cx, double cy, double r,
double dirX, double dirY,
out double t1, out double t2)
double vx,
double vy,
double cx,
double cy,
double r,
double dirX,
double dirY,
out double t1,
out double t2
)
{
var ox = vx - cx;
var oy = vy - cy;
@@ -143,12 +174,20 @@ namespace OpenNest.Geometry
/// angular span. Returns double.MaxValue if no hit.
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayArcDistance(
double vx, double vy,
double cx, double cy, double r,
double startAngle, double endAngle, bool reversed,
double dirX, double dirY)
double vx,
double vy,
double cx,
double cy,
double r,
double startAngle,
double endAngle,
bool reversed,
double dirX,
double dirY
)
{
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
return double.MaxValue;
@@ -157,16 +196,18 @@ namespace OpenNest.Geometry
if (t1 > -Tolerance.Epsilon)
{
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
vy + t1 * dirY - cy, vx + t1 * dirX - cx));
var hitAngle = Angle.NormalizeRad(
System.Math.Atan2(vy + t1 * dirY - cy, vx + t1 * dirX - cx)
);
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
best = t1 > Tolerance.Epsilon ? t1 : 0;
}
if (t2 > -Tolerance.Epsilon && t2 < best)
{
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
vy + t2 * dirY - cy, vx + t2 * dirX - cx));
var hitAngle = Angle.NormalizeRad(
System.Math.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx)
);
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
best = t2 > Tolerance.Epsilon ? t2 : 0;
}
@@ -179,19 +220,29 @@ namespace OpenNest.Geometry
/// Returns double.MaxValue if no hit.
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayCircleDistance(
double vx, double vy,
double cx, double cy, double r,
double dirX, double dirY)
double vx,
double vy,
double cx,
double cy,
double r,
double dirX,
double dirY
)
{
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
return double.MaxValue;
if (t1 > Tolerance.Epsilon) return t1;
if (t1 >= -Tolerance.Epsilon) return 0;
if (t2 > Tolerance.Epsilon) return t2;
if (t2 >= -Tolerance.Epsilon) return 0;
if (t1 > Tolerance.Epsilon)
return t1;
if (t1 >= -Tolerance.Epsilon)
return 0;
if (t2 > Tolerance.Epsilon)
return t2;
if (t2 >= -Tolerance.Epsilon)
return 0;
return double.MaxValue;
}
@@ -201,7 +252,11 @@ namespace OpenNest.Geometry
/// any edge of movingLines contacts any edge of stationaryLines.
/// Returns double.MaxValue if no collision path exists.
/// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
public static double DirectionalDistance(
List<Line> movingLines,
List<Line> stationaryLines,
PushDirection direction
)
{
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
}
@@ -211,8 +266,12 @@ namespace OpenNest.Geometry
/// by (movingDx, movingDy) without creating new Line objects.
/// </summary>
public static double DirectionalDistance(
List<Line> movingLines, double movingDx, double movingDy,
List<Line> stationaryLines, PushDirection direction)
List<Line> movingLines,
double movingDx,
double movingDy,
List<Line> stationaryLines,
PushDirection direction
)
{
var minDist = double.MaxValue;
var movingOffset = new Vector(movingDx, movingDy);
@@ -226,7 +285,8 @@ namespace OpenNest.Geometry
foreach (var mv in movingVertices)
{
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
if (d < minDist) minDist = d;
if (d < minDist)
minDist = d;
}
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
@@ -239,7 +299,8 @@ namespace OpenNest.Geometry
foreach (var sv in stationaryVertices)
{
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
if (d < minDist) minDist = d;
if (d < minDist)
minDist = d;
}
return minDist;
@@ -267,9 +328,12 @@ namespace OpenNest.Geometry
/// to avoid all intermediate object allocations.
/// </summary>
public static double DirectionalDistance(
(Vector start, Vector end)[] movingEdges, Vector movingOffset,
(Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset,
PushDirection direction)
(Vector start, Vector end)[] movingEdges,
Vector movingOffset,
(Vector start, Vector end)[] stationaryEdges,
Vector stationaryOffset,
PushDirection direction
)
{
var minDist = double.MaxValue;
@@ -281,7 +345,8 @@ namespace OpenNest.Geometry
foreach (var mv in movingVertices)
{
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
if (d < minDist) minDist = d;
if (d < minDist)
minDist = d;
}
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
@@ -293,15 +358,19 @@ namespace OpenNest.Geometry
foreach (var sv in stationaryVertices)
{
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
if (d < minDist) minDist = d;
if (d < minDist)
minDist = d;
}
return minDist;
}
public static double OneWayDistance(
Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset,
PushDirection direction)
Vector vertex,
(Vector start, Vector end)[] edges,
Vector edgeOffset,
PushDirection direction
)
{
var minDist = double.MaxValue;
var vx = vertex.X;
@@ -315,7 +384,9 @@ namespace OpenNest.Geometry
var e1 = edges[i].start + edgeOffset;
var e2 = edges[i].end + edgeOffset;
double perpValue, edgeMin, edgeMax;
double perpValue,
edgeMin,
edgeMax;
if (horizontal)
{
perpValue = vy;
@@ -337,7 +408,8 @@ namespace OpenNest.Geometry
continue;
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
if (d < minDist) minDist = d;
if (d < minDist)
minDist = d;
}
return minDist;
@@ -347,11 +419,16 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return PushDirection.Right;
case PushDirection.Right: return PushDirection.Left;
case PushDirection.Up: return PushDirection.Down;
case PushDirection.Down: return PushDirection.Up;
default: return direction;
case PushDirection.Left:
return PushDirection.Right;
case PushDirection.Right:
return PushDirection.Left;
case PushDirection.Up:
return PushDirection.Down;
case PushDirection.Down:
return PushDirection.Up;
default:
return direction;
}
}
@@ -364,11 +441,16 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return box.Left - boundary.Left;
case PushDirection.Right: return boundary.Right - box.Right;
case PushDirection.Up: return boundary.Top - box.Top;
case PushDirection.Down: return box.Bottom - boundary.Bottom;
default: return double.MaxValue;
case PushDirection.Left:
return box.Left - boundary.Left;
case PushDirection.Right:
return boundary.Right - box.Right;
case PushDirection.Up:
return boundary.Top - box.Top;
case PushDirection.Down:
return box.Bottom - boundary.Bottom;
default:
return double.MaxValue;
}
}
@@ -376,11 +458,16 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return new Vector(-distance, 0);
case PushDirection.Right: return new Vector(distance, 0);
case PushDirection.Up: return new Vector(0, distance);
case PushDirection.Down: return new Vector(0, -distance);
default: return new Vector();
case PushDirection.Left:
return new Vector(-distance, 0);
case PushDirection.Right:
return new Vector(distance, 0);
case PushDirection.Up:
return new Vector(0, distance);
case PushDirection.Down:
return new Vector(0, -distance);
default:
return new Vector();
}
}
@@ -388,11 +475,16 @@ namespace OpenNest.Geometry
{
switch (direction)
{
case PushDirection.Left: return from.Left - to.Right;
case PushDirection.Right: return to.Left - from.Right;
case PushDirection.Up: return to.Bottom - from.Top;
case PushDirection.Down: return from.Bottom - to.Top;
default: return double.MaxValue;
case PushDirection.Left:
return from.Left - to.Right;
case PushDirection.Right:
return to.Left - from.Right;
case PushDirection.Up:
return to.Bottom - from.Top;
case PushDirection.Down:
return from.Bottom - to.Top;
default:
return double.MaxValue;
}
}
@@ -409,23 +501,27 @@ namespace OpenNest.Geometry
if (direction.X < -Tolerance.Epsilon)
{
var d = (box.Left - boundary.Left) / -direction.X;
if (d < dist) dist = d;
if (d < dist)
dist = d;
}
else if (direction.X > Tolerance.Epsilon)
{
var d = (boundary.Right - box.Right) / direction.X;
if (d < dist) dist = d;
if (d < dist)
dist = d;
}
if (direction.Y < -Tolerance.Epsilon)
{
var d = (box.Bottom - boundary.Bottom) / -direction.Y;
if (d < dist) dist = d;
if (d < dist)
dist = d;
}
else if (direction.Y > Tolerance.Epsilon)
{
var d = (boundary.Top - box.Top) / direction.Y;
if (d < dist) dist = d;
if (d < dist)
dist = d;
}
return dist < 0 ? 0 : dist;
@@ -463,7 +559,11 @@ namespace OpenNest.Geometry
/// Computes the minimum translation distance along an arbitrary unit direction
/// before any edge of movingLines contacts any edge of stationaryLines.
/// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction)
public static double DirectionalDistance(
List<Line> movingLines,
List<Line> stationaryLines,
Vector direction
)
{
var minDist = double.MaxValue;
var dirX = direction.X;
@@ -476,8 +576,18 @@ namespace OpenNest.Geometry
for (var i = 0; i < stationaryLines.Count; i++)
{
var e = stationaryLines[i];
var d = RayEdgeDistance(mv.X, mv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, dirX, dirY);
if (d < minDist) minDist = d;
var d = RayEdgeDistance(
mv.X,
mv.Y,
e.pt1.X,
e.pt1.Y,
e.pt2.X,
e.pt2.Y,
dirX,
dirY
);
if (d < minDist)
minDist = d;
}
}
@@ -491,8 +601,18 @@ namespace OpenNest.Geometry
for (var i = 0; i < movingLines.Count; i++)
{
var e = movingLines[i];
var d = RayEdgeDistance(sv.X, sv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, oppX, oppY);
if (d < minDist) minDist = d;
var d = RayEdgeDistance(
sv.X,
sv.Y,
e.pt1.X,
e.pt1.Y,
e.pt2.X,
e.pt2.Y,
oppX,
oppY
);
if (d < minDist)
minDist = d;
}
}
@@ -505,9 +625,16 @@ namespace OpenNest.Geometry
/// stationaryEntities. Delegates to the Vector-based overload.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
List<Entity> movingEntities,
List<Entity> stationaryEntities,
PushDirection direction
)
{
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
return DirectionalDistance(
movingEntities,
stationaryEntities,
DirectionToOffset(direction, 1.0)
);
}
/// <summary>
@@ -517,7 +644,10 @@ namespace OpenNest.Geometry
/// without tessellation.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
List<Entity> movingEntities,
List<Entity> stationaryEntities,
Vector direction
)
{
var minDist = double.MaxValue;
var dirX = direction.X;
@@ -536,7 +666,8 @@ namespace OpenNest.Geometry
if (d < minDist)
{
minDist = d;
if (d <= 0) return 0;
if (d <= 0)
return 0;
}
}
}
@@ -557,7 +688,8 @@ namespace OpenNest.Geometry
if (d < minDist)
{
minDist = d;
if (d <= 0) return 0;
if (d <= 0)
return 0;
}
}
}
@@ -566,10 +698,24 @@ namespace OpenNest.Geometry
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
// closest point on a small corner arc to a straight edge may lie between
// those samples. Use ClosestPointTo to find it and fire a ray from there.
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist);
if (minDist <= 0) return 0;
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist);
if (minDist <= 0) return 0;
minDist = ArcToLineClosestDistance(
movingEntities,
stationaryEntities,
dirX,
dirY,
minDist
);
if (minDist <= 0)
return 0;
minDist = ArcToLineClosestDistance(
stationaryEntities,
movingEntities,
oppX,
oppY,
minDist
);
if (minDist <= 0)
return 0;
// Phase 4: Curve-to-curve direct distance.
// The vertex-to-entity approach misses the closest contact between two
@@ -605,20 +751,35 @@ namespace OpenNest.Geometry
if (me is Arc mArc)
{
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
if (
!Angle.IsBetweenRad(
angle,
mArc.StartAngle,
mArc.EndAngle,
mArc.IsReversed
)
)
continue;
}
if (se is Arc sArc)
{
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
if (
!Angle.IsBetweenRad(
angle,
sArc.StartAngle,
sArc.EndAngle,
sArc.IsReversed
)
)
continue;
}
}
minDist = d;
if (d <= 0) return 0;
if (d <= 0)
return 0;
}
}
@@ -626,8 +787,12 @@ namespace OpenNest.Geometry
}
private static double ArcToLineClosestDistance(
List<Entity> arcEntities, List<Entity> lineEntities,
double dirX, double dirY, double minDist)
List<Entity> arcEntities,
List<Entity> lineEntities,
double dirX,
double dirY,
double minDist
)
{
for (var i = 0; i < arcEntities.Count; i++)
{
@@ -662,15 +827,30 @@ namespace OpenNest.Geometry
{
var theta = k == 0 ? theta1 : theta2;
if (!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed))
if (
!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)
)
continue;
var qx = cx + r * System.Math.Cos(theta);
var qy = cy + r * System.Math.Sin(theta);
var d = RayEdgeDistance(qx, qy, p1x, p1y, line.pt2.X, line.pt2.Y,
dirX, dirY);
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
var d = RayEdgeDistance(
qx,
qy,
p1x,
p1y,
line.pt2.X,
line.pt2.Y,
dirX,
dirY
);
if (d < minDist)
{
minDist = d;
if (d <= 0)
return 0;
}
}
}
}
@@ -678,28 +858,54 @@ namespace OpenNest.Geometry
}
private static double RayEntityDistance(
double vx, double vy, Entity entity, double dirX, double dirY)
double vx,
double vy,
Entity entity,
double dirX,
double dirY
)
{
if (entity is Line line)
{
return RayEdgeDistance(vx, vy,
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
dirX, dirY);
return RayEdgeDistance(
vx,
vy,
line.pt1.X,
line.pt1.Y,
line.pt2.X,
line.pt2.Y,
dirX,
dirY
);
}
if (entity is Arc arc)
{
return RayArcDistance(vx, vy,
arc.Center.X, arc.Center.Y, arc.Radius,
arc.StartAngle, arc.EndAngle, arc.IsReversed,
dirX, dirY);
return RayArcDistance(
vx,
vy,
arc.Center.X,
arc.Center.Y,
arc.Radius,
arc.StartAngle,
arc.EndAngle,
arc.IsReversed,
dirX,
dirY
);
}
if (entity is Circle circle)
{
return RayCircleDistance(vx, vy,
circle.Center.X, circle.Center.Y, circle.Radius,
dirX, dirY);
return RayCircleDistance(
vx,
vy,
circle.Center.X,
circle.Center.Y,
circle.Radius,
dirX,
dirY
);
}
return double.MaxValue;
@@ -759,7 +965,10 @@ namespace OpenNest.Geometry
return CollectVertices(ToEdgeArray(lines), offset);
}
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
private static HashSet<Vector> CollectVertices(
(Vector start, Vector end)[] edges,
Vector offset
)
{
var vertices = new HashSet<Vector>();
for (var i = 0; i < edges.Length; i++)
@@ -778,26 +987,48 @@ namespace OpenNest.Geometry
return edges;
}
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
private static void SortEdgesForPruning(
(Vector start, Vector end)[] edges,
PushDirection direction
)
{
if (direction == PushDirection.Left || direction == PushDirection.Right)
System.Array.Sort(edges, (a, b) =>
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
System.Array.Sort(
edges,
(a, b) =>
System
.Math.Min(a.start.Y, a.end.Y)
.CompareTo(System.Math.Min(b.start.Y, b.end.Y))
);
else
System.Array.Sort(edges, (a, b) =>
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
System.Array.Sort(
edges,
(a, b) =>
System
.Math.Min(a.start.X, a.end.X)
.CompareTo(System.Math.Min(b.start.X, b.end.X))
);
}
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
private static bool TryGetCurveParams(
Entity entity,
out double cx,
out double cy,
out double r
)
{
if (entity is Circle circle)
{
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
cx = circle.Center.X;
cy = circle.Center.Y;
r = circle.Radius;
return true;
}
if (entity is Arc arc)
{
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
cx = arc.Center.X;
cy = arc.Center.Y;
r = arc.Radius;
return true;
}
cx = cy = r = 0;
@@ -850,7 +1081,13 @@ namespace OpenNest.Geometry
return new Box(lft, btm, rgt - lft, top - btm);
}
private static bool FindVerticalLimits(Vector pt, Box bounds, List<Box> boxes, out double top, out double btm)
private static bool FindVerticalLimits(
Vector pt,
Box bounds,
List<Box> boxes,
out double top,
out double btm
)
{
top = double.MaxValue;
btm = double.MinValue;
@@ -868,20 +1105,30 @@ namespace OpenNest.Geometry
if (top == double.MaxValue)
{
if (bounds.Top > pt.Y) top = bounds.Top;
else return false;
if (bounds.Top > pt.Y)
top = bounds.Top;
else
return false;
}
if (btm == double.MinValue)
{
if (bounds.Bottom < pt.Y) btm = bounds.Bottom;
else return false;
if (bounds.Bottom < pt.Y)
btm = bounds.Bottom;
else
return false;
}
return true;
}
private static bool FindHorizontalLimits(Vector pt, Box bounds, List<Box> boxes, out double lft, out double rgt)
private static bool FindHorizontalLimits(
Vector pt,
Box bounds,
List<Box> boxes,
out double lft,
out double rgt
)
{
lft = double.MinValue;
rgt = double.MaxValue;
@@ -899,14 +1146,18 @@ namespace OpenNest.Geometry
if (rgt == double.MaxValue)
{
if (bounds.Right > pt.X) rgt = bounds.Right;
else return false;
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;
if (bounds.Left < pt.X)
lft = bounds.Left;
else
return false;
}
return true;
+32 -16
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -8,7 +8,11 @@ namespace OpenNest.Geometry
{
private const int MinPointsForArc = 3;
public static List<Entity> Convert(List<Vector> points, bool isClosed, double tolerance = 0.001)
public static List<Entity> Convert(
List<Vector> points,
bool isClosed,
double tolerance = 0.001
)
{
if (points == null || points.Count < 2)
return new List<Entity>();
@@ -37,8 +41,12 @@ namespace OpenNest.Geometry
return entities;
}
private static ArcFitResult TryFitArc(List<Vector> points, int start,
Vector chainedTangent, double tolerance)
private static ArcFitResult TryFitArc(
List<Vector> points,
int start,
Vector chainedTangent,
double tolerance
)
{
var minEnd = start + MinPointsForArc - 1;
if (minEnd >= points.Count)
@@ -83,7 +91,8 @@ namespace OpenNest.Geometry
}
private static (Vector center, double radius, double deviation) FitCircumscribed(
List<Vector> points)
List<Vector> points
)
{
if (points.Count < 3)
return (Vector.Invalid, 0, double.MaxValue);
@@ -131,11 +140,16 @@ namespace OpenNest.Geometry
}
private static (Vector center, double radius, double deviation) FitWithStartTangent(
List<Vector> points, Vector tangent) =>
ArcFit.FitWithStartTangent(points, tangent);
List<Vector> points,
Vector tangent
) => ArcFit.FitWithStartTangent(points, tangent);
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
private static double MaxRadialDeviation(
List<Vector> points,
double cx,
double cy,
double radius
) => ArcFit.MaxRadialDeviation(points, cx, cy, radius);
private static double SumSignedAngles(Vector center, List<Vector> points)
{
@@ -145,8 +159,10 @@ namespace OpenNest.Geometry
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
var da = a2 - a1;
while (da > System.Math.PI) da -= Angle.TwoPI;
while (da < -System.Math.PI) da += Angle.TwoPI;
while (da > System.Math.PI)
da -= Angle.TwoPI;
while (da < -System.Math.PI)
da += Angle.TwoPI;
total += da;
}
return total;
@@ -160,9 +176,7 @@ namespace OpenNest.Geometry
var rx = lastPt.X - center.X;
var ry = lastPt.Y - center.Y;
return totalAngle >= 0
? new Vector(-ry, rx)
: new Vector(ry, -rx);
return totalAngle >= 0 ? new Vector(-ry, rx) : new Vector(ry, -rx);
}
private static Arc CreateArc(Vector center, double radius, List<Vector> points)
@@ -174,8 +188,10 @@ namespace OpenNest.Geometry
var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X);
var isReversed = SumSignedAngles(center, points) < 0;
if (startAngle < 0) startAngle += Angle.TwoPI;
if (endAngle < 0) endAngle += Angle.TwoPI;
if (startAngle < 0)
startAngle += Angle.TwoPI;
if (endAngle < 0)
endAngle += Angle.TwoPI;
return new Arc(center, radius, startAngle, endAngle, isReversed);
}
+3 -3
View File
@@ -1,5 +1,5 @@
using OpenNest.Math;
using System;
using System;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -31,7 +31,7 @@ namespace OpenNest.Geometry
{
unchecked
{
// Use a simple but effective hash combine.
// Use a simple but effective hash combine.
// We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet
// during a single operation, raw bits or slightly rounded is usually fine.
// However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision.