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;