Files
OpenNest/OpenNest.Tests/CuttingPlanning/ContourCutPinTests.cs
T
ajandClaude Opus 5.5 1bd0b258af feat(core): contour cut pins
A library drawing can say how individual contours must be cut. A
`ContourCutPin` holds the entry point in the drawing's source frame plus
the lead-in, lead-out and optional tab used there; `Drawing.CutPins`
lists them. Nothing plans with pins yet (Slice 5).

- Pins accept only the exact built-in lead and tab types, copied through
  the same owned copiers the planner uses, so a pin cannot carry a type
  the emitter would not certify. Properties return copies: the pin is
  immutable.
- A tab requires no lead-out, because the planner refuses a lead-out
  after an open contour.
- The source frame is the frame of the source entities. It does not move
  when BuildDrawing re-origins the program at its first rapid, so pins
  survive converter edits that change Source.Offset.

`CutPinResolver` maps pins to (contour ordinal, entity ordinal, local
point) for a placement rotation, using the same `PreparedContours`
ordinals Plan Cutting will use. It never guesses: a pin must lie within
the contour closure tolerance of exactly one contour that no other pin
claims, otherwise it is reported unresolved with a typed reason. A
drawing program rotated in place (a canonical copy) is not the source
frame and resolves nothing; canonical copies and split pieces carry no
pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:20 -04:00

68 lines
2.4 KiB
C#

using OpenNest.CNC.CuttingStrategy;
using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
public class ContourCutPinTests
{
private sealed class DerivedLineLeadIn : LineLeadIn { }
[Fact]
public void Pin_OwnsCopiesOfItsLeadsAndTab()
{
var leadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 };
var tab = new NormalTab { Size = 0.03 };
var pin = new ContourCutPin(new Vector(1, 2), leadIn, new NoLeadOut(), tab);
leadIn.Length = 9;
tab.Size = 9;
((LineLeadIn)pin.LeadIn).Length = 7;
pin.Tab.Size = 7;
Assert.Equal(new Vector(1, 2), pin.Point);
Assert.Equal(0.25, Assert.IsType<LineLeadIn>(pin.LeadIn).Length);
Assert.Equal(0.03, Assert.IsType<NormalTab>(pin.Tab).Size);
Assert.IsType<NoLeadOut>(pin.LeadOut);
Assert.True(pin.HasTab);
}
[Fact]
public void Pin_WithoutTab_KeepsItsLeadOut()
{
var pin = new ContourCutPin(Vector.Zero, new ArcLeadIn { Radius = 0.1 }, new LineLeadOut { Length = 0.05 });
Assert.False(pin.HasTab);
Assert.Null(pin.Tab);
Assert.Equal(0.05, Assert.IsType<LineLeadOut>(pin.LeadOut).Length);
}
[Fact]
public void Pin_WithTab_RefusesALeadOut()
{
var ex = Assert.Throws<ArgumentException>(() => new ContourCutPin(
Vector.Zero, new LineLeadIn { Length = 0.25 }, new LineLeadOut { Length = 0.05 }, new NormalTab()));
Assert.Contains("tab", ex.Message);
}
[Fact]
public void Pin_RefusesInvalidInput()
{
var lead = new LineLeadIn { Length = 0.25 };
Assert.Throws<ArgumentException>(() => new ContourCutPin(new Vector(double.NaN, 0), lead, new NoLeadOut()));
Assert.Throws<ArgumentException>(() => new ContourCutPin(Vector.Zero, null, new NoLeadOut()));
Assert.Throws<ArgumentException>(() => new ContourCutPin(Vector.Zero, lead, null));
Assert.Throws<ArgumentException>(() => new ContourCutPin(
Vector.Zero, new LineLeadIn { Length = -0.1 }, new NoLeadOut()));
Assert.Throws<ArgumentException>(() => new ContourCutPin(
Vector.Zero, lead, new NoLeadOut(), new NormalTab { Size = double.PositiveInfinity }));
}
[Fact]
public void Pin_RefusesTypesThatAreNotBuiltIn()
{
Assert.Throws<NotSupportedException>(() => new ContourCutPin(
Vector.Zero, new DerivedLineLeadIn { Length = 0.25 }, new NoLeadOut()));
}
}