mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 03:42:10 -04:00
feat(cutting): validate actual leads against owned material
This commit is contained in:
1 parent
e71fcea73d
commit
018e4e3dd4
4 files changed
+659
No files matched your search
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingPlanning;
|
||||
|
||||
/// <summary>Owned nominal material: one simple perimeter minus disjoint simple holes.</summary>
|
||||
public sealed class LeadMaterialSnapshot
|
||||
{
|
||||
private LeadMaterialSnapshot(IReadOnlyList<PostVerificationGeometry.Curve[]> rings, string reason)
|
||||
{
|
||||
Rings = rings;
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public bool IsComplete => Reason == null;
|
||||
public string Reason { get; }
|
||||
internal IReadOnlyList<PostVerificationGeometry.Curve[]> Rings { get; }
|
||||
|
||||
/// <summary>Capture a stable, clean, rotation-baked program, applying location once.
|
||||
/// Unsupported or malformed geometry produces an incomplete snapshot; cancellation throws.</summary>
|
||||
public static LeadMaterialSnapshot Capture(Program cleanProgram, Vector location,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var execution = ExecutionMotionReader.Read(cleanProgram, location, null, token);
|
||||
var rings = new List<PostVerificationGeometry.Curve[]>();
|
||||
var chain = new List<PostVerificationGeometry.Curve>();
|
||||
foreach (var motion in execution.Motions)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (motion.Rapid || motion.Layer is LayerType.Scribe or LayerType.Leadin or LayerType.Leadout)
|
||||
{
|
||||
Finish();
|
||||
continue;
|
||||
}
|
||||
if (motion.Layer is not (LayerType.Cut or LayerType.Display))
|
||||
throw new ArgumentException("Material capture contains an unsupported layer.");
|
||||
var curve = motion.Curve;
|
||||
if (curve == null || !double.IsFinite(curve.Length) || curve.Length <= PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("Material contains a degenerate motion.");
|
||||
if (chain.Count > 0 && chain[^1].End.DistanceTo(curve.Start) > PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("Material contour is discontinuous.");
|
||||
chain.Add(curve);
|
||||
if (PostVerificationGeometry.Closed(chain))
|
||||
Finish();
|
||||
}
|
||||
Finish();
|
||||
if (rings.Count == 0)
|
||||
throw new ArgumentException("Material has no closed contour.");
|
||||
var budget = 1000000;
|
||||
// Certify simple rings and mutually disjoint boundaries before containment.
|
||||
for (var r = 0; r < rings.Count; r++)
|
||||
for (var s = r; s < rings.Count; s++)
|
||||
for (var i = 0; i < rings[r].Length; i++)
|
||||
for (var j = s == r ? i + 1 : 0; j < rings[s].Length; j++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (--budget < 0)
|
||||
throw new NotSupportedException("Material validation exceeds the native query limit.");
|
||||
var a = rings[r][i];
|
||||
var b = rings[s][j];
|
||||
var contacts = a.Contacts(b, out var overlap);
|
||||
var adjacent = r == s && (j == i + 1 || (i == 0 && j == rings[r].Length - 1));
|
||||
if (overlap || contacts.Any(p => !adjacent
|
||||
|| !(p.DistanceTo(a.End) <= PostVerificationGeometry.Epsilon
|
||||
&& p.DistanceTo(b.Start) <= PostVerificationGeometry.Epsilon)
|
||||
&& !(p.DistanceTo(a.Start) <= PostVerificationGeometry.Epsilon
|
||||
&& p.DistanceTo(b.End) <= PostVerificationGeometry.Epsilon)))
|
||||
throw new ArgumentException($"Material boundaries overlap, touch or self-intersect ({r}:{i}, {s}:{j}, overlap={overlap}).");
|
||||
}
|
||||
var outer = -1;
|
||||
for (var i = 0; i < rings.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (Enumerable.Range(0, rings.Count).All(j => i == j || Inside(rings[j][0].Start, rings[i], token)))
|
||||
{
|
||||
if (outer >= 0)
|
||||
throw new ArgumentException("Material perimeter is ambiguous.");
|
||||
outer = i;
|
||||
}
|
||||
}
|
||||
if (outer < 0)
|
||||
throw new ArgumentException("Material must have exactly one enclosing perimeter.");
|
||||
var holes = rings.Where((_, i) => i != outer).ToArray();
|
||||
for (var i = 0; i < holes.Length; i++)
|
||||
for (var j = i + 1; j < holes.Length; j++)
|
||||
if (Inside(holes[i][0].Start, holes[j], token) || Inside(holes[j][0].Start, holes[i], token))
|
||||
throw new ArgumentException("Nested cutouts are not supported material.");
|
||||
return new LeadMaterialSnapshot(new[] { rings[outer] }.Concat(holes).ToArray(), null);
|
||||
|
||||
void Finish()
|
||||
{
|
||||
if (chain.Count == 0)
|
||||
return;
|
||||
if (!PostVerificationGeometry.Closed(chain))
|
||||
throw new ArgumentException("Nominal material contour is open; tab gaps cannot be filled implicitly.");
|
||||
rings.Add(chain.ToArray());
|
||||
chain.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
||||
{
|
||||
return new LeadMaterialSnapshot(Array.Empty<PostVerificationGeometry.Curve[]>(), ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool ContainsMaterial(Vector point, CancellationToken token) => Inside(point, Rings[0], token)
|
||||
&& !Rings.Skip(1).Any(hole => Inside(point, hole, token));
|
||||
|
||||
internal static bool Inside(Vector point, IReadOnlyList<PostVerificationGeometry.Curve> ring, CancellationToken token)
|
||||
{
|
||||
var inside = false;
|
||||
foreach (var curve in ring)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (curve.CrossesRay(point))
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingPlanning;
|
||||
|
||||
/// <summary>A complete unsafe result is distinct from an incomplete/unsupported check.</summary>
|
||||
public sealed record LeadPathValidationResult(bool IsComplete, bool IsClear, string Reason);
|
||||
|
||||
/// <summary>Certifies actual emitted native lead paths against owned nominal material.</summary>
|
||||
public static class LeadPathValidator
|
||||
{
|
||||
public static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
|
||||
IReadOnlyList<LeadMaterialSnapshot> otherMaterials, CancellationToken token = default)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (execution == null || target == null || otherMaterials == null)
|
||||
return new(false, false, "Missing execution or material snapshots.");
|
||||
if (!target.IsComplete || otherMaterials.Any(m => m == null || !m.IsComplete))
|
||||
return new(false, false, "Material snapshot is incomplete.");
|
||||
try
|
||||
{
|
||||
var budget = 1000000;
|
||||
for (var i = 0; i < execution.Motions.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var move = execution.Motions[i];
|
||||
if (move.Layer is not (LayerType.Leadin or LayerType.Leadout))
|
||||
continue;
|
||||
if (move.Rapid || move.Start is not { } start || move.Curve == null
|
||||
|| !double.IsFinite(move.Length) || move.Length <= PostVerificationGeometry.Epsilon
|
||||
|| start.DistanceTo(move.Curve.Start) > PostVerificationGeometry.Epsilon
|
||||
|| move.End.DistanceTo(move.Curve.End) > PostVerificationGeometry.Epsilon)
|
||||
return new(false, false, "Lead motion is missing, degenerate or inconsistent.");
|
||||
Vector? allowed = null;
|
||||
var groupEdge = i;
|
||||
var step = move.Layer == LayerType.Leadin ? 1 : -1;
|
||||
while (groupEdge + step >= 0 && groupEdge + step < execution.Motions.Count
|
||||
&& execution.Motions[groupEdge + step].Layer == move.Layer)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (--budget < 0)
|
||||
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
||||
groupEdge += step;
|
||||
}
|
||||
var adjacentIndex = groupEdge + step;
|
||||
var genuineJoint = false;
|
||||
if (adjacentIndex >= 0 && adjacentIndex < execution.Motions.Count)
|
||||
{
|
||||
var adjacent = execution.Motions[adjacentIndex];
|
||||
if (!adjacent.Rapid && adjacent.Layer is LayerType.Cut or LayerType.Display
|
||||
&& adjacent.Curve != null && adjacent.Length > PostVerificationGeometry.Epsilon)
|
||||
{
|
||||
var edge = execution.Motions[groupEdge];
|
||||
var joint = move.Layer == LayerType.Leadin ? edge.End : edge.Curve.Start;
|
||||
var contourJoint = move.Layer == LayerType.Leadin ? adjacent.Curve.Start : adjacent.End;
|
||||
foreach (var boundary in target.Rings.SelectMany(r => r))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (--budget < 0)
|
||||
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
||||
if (joint.DistanceTo(contourJoint) <= PostVerificationGeometry.Epsilon
|
||||
&& boundary.SameSupport(adjacent.Curve)
|
||||
&& boundary.Contains(adjacent.Curve.Start) && boundary.Contains(adjacent.End)
|
||||
&& boundary.Contains(adjacent.Curve.Midpoint)
|
||||
&& adjacent.Length <= boundary.Length + PostVerificationGeometry.Epsilon)
|
||||
{
|
||||
genuineJoint = true;
|
||||
if (groupEdge == i)
|
||||
allowed = joint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!genuineJoint)
|
||||
return new(true, false, "Lead chain has no genuine adjacent target contour entry or exit.");
|
||||
if (allowed is { } jointPoint
|
||||
&& (move.Layer == LayerType.Leadin ? start : move.End).DistanceTo(jointPoint) <= PostVerificationGeometry.Epsilon)
|
||||
return new(true, false, "A positive-length lead returns to its contour joint; contact is not endpoint-only.");
|
||||
var failure = CheckMaterial(target, allowed);
|
||||
if (failure != null)
|
||||
return new(true, false, $"Lead motion {i} contacts or enters target material outside its adjacent contour joint ({failure}).");
|
||||
foreach (var material in otherMaterials)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (ReferenceEquals(material, target))
|
||||
continue;
|
||||
if (CheckMaterial(material, null) != null)
|
||||
return new(true, false, "Lead contacts or enters another placed material.");
|
||||
}
|
||||
|
||||
string CheckMaterial(LeadMaterialSnapshot material, Vector? permittedJoint)
|
||||
{
|
||||
foreach (var boundary in material.Rings.SelectMany(r => r))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (--budget < 0)
|
||||
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
||||
var contacts = move.Curve.Contacts(boundary, out var overlap);
|
||||
if (overlap || contacts.Any(p => permittedJoint is not { } joint
|
||||
|| p.DistanceTo(joint) > PostVerificationGeometry.Epsilon))
|
||||
return $"boundary; allowed={permittedJoint}; contacts={string.Join(";", contacts)}; overlap={overlap}";
|
||||
}
|
||||
// With all other boundary contacts excluded, the connected open path
|
||||
// has constant material membership. Use the native arc midpoint, not
|
||||
// the chord midpoint or endpoints (which can both lie in scrap).
|
||||
return material.ContainsMaterial(move.Curve.Midpoint, token) ? "interior" : null;
|
||||
}
|
||||
}
|
||||
// Missing leads are the ReleasedContourState check's responsibility.
|
||||
return new(true, true, null);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
||||
{
|
||||
return new(false, false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,89 @@ internal static class PostVerificationGeometry
|
||||
private double Sweep { get; }
|
||||
internal double Length => Center.HasValue ? Radius * System.Math.Abs(Sweep) : Start.DistanceTo(End);
|
||||
|
||||
// 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);
|
||||
|
||||
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;
|
||||
|
||||
internal bool SameSupport(Curve other)
|
||||
{
|
||||
if (Center is { } center)
|
||||
return other.Center is { } c && center.DistanceTo(c) <= Epsilon
|
||||
&& System.Math.Abs(Radius - other.Radius) <= Epsilon;
|
||||
if (other.Center.HasValue || Length <= Epsilon || other.Length <= Epsilon)
|
||||
return false;
|
||||
var direction = (End - Start) * (1 / Length);
|
||||
return System.Math.Abs(Cross(other.Start - Start, direction)) <= Epsilon
|
||||
&& System.Math.Abs(Cross(other.End - Start, direction)) <= Epsilon;
|
||||
}
|
||||
|
||||
internal bool Contains(Vector point) => ToEntity().ClosestPointTo(point).DistanceTo(point) <= Epsilon;
|
||||
|
||||
internal IReadOnlyList<Vector> Contacts(Curve other, out bool overlap)
|
||||
{
|
||||
var entity = ToEntity();
|
||||
var candidate = other.ToEntity();
|
||||
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.
|
||||
overlap = SameSupport(other) && (InteriorWitness(Midpoint, other)
|
||||
|| InteriorWitness(other.Midpoint, this)
|
||||
|| InteriorWitness(Start, other) || InteriorWitness(End, other)
|
||||
|| InteriorWitness(other.Start, this) || InteriorWitness(other.End, this));
|
||||
if (SameSupport(other))
|
||||
points.Clear();
|
||||
foreach (var point in points)
|
||||
Validate(point);
|
||||
var nativeContactCount = points.Count;
|
||||
if (Center is { } c && other.Center is { } oc && SameSupport(other)
|
||||
&& (c.X != oc.X || c.Y != oc.Y || Radius != other.Radius))
|
||||
overlap = true; // Nearly coincident supports are uncertain, never clear.
|
||||
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))
|
||||
{
|
||||
var direction = (End - Start) * (1 / Length);
|
||||
if ((other.ContactAfterStart(Start, direction, Length)
|
||||
|| other.ContactAfterStart(End, direction * -1, Length)) && nativeContactCount == 0)
|
||||
throw new NotSupportedException("Native contact query is numerically uncertain.");
|
||||
}
|
||||
if (other.Center is null && Center.HasValue)
|
||||
{
|
||||
var direction = (other.End - other.Start) * (1 / other.Length);
|
||||
if ((ContactAfterStart(other.Start, direction, other.Length)
|
||||
|| ContactAfterStart(other.End, direction * -1, other.Length)) && nativeContactCount == 0)
|
||||
throw new NotSupportedException("Native contact query is numerically uncertain.");
|
||||
}
|
||||
return points;
|
||||
|
||||
static bool InteriorWitness(Vector point, Curve curve) => curve.Contains(point)
|
||||
&& (curve.Start.DistanceTo(curve.End) <= Epsilon
|
||||
|| (point.DistanceTo(curve.Start) > Epsilon && point.DistanceTo(curve.End) > Epsilon));
|
||||
}
|
||||
|
||||
|
||||
internal static Curve Create(Vector start, Vector end, Vector? center, bool clockwise)
|
||||
{
|
||||
if (center is not { } c)
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class LeadPathValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExternalStraightLead_SafeButRejectsCrossingPlacedMaterial()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
Assert.True(target.IsComplete, target.Reason);
|
||||
var execution = Read(ExternalLead());
|
||||
Assert.True(LeadPathValidator.Check(execution, target, [target]).IsClear);
|
||||
var obstacle = LeadMaterialSnapshot.Capture(Rectangle(-2, 4, -1, 6), Vector.Zero);
|
||||
var result = LeadPathValidator.Check(execution, target, [obstacle]);
|
||||
Assert.True(result.IsComplete, result.Reason);
|
||||
Assert.False(result.IsClear);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoleScrap_SafeStraightAndArcButRejectsBulgingArc()
|
||||
{
|
||||
var clean = Rectangle(0, 0, 10, 10);
|
||||
clean.Codes.AddRange(Rectangle(2, 2, 8, 8).Codes);
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
Assert.True(target.IsComplete, target.Reason);
|
||||
var straight = new Program();
|
||||
straight.MoveTo(5, 5);
|
||||
straight.Codes.Add(new LinearMove(2, 5) { Layer = LayerType.Leadin });
|
||||
straight.LineTo(2, 8);
|
||||
AssertClear(straight, target);
|
||||
var curved = new Program();
|
||||
curved.MoveTo(3, 4);
|
||||
curved.Codes.Add(new ArcMove(new Vector(2, 5), new Vector(3, 5), RotationType.CW) { Layer = LayerType.Leadin });
|
||||
curved.LineTo(2, 8);
|
||||
AssertClear(curved, target);
|
||||
var bulge = new Program();
|
||||
bulge.MoveTo(4, 3);
|
||||
bulge.Codes.Add(new ArcMove(new Vector(2, 5), new Vector(3, 4), RotationType.CW) { Layer = LayerType.Leadin });
|
||||
bulge.LineTo(2, 8);
|
||||
AssertUnsafe(bulge, target);
|
||||
straight.Codes[0] = new RapidMove(1, 5); // Begins in material, not the hole.
|
||||
AssertUnsafe(straight, target);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtherHoleIsScrap_AndLineArcTangenciesReject()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
var ring = Rectangle(-4, 2, 2, 8);
|
||||
ring.Codes.AddRange(Rectangle(-3.5, 3, 1, 7).Codes);
|
||||
var hole = LeadMaterialSnapshot.Capture(ring, Vector.Zero);
|
||||
Assert.True(hole.IsComplete, hole.Reason);
|
||||
AssertClear(ExternalLead(), target, hole);
|
||||
var tangent = LeadMaterialSnapshot.Capture(Circle(-1.5, 6, 1), Vector.Zero);
|
||||
Assert.True(tangent.IsComplete, tangent.Reason);
|
||||
AssertUnsafe(ExternalLead(), target, tangent);
|
||||
var arc = new Program();
|
||||
arc.MoveTo(-2, 5);
|
||||
arc.Codes.Add(new ArcMove(new Vector(0, 5), new Vector(-1, 5), RotationType.CW) { Layer = LayerType.Leadin });
|
||||
arc.LineTo(0, 10);
|
||||
AssertClear(arc, target);
|
||||
AssertUnsafe(arc, target, LeadMaterialSnapshot.Capture(Circle(-1, 7, 1), Vector.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoincidentBoundariesAndArbitraryJointsAreNotClear()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
var along = new Program();
|
||||
along.MoveTo(0, 2);
|
||||
along.Codes.Add(new LinearMove(0, 5) { Layer = LayerType.Leadin });
|
||||
along.LineTo(0, 10);
|
||||
AssertUnsafe(along, target);
|
||||
var arbitrary = ExternalLead();
|
||||
arbitrary.Codes[2] = new LinearMove(5, 5); // Adjacent move is NOT nominal contour.
|
||||
AssertUnsafe(arbitrary, target);
|
||||
var circular = LeadMaterialSnapshot.Capture(Circle(0, 0, 2), Vector.Zero);
|
||||
var coincident = new Program();
|
||||
coincident.MoveTo(2, 0);
|
||||
coincident.Codes.Add(new ArcMove(new Vector(-2, 0), Vector.Zero) { Layer = LayerType.Leadin });
|
||||
coincident.Codes.Add(new ArcMove(new Vector(2, 0), Vector.Zero));
|
||||
AssertUnsafe(coincident, circular);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullCircleLeadCannotReuseItsJointAsAnArbitraryPierceEndpoint()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
var p = new Program(); p.MoveTo(0, 5);
|
||||
p.Codes.Add(new ArcMove(new Vector(0, 5), new Vector(-1, 5)) { Layer = LayerType.Leadin });
|
||||
p.LineTo(0, 10);
|
||||
AssertUnsafe(p, target);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TinyNativeUncertainContactRefusesRatherThanApproves()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
var obstacle = LeadMaterialSnapshot.Capture(Circle(-0.000005, 5, 0.000001), Vector.Zero);
|
||||
Assert.True(obstacle.IsComplete, obstacle.Reason);
|
||||
var p = ExternalLead(); p.Codes[0] = new RapidMove(-0.00001, 5);
|
||||
var check = LeadPathValidator.Check(Read(p), target, [obstacle]);
|
||||
Assert.False(check.IsComplete); Assert.False(check.IsClear);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadoutUsesActualTabbedCutEndpoint_NotNominalClosure()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
var p = new Program();
|
||||
p.MoveTo(0, 5); p.LineTo(0, 10); p.LineTo(10, 10); p.LineTo(10, 0); p.LineTo(0, 0); p.LineTo(0, 4.8);
|
||||
p.Codes.Add(new LinearMove(-2, 4.8) { Layer = LayerType.Leadout });
|
||||
AssertClear(p, target);
|
||||
p.Codes[^1] = new LinearMove(-2, 5) { Layer = LayerType.Leadout };
|
||||
AssertClear(p, target); // Departure is still (0,4.8), not the entry (0,5).
|
||||
p.Codes.Insert(p.Codes.Count - 1, new RapidMove(0, 5));
|
||||
AssertUnsafe(p, target); // No adjacent cut: a rapid cannot bridge the tab.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActualArcLeadoutChecksFullSweep_NotOnlyScrapEndpoints()
|
||||
{
|
||||
var target = LeadMaterialSnapshot.Capture(Rectangle(0, 0, 10, 10), Vector.Zero);
|
||||
var p = new Program(); p.MoveTo(0, 10); p.LineTo(0, 5);
|
||||
p.Codes.Add(new ArcMove(new Vector(-1, 4), new Vector(-1, 5), RotationType.CW) { Layer = LayerType.Leadout });
|
||||
AssertClear(p, target);
|
||||
AssertUnsafe(p, target, LeadMaterialSnapshot.Capture(Circle(-1, 3, 1), Vector.Zero));
|
||||
p.Codes[^1] = new ArcMove(new Vector(-1, 4), new Vector(0, 4), RotationType.CW) { Layer = LayerType.Leadout };
|
||||
AssertUnsafe(p, target);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void RealStrategyLeadouts_SafePathsOrRefusedMalformedTabbedArc(bool tabs, bool arc)
|
||||
{
|
||||
var clean = ExplicitContourTests.Square(false);
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.TabsEnabled = tabs;
|
||||
parameters.TabConfig = new NormalTab { Size = 0.2 };
|
||||
parameters.ExternalLeadOut = arc ? new ArcLeadOut { Radius = 0.2 } : new LineLeadOut { Length = 0.2 };
|
||||
var emitted = new ContourCuttingStrategy { Parameters = parameters }.Apply(clean, new Vector(-2, 5));
|
||||
if (tabs && arc)
|
||||
{
|
||||
// The retained emitter generates this arc from nominal closure, not the
|
||||
// actual tab endpoint. Do not fit a different center or bridge the gap.
|
||||
Assert.Throws<ArgumentException>(() => Read(emitted.Program));
|
||||
return;
|
||||
}
|
||||
var execution = Read(emitted.Program);
|
||||
Assert.NotEmpty(execution.Motions.Where(m => m.Layer == LayerType.Leadout));
|
||||
var result = LeadPathValidator.Check(execution, target, []);
|
||||
Assert.True(result.IsComplete, result.Reason); Assert.True(result.IsClear, result.Reason);
|
||||
if (tabs)
|
||||
{
|
||||
var cuts = execution.Motions.Where(m => m.Layer == LayerType.Display && !m.Rapid).ToArray();
|
||||
var leadout = Assert.Single(execution.Motions.Where(m => m.Layer == LayerType.Leadout));
|
||||
Assert.True(cuts[0].Start!.Value.DistanceTo(cuts[^1].End) > 0.1);
|
||||
Assert.Equal(cuts[^1].End, leadout.Start);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceMutationDoesNotAffectSnapshotOrExecution()
|
||||
{
|
||||
var clean = Rectangle(0, 0, 10, 10);
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
var emitted = ExternalLead();
|
||||
var execution = Read(emitted);
|
||||
clean.Codes.Clear(); emitted.Codes.Clear();
|
||||
Assert.True(LeadPathValidator.Check(execution, target, []).IsClear);
|
||||
Assert.Empty(typeof(LeadMaterialSnapshot).GetProperties().Where(p => p.PropertyType == typeof(Program)
|
||||
|| typeof(Entity).IsAssignableFrom(p.PropertyType)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RotatedIncrementalSubprogramActualEmission_AppliesLocationOnce()
|
||||
{
|
||||
var clean = Rectangle(0, 0, 10, 10);
|
||||
var emitted = ExternalLead();
|
||||
clean.Rotate(System.Math.PI / 2); emitted.Rotate(System.Math.PI / 2);
|
||||
clean.Mode = Mode.Incremental; emitted.Mode = Mode.Incremental;
|
||||
var cleanRoot = new Program();
|
||||
cleanRoot.Codes.Add(new SubProgramCall(clean, 90) { Offset = new Vector(4, 6) });
|
||||
var emittedRoot = new Program();
|
||||
emittedRoot.Codes.Add(new SubProgramCall(emitted, 90) { Offset = new Vector(4, 6) });
|
||||
var location = new Vector(20, 30);
|
||||
var target = LeadMaterialSnapshot.Capture(cleanRoot, location);
|
||||
Assert.True(target.IsComplete, target.Reason);
|
||||
var execution = Read(emittedRoot, location);
|
||||
var lead = Assert.Single(execution.Motions.Where(m => m.Layer == LayerType.Leadin));
|
||||
Assert.Equal(19, lead.End.X, 8); Assert.Equal(36, lead.End.Y, 8);
|
||||
Assert.True(LeadPathValidator.Check(execution, target, []).IsClear);
|
||||
var obstacle = LeadMaterialSnapshot.Capture(Rectangle(18, 33.5, 20, 34.5), Vector.Zero);
|
||||
Assert.False(LeadPathValidator.Check(execution, target, [obstacle]).IsClear);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("open")]
|
||||
[InlineData("self")]
|
||||
[InlineData("disjoint")]
|
||||
[InlineData("nested")]
|
||||
[InlineData("nonfinite")]
|
||||
[InlineData("zero")]
|
||||
[InlineData("suppressed")]
|
||||
[InlineData("lead")]
|
||||
[InlineData("recursive")]
|
||||
public void MalformedMaterialRefuses(string kind)
|
||||
{
|
||||
var p = Rectangle(0, 0, 10, 10);
|
||||
switch (kind)
|
||||
{
|
||||
case "open": p.Codes.RemoveAt(p.Codes.Count - 1); break;
|
||||
case "self": p = new Program(); p.MoveTo(0, 0); p.LineTo(10, 10); p.LineTo(0, 10); p.LineTo(10, 0); p.LineTo(0, 0); break;
|
||||
case "disjoint": p.Codes.AddRange(Rectangle(20, 20, 30, 30).Codes); break;
|
||||
case "nested": p.Codes.AddRange(Rectangle(2, 2, 8, 8).Codes); p.Codes.AddRange(Rectangle(3, 3, 7, 7).Codes); break;
|
||||
case "nonfinite": p.Codes[1] = new LinearMove(double.NaN, 2); break;
|
||||
case "zero": p.Codes.Insert(1, new LinearMove(0, 0)); break;
|
||||
case "suppressed": ((Motion)p.Codes[1]).Suppressed = true; break;
|
||||
case "lead": ((LinearMove)p.Codes[1]).Layer = LayerType.Leadin; break;
|
||||
case "recursive": p.Codes.Add(new SubProgramCall(p, 0)); break;
|
||||
}
|
||||
var target = LeadMaterialSnapshot.Capture(p, Vector.Zero);
|
||||
Assert.False(target.IsComplete);
|
||||
var result = LeadPathValidator.Check(Read(ExternalLead()), target, []);
|
||||
Assert.False(result.IsComplete); Assert.False(result.IsClear);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroLeadRefuses_MissingLeadIsDelegated_MarksAreNotMaterial_CancellationThrows()
|
||||
{
|
||||
var clean = Rectangle(0, 0, 10, 10);
|
||||
clean.MoveTo(20, 20);
|
||||
clean.Codes.Add(new LinearMove(30, 30) { Layer = LayerType.Scribe });
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
Assert.True(target.IsComplete, target.Reason);
|
||||
AssertClear(Rectangle(0, 0, 10, 10), target);
|
||||
var p = ExternalLead(); p.Codes[0] = new RapidMove(0, 5);
|
||||
var result = LeadPathValidator.Check(Read(p), target, []);
|
||||
Assert.False(result.IsComplete); Assert.False(result.IsClear);
|
||||
var token = new CancellationToken(true);
|
||||
Assert.Throws<OperationCanceledException>(() => LeadMaterialSnapshot.Capture(clean, Vector.Zero, token));
|
||||
Assert.Throws<OperationCanceledException>(() => LeadPathValidator.Check(Read(ExternalLead()), target, [], token));
|
||||
p.Codes[1] = new ArcMove(new Vector(0, 5), new Vector(0, 5)) { Layer = LayerType.Leadin };
|
||||
Assert.Throws<ArgumentException>(() => Read(p));
|
||||
p.Codes[1] = new LinearMove(double.PositiveInfinity, 5) { Layer = LayerType.Leadin };
|
||||
Assert.Throws<ArgumentException>(() => Read(p));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("line")]
|
||||
[InlineData("arc")]
|
||||
[InlineData("lineline")]
|
||||
[InlineData("linearc")]
|
||||
public void RealStrategyEmission_ClearExternalAndInternalLeads(string style)
|
||||
{
|
||||
var clean = ExplicitContourTests.Square(false);
|
||||
clean.Codes.AddRange(Circle(5, 5, 2).Codes);
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
Assert.True(target.IsComplete, target.Reason);
|
||||
var parameters = ExplicitContourTests.Parameters(style);
|
||||
if (style == "linearc")
|
||||
((LineArcLeadIn)parameters.ExternalLeadIn).ApproachAngle = 90;
|
||||
var result = new ContourCuttingStrategy { Parameters = parameters }.Apply(clean, new Vector(-2, 5));
|
||||
var execution = Read(result.Program);
|
||||
Assert.NotEmpty(execution.Motions.Where(m => m.Layer == LayerType.Leadin));
|
||||
var check = LeadPathValidator.Check(execution, target, []);
|
||||
Assert.True(check.IsComplete, check.Reason);
|
||||
Assert.True(check.IsClear, check.Reason + "\n" + string.Join("\n", execution.Motions.Select(m => $"{m.Layer} {m.Start} -> {m.End}, {m.Length}")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealStrategyEmission_UnsafeCompositePierceRefusesWithoutRepair()
|
||||
{
|
||||
var clean = ExplicitContourTests.Square(false);
|
||||
clean.Codes.AddRange(Circle(5, 5, 2).Codes);
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
var emitted = new ContourCuttingStrategy { Parameters = ExplicitContourTests.Parameters("linearc") }.Apply(clean, new Vector(-2, 5));
|
||||
AssertUnsafe(emitted.Program, target);
|
||||
var orphan = new Program(); orphan.MoveTo(-5, 5);
|
||||
orphan.Codes.Add(new LinearMove(-3, 5) { Layer = LayerType.Leadin });
|
||||
AssertUnsafe(orphan, target);
|
||||
}
|
||||
|
||||
private static Program Circle(double x, double y, double radius)
|
||||
{
|
||||
var p = new Program(); p.MoveTo(x + radius, y); p.ArcTo(x + radius, y, x, y, RotationType.CCW); return p;
|
||||
}
|
||||
|
||||
private static void AssertClear(Program p, LeadMaterialSnapshot target, params LeadMaterialSnapshot[] others)
|
||||
{
|
||||
var result = LeadPathValidator.Check(Read(p), target, others);
|
||||
Assert.True(result.IsComplete, result.Reason); Assert.True(result.IsClear, result.Reason);
|
||||
}
|
||||
|
||||
private static void AssertUnsafe(Program p, LeadMaterialSnapshot target, params LeadMaterialSnapshot[] others)
|
||||
{
|
||||
var result = LeadPathValidator.Check(Read(p), target, others);
|
||||
Assert.True(result.IsComplete, result.Reason); Assert.False(result.IsClear);
|
||||
}
|
||||
|
||||
internal static Program Rectangle(double x1, double y1, double x2, double y2)
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(x1, y1); p.LineTo(x1, y2); p.LineTo(x2, y2);
|
||||
p.LineTo(x2, y1); p.LineTo(x1, y1);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static Program ExternalLead()
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(-3, 5);
|
||||
p.Codes.Add(new LinearMove(0, 5) { Layer = LayerType.Leadin });
|
||||
p.LineTo(0, 10);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static OwnedExecution Read(Program p, Vector? location = null) =>
|
||||
ExecutionMotionReader.Read(p, location ?? Vector.Zero, null, default);
|
||||
}
|
||||
Reference in new issue
Block a user