mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 02:41:06 -04:00
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>
This commit is contained in:
1 parent
2aecafb356
commit
1bd0b258af
7 files changed
+502
-3
No files matched your search
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingPlanning;
|
||||
|
||||
public enum CutPinStatus
|
||||
{
|
||||
/// <summary>The pin lies on exactly one contour, which no other pin claims.</summary>
|
||||
Resolved,
|
||||
|
||||
/// <summary>The pin no longer lies on any contour of the drawing.</summary>
|
||||
NotOnContour,
|
||||
|
||||
/// <summary>The pin lies on more than one contour, so which one it pins is unknown.</summary>
|
||||
OnSeveralContours,
|
||||
|
||||
/// <summary>Another pin lies on the same contour; neither is used.</summary>
|
||||
ContourPinnedTwice,
|
||||
|
||||
/// <summary>The drawing's program cannot be prepared, or is not in the frame its source offset describes.</summary>
|
||||
UnsupportedGeometry,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where one drawing pin lands on a placed contour. <see cref="Point"/> is in the placed
|
||||
/// part's local frame (add the part's location for plate coordinates): the exact native point
|
||||
/// when resolved, otherwise where the pin maps to (<see cref="Vector.Invalid"/> when the drawing
|
||||
/// frame is unknown). Ordinals match <see cref="PreparedContours"/> captured from the same
|
||||
/// rotated clean program.
|
||||
/// </summary>
|
||||
public sealed record CutPinResolution(int PinIndex, ContourCutPin Pin, CutPinStatus Status, int ContourOrdinal,
|
||||
int EntityOrdinal, Vector Point)
|
||||
{
|
||||
public bool IsResolved => Status == CutPinStatus.Resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a drawing's source-frame pins onto its program contours at a placement rotation. A
|
||||
/// pin never resolves by guessing: it must lie within the contour closure tolerance of exactly
|
||||
/// one contour that no other pin claims, or it is reported unresolved with the reason.
|
||||
/// </summary>
|
||||
public static class CutPinResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the drawing's pins for a part rotated by <paramref name="rotation"/> radians
|
||||
/// about the drawing program's origin, as <see cref="Part.Rotate(double)"/> does.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<CutPinResolution> Resolve(Drawing drawing, double rotation, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawing);
|
||||
if (!double.IsFinite(rotation))
|
||||
throw new ArgumentOutOfRangeException(nameof(rotation), rotation, "Rotation must be finite.");
|
||||
var pins = drawing.CutPins ?? new List<ContourCutPin>();
|
||||
if (pins.Count == 0)
|
||||
return Array.Empty<CutPinResolution>();
|
||||
if (drawing.Program == null || !IsSourceFrame(drawing))
|
||||
return Unsupported(pins);
|
||||
|
||||
var program = (Program)drawing.Program.Clone();
|
||||
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
||||
program.Rotate(rotation);
|
||||
|
||||
PreparedContours contours;
|
||||
try
|
||||
{
|
||||
contours = PreparedContours.Capture(program, new CuttingParameters(), token);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException)
|
||||
{
|
||||
return Unsupported(pins);
|
||||
}
|
||||
|
||||
return Resolve(drawing, rotation, contours, token);
|
||||
}
|
||||
|
||||
/// <summary>Resolves the pins of a placed part at its current rotation.</summary>
|
||||
public static IReadOnlyList<CutPinResolution> Resolve(Part part, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(part);
|
||||
return Resolve(part.BaseDrawing, part.Rotation, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves against contours the caller already captured. <paramref name="contours"/> must
|
||||
/// come from the drawing's clean program rotated by <paramref name="rotation"/>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<CutPinResolution> Resolve(Drawing drawing, double rotation, PreparedContours contours,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawing);
|
||||
ArgumentNullException.ThrowIfNull(contours);
|
||||
if (!double.IsFinite(rotation))
|
||||
throw new ArgumentOutOfRangeException(nameof(rotation), rotation, "Rotation must be finite.");
|
||||
var pins = drawing.CutPins ?? new List<ContourCutPin>();
|
||||
if (pins.Count == 0)
|
||||
return Array.Empty<CutPinResolution>();
|
||||
if (!IsSourceFrame(drawing))
|
||||
return Unsupported(pins);
|
||||
|
||||
var offset = drawing.Source?.Offset ?? Vector.Zero;
|
||||
var results = new CutPinResolution[pins.Count];
|
||||
for (var i = 0; i < pins.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var pin = pins[i] ?? throw new ArgumentException($"Drawing pin {i + 1} is missing.", nameof(drawing));
|
||||
var local = (pin.Point - offset).Rotate(rotation);
|
||||
var near = contours.ContoursNear(local, NominalContourClosure.Tolerance);
|
||||
results[i] = near.Count switch
|
||||
{
|
||||
0 => new(i, pin, CutPinStatus.NotOnContour, -1, -1, local),
|
||||
1 => new(i, pin, CutPinStatus.Resolved, near[0].ContourOrdinal, near[0].EntityOrdinal, near[0].Point),
|
||||
_ => new(i, pin, CutPinStatus.OnSeveralContours, -1, -1, local),
|
||||
};
|
||||
}
|
||||
|
||||
// At most one pin per contour. Choosing between two would be a guess, so neither is used.
|
||||
foreach (var claimed in results.Where(r => r.IsResolved).GroupBy(r => r.ContourOrdinal).Where(g => g.Count() > 1))
|
||||
foreach (var result in claimed)
|
||||
results[result.PinIndex] = result with { Status = CutPinStatus.ContourPinnedTwice };
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Pins are stored relative to the source entities; the program is that frame moved by
|
||||
// -Source.Offset and never rotated in place. A rotated program (a canonical copy) is not.
|
||||
private static bool IsSourceFrame(Drawing drawing) =>
|
||||
drawing.Program != null && Math.Tolerance.IsEqualTo(drawing.Program.Rotation, 0);
|
||||
|
||||
private static CutPinResolution[] Unsupported(List<ContourCutPin> pins) =>
|
||||
pins.Select((pin, i) => new CutPinResolution(i, pin, CutPinStatus.UnsupportedGeometry, -1, -1, Vector.Invalid))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -65,7 +65,7 @@ internal static class OwnedCuttingParameters
|
||||
};
|
||||
}
|
||||
|
||||
private static LeadIn CopyLeadIn(LeadIn source) => source switch
|
||||
internal static LeadIn CopyLeadIn(LeadIn source) => source switch
|
||||
{
|
||||
NoLeadIn n when n.GetType() == typeof(NoLeadIn) => new NoLeadIn(),
|
||||
LineLeadIn n when n.GetType() == typeof(LineLeadIn) => new LineLeadIn
|
||||
@@ -86,7 +86,7 @@ internal static class OwnedCuttingParameters
|
||||
_ => throw new NotSupportedException("Unsupported lead-in runtime type.")
|
||||
};
|
||||
|
||||
private static LeadOut CopyLeadOut(LeadOut source) => source switch
|
||||
internal static LeadOut CopyLeadOut(LeadOut source) => source switch
|
||||
{
|
||||
NoLeadOut n when n.GetType() == typeof(NoLeadOut) => new NoLeadOut(),
|
||||
LineLeadOut n when n.GetType() == typeof(LineLeadOut) => new LineLeadOut
|
||||
@@ -96,7 +96,7 @@ internal static class OwnedCuttingParameters
|
||||
_ => throw new NotSupportedException("Unsupported lead-out runtime type.")
|
||||
};
|
||||
|
||||
private static Tab CopyTab(Tab source)
|
||||
internal static Tab CopyTab(Tab source)
|
||||
{
|
||||
Tab copy = source switch
|
||||
{
|
||||
|
||||
@@ -149,6 +149,25 @@ public sealed class PreparedContours
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every contour with a native entity within <paramref name="tolerance"/> of
|
||||
/// <paramref name="point"/>, in ordinal order, each with its closest entity and the exact
|
||||
/// closest native point on it. A locating aid for drawing pins, not an entry choice.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<(int ContourOrdinal, int EntityOrdinal, Vector Point)> ContoursNear(Vector point, double tolerance)
|
||||
{
|
||||
PostVerificationGeometry.Validate(point);
|
||||
var found = new List<(int, int, Vector)>();
|
||||
for (var ordinal = 0; ordinal < Count; ordinal++)
|
||||
{
|
||||
var shape = shapes[ordinal];
|
||||
var closest = shape.ClosestPointTo(point, out var entity);
|
||||
if (closest.DistanceTo(point) <= tolerance)
|
||||
found.Add((ordinal, shape.Entities.IndexOf(entity), closest));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public ContourChoice Entry(int contourOrdinal, int entityOrdinal, Vector point)
|
||||
{
|
||||
var choice = new ContourChoice(contourOrdinal, entityOrdinal, point) { Owner = this };
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// How one contour of a drawing must be cut: where it is entered and the lead-in,
|
||||
/// lead-out and optional tab used there. <see cref="Point"/> is in the drawing's source
|
||||
/// frame (the frame of <see cref="Drawing.SourceEntities"/>), which does not move when the
|
||||
/// program is re-origined; <see cref="CutPinResolver"/> maps it to a placed contour.
|
||||
/// Immutable: only the exact built-in lead and tab types are accepted, they are copied
|
||||
/// in, and every property returns a fresh copy.
|
||||
/// </summary>
|
||||
public sealed class ContourCutPin
|
||||
{
|
||||
private readonly LeadIn leadIn;
|
||||
private readonly LeadOut leadOut;
|
||||
private readonly Tab tab;
|
||||
|
||||
/// <exception cref="ArgumentException">A missing or invalid lead, a nonfinite point, or a tab with a lead-out.</exception>
|
||||
/// <exception cref="NotSupportedException">A lead or tab that is not an exact built-in type.</exception>
|
||||
public ContourCutPin(Vector point, LeadIn leadIn, LeadOut leadOut, Tab tab = null)
|
||||
{
|
||||
PostVerificationGeometry.Validate(point);
|
||||
var ownedLeadIn = OwnedCuttingParameters.CopyLeadIn(leadIn);
|
||||
var ownedLeadOut = OwnedCuttingParameters.CopyLeadOut(leadOut);
|
||||
var ownedTab = tab == null ? null : OwnedCuttingParameters.CopyTab(tab);
|
||||
// A tab leaves the contour open at its end; a lead-out after an open contour is
|
||||
// refused by the planner, so the pin cannot ask for both.
|
||||
if (ownedTab != null && ownedLeadOut.GetType() != typeof(NoLeadOut))
|
||||
throw new ArgumentException("A pin with a tab cannot have a lead-out.", nameof(leadOut));
|
||||
|
||||
Point = point;
|
||||
this.leadIn = ownedLeadIn;
|
||||
this.leadOut = ownedLeadOut;
|
||||
this.tab = ownedTab;
|
||||
}
|
||||
|
||||
/// <summary>The contour entry in the drawing's source frame.</summary>
|
||||
public Vector Point { get; }
|
||||
|
||||
/// <summary>A copy of the pinned lead-in.</summary>
|
||||
public LeadIn LeadIn => OwnedCuttingParameters.CopyLeadIn(leadIn);
|
||||
|
||||
/// <summary>A copy of the pinned lead-out; always <see cref="NoLeadOut"/> when <see cref="HasTab"/>.</summary>
|
||||
public LeadOut LeadOut => OwnedCuttingParameters.CopyLeadOut(leadOut);
|
||||
|
||||
/// <summary>A copy of the pinned tab, or null for a closed contour.</summary>
|
||||
public Tab Tab => tab == null ? null : OwnedCuttingParameters.CopyTab(tab);
|
||||
|
||||
public bool HasTab => tab != null;
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,13 @@ namespace OpenNest
|
||||
/// </summary>
|
||||
public DrawingLibraryLink Library { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contours this drawing says must be cut a fixed way, in the source frame. At most one
|
||||
/// pin per contour is used; <see cref="CNC.CuttingPlanning.CutPinResolver"/> maps them to
|
||||
/// a placement. Pins do not follow geometry into new drawings (split pieces).
|
||||
/// </summary>
|
||||
public List<CNC.CuttingStrategy.ContourCutPin> CutPins { get; set; } = new List<CNC.CuttingStrategy.ContourCutPin>();
|
||||
|
||||
/// <summary>
|
||||
/// Complete set of source entities with stable GUIDs.
|
||||
/// Null when the drawing was created from G-code or an older nest file.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user