using System; using System.Collections.Generic; using System.Linq; using OpenNest.Geometry; namespace OpenNest.Support { /// /// Decides whether a cut part stays level on its support bed. Plate is of even thickness, /// so the part's center of gravity is its area centroid; the part rests on the bed's /// contacts under its material and stays level while that center lies inside their convex /// hull. The margin is how far inside (positive) or outside (negative) it lies; a part with /// fewer than three contacts that are not in line can only balance, so its margin is at /// most zero. With a , the bed is also /// checked shifted by that much along each axis and diagonally, keeping the worst margin. /// A perimeter tab holds the part at the tab, so a tab point counts as one more contact. /// A tab never sits in an inside corner, where the operator cannot grind its nub flush. /// public static class SupportAnalyzer { /// Points sampled along the outline, besides its vertices, to find the best tab point. public const int TabSamples = 256; /// /// The least the outline must turn inward, in radians, for an inside corner; a run of /// inward-turning vertices (a chorded fillet) counts as one corner by its whole turn. /// public const double InsideCornerAngle = System.Math.PI / 6; /// How far a tab of size keeps from an inside corner: its uncut span and as much again. public static double InsideCornerClearance(double tabSize) => tabSize > 0 ? 2 * tabSize : 0; /// /// How far a tab point keeps from an inside corner; 0 keeps it off the corner only. /// public static PartSupport Analyze(SupportBed bed, int quadrant, Part part, double insideCornerClearance = 0) => Analyze(bed, quadrant, SupportMaterial.FromPart(part), insideCornerClearance); /// /// How far a tab point keeps from an inside corner; 0 keeps it off the corner only. /// public static PartSupport Analyze(SupportBed bed, int quadrant, SupportMaterial material, double insideCornerClearance = 0) { ArgumentNullException.ThrowIfNull(bed); ArgumentNullException.ThrowIfNull(material); if (!(insideCornerClearance >= 0) || double.IsInfinity(insideCornerClearance)) throw new ArgumentOutOfRangeException(nameof(insideCornerClearance)); if (!material.IsComplete) return new PartSupport(material.Reason); if (!material.TryGetCenterOfGravity(out var center, out _)) return new PartSupport("The part encloses no area."); var hulls = Shifts(bed.RegistrationTolerance) .Select(shift => Hull(bed.Contacts(material, quadrant, shift, extremesOnly: true))) .ToArray(); var margin = hulls.Min(hull => Margin(center, hull)); var contacts = bed.Contacts(material, quadrant, Vector.Zero); var corners = InsideCorners(material.Outline); var samples = OutlineSamples(material.Outline) .Select(point => (Point: point, Clear: Clearance(corners, point))) .ToList(); // Clear of every inside corner where the outline allows it. var allowed = samples.Where(s => IsClear(s.Clear, insideCornerClearance)).ToList(); if (allowed.Count > 0) samples = allowed; var scored = samples.Select(s => (s.Point, s.Clear, Margin: MarginWith(center, hulls, s.Point))).ToList(); // A tab cannot lift the margin past the contacts' own hull edges, so a whole stretch // of outline often ties; of those, the one nearest the line through the center across // the outline's long side, then the one farthest from an inside corner, or the first. var bestMargin = scored.Count == 0 ? double.NegativeInfinity : scored.Max(s => s.Margin); var along = scored.Count == 0 ? Vector.Zero : LongSide(material.Outline); Vector? best = null; var bestOffset = double.PositiveInfinity; var bestClear = double.NegativeInfinity; foreach (var sample in scored) { if (sample.Margin < bestMargin - Math.Tolerance.Epsilon) continue; var offset = System.Math.Abs((sample.Point - center).DotProduct(along)); if (offset < bestOffset - Math.Tolerance.Epsilon || (offset <= bestOffset + Math.Tolerance.Epsilon && sample.Clear > bestClear)) { best = sample.Point; bestOffset = offset; bestClear = sample.Clear; } } return new PartSupport(center, contacts, margin, bed.StabilityMargin, hulls, best, bestMargin, corners, insideCornerClearance); } /// The unit direction of the longer side of the outline's minimum-area bounding rectangle. private static Vector LongSide(IReadOnlyList outline) { // The full angle range checks every hull edge. var box = RotatingCalipers.MinimumBoundingRectangle(ConvexHull.Compute(outline.ToList()), 0, Math.Angle.TwoPI); var angle = box.Width >= box.Height ? box.Angle : box.Angle + Math.Angle.HalfPI; return new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); } // A point `clear` from the nearest inside corner keeps `clearance` from it; with no // clearance it must still not be the corner. internal static bool IsClear(double clear, double clearance) => clearance > 0 ? clear >= clearance : clear > Math.Tolerance.Epsilon; internal static double Clearance(IReadOnlyList corners, Vector point) { var clear = double.PositiveInfinity; foreach (var corner in corners) clear = System.Math.Min(clear, corner.DistanceTo(point)); return clear; } /// /// The outline vertices that lie in an inside corner: each run of consecutive vertices /// turning inward whose turns add up to at least . /// internal static Vector[] InsideCorners(IReadOnlyList outline) { var count = outline.Count; if (count < 3) return Array.Empty(); var area = 0.0; for (var i = 0; i < count; i++) { var a = outline[i]; var b = outline[(i + 1) % count]; area += a.X * b.Y - b.X * a.Y; } if (area == 0) return Array.Empty(); // Signed turn at each vertex: negative turns inward, whatever the outline's winding. var turns = new double[count]; for (var i = 0; i < count; i++) { var into = outline[i] - outline[(i + count - 1) % count]; var outOf = outline[(i + 1) % count] - outline[i]; var cross = into.X * outOf.Y - into.Y * outOf.X; var dot = into.X * outOf.X + into.Y * outOf.Y; turns[i] = System.Math.Atan2(cross, dot) * System.Math.Sign(area); } // Start after a vertex that does not turn inward so no run is split by the wrap. var start = Array.FindIndex(turns, turn => turn >= 0); if (start < 0) return Array.Empty(); var corners = new List(); var run = new List(); var turned = 0.0; for (var step = 1; step <= count; step++) { var i = (start + step) % count; if (turns[i] < 0) { run.Add(outline[i]); turned -= turns[i]; continue; } if (turned >= InsideCornerAngle - Math.Tolerance.Epsilon) corners.AddRange(run); run.Clear(); turned = 0; } return corners.ToArray(); } /// The worst margin of over every shifted hull, each with added. internal static double MarginWith(Vector center, IReadOnlyList hulls, Vector tab) { var worst = double.PositiveInfinity; foreach (var hull in hulls) { var points = new Vector[hull.Length + 1]; hull.CopyTo(points, 0); points[^1] = tab; worst = System.Math.Min(worst, Margin(center, Hull(points))); } return worst; } /// /// Signed distance of inside the convex polygon /// (counter-clockwise): positive inside, negative outside, and /// never positive for a point or segment hull. An empty hull is negative infinity. /// internal static double Margin(Vector point, IReadOnlyList hull) { switch (hull.Count) { case 0: return double.NegativeInfinity; case 1: return -point.DistanceTo(hull[0]); case 2: return -SegmentDistance(point, hull[0], hull[1]); } var inside = double.PositiveInfinity; var outside = double.PositiveInfinity; var isInside = true; for (var i = 0; i < hull.Count; i++) { var a = hull[i]; var b = hull[(i + 1) % hull.Count]; var edge = b - a; var length = System.Math.Sqrt(edge.X * edge.X + edge.Y * edge.Y); var side = (edge.X * (point.Y - a.Y) - edge.Y * (point.X - a.X)) / length; if (side < 0) isInside = false; inside = System.Math.Min(inside, side); outside = System.Math.Min(outside, SegmentDistance(point, a, b)); } return isInside ? inside : -outside; } /// The convex hull, counter-clockwise, without repeated or collinear points (monotone chain). internal static Vector[] Hull(IEnumerable points) { var sorted = points.Distinct().OrderBy(p => p.X).ThenBy(p => p.Y).ToArray(); if (sorted.Length < 3) return sorted; var hull = new Vector[sorted.Length * 2]; var count = 0; foreach (var point in sorted) { while (count >= 2 && Cross(hull[count - 2], hull[count - 1], point) <= 0) count--; hull[count++] = point; } var lower = count + 1; for (var i = sorted.Length - 2; i >= 0; i--) { while (count >= lower && Cross(hull[count - 2], hull[count - 1], sorted[i]) <= 0) count--; hull[count++] = sorted[i]; } // The last point repeats the first. return hull.Take(count - 1).ToArray(); } private static double Cross(Vector o, Vector a, Vector b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); private static double SegmentDistance(Vector point, Vector a, Vector b) { var edge = b - a; var lengthSquared = edge.X * edge.X + edge.Y * edge.Y; if (lengthSquared <= 0) return point.DistanceTo(a); var t = System.Math.Clamp(((point.X - a.X) * edge.X + (point.Y - a.Y) * edge.Y) / lengthSquared, 0, 1); return point.DistanceTo(a + edge * t); } private static IEnumerable Shifts(double tolerance) { if (tolerance <= 0) return new[] { Vector.Zero }; var steps = new[] { -tolerance, 0, tolerance }; return steps.SelectMany(x => steps.Select(y => new Vector(x, y))); } // Every outline vertex, then points spaced evenly along the outline. private static IEnumerable OutlineSamples(IReadOnlyList outline) { foreach (var vertex in outline) yield return vertex; var perimeter = 0.0; for (var i = 0; i < outline.Count; i++) perimeter += outline[i].DistanceTo(outline[(i + 1) % outline.Count]); if (!(perimeter > 0)) yield break; var step = perimeter / TabSamples; var travelled = 0.0; var next = step; for (var i = 0; i < outline.Count; i++) { var a = outline[i]; var b = outline[(i + 1) % outline.Count]; var length = a.DistanceTo(b); while (length > 0 && next < travelled + length) { yield return a + (b - a) * ((next - travelled) / length); next += step; } travelled += length; } } } /// /// How one placed part sits on its support bed (world coordinates): see /// . Immutable. /// public sealed class PartSupport { private readonly IReadOnlyList hulls = Array.Empty(); internal PartSupport(string reason) { Reason = reason ?? "The part's support could not be analyzed."; Contacts = Array.Empty(); } internal PartSupport(Vector centerOfGravity, IReadOnlyList contacts, double margin, double requiredMargin, IReadOnlyList hulls, Vector? bestTabPoint, double bestTabMargin, IReadOnlyList insideCorners, double insideCornerClearance) { CenterOfGravity = centerOfGravity; Contacts = contacts; Margin = margin; RequiredMargin = requiredMargin; this.hulls = hulls; BestTabPoint = bestTabPoint; BestTabMargin = bestTabMargin; InsideCorners = insideCorners; InsideCornerClearance = insideCornerClearance; } /// True when the part was analyzed; otherwise says why not. public bool IsComplete => Reason == null; public string Reason { get; } public Vector CenterOfGravity { get; } = Vector.Invalid; /// Every bed contact under the part at the bed's nominal position. public IReadOnlyList Contacts { get; } /// The outline vertices in an inside corner (see ). public IReadOnlyList InsideCorners { get; } = Array.Empty(); /// How far an accepted tab point keeps from an inside corner. public double InsideCornerClearance { get; } /// The worst signed distance of the center of gravity inside the contacts' hull. public double Margin { get; } = double.NegativeInfinity; /// The bed's . public double RequiredMargin { get; } /// True when the bed alone holds the part level by at least the required margin. public bool IsSupported => IsComplete && Meets(Margin); /// /// The outline point clear of inside corners where one tab steadies the part most, or /// null when not analyzed. Of equally good points, the one farthest from an inside corner. /// public Vector? BestTabPoint { get; } /// The margin with a tab at . public double BestTabMargin { get; } = double.NegativeInfinity; /// True when a tab at holds the part level by the required margin. public bool TabCanSupport => IsComplete && Meets(BestTabMargin); /// The worst margin with a tab holding the part at . public double MarginWith(Vector tab) => IsComplete ? SupportAnalyzer.MarginWith(CenterOfGravity, hulls, tab) : double.NegativeInfinity; /// True when a tab at holds the part level by the required margin. public bool IsSteadiedByTabAt(Vector tab) => IsComplete && Meets(MarginWith(tab)); /// /// Whether a tab at is good enough: it keeps clear of inside corners, /// and it holds the part level by the required margin or, where no single tab can, it is /// as good as the best one found. /// public bool AcceptsTabAt(Vector tab) { if (!IsComplete || !IsClearOfInsideCorners(tab)) return false; var margin = MarginWith(tab); return TabCanSupport ? Meets(margin) : margin >= BestTabMargin - Math.Tolerance.Epsilon; } /// Whether a tab at holds the part level by the required margin. public bool HoldsLevelWithTabAt(Vector tab) => IsComplete && Meets(MarginWith(tab)); /// Whether keeps from every inside corner. public bool IsClearOfInsideCorners(Vector point) => SupportAnalyzer.IsClear(SupportAnalyzer.Clearance(InsideCorners, point), InsideCornerClearance); private bool Meets(double margin) => margin > 0 && margin >= RequiredMargin; } }