#nullable enable using System; using System.Collections.Generic; using System.Linq; using Clipper2Lib; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Engine.NestingEngines.Irregular; /// /// Finds a free rotation for a part that fits no sampled orientation, from the contacts it makes /// rather than from more sampled angles. /// /// A part on a sheet moves three ways: x, y and rotation. Its pose starts at a catalog /// orientation with its center on the largest free pocket. Each pass collects its contacts: /// every footprint vertex inside or near a neighbor's footprint, every neighbor vertex inside or /// near its own, and every outline vertex at or past the work area. A contact is a linear /// constraint on the small move (dx, dy, d-theta) that clears it with its normal, and nearby /// clear contacts keep the move from opening new overlaps. Cyclic projection finds the least move /// meeting them all; the pose takes it (at most one unit) and the pass repeats until nothing /// overlaps. Three contacts then hold the part, so its angle is set by the neighbors around it. /// /// The solver only proposes a pose. The caller checks it on the exact free region of an /// orientation flattened at that angle, the same no-fit polygons every other placement uses, /// so spacing and validation are unchanged. Deterministic: no clocks or randomness. /// internal static class ContactFit { /// Most passes from one starting pose. private const int MaxPasses = 400; /// Projection sweeps over the contacts of one pass. private const int Sweeps = 60; /// Clear contacts nearer than this still constrain the move. private const double Margin = 0.3; /// Penetration still counted as touching. private const double Touch = 1e-4; /// Passes without a smaller worst overlap before the move is halved. private const int StallPasses = 8; /// Shortest move cap before the solve gives up. private const double MinStep = 1e-3; /// Worst overlap, as a share of the part's half size, of a pose still handed back /// for the exact check. private const double NearlyClear = 0.01; /// Largest move of one pass, in job units (rotation measured at the part's half size). private const double MaxStep = 1.0; /// An obstacle polygon and whether the moving footprint (true) or bare outline /// (false) must clear it: placed parts are footprints, clamp keep-outs already hold their /// clearance. internal readonly record struct Obstacle(PathD Polygon, bool AgainstFootprint); /// A solved pose: the absolute rotation and the translation of the snapshot origin. internal readonly record struct Pose(double Rotation, double X, double Y); /// /// Solves from at the given center; /// belongs to . Returns the clear pose, else the least-overlapping one /// when its worst overlap is under one percent of the part's half size, else null. /// public static Pose? Solve( Orientation start, PathD footprint, IReadOnlyList obstacles, Box work, double centerX, double centerY, WorkCounter counter) { var bounds = Clipper.GetBounds(start.Outline); var pivotX = (bounds.left + bounds.right) / 2; var pivotY = (bounds.top + bounds.bottom) / 2; var radius = System.Math.Max(1e-6, System.Math.Max(bounds.right - bounds.left, bounds.bottom - bounds.top) / 2); var foot = Local(footprint, pivotX, pivotY); var line = Local(start.Outline, pivotX, pivotY); var near = obstacles.Select(o => (o.Polygon, o.AgainstFootprint, Bounds: Clipper.GetBounds(o.Polygon))).ToArray(); double theta = 0, x = centerX, y = centerY; Pose At() => new(start.Rotation + theta, x - (PortableMath.Cos(theta) * pivotX - PortableMath.Sin(theta) * pivotY), y - (PortableMath.Sin(theta) * pivotX + PortableMath.Cos(theta) * pivotY)); Pose? best = null; var bestWorst = double.PositiveInfinity; var cap = MaxStep; var stalled = 0; var constraints = new List<(double Gx, double Gy, double Gt, double Depth)>(); for (var pass = 0; pass < MaxPasses; pass++) { var movingFoot = Place(foot, theta, x, y); var movingLine = Place(line, theta, x, y); constraints.Clear(); void Add(PointD at, double nx, double ny, double depth) { var rx = at.x - x; var ry = at.y - y; constraints.Add((nx, ny, (rx * ny - ry * nx) / radius, depth)); } var footBounds = Clipper.GetBounds(movingFoot); var lineBounds = Clipper.GetBounds(movingLine); foreach (var (polygon, againstFoot, box) in near) { var moving = againstFoot ? movingFoot : movingLine; var movingBox = againstFoot ? footBounds : lineBounds; if (box.left > movingBox.right + Margin || box.right < movingBox.left - Margin || box.top > movingBox.bottom + Margin || box.bottom < movingBox.top - Margin) continue; foreach (var q in moving) { if (q.x < box.left - Margin || q.x > box.right + Margin || q.y < box.top - Margin || q.y > box.bottom + Margin) continue; var (distance, nx, ny) = Nearest(polygon, q); if (Clipper.PointInPolygon(q, polygon) == PointInPolygonResult.IsInside) Add(q, nx, ny, distance); else if (distance < Margin) Add(q, -nx, -ny, -distance); } foreach (var w in polygon) { if (w.x < movingBox.left - Margin || w.x > movingBox.right + Margin || w.y < movingBox.top - Margin || w.y > movingBox.bottom + Margin) continue; var (distance, nx, ny) = Nearest(moving, w); if (Clipper.PointInPolygon(w, moving) == PointInPolygonResult.IsInside) Add(w, -nx, -ny, distance); else if (distance < Margin) Add(w, nx, ny, -distance); } counter.Add(1); } foreach (var v in movingLine) { if (v.x - work.Left < Margin) Add(v, 1, 0, work.Left - v.x); if (work.Right - v.x < Margin) Add(v, -1, 0, v.x - work.Right); if (v.y - work.Bottom < Margin) Add(v, 0, 1, work.Bottom - v.y); if (work.Top - v.y < Margin) Add(v, 0, -1, v.y - work.Top); } var worst = constraints.Count == 0 ? 0 : constraints.Max(c => c.Depth); if (worst <= Touch) return At(); if (worst < bestWorst - Touch) { best = At(); bestWorst = worst; stalled = 0; } else if (++stalled >= StallPasses) { // Linearized contacts overshoot near the end; settle with shorter moves. cap /= 2; stalled = 0; if (cap < MinStep) break; } double dx = 0, dy = 0, du = 0; for (var sweep = 0; sweep < Sweeps; sweep++) foreach (var (gx, gy, gt, depth) in constraints) { var need = depth + Touch; var have = gx * dx + gy * dy + gt * du; if (have >= need) continue; var scale = (need - have) / (gx * gx + gy * gy + gt * gt); dx += scale * gx; dy += scale * gy; du += scale * gt; } var step = System.Math.Sqrt(dx * dx + dy * dy + du * du); if (step < 1e-9) break; // The contacts pull against each other: no move clears them. if (step > cap) { dx *= cap / step; dy *= cap / step; du *= cap / step; } x += dx; y += dy; theta += du / radius; } // Nearly clear: the angle is what the caller needs, and its exact free region finds the // position, so a pose still overlapping slightly is worth checking. return bestWorst <= NearlyClear * radius ? best : null; } private static PathD Local(PathD path, double pivotX, double pivotY) { var result = new PathD(path.Count); foreach (var p in path) result.Add(new PointD(p.x - pivotX, p.y - pivotY)); return result; } private static PathD Place(PathD local, double theta, double x, double y) { var cos = PortableMath.Cos(theta); var sin = PortableMath.Sin(theta); var result = new PathD(local.Count); foreach (var p in local) result.Add(new PointD(cos * p.x - sin * p.y + x, sin * p.x + cos * p.y + y)); return result; } /// Distance from to the nearest edge of a CCW polygon, with /// that edge's outward normal. private static (double Distance, double Nx, double Ny) Nearest(PathD polygon, PointD q) { double best = double.PositiveInfinity, bestNx = 0, bestNy = 0; for (var i = 0; i < polygon.Count; i++) { var a = polygon[i]; var b = polygon[(i + 1) % polygon.Count]; var ex = b.x - a.x; var ey = b.y - a.y; var lengthSquared = ex * ex + ey * ey; if (lengthSquared < 1e-18) continue; var t = System.Math.Clamp(((q.x - a.x) * ex + (q.y - a.y) * ey) / lengthSquared, 0, 1); var px = a.x + t * ex - q.x; var py = a.y + t * ey - q.y; var distance = System.Math.Sqrt(px * px + py * py); if (distance >= best) continue; var length = System.Math.Sqrt(lengthSquared); best = distance; bestNx = ey / length; bestNy = -ex / length; } return (best, bestNx, bestNy); } }