diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs
index d45fa5d..df457ac 100644
--- a/OpenNest.Core/Geometry/SpatialQuery.cs
+++ b/OpenNest.Core/Geometry/SpatialQuery.cs
@@ -596,6 +596,138 @@ namespace OpenNest.Geometry
return minDist;
}
+ ///
+ /// Computes the minimum translation distance along an arbitrary unit direction
+ /// before any vertex/edge of movingEntities contacts any vertex/edge of
+ /// stationaryEntities. Works with native Line, Arc, and Circle entities
+ /// without tessellation.
+ ///
+ public static double DirectionalDistance(
+ List movingEntities, List stationaryEntities, Vector direction)
+ {
+ var minDist = double.MaxValue;
+ var dirX = direction.X;
+ var dirY = direction.Y;
+
+ var movingVertices = ExtractEntityVertices(movingEntities);
+
+ for (var v = 0; v < movingVertices.Length; v++)
+ {
+ var vx = movingVertices[v].X;
+ var vy = movingVertices[v].Y;
+
+ for (var j = 0; j < stationaryEntities.Count; j++)
+ {
+ var d = RayEntityDistance(vx, vy, stationaryEntities[j], dirX, dirY);
+ if (d < minDist)
+ {
+ minDist = d;
+ if (d <= 0) return 0;
+ }
+ }
+ }
+
+ var oppX = -dirX;
+ var oppY = -dirY;
+
+ var stationaryVertices = ExtractEntityVertices(stationaryEntities);
+
+ for (var v = 0; v < stationaryVertices.Length; v++)
+ {
+ var vx = stationaryVertices[v].X;
+ var vy = stationaryVertices[v].Y;
+
+ for (var j = 0; j < movingEntities.Count; j++)
+ {
+ var d = RayEntityDistance(vx, vy, movingEntities[j], oppX, oppY);
+ if (d < minDist)
+ {
+ minDist = d;
+ if (d <= 0) return 0;
+ }
+ }
+ }
+
+ return minDist;
+ }
+
+ private static double RayEntityDistance(
+ 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);
+ }
+
+ 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);
+ }
+
+ if (entity is Circle circle)
+ {
+ return RayCircleDistance(vx, vy,
+ circle.Center.X, circle.Center.Y, circle.Radius,
+ dirX, dirY);
+ }
+
+ return double.MaxValue;
+ }
+
+ private static Vector[] ExtractEntityVertices(List entities)
+ {
+ var vertices = new HashSet();
+
+ for (var i = 0; i < entities.Count; i++)
+ {
+ var entity = entities[i];
+
+ if (entity is Line line)
+ {
+ vertices.Add(line.pt1);
+ vertices.Add(line.pt2);
+ }
+ else if (entity is Arc arc)
+ {
+ vertices.Add(arc.StartPoint());
+ vertices.Add(arc.EndPoint());
+ AddArcExtremeVertices(vertices, arc);
+ }
+ else if (entity is Circle circle)
+ {
+ vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y));
+ vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y));
+ vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius));
+ vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius));
+ }
+ }
+
+ return vertices.ToArray();
+ }
+
+ private static void AddArcExtremeVertices(HashSet points, Arc arc)
+ {
+ var a1 = arc.StartAngle;
+ var a2 = arc.EndAngle;
+
+ if (arc.IsReversed)
+ Generic.Swap(ref a1, ref a2);
+
+ if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
+ points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
+ if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
+ points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
+ if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
+ points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
+ if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
+ points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
+ }
+
private static double BoxProjectionMin(Box box, double dx, double dy)
{
var x = dx >= 0 ? box.Left : box.Right;
diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs
index d6149fe..c30f6e0 100644
--- a/OpenNest.Core/PartGeometry.cs
+++ b/OpenNest.Core/PartGeometry.cs
@@ -61,6 +61,91 @@ namespace OpenNest
return offsetShape.Entities;
}
+ ///
+ /// Returns all entities (perimeter + cutouts) with spacing offset applied,
+ /// without tessellation. Perimeter is offset outward, cutouts inward.
+ ///
+ public static List GetOffsetPartEntities(Part part, double spacing)
+ {
+ var geoEntities = ConvertProgram.ToGeometry(part.Program);
+ var profile = new ShapeProfile(
+ geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
+ var entities = new List();
+
+ var perimeter = profile.Perimeter.OffsetOutward(spacing);
+ if (perimeter != null)
+ {
+ foreach (var entity in perimeter.Entities)
+ entity.Offset(part.Location);
+ entities.AddRange(perimeter.Entities);
+ }
+
+ foreach (var cutout in profile.Cutouts)
+ {
+ var inset = cutout.OffsetInward(spacing);
+ if (inset == null) continue;
+ foreach (var entity in inset.Entities)
+ entity.Offset(part.Location);
+ entities.AddRange(inset.Entities);
+ }
+
+ return entities;
+ }
+
+ ///
+ /// Returns perimeter entities at the part's world location, without tessellation
+ /// or spacing offset.
+ ///
+ public static List GetPerimeterEntities(Part part)
+ {
+ var geoEntities = ConvertProgram.ToGeometry(part.Program);
+ var profile = new ShapeProfile(
+ geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
+
+ return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
+ }
+
+ ///
+ /// Returns all entities (perimeter + cutouts) at the part's world location,
+ /// without tessellation or spacing offset.
+ ///
+ public static List GetPartEntities(Part part)
+ {
+ var geoEntities = ConvertProgram.ToGeometry(part.Program);
+ var profile = new ShapeProfile(
+ geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
+ var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
+
+ foreach (var cutout in profile.Cutouts)
+ entities.AddRange(CopyEntitiesAtLocation(cutout.Entities, part.Location));
+
+ return entities;
+ }
+
+ private static List CopyEntitiesAtLocation(List source, Vector location)
+ {
+ var result = new List(source.Count);
+
+ for (var i = 0; i < source.Count; i++)
+ {
+ var entity = source[i];
+ Entity copy;
+
+ if (entity is Line line)
+ copy = new Line(line.StartPoint + location, line.EndPoint + location);
+ else if (entity is Arc arc)
+ copy = new Arc(arc.Center + location, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed);
+ else if (entity is Circle circle)
+ copy = new Circle(circle.Center + location, circle.Radius);
+ else
+ continue;
+
+ result.Add(copy);
+ }
+
+ return result;
+ }
+
public static List GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
bool perimeterOnly = false)
{
diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs
index cc6b4fe..26cef71 100644
--- a/OpenNest.Engine/Fill/Compactor.cs
+++ b/OpenNest.Engine/Fill/Compactor.cs
@@ -11,8 +11,6 @@ namespace OpenNest.Engine.Fill
///
public static class Compactor
{
- private const double ChordTolerance = 0.001;
-
public static double Push(List movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
@@ -44,7 +42,7 @@ namespace OpenNest.Engine.Fill
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
- var obstacleLines = new List[obstacleParts.Count];
+ var obstacleEntities = new List[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
@@ -61,7 +59,19 @@ namespace OpenNest.Engine.Fill
distance = edgeDist;
var movingBox = moving.BoundingBox;
- List movingLines = null;
+ List movingEntities = null;
+
+ // Check if any obstacle is inside the moving part — only then
+ // do we need cutout entities on the moving part.
+ var needCutouts = false;
+ for (var i = 0; i < obstacleBoxes.Length; i++)
+ {
+ if (movingBox.Contains(obstacleBoxes[i]))
+ {
+ needCutouts = true;
+ break;
+ }
+ }
for (var i = 0; i < obstacleBoxes.Length; i++)
{
@@ -76,15 +86,19 @@ namespace OpenNest.Engine.Fill
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue;
- movingLines ??= halfSpacing > 0
- ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
- : PartGeometry.GetPartLines(moving, direction, ChordTolerance);
+ movingEntities ??= halfSpacing > 0
+ ? (needCutouts
+ ? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
+ : PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing))
+ : (needCutouts
+ ? PartGeometry.GetPartEntities(moving)
+ : PartGeometry.GetPerimeterEntities(moving));
- obstacleLines[i] ??= halfSpacing > 0
- ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
- : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
+ obstacleEntities[i] ??= halfSpacing > 0
+ ? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
+ : PartGeometry.GetPerimeterEntities(obstacleParts[i]);
- var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
+ var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
if (d < distance)
distance = d;
}
@@ -157,7 +171,7 @@ namespace OpenNest.Engine.Fill
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
- var d = gap - partSpacing - 2 * ChordTolerance;
+ var d = gap - partSpacing - 0.002;
if (d < 0) d = 0;
if (d < distance)
distance = d;