using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
///
/// A part's native contours in world space, flattened at , to
/// confirm a part-spacing candidate that found. The outline
/// circumscribes arcs by up to its chord tolerance, more than ,
/// so two convex arcs facing each other exactly at the spacing can overlap there; these
/// contours measure that gap to within a tenth of the slack. Every spacing verdict (Auto
/// Nest's layout check, the whole-job runner's candidate validation and the pre-post review)
/// confirms with them, so none can disagree.
///
public sealed class NativeContours
{
/// Chord tolerance of the confirming measurement. Circumscribed on both sides, it
/// understates a gap by at most twice this, a tenth of .
public const double ChordTolerance = SpacingOutline.SpacingSlack / 20;
private Polygon perimeter;
private List holes;
private List perimeterLines;
private List> holeLines;
private NativeContours() { }
public Box Bounds { get; private set; }
/// Flattens (already rotated) at .
public static NativeContours Of(ShapeProfile profile, Vector location) =>
Of(profile, location, ChordTolerance);
/// As at a coarser
/// , for searches that confirm their answer at the fine one.
public static NativeContours Of(ShapeProfile profile, Vector location, double chordTolerance)
{
var perimeter = World(profile.Perimeter, location, chordTolerance);
var holes = profile.Cutouts.Select(cutout => World(cutout, location, chordTolerance)).ToList();
return new NativeContours
{
perimeter = perimeter,
holes = holes,
perimeterLines = perimeter.ToLines(),
holeLines = holes.Select(hole => hole.ToLines()).ToList(),
Bounds = perimeter.BoundingBox,
};
}
///
/// True when the two parts are closer than less
/// , or share material: a boundary within that
/// gap, or one part wholly inside the other's material rather than in a cutout.
///
public bool TooClose(NativeContours other, double spacing)
{
if (GapTo(other, spacing, out _) < spacing - SpacingOutline.SpacingSlack)
return true;
// No boundaries meet, so either part is wholly inside or outside the other's material.
return Nested(other);
}
/// True when a vertex of either part lies in the other's material rather than in
/// a cutout or outside: with no boundary crossing, one part is inside the other.
public bool Nested(NativeContours other) =>
other.InMaterial(perimeter.Vertices[0]) || InMaterial(other.perimeter.Vertices[0]);
/// These contours moved by , reusing the flattening.
public NativeContours Translated(Vector offset) => Transformed(0, offset);
/// These contours turned by (radians) about the origin,
/// then moved by , reusing the flattening: a rigid turn keeps
/// every chord within its tolerance.
public NativeContours Transformed(double angle, Vector offset)
{
var moved = Moved(perimeter, angle, offset);
var movedHoles = holes.Select(hole => Moved(hole, angle, offset)).ToList();
return new NativeContours
{
perimeter = moved,
holes = movedHoles,
perimeterLines = moved.ToLines(),
holeLines = movedHoles.Select(hole => hole.ToLines()).ToList(),
Bounds = moved.BoundingBox,
};
}
private static Polygon Moved(Polygon polygon, double angle, Vector offset)
{
var moved = new Polygon();
if (angle == 0)
moved.Vertices.AddRange(polygon.Vertices.Select(vertex => vertex + offset));
else
{
var cos = PortableMath.Cos(angle);
var sin = PortableMath.Sin(angle);
moved.Vertices.AddRange(polygon.Vertices.Select(vertex =>
new Vector(vertex.X * cos - vertex.Y * sin + offset.X, vertex.X * sin + vertex.Y * cos + offset.Y)));
}
moved.UpdateBounds();
return moved;
}
/// Closest boundary approach between two parts, including a part inside the other's
/// cutout; is the middle of that gap. Only edges within
/// of the other part's bounds can be closer than it, so the rest are
/// skipped. Infinity when no edges are that near.
public double GapTo(NativeContours other, double reach, out Vector marker)
{
var gap = GapTo(other, reach, out var onThis, out var onOther);
marker = double.IsPositiveInfinity(gap) ? Bounds.Center : (onThis + onOther) * 0.5;
return gap;
}
/// As , returning the closest
/// points themselves: the gap runs from to .
public double GapTo(NativeContours other, double reach, out Vector onThis, out Vector onOther)
{
var best = double.PositiveInfinity;
var bestThis = Bounds.Center;
var bestOther = Bounds.Center;
var perimeterA = Near(perimeterLines, other.Bounds, reach);
var perimeterB = Near(other.perimeterLines, Bounds, reach);
Measure(perimeterA, perimeterB);
foreach (var hole in holeLines)
Measure(Near(hole, other.Bounds, reach), perimeterB);
foreach (var hole in other.holeLines)
Measure(perimeterA, Near(hole, Bounds, reach));
onThis = bestThis;
onOther = bestOther;
return best;
void Measure(List ringA, List ringB)
{
if (ringA.Count == 0 || ringB.Count == 0)
return;
var distance = Clearance.ClosestPoints(ringA, ringB, out var onA, out var onB);
if (distance < best)
{
best = distance;
bestThis = onA;
bestOther = onB;
}
}
}
private bool InMaterial(Vector point) =>
perimeter.ContainsPoint(point) && !holes.Any(hole => hole.ContainsPoint(point));
private static List Near(List lines, Box other, double reach) =>
lines.Where(line =>
System.Math.Min(line.StartPoint.X, line.EndPoint.X) <= other.Right + reach
&& System.Math.Max(line.StartPoint.X, line.EndPoint.X) >= other.Left - reach
&& System.Math.Min(line.StartPoint.Y, line.EndPoint.Y) <= other.Top + reach
&& System.Math.Max(line.StartPoint.Y, line.EndPoint.Y) >= other.Bottom - reach).ToList();
private static Polygon World(Shape shape, Vector location, double chordTolerance)
{
var polygon = ClipperBridge.Flatten(shape, chordTolerance, true);
polygon.Offset(location);
polygon.UpdateBounds();
return polygon;
}
}
}