mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 19:21:35 -05:00
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>
216 lines
7.2 KiB
C#
216 lines
7.2 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Tests.CuttingPlanning;
|
|
|
|
public class CutPinResolverTests
|
|
{
|
|
// The program frame is the source frame moved by -Offset.
|
|
private static readonly Vector Offset = new(5, -2);
|
|
|
|
/// <summary>A 10 x 6 plate with a radius-1 hole at (3, 3), program-local; pins go in source coordinates.</summary>
|
|
private static Drawing Bracket(params Vector[] sourcePins)
|
|
{
|
|
var entities = new List<Entity>
|
|
{
|
|
new Line(0, 0, 10, 0),
|
|
new Line(10, 0, 10, 6),
|
|
new Line(10, 6, 0, 6),
|
|
new Line(0, 6, 0, 0),
|
|
new Circle(3, 3, 1),
|
|
};
|
|
var drawing = new Drawing("Synthetic bracket", ConvertGeometry.ToProgram(entities));
|
|
drawing.Source.Offset = Offset;
|
|
foreach (var point in sourcePins)
|
|
drawing.CutPins.Add(Pin(point));
|
|
return drawing;
|
|
}
|
|
|
|
private static ContourCutPin Pin(Vector sourcePoint) =>
|
|
new(sourcePoint, new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, new NoLeadOut(), new NormalTab { Size = 0.03 });
|
|
|
|
private static Vector Source(double x, double y) => new Vector(x, y) + Offset;
|
|
|
|
[Fact]
|
|
public void NoPins_ResolveToNothing()
|
|
{
|
|
Assert.Empty(CutPinResolver.Resolve(Bracket(), 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pins_ResolveToThePerimeterAndHoleInTheProgramFrame()
|
|
{
|
|
var drawing = Bracket(Source(10, 6), Source(4, 3));
|
|
|
|
var results = CutPinResolver.Resolve(drawing, 0);
|
|
|
|
Assert.Equal(2, results.Count);
|
|
Assert.All(results, r => Assert.Equal(CutPinStatus.Resolved, r.Status));
|
|
Assert.Equal((1, new Vector(10, 6)), (results[0].ContourOrdinal, results[0].Point));
|
|
Assert.Equal((0, 0), (results[1].ContourOrdinal, results[1].EntityOrdinal));
|
|
AssertNear(new Vector(4, 3), results[1].Point);
|
|
Assert.Same(drawing.CutPins[1], results[1].Pin);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(90)]
|
|
[InlineData(37)]
|
|
public void Pins_FollowTheRotationAboutTheProgramOrigin(double degrees)
|
|
{
|
|
var rotation = Angle.ToRadians(degrees);
|
|
var drawing = Bracket(Source(10, 6));
|
|
|
|
var result = Assert.Single(CutPinResolver.Resolve(drawing, rotation));
|
|
|
|
Assert.True(result.IsResolved);
|
|
AssertNear(new Vector(10 * System.Math.Cos(rotation) - 6 * System.Math.Sin(rotation),
|
|
10 * System.Math.Sin(rotation) + 6 * System.Math.Cos(rotation)), result.Point);
|
|
}
|
|
|
|
[Fact]
|
|
public void Resolutions_AreEntriesOfTheSameRotatedPreparation()
|
|
{
|
|
var rotation = Angle.ToRadians(37);
|
|
var drawing = Bracket(Source(10, 3), Source(3, 4));
|
|
var program = (Program)drawing.Program.Clone();
|
|
program.Rotate(rotation);
|
|
var contours = PreparedContours.Capture(program, ExplicitContourTests.Parameters());
|
|
|
|
var results = CutPinResolver.Resolve(drawing, rotation, contours);
|
|
|
|
Assert.All(results, r =>
|
|
{
|
|
Assert.True(r.IsResolved);
|
|
var choice = contours.Entry(r.ContourOrdinal, r.EntityOrdinal, r.Point);
|
|
Assert.Equal(r.Point, choice.Point);
|
|
});
|
|
Assert.Equal(contours.PerimeterOrdinal, results[0].ContourOrdinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void PinWithinClosureTolerance_SnapsOntoTheEntity()
|
|
{
|
|
var drawing = Bracket(Source(10, 3) + new Vector(5e-7, 0));
|
|
|
|
var result = Assert.Single(CutPinResolver.Resolve(drawing, 0));
|
|
|
|
Assert.True(result.IsResolved);
|
|
Assert.Equal(new Vector(10, 3), result.Point);
|
|
}
|
|
|
|
[Fact]
|
|
public void PinOffTheGeometry_IsNotOnAContourAndOthersStillResolve()
|
|
{
|
|
var drawing = Bracket(Source(10, 3) + new Vector(1e-5, 0), Source(3, 2));
|
|
|
|
var results = CutPinResolver.Resolve(drawing, 0);
|
|
|
|
Assert.Equal(CutPinStatus.NotOnContour, results[0].Status);
|
|
Assert.Equal(-1, results[0].ContourOrdinal);
|
|
AssertNear(new Vector(10.00001, 3), results[0].Point);
|
|
Assert.True(results[1].IsResolved);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoPinsOnOneContour_AreBothUnused()
|
|
{
|
|
var drawing = Bracket(Source(0, 0), Source(3, 4), Source(10, 6));
|
|
|
|
var results = CutPinResolver.Resolve(drawing, 0);
|
|
|
|
Assert.Equal(CutPinStatus.ContourPinnedTwice, results[0].Status);
|
|
Assert.Equal(CutPinStatus.ContourPinnedTwice, results[2].Status);
|
|
Assert.Equal(1, results[0].ContourOrdinal);
|
|
Assert.True(results[1].IsResolved);
|
|
}
|
|
|
|
[Fact]
|
|
public void PinTouchingTwoContours_IsAmbiguous()
|
|
{
|
|
var entities = new List<Entity>
|
|
{
|
|
new Line(0, 0, 10, 0), new Line(10, 0, 10, 6), new Line(10, 6, 0, 6), new Line(0, 6, 0, 0),
|
|
new Circle(3, 3, 1), new Circle(5, 3, 1),
|
|
};
|
|
var drawing = new Drawing("Touching holes", ConvertGeometry.ToProgram(entities));
|
|
drawing.CutPins.Add(Pin(new Vector(4, 3)));
|
|
|
|
var result = Assert.Single(CutPinResolver.Resolve(drawing, 0));
|
|
|
|
Assert.Equal(CutPinStatus.OnSeveralContours, result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void RotatedDrawingProgram_IsNotTheSourceFrame()
|
|
{
|
|
var drawing = Bracket(Source(10, 6));
|
|
drawing.Program.Rotate(Angle.HalfPI);
|
|
|
|
var result = Assert.Single(CutPinResolver.Resolve(drawing, 0));
|
|
|
|
Assert.Equal(CutPinStatus.UnsupportedGeometry, result.Status);
|
|
Assert.False(result.Point.IsValid());
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenGeometry_IsUnsupported()
|
|
{
|
|
var program = new Program();
|
|
program.MoveTo(0, 0);
|
|
program.LineTo(10, 0);
|
|
program.LineTo(10, 6);
|
|
var drawing = new Drawing("Open", program);
|
|
drawing.CutPins.Add(Pin(new Vector(10, 0)));
|
|
|
|
Assert.Equal(CutPinStatus.UnsupportedGeometry, Assert.Single(CutPinResolver.Resolve(drawing, 0)).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void Part_ResolvesAtItsRotation()
|
|
{
|
|
var drawing = Bracket(Source(10, 6));
|
|
var part = new Part(drawing);
|
|
part.Rotate(Angle.HalfPI);
|
|
|
|
var result = Assert.Single(CutPinResolver.Resolve(part));
|
|
|
|
AssertNear(new Vector(-6, 10), result.Point);
|
|
}
|
|
|
|
[Fact]
|
|
public void CanonicalCopy_CarriesNoPins()
|
|
{
|
|
// A canonical copy's program is rotated about its centre, so source-frame pins would
|
|
// not map onto it; its parts are rebound to the original drawing, which keeps them.
|
|
var drawing = Bracket(Source(10, 6));
|
|
|
|
var copy = CanonicalFrame.AsCanonicalCopy(drawing);
|
|
|
|
Assert.Empty(copy.CutPins);
|
|
Assert.Single(drawing.CutPins);
|
|
}
|
|
|
|
[Fact]
|
|
public void SplitPieces_CarryNoPins()
|
|
{
|
|
var drawing = Bracket(Source(10, 6));
|
|
|
|
var pieces = DrawingSplitter.Split(drawing,
|
|
new List<SplitLine> { new(5.0, CutOffAxis.Vertical) },
|
|
new SplitParameters { Type = SplitType.Straight });
|
|
|
|
Assert.NotEmpty(pieces);
|
|
Assert.All(pieces, piece => Assert.Empty(piece.CutPins));
|
|
}
|
|
|
|
private static void AssertNear(Vector expected, Vector actual)
|
|
{
|
|
Assert.True(expected.DistanceTo(actual) <= 1e-9, $"Expected {expected}, got {actual}.");
|
|
}
|
|
}
|