mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 06:01:06 -04:00
A lead check scanned every other part's material with native contact
queries. On the 5,742-washer plate that was about 80% of planning and
it never finished inside 120 s.
The earlier extent filter (reverted in ea1270e1) used a fixed 0.001 gap,
which native line/circle rounding can exceed for long leads beside small
circles. This skip adds 1e-6 times the span of both extents. The
discriminant's rounding is bounded by about 1e-7 times that span, and
about 2e-9 is seen. So a skipped material has no native contact, and the
900000-long lead beside a small circle is still checked. Each skipped
scan is charged to the native query budget as if it ran. Ray parity is
still asked natively unless the lead's midpoint is outside the
material's Y range. The pierce zone skips only material farther than its
radius by the same gap.
A randomized test checks that skipped material never has a native
contact. It fails with the span allowance at zero and passes with it as
low as 1e-8. Plate 1 of the washer nest now plans Ready in about 54 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
504 lines
27 KiB
C#
504 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Diagnostics;
|
|
|
|
/// <summary>Local native line/arc queries, without changing the engine's geometry semantics.</summary>
|
|
internal static class PostVerificationGeometry
|
|
{
|
|
internal const double Epsilon = 1e-8;
|
|
// Matches NominalContourClosure.Tolerance: the largest gap capture admits as a closed ring.
|
|
// Ray parity snaps vertex Y comparisons within this band so an admitted seam cannot flip an
|
|
// exterior point to interior (see Curve.CrossesRay).
|
|
internal const double RaySeamTolerance = 1e-6;
|
|
private const double TwoPi = 2 * System.Math.PI;
|
|
|
|
/// <summary>An axis-aligned extent used only to skip queries that cannot meet.</summary>
|
|
internal readonly record struct Extent(double MinX, double MinY, double MaxX, double MaxY)
|
|
{
|
|
/// <summary>No extent: unions with anything give the other extent.</summary>
|
|
internal static Extent None => new(double.PositiveInfinity, double.PositiveInfinity,
|
|
double.NegativeInfinity, double.NegativeInfinity);
|
|
|
|
internal Extent Union(Extent other) => new(System.Math.Min(MinX, other.MinX),
|
|
System.Math.Min(MinY, other.MinY), System.Math.Max(MaxX, other.MaxX), System.Math.Max(MaxY, other.MaxY));
|
|
|
|
/// <summary>
|
|
/// The gap two extents need before their checks may be skipped: ten times the widest
|
|
/// absolute band any native contact query allows beyond an extent (the 0.00001 bounding-box
|
|
/// allowance of native intersections, the 0.0001 contact reach of tiny arcs).
|
|
/// </summary>
|
|
internal const double ClearMargin = 1e-3;
|
|
|
|
/// <summary>
|
|
/// Coordinates up to which extents may be skipped at all. Within it rounding stays far below
|
|
/// <see cref="ClearMargin"/>; beyond it, where rounding of large supports can exceed any fixed
|
|
/// margin, every check runs.
|
|
/// </summary>
|
|
internal const double WellConditionedLimit = 1e6;
|
|
|
|
/// <summary>
|
|
/// True only when both extents are finite, lie within <see cref="WellConditionedLimit"/> and
|
|
/// are more than <see cref="ClearMargin"/> apart on some axis, so no native query can count
|
|
/// anything in one as touching or entering the other. Everything else must be checked.
|
|
/// </summary>
|
|
internal bool IsClearOf(Extent other) => IsClearOf(other, ClearMargin);
|
|
|
|
/// <summary>
|
|
/// The relative rounding allowance of native line/circle intersections. Their discriminant
|
|
/// b^2 - 4ac cancels: its absolute error stays below about 30 units of roundoff times
|
|
/// |d1|^2 (|d2|^2 + r^2) (d1 the line, d2 from the circle's centre to the line's start), so a
|
|
/// spurious root can lie at most about 1e-7 times the span of both extents beyond the true
|
|
/// distance between line and circle (about 2e-9 is seen in practice). A long lead beside a
|
|
/// small circle needs the full check; a short lead beside a nearby part does not.
|
|
/// </summary>
|
|
internal const double SpanRounding = 1e-6;
|
|
|
|
/// <summary>
|
|
/// As <see cref="IsClearOf(Extent)"/>, with a gap that also covers the rounding of native
|
|
/// line/circle queries over the span of both extents (<see cref="SpanRounding"/>), plus
|
|
/// <paramref name="reach"/>. Lead checks skip a material only on this test.
|
|
/// </summary>
|
|
internal bool IsClearOfQueriesFrom(Extent other, double reach = 0)
|
|
{
|
|
var union = Union(other);
|
|
var span = System.Math.Max(union.MaxX - union.MinX, union.MaxY - union.MinY);
|
|
return IsClearOf(other, ClearMargin + reach + SpanRounding * span);
|
|
}
|
|
|
|
/// <summary>True when <paramref name="y"/> lies more than <see cref="ClearMargin"/> outside the Y range.</summary>
|
|
internal bool IsClearOfRow(double y) => IsWellConditioned && System.Math.Abs(y) <= WellConditionedLimit
|
|
&& (y > MaxY + ClearMargin || y < MinY - ClearMargin);
|
|
|
|
private bool IsClearOf(Extent other, double margin) => IsWellConditioned && other.IsWellConditioned
|
|
&& (MaxX + margin < other.MinX || other.MaxX + margin < MinX
|
|
|| MaxY + margin < other.MinY || other.MaxY + margin < MinY);
|
|
|
|
// Math.Max propagates NaN and no comparison with NaN holds, so NaN and infinite bounds
|
|
// (including the empty extent's) fail this test too.
|
|
private bool IsWellConditioned =>
|
|
System.Math.Max(System.Math.Max(System.Math.Abs(MinX), System.Math.Abs(MaxX)),
|
|
System.Math.Max(System.Math.Abs(MinY), System.Math.Abs(MaxY))) <= WellConditionedLimit;
|
|
}
|
|
|
|
internal static void Validate(Vector point)
|
|
{
|
|
if (!double.IsFinite(point.X) || !double.IsFinite(point.Y)
|
|
|| System.Math.Abs(point.X) > 1e12 || System.Math.Abs(point.Y) > 1e12)
|
|
throw new ArgumentException("Nonfinite or numerically unsupported program coordinates.");
|
|
}
|
|
|
|
internal static bool Closed(IReadOnlyList<Curve> curves) => curves.Count > 0
|
|
&& curves[0].Start.DistanceTo(curves[^1].End) <= Epsilon;
|
|
|
|
internal static bool Crosses(Vector start, Vector end, IReadOnlyList<Curve> curves,
|
|
CancellationToken token)
|
|
{
|
|
var delta = end - start;
|
|
var length = start.DistanceTo(end);
|
|
if (length <= Epsilon)
|
|
return false;
|
|
var direction = delta * (1 / length);
|
|
foreach (var curve in curves)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (curve.ContactAfterStart(start, direction, length))
|
|
return true;
|
|
}
|
|
// If there are no contacts except possibly departure, all open-segment points
|
|
// have the same inside/outside status. A midpoint catches travel entirely inside
|
|
// and departure into the interior, without flagging start-only outward contact.
|
|
var midpoint = start + delta * 0.5;
|
|
var inside = false;
|
|
foreach (var curve in curves)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (curve.CrossesRay(midpoint))
|
|
inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
internal static IReadOnlyList<Vector> ContactPoints(Vector start, Vector end,
|
|
IReadOnlyList<Curve> curves, CancellationToken token)
|
|
{
|
|
var length = start.DistanceTo(end);
|
|
if (length <= Epsilon)
|
|
return Array.Empty<Vector>();
|
|
var direction = (end - start) * (1 / length);
|
|
var distances = new List<double>();
|
|
foreach (var curve in curves)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
curve.ContactAfterStart(start, direction, length, distances);
|
|
}
|
|
distances.Sort();
|
|
var points = new List<Vector>();
|
|
foreach (var distance in distances)
|
|
{
|
|
var point = start + direction * distance;
|
|
if (points.Count == 0 || points[^1].DistanceTo(point) > Epsilon)
|
|
points.Add(point);
|
|
}
|
|
return points.AsReadOnly();
|
|
}
|
|
|
|
private static double Dot(Vector a, Vector b) => a.X * b.X + a.Y * b.Y;
|
|
private static double Cross(Vector a, Vector b) => a.X * b.Y - a.Y * b.X;
|
|
private static double Normalize(double angle)
|
|
{
|
|
angle %= TwoPi;
|
|
return angle < 0 ? angle + TwoPi : angle;
|
|
}
|
|
|
|
/// <summary>Exact identity of a <see cref="Curve"/>'s defining values (see <see cref="Curve.Key"/>).</summary>
|
|
internal readonly record struct CurveKey(long StartX, long StartY, long EndX, long EndY,
|
|
bool HasCenter, long CenterX, long CenterY, long Radius, long Sweep);
|
|
|
|
internal sealed class Curve
|
|
{
|
|
private Curve(Vector start, Vector end, Vector? center, double radius, double sweep)
|
|
{
|
|
Start = start;
|
|
End = end;
|
|
Center = center;
|
|
Radius = radius;
|
|
Sweep = sweep;
|
|
nativeEntity = new Lazy<Entity>(ToEntity);
|
|
}
|
|
|
|
// Never expose this mutable native wrapper; its geometry is read-only after creation.
|
|
// Lazy preserves the old conversion point for unsupported curves and is safe for concurrent checks.
|
|
private readonly Lazy<Entity> nativeEntity;
|
|
|
|
internal Vector Start { get; }
|
|
internal Vector End { get; }
|
|
private Vector? Center { get; }
|
|
private double Radius { get; }
|
|
private double Sweep { get; }
|
|
internal double Length => Center.HasValue ? Radius * System.Math.Abs(Sweep) : Start.DistanceTo(End);
|
|
internal bool IsLine => !Center.HasValue;
|
|
|
|
// Native entities are freshly allocated; the immutable curve never exposes state.
|
|
internal Entity ToEntity() => Center is { } center
|
|
? System.Math.Abs(Sweep) >= TwoPi
|
|
? new Circle(center, Radius)
|
|
: new Arc(center, Radius, Normalize(StartAngle), Normalize(StartAngle + Sweep), Sweep < 0)
|
|
: new Line(Start, End);
|
|
|
|
/// <summary>
|
|
/// A conservative axis-aligned extent of everything a native query may count as touching
|
|
/// this curve: for an arc, its whole supporting circle widened by the contact band of
|
|
/// <see cref="ContactAfterStart"/>; for a line, its endpoints. Nonfinite geometry yields NaN
|
|
/// bounds, which no separation test can pass.
|
|
/// </summary>
|
|
internal Extent Extent => Center is { } center
|
|
? new(center.X - ContactReach, center.Y - ContactReach, center.X + ContactReach, center.Y + ContactReach)
|
|
: new(System.Math.Min(Start.X, End.X), System.Math.Min(Start.Y, End.Y),
|
|
System.Math.Max(Start.X, End.X), System.Math.Max(Start.Y, End.Y));
|
|
|
|
internal Vector Midpoint => Center is { } center
|
|
? new Vector(center.X + Radius * System.Math.Cos(StartAngle + Sweep / 2),
|
|
center.Y + Radius * System.Math.Sin(StartAngle + Sweep / 2))
|
|
: (Start + End) * 0.5;
|
|
|
|
/// <summary>
|
|
/// The exact bits of every value a native query reads, so two curves with equal keys
|
|
/// give identical query results. Not a geometric tolerance: equal shapes built from
|
|
/// different roundoff get different keys.
|
|
/// </summary>
|
|
internal CurveKey Key => new(Bits(Start.X), Bits(Start.Y), Bits(End.X), Bits(End.Y),
|
|
Center.HasValue, Bits(Center?.X ?? 0), Bits(Center?.Y ?? 0), Bits(Radius), Bits(Sweep));
|
|
|
|
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
|
|
|
|
internal bool SameSupport(Curve other) => SameSupport(other, Epsilon);
|
|
|
|
/// <summary>
|
|
/// Support agreement with an explicit positional allowance. Used only where an upstream
|
|
/// stage has already admitted a larger nominal discrepancy — currently the inclusive
|
|
/// <see cref="NominalContourClosure.Tolerance"/> joint that a replayed ring bridges. It is
|
|
/// never a collision tolerance: safety queries keep the exact epsilon.
|
|
/// </summary>
|
|
internal bool SameSupport(Curve other, double positionTolerance)
|
|
{
|
|
if (Center is { } center)
|
|
return other.Center is { } c && center.DistanceTo(c) <= positionTolerance
|
|
&& System.Math.Abs(Radius - other.Radius) <= positionTolerance;
|
|
if (other.Center.HasValue || Length <= positionTolerance || other.Length <= positionTolerance)
|
|
return false;
|
|
var direction = (End - Start) * (1 / Length);
|
|
return System.Math.Abs(Cross(other.Start - Start, direction)) <= positionTolerance
|
|
&& System.Math.Abs(Cross(other.End - Start, direction)) <= positionTolerance;
|
|
}
|
|
|
|
// Directed native arc-length coordinates for contour replay, not collision queries.
|
|
internal bool SameDirection(Curve other) => SameDirection(other, Epsilon);
|
|
|
|
/// <summary>Directed coverage check with an admitted positional allowance; direction sign stays exact.</summary>
|
|
internal bool SameDirection(Curve other, double positionTolerance) => SameSupport(other, positionTolerance)
|
|
&& (Center.HasValue ? System.Math.Sign(Sweep) == System.Math.Sign(other.Sweep)
|
|
: Dot(End - Start, other.End - other.Start) > 0);
|
|
|
|
internal double DistanceAlong(Vector point) => Center is { } center
|
|
? point.DistanceTo(Start) <= Epsilon ? 0
|
|
: Travel(System.Math.Atan2(point.Y - center.Y, point.X - center.X)) * Radius
|
|
: Dot(point - Start, (End - Start) * (1 / Length));
|
|
|
|
/// <summary>Unit direction of travel at the curve's start or end.</summary>
|
|
internal Vector Tangent(bool atEnd)
|
|
{
|
|
if (Center is not { } center)
|
|
return (End - Start) * (1 / Length);
|
|
var radial = (atEnd ? End : Start) - center;
|
|
var turn = System.Math.Sign(Sweep);
|
|
return new Vector(-radial.Y * turn, radial.X * turn) * (1 / Radius);
|
|
}
|
|
|
|
/// <summary>How far the curve reaches from <paramref name="origin"/> along unit <paramref name="direction"/>.</summary>
|
|
internal double Reach(Vector origin, Vector direction)
|
|
{
|
|
var reach = System.Math.Max(Dot(Start - origin, direction), Dot(End - origin, direction));
|
|
if (Center is { } center && OnArc(center + direction * Radius))
|
|
reach = System.Math.Max(reach, Dot(center - origin, direction) + Radius);
|
|
return reach;
|
|
}
|
|
|
|
internal Vector PointAtLength(double distance)
|
|
{
|
|
if (distance <= 0) return Start;
|
|
if (distance >= Length) return End;
|
|
return Center is { } center
|
|
? center + new Vector(System.Math.Cos(StartAngle + System.Math.Sign(Sweep) * distance / Radius),
|
|
System.Math.Sin(StartAngle + System.Math.Sign(Sweep) * distance / Radius)) * Radius
|
|
: Start + (End - Start) * (distance / Length);
|
|
}
|
|
|
|
/// <summary>Exact native distance from <paramref name="point"/> to this finite curve.</summary>
|
|
internal double DistanceTo(Vector point) => nativeEntity.Value.ClosestPointTo(point).DistanceTo(point);
|
|
|
|
internal bool Contains(Vector point) => Contains(point, Epsilon);
|
|
|
|
/// <summary>On-support test with an admitted positional allowance (see <see cref="SameSupport(Curve, double)"/>).</summary>
|
|
internal bool Contains(Vector point, double positionTolerance)
|
|
=> nativeEntity.Value.ClosestPointTo(point).DistanceTo(point) <= positionTolerance;
|
|
|
|
/// <summary>
|
|
/// Filters supporting-circle contacts against the actual finite arc span. The
|
|
/// native angular padding is larger and must not turn an arc extension into a
|
|
/// self-intersection at an adjacent nominal joint. This is not a radial test.
|
|
/// </summary>
|
|
internal bool IsOutsideArcSpan(Vector point) => Center.HasValue && !OnArc(point);
|
|
|
|
internal bool IsFiniteArc => Center.HasValue && System.Math.Abs(Sweep) < TwoPi;
|
|
|
|
internal IReadOnlyList<Vector> Contacts(Curve other, out bool overlap)
|
|
{
|
|
var entity = nativeEntity.Value;
|
|
var candidate = other.nativeEntity.Value;
|
|
// Native arc filters discard NaN supporting-circle intersections. Inspect
|
|
// both unfiltered queries first: roundoff can differ by operand direction.
|
|
// Coincident supports have separate overlap handling below.
|
|
if (Center is { } center && other.Center is { } otherCenter && !SameSupport(other))
|
|
{
|
|
var support = new Circle(center, Radius);
|
|
var otherSupport = new Circle(otherCenter, other.Radius);
|
|
support.Intersects(otherSupport, out var forward);
|
|
otherSupport.Intersects(support, out var reverse);
|
|
foreach (var point in forward)
|
|
Validate(point);
|
|
foreach (var point in reverse)
|
|
Validate(point);
|
|
}
|
|
List<Vector> points;
|
|
bool intersects;
|
|
switch (candidate)
|
|
{
|
|
case Line line: intersects = entity.Intersects(line, out points); break;
|
|
case Arc arc: intersects = entity.Intersects(arc, out points); break;
|
|
case Circle circle: intersects = entity.Intersects(circle, out points); break;
|
|
default: throw new NotSupportedException("Unsupported native boundary.");
|
|
}
|
|
if (!intersects)
|
|
points.Clear();
|
|
// Coincident circles produce NaNs in the native discrete-contact query.
|
|
// Their support overlap is handled separately, without inventing crossings.
|
|
// Supports that agree only within Epsilon (arcs of one circle whose centers were
|
|
// each derived from their own endpoints) put a witness up to that far off the other
|
|
// support. Test those spans at the admitted seam tolerance instead: shared spans
|
|
// still refuse, while arcs that only meet at a joint or lie apart are not overlap.
|
|
var witness = Center is { } c && other.Center is { } oc
|
|
&& (c.X != oc.X || c.Y != oc.Y || Radius != other.Radius) ? RaySeamTolerance : Epsilon;
|
|
overlap = SameSupport(other) && (InteriorWitness(Midpoint, other, witness)
|
|
|| InteriorWitness(other.Midpoint, this, witness)
|
|
|| InteriorWitness(Start, other, witness) || InteriorWitness(End, other, witness)
|
|
|| InteriorWitness(other.Start, this, witness) || InteriorWitness(other.End, this, witness));
|
|
if (SameSupport(other))
|
|
points.Clear();
|
|
foreach (var point in points)
|
|
Validate(point);
|
|
var nativeContactCount = points.Count;
|
|
foreach (var point in new[] { Start, End, other.Start, other.End })
|
|
if (Contains(point) && other.Contains(point)
|
|
&& !points.Exists(p => p.DistanceTo(point) <= Epsilon))
|
|
points.Add(point);
|
|
// Existing exact line/ray contact semantics guard native queries which
|
|
// suppress very short or nearly parallel intersections. Uncertainty refuses.
|
|
if (Center is null && !SameSupport(other) && nativeContactCount == 0 && ExactLineContact(this, other))
|
|
throw new NotSupportedException("Native contact query is numerically uncertain.");
|
|
if (other.Center is null && Center.HasValue && nativeContactCount == 0 && ExactLineContact(other, this))
|
|
throw new NotSupportedException("Native contact query is numerically uncertain.");
|
|
return points;
|
|
|
|
static bool InteriorWitness(Vector point, Curve curve, double tolerance) => curve.Contains(point, tolerance)
|
|
&& (curve.Start.DistanceTo(curve.End) <= Epsilon
|
|
|| (point.DistanceTo(curve.Start) > tolerance && point.DistanceTo(curve.End) > tolerance));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether exact ray semantics find <paramref name="curve"/> touching <paramref name="line"/>
|
|
/// anywhere except at a line endpoint the curve contains, which is already a recorded contact.
|
|
/// Rays from both ends together cover the whole line. A ray stops short of a contained
|
|
/// endpoint (both stop half-way when both endpoints are contained): a line meeting a
|
|
/// tangent arc at their shared vertex is otherwise rediscovered there as a contact the
|
|
/// native query missed, because rounding can drop the tangent root of its quadratic.
|
|
/// </summary>
|
|
private static bool ExactLineContact(Curve line, Curve curve)
|
|
{
|
|
var direction = (line.End - line.Start) * (1 / line.Length);
|
|
var startRecorded = curve.Contains(line.Start);
|
|
var endRecorded = curve.Contains(line.End);
|
|
var forward = endRecorded ? startRecorded ? line.Length / 2 : 0 : line.Length;
|
|
var backward = startRecorded ? endRecorded ? line.Length / 2 : 0 : line.Length;
|
|
return (forward > 0 && curve.ContactAfterStart(line.Start, direction, forward))
|
|
|| (backward > 0 && curve.ContactAfterStart(line.End, direction * -1, backward));
|
|
}
|
|
|
|
|
|
internal static Curve Create(Vector start, Vector end, Vector? center, bool clockwise)
|
|
{
|
|
if (center is not { } c)
|
|
return new Curve(start, end, null, 0, 0);
|
|
Validate(c);
|
|
var radius = start.DistanceTo(c);
|
|
if (radius <= Epsilon || !double.IsFinite(radius)
|
|
|| System.Math.Abs(radius - end.DistanceTo(c)) > Epsilon * System.Math.Max(1, radius))
|
|
throw new ArgumentException("Arc has zero or inconsistent radius.");
|
|
var a = System.Math.Atan2(start.Y - c.Y, start.X - c.X);
|
|
var b = System.Math.Atan2(end.Y - c.Y, end.X - c.X);
|
|
var sweep = start.DistanceTo(end) <= Epsilon ? TwoPi
|
|
: Normalize(clockwise ? a - b : b - a);
|
|
return new Curve(start, end, c, radius, clockwise ? -sweep : sweep);
|
|
}
|
|
|
|
private double StartAngle => System.Math.Atan2(Start.Y - Center.Value.Y, Start.X - Center.Value.X);
|
|
private double Travel(double angle) => Normalize(Sweep < 0 ? StartAngle - angle : angle - StartAngle);
|
|
private bool OnArc(Vector point) => Travel(System.Math.Atan2(point.Y - Center.Value.Y,
|
|
point.X - Center.Value.X)) <= System.Math.Abs(Sweep) + Epsilon / Radius
|
|
|| point.DistanceTo(Start) <= Epsilon || point.DistanceTo(End) <= Epsilon;
|
|
|
|
// How far past r^2 a squared distance from the centre still counts as touching the arc.
|
|
// For small arcs this band reaches well beyond the radius (sqrt(1e-8) = 1e-4 as r -> 0).
|
|
private double ContactSlack => Epsilon * System.Math.Max(1, Radius * 2);
|
|
|
|
// The largest distance from the centre that ContactAfterStart can count as contact.
|
|
private double ContactReach => System.Math.Sqrt(Radius * Radius + ContactSlack);
|
|
|
|
internal bool ContactAfterStart(Vector origin, Vector direction, double length,
|
|
List<double> contacts = null)
|
|
{
|
|
if (Center is { } center)
|
|
{
|
|
// Intersect the actual circle, not an inscribed chord polygon: tangencies
|
|
// and short arcs must not vanish between tessellation vertices.
|
|
var relative = center - origin;
|
|
var projection = Dot(relative, direction);
|
|
var perpendicular = Cross(relative, direction);
|
|
var square = Radius * Radius - perpendicular * perpendicular;
|
|
if (square < -ContactSlack)
|
|
return false;
|
|
var offset = System.Math.Sqrt(System.Math.Max(0, square));
|
|
return contacts == null
|
|
? Hit(projection - offset) || Hit(projection + offset)
|
|
: Hit(projection - offset) | Hit(projection + offset);
|
|
|
|
bool Hit(double distance) => distance > Epsilon && distance <= length + Epsilon
|
|
&& OnArc(origin + direction * System.Math.Clamp(distance, 0, length))
|
|
&& Record(distance);
|
|
}
|
|
var edge = End - Start;
|
|
var relativeStart = Start - origin;
|
|
var denominator = Cross(direction, edge);
|
|
if (System.Math.Abs(denominator) <= 1e-12 * System.Math.Max(1, Length))
|
|
{
|
|
if (System.Math.Abs(Cross(relativeStart, direction)) > Epsilon)
|
|
return false;
|
|
var a = Dot(relativeStart, direction);
|
|
var b = Dot(End - origin, direction);
|
|
var low = System.Math.Max(0, System.Math.Min(a, b));
|
|
var high = System.Math.Min(length, System.Math.Max(a, b));
|
|
return high > Epsilon && low <= high + Epsilon && Record(low) && Record(high);
|
|
}
|
|
var distanceAlongRapid = Cross(relativeStart, edge) / denominator;
|
|
var fractionAlongEdge = Cross(relativeStart, direction) / denominator;
|
|
return distanceAlongRapid > Epsilon && distanceAlongRapid <= length + Epsilon
|
|
&& fractionAlongEdge >= -Epsilon / System.Math.Max(Length, Epsilon)
|
|
&& fractionAlongEdge <= 1 + Epsilon / System.Math.Max(Length, Epsilon)
|
|
&& Record(distanceAlongRapid);
|
|
|
|
bool Record(double distance)
|
|
{
|
|
contacts?.Add(System.Math.Clamp(distance, 0, length));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
internal bool CrossesRay(Vector point)
|
|
{
|
|
// Ray parity needs both edges meeting at a shared vertex to agree which side of the ray
|
|
// that vertex is on. Rings are only nominally closed: capture admits closure seams and
|
|
// PEP-converted or rotated geometry carries sub-micro-inch vertex jogs, so an exact
|
|
// comparison can split one vertex across the ray and report far-away exterior points as
|
|
// interior — which made LeadPathValidator refuse leads that could touch nothing.
|
|
// Snapping within the admitted seam tolerance keeps the decision consistent on both
|
|
// edges; real features (notches, tabs, arc spans) are far larger than the tolerance and
|
|
// still count. Containment of a point that itself lies within the tolerance of a
|
|
// boundary edge stays ambiguous by design, exactly as it was before.
|
|
var rayY = point.Y + RaySeamTolerance;
|
|
if (Center is not { } center)
|
|
return (Start.Y > rayY) != (End.Y > rayY)
|
|
&& Start.X + (point.Y - Start.Y) * (End.X - Start.X) / (End.Y - Start.Y) > point.X;
|
|
|
|
// Split arcs at vertical extrema, giving monotone-Y pieces. Apply the same
|
|
// half-open endpoint rule as a polygon ray test, solving X on the native
|
|
// circle. This handles full circles, reversed arcs and shared vertices.
|
|
var breaks = new List<double> { 0, System.Math.Abs(Sweep) };
|
|
foreach (var angle in new[] { System.Math.PI / 2, 3 * System.Math.PI / 2 })
|
|
{
|
|
var travel = Travel(angle);
|
|
if (travel > 0 && travel < System.Math.Abs(Sweep))
|
|
breaks.Add(travel);
|
|
}
|
|
breaks.Sort();
|
|
var inside = false;
|
|
for (var i = 1; i < breaks.Count; i++)
|
|
{
|
|
var a = StartAngle + System.Math.Sign(Sweep) * breaks[i - 1];
|
|
var b = StartAngle + System.Math.Sign(Sweep) * breaks[i];
|
|
var ya = i == 1 ? Start.Y : center.Y + Radius * System.Math.Sin(a);
|
|
var yb = i == breaks.Count - 1 ? End.Y : center.Y + Radius * System.Math.Sin(b);
|
|
if ((ya > rayY) == (yb > rayY))
|
|
continue;
|
|
var dy = point.Y - center.Y;
|
|
var dx = System.Math.Sqrt(System.Math.Max(0, Radius * Radius - dy * dy));
|
|
var x = center.X + (System.Math.Cos((a + b) / 2) >= 0 ? dx : -dx);
|
|
if (x > point.X)
|
|
inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
}
|
|
}
|