diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs index dca54cf..39a226b 100644 --- a/OpenNest.Core/Geometry/SpatialQuery.cs +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -71,6 +71,40 @@ namespace OpenNest.Geometry } } + /// + /// Generalized ray-edge distance along an arbitrary unit direction vector. + /// Returns double.MaxValue if the ray does not hit the segment. + /// + [System.Runtime.CompilerServices.MethodImpl( + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static double RayEdgeDistance( + double vx, double vy, + double p1x, double p1y, double p2x, double p2y, + double dirX, double dirY) + { + var ex = p2x - p1x; + var ey = p2y - p1y; + + var det = ex * dirY - ey * dirX; + if (System.Math.Abs(det) < Tolerance.Epsilon) + return double.MaxValue; + + var dvx = p1x - vx; + var dvy = p1y - vy; + + var t = (ex * dvy - ey * dvx) / det; + if (t < -Tolerance.Epsilon) + return double.MaxValue; + + var s = (dirX * dvy - dirY * dvx) / det; + if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + if (t > Tolerance.Epsilon) return t; + if (t >= -Tolerance.Epsilon) return 0; + return double.MaxValue; + } + /// /// Computes the minimum translation distance along a push direction before /// any edge of movingLines contacts any edge of stationaryLines. @@ -361,6 +395,135 @@ namespace OpenNest.Geometry } } + #region Generalized direction (Vector) overloads + + /// + /// Computes how far a box can travel along the given unit direction + /// before exiting the boundary box. + /// + public static double EdgeDistance(Box box, Box boundary, Vector direction) + { + var dist = double.MaxValue; + + if (direction.X < -Tolerance.Epsilon) + { + var d = (box.Left - boundary.Left) / -direction.X; + 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 (direction.Y < -Tolerance.Epsilon) + { + var d = (box.Bottom - boundary.Bottom) / -direction.Y; + if (d < dist) dist = d; + } + else if (direction.Y > Tolerance.Epsilon) + { + var d = (boundary.Top - box.Top) / direction.Y; + if (d < dist) dist = d; + } + + return dist < 0 ? 0 : dist; + } + + /// + /// Computes the directional gap between two boxes along an arbitrary unit direction. + /// Positive means 'to' is ahead of 'from' in the push direction. + /// + public static double DirectionalGap(Box from, Box to, Vector direction) + { + var fromMax = BoxProjectionMax(from, direction.X, direction.Y); + var toMin = BoxProjectionMin(to, direction.X, direction.Y); + return toMin - fromMax; + } + + /// + /// Returns true if two boxes overlap when projected onto the axis + /// perpendicular to the given unit direction. + /// + public static bool PerpendicularOverlap(Box a, Box b, Vector direction) + { + var px = -direction.Y; + var py = direction.X; + + var aMin = BoxProjectionMin(a, px, py); + var aMax = BoxProjectionMax(a, px, py); + var bMin = BoxProjectionMin(b, px, py); + var bMax = BoxProjectionMax(b, px, py); + + return aMin <= bMax + Tolerance.Epsilon && bMin <= aMax + Tolerance.Epsilon; + } + + /// + /// Computes the minimum translation distance along an arbitrary unit direction + /// before any edge of movingLines contacts any edge of stationaryLines. + /// + public static double DirectionalDistance(List movingLines, List stationaryLines, Vector direction) + { + var minDist = double.MaxValue; + var dirX = direction.X; + var dirY = direction.Y; + + var movingVertices = new HashSet(); + for (var i = 0; i < movingLines.Count; i++) + { + movingVertices.Add(movingLines[i].pt1); + movingVertices.Add(movingLines[i].pt2); + } + + foreach (var mv in movingVertices) + { + 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 oppX = -dirX; + var oppY = -dirY; + + var stationaryVertices = new HashSet(); + for (var i = 0; i < stationaryLines.Count; i++) + { + stationaryVertices.Add(stationaryLines[i].pt1); + stationaryVertices.Add(stationaryLines[i].pt2); + } + + foreach (var sv in stationaryVertices) + { + 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; + } + } + + return minDist; + } + + private static double BoxProjectionMin(Box box, double dx, double dy) + { + var x = dx >= 0 ? box.Left : box.Right; + var y = dy >= 0 ? box.Bottom : box.Top; + return x * dx + y * dy; + } + + private static double BoxProjectionMax(Box box, double dx, double dy) + { + var x = dx >= 0 ? box.Right : box.Left; + var y = dy >= 0 ? box.Top : box.Bottom; + return x * dx + y * dy; + } + + #endregion + public static double ClosestDistanceLeft(Box box, List boxes) { var closestDistance = double.MaxValue; diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs index be5c3d7..3b9234f 100644 --- a/OpenNest.Core/PartGeometry.cs +++ b/OpenNest.Core/PartGeometry.cs @@ -85,6 +85,73 @@ namespace OpenNest return lines; } + public static List GetPartLines(Part part, Vector facingDirection, double chordTolerance = 0.001) + { + var entities = ConvertProgram.ToGeometry(part.Program); + var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var lines = new List(); + + foreach (var shape in shapes) + { + var polygon = shape.ToPolygonWithTolerance(chordTolerance); + polygon.Offset(part.Location); + lines.AddRange(GetDirectionalLines(polygon, facingDirection)); + } + + return lines; + } + + public static List GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001) + { + var entities = ConvertProgram.ToGeometry(part.Program); + var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var lines = new List(); + + foreach (var shape in shapes) + { + var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape; + + if (offsetEntity == null) + continue; + + var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance); + polygon.RemoveSelfIntersections(); + polygon.Offset(part.Location); + lines.AddRange(GetDirectionalLines(polygon, facingDirection)); + } + + return lines; + } + + /// + /// Returns only polygon edges whose outward normal faces the specified direction vector. + /// + private static List GetDirectionalLines(Polygon polygon, Vector direction) + { + if (polygon.Vertices.Count < 3) + return polygon.ToLines(); + + var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0; + var lines = new List(); + var last = polygon.Vertices[0]; + + for (var i = 1; i < polygon.Vertices.Count; i++) + { + var current = polygon.Vertices[i]; + var edx = current.X - last.X; + var edy = current.Y - last.Y; + + var keep = sign * (edy * direction.X - edx * direction.Y) > 0; + + if (keep) + lines.Add(new Line(last, current)); + + last = current; + } + + return lines; + } + /// /// Returns only polygon edges whose outward normal faces the specified direction. /// diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs index 747e20f..90dd994 100644 --- a/OpenNest.Engine/Compactor.cs +++ b/OpenNest.Engine/Compactor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using OpenNest.Geometry; @@ -84,6 +85,85 @@ namespace OpenNest return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); } + /// + /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). + /// + public static double Push(List movingParts, Plate plate, double angle) + { + var obstacleParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle); + } + + /// + /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). + /// + public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, double angle) + { + var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); + var opposite = -direction; + + var obstacleBoxes = new Box[obstacleParts.Count]; + var obstacleLines = new List[obstacleParts.Count]; + + for (var i = 0; i < obstacleParts.Count; i++) + obstacleBoxes[i] = obstacleParts[i].BoundingBox; + + var halfSpacing = partSpacing / 2; + var distance = double.MaxValue; + + foreach (var moving in movingParts) + { + var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); + if (edgeDist <= 0) + distance = 0; + else if (edgeDist < distance) + distance = edgeDist; + + var movingBox = moving.BoundingBox; + List movingLines = null; + + for (var i = 0; i < obstacleBoxes.Length; i++) + { + var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + if (reverseGap > 0) + continue; + + var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); + if (gap >= distance) + continue; + + if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction)) + continue; + + movingLines ??= halfSpacing > 0 + ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) + : PartGeometry.GetPartLines(moving, direction, ChordTolerance); + + obstacleLines[i] ??= halfSpacing > 0 + ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) + : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); + + var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); + if (d < distance) + distance = d; + } + } + + if (distance < double.MaxValue && distance > 0) + { + var offset = direction * distance; + foreach (var moving in movingParts) + moving.Offset(offset); + return distance; + } + + return 0; + } + public static double Push(List movingParts, List obstacleParts, Box workArea, double partSpacing, PushDirection direction) { @@ -158,6 +238,73 @@ namespace OpenNest return 0; } + /// + /// Pushes movingParts using bounding-box distances only (no geometry lines). + /// Much faster but less precise — use as a coarse positioning pass before + /// a full geometry Push. + /// + public static double PushBoundingBox(List movingParts, Plate plate, PushDirection direction) + { + var obstacleParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + } + + public static double PushBoundingBox(List movingParts, List obstacleParts, + Box workArea, double partSpacing, PushDirection direction) + { + var obstacleBoxes = new Box[obstacleParts.Count]; + for (var i = 0; i < obstacleParts.Count; i++) + obstacleBoxes[i] = obstacleParts[i].BoundingBox; + + var opposite = SpatialQuery.OppositeDirection(direction); + var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); + var distance = double.MaxValue; + + foreach (var moving in movingParts) + { + var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); + if (edgeDist <= 0) + distance = 0; + else if (edgeDist < distance) + distance = edgeDist; + + var movingBox = moving.BoundingBox; + + for (var i = 0; i < obstacleBoxes.Length; i++) + { + var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + if (reverseGap > 0) + continue; + + var perpOverlap = isHorizontal + ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _) + : movingBox.IsVerticalTo(obstacleBoxes[i], out _); + + if (!perpOverlap) + continue; + + var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); + var d = gap - partSpacing; + if (d < 0) d = 0; + if (d < distance) + distance = d; + } + } + + if (distance < double.MaxValue && distance > 0) + { + var offset = SpatialQuery.DirectionToOffset(direction, distance); + foreach (var moving in movingParts) + moving.Offset(offset); + return distance; + } + + return 0; + } + /// /// Compacts parts individually toward the bottom-left of the work area. /// Each part is pushed against all others as obstacles, closing geometry-based gaps. diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index 793c6eb..669ccf5 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -141,28 +141,28 @@ namespace OpenNest.Actions { if ((Control.ModifierKeys & Keys.Shift) == Keys.Shift) { + var movingParts = parts.Select(p => p.BasePart).ToList(); + + PushDirection hDir, vDir; switch (plateView.Plate.Quadrant) { - case 1: - plateView.PushSelected(PushDirection.Left); - plateView.PushSelected(PushDirection.Down); - break; - - case 2: - plateView.PushSelected(PushDirection.Right); - plateView.PushSelected(PushDirection.Down); - break; - - case 3: - plateView.PushSelected(PushDirection.Right); - plateView.PushSelected(PushDirection.Up); - break; - - case 4: - plateView.PushSelected(PushDirection.Left); - plateView.PushSelected(PushDirection.Up); - break; + case 1: hDir = PushDirection.Left; vDir = PushDirection.Down; break; + case 2: hDir = PushDirection.Right; vDir = PushDirection.Down; break; + case 3: hDir = PushDirection.Right; vDir = PushDirection.Up; break; + case 4: hDir = PushDirection.Left; vDir = PushDirection.Up; break; + default: hDir = PushDirection.Left; vDir = PushDirection.Down; break; } + + // Phase 1: BB-only push to get past irregular geometry quickly. + Compactor.PushBoundingBox(movingParts, plateView.Plate, hDir); + Compactor.PushBoundingBox(movingParts, plateView.Plate, vDir); + + // Phase 2: Geometry push to settle against actual contours. + Compactor.Push(movingParts, plateView.Plate, hDir); + Compactor.Push(movingParts, plateView.Plate, vDir); + + parts.ForEach(p => p.IsDirty = true); + plateView.Invalidate(); } parts.ForEach(p => plateView.Plate.Parts.Add(p.BasePart.Clone() as Part));