mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 15:52:10 -04:00
feat(cutting): emit owned contours in explicit order
This commit is contained in:
1 parent
6addad7b2e
commit
e71fcea73d
6 files changed
+899
No files matched your search
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
|
||||
namespace OpenNest.CNC.CuttingPlanning;
|
||||
|
||||
/// <summary>Copies only supported, exact setting types; never slices custom subclasses.</summary>
|
||||
internal static class OwnedCuttingParameters
|
||||
{
|
||||
internal static CuttingParameters Copy(CuttingParameters source)
|
||||
{
|
||||
Exact<CuttingParameters>(source);
|
||||
Exact<SequenceParameters>(source.Sequencing);
|
||||
Exact<AssignmentParameters>(source.Assignment);
|
||||
var sequence = source.Sequencing;
|
||||
var assignment = source.Assignment;
|
||||
if (!Enum.IsDefined(sequence.Method) || !Enum.IsDefined(assignment.Method))
|
||||
throw new ArgumentException("Unsupported sequence method.");
|
||||
if (assignment.Preference == null)
|
||||
throw new ArgumentException("Missing assignment preference.");
|
||||
if (!double.IsFinite(source.LeadInAngleIncrement) || source.LeadInAngleIncrement <= 0
|
||||
|| source.LeadInAngleIncrement > 360)
|
||||
throw new ArgumentException("Angle increment must be in (0, 360] degrees.");
|
||||
return new CuttingParameters
|
||||
{
|
||||
Id = source.Id,
|
||||
MachineName = source.MachineName,
|
||||
MaterialName = source.MaterialName,
|
||||
Grade = source.Grade,
|
||||
Thickness = Dimension(source.Thickness),
|
||||
Kerf = Dimension(source.Kerf),
|
||||
PartSpacing = Dimension(source.PartSpacing),
|
||||
PierceClearance = Dimension(source.PierceClearance),
|
||||
ExternalLeadIn = CopyLeadIn(source.ExternalLeadIn),
|
||||
InternalLeadIn = CopyLeadIn(source.InternalLeadIn),
|
||||
ArcCircleLeadIn = CopyLeadIn(source.ArcCircleLeadIn),
|
||||
ExternalLeadOut = CopyLeadOut(source.ExternalLeadOut),
|
||||
InternalLeadOut = CopyLeadOut(source.InternalLeadOut),
|
||||
ArcCircleLeadOut = CopyLeadOut(source.ArcCircleLeadOut),
|
||||
RoundLeadInAngles = source.RoundLeadInAngles,
|
||||
LeadInAngleIncrement = source.LeadInAngleIncrement,
|
||||
AutoTabMinSize = Dimension(source.AutoTabMinSize),
|
||||
AutoTabMaxSize = Dimension(source.AutoTabMaxSize),
|
||||
TabConfig = source.TabConfig == null
|
||||
? source.TabsEnabled ? throw new ArgumentException("Enabled tabs require a configuration.") : null
|
||||
: CopyTab(source.TabConfig),
|
||||
TabsEnabled = source.TabsEnabled,
|
||||
Sequencing = new SequenceParameters
|
||||
{
|
||||
Method = sequence.Method,
|
||||
SmallCutoutWidth = Dimension(sequence.SmallCutoutWidth),
|
||||
SmallCutoutHeight = Dimension(sequence.SmallCutoutHeight),
|
||||
MediumCutoutWidth = Dimension(sequence.MediumCutoutWidth),
|
||||
MediumCutoutHeight = Dimension(sequence.MediumCutoutHeight),
|
||||
DistanceMediumSmall = Dimension(sequence.DistanceMediumSmall),
|
||||
AlternateRowsColumns = sequence.AlternateRowsColumns,
|
||||
AlternateCutoutsWithinRowColumn = sequence.AlternateCutoutsWithinRowColumn,
|
||||
MinDistanceBetweenRowsColumns = Dimension(sequence.MinDistanceBetweenRowsColumns)
|
||||
},
|
||||
Assignment = new AssignmentParameters
|
||||
{
|
||||
Method = assignment.Method,
|
||||
Preference = assignment.Preference,
|
||||
MinGeometryLength = Dimension(assignment.MinGeometryLength)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private 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
|
||||
{ Length = Dimension(n.Length), ApproachAngle = Angle(n.ApproachAngle) },
|
||||
ArcLeadIn n when n.GetType() == typeof(ArcLeadIn) => new ArcLeadIn { Radius = Dimension(n.Radius) },
|
||||
LineArcLeadIn n when n.GetType() == typeof(LineArcLeadIn) => new LineArcLeadIn
|
||||
{ LineLength = Dimension(n.LineLength), ArcRadius = Dimension(n.ArcRadius), ApproachAngle = Angle(n.ApproachAngle) },
|
||||
LineLineLeadIn n when n.GetType() == typeof(LineLineLeadIn) => new LineLineLeadIn
|
||||
{
|
||||
Length1 = Dimension(n.Length1),
|
||||
Length2 = Dimension(n.Length2),
|
||||
ApproachAngle1 = Angle(n.ApproachAngle1),
|
||||
ApproachAngle2 = Angle(n.ApproachAngle2)
|
||||
},
|
||||
CleanHoleLeadIn n when n.GetType() == typeof(CleanHoleLeadIn) => new CleanHoleLeadIn
|
||||
{ LineLength = Dimension(n.LineLength), ArcRadius = Dimension(n.ArcRadius), Kerf = Dimension(n.Kerf) },
|
||||
null => throw new ArgumentException("Missing lead-in settings."),
|
||||
_ => throw new NotSupportedException("Unsupported lead-in runtime type.")
|
||||
};
|
||||
|
||||
private 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
|
||||
{ Length = Dimension(n.Length), ApproachAngle = Angle(n.ApproachAngle) },
|
||||
ArcLeadOut n when n.GetType() == typeof(ArcLeadOut) => new ArcLeadOut { Radius = Dimension(n.Radius) },
|
||||
null => throw new ArgumentException("Missing lead-out settings."),
|
||||
_ => throw new NotSupportedException("Unsupported lead-out runtime type.")
|
||||
};
|
||||
|
||||
private static Tab CopyTab(Tab source)
|
||||
{
|
||||
Tab copy = source switch
|
||||
{
|
||||
NormalTab n when n.GetType() == typeof(NormalTab) => new NormalTab
|
||||
{
|
||||
CutoutMinWidth = Dimension(n.CutoutMinWidth),
|
||||
CutoutMinHeight = Dimension(n.CutoutMinHeight),
|
||||
CutoutMaxWidth = Dimension(n.CutoutMaxWidth),
|
||||
CutoutMaxHeight = Dimension(n.CutoutMaxHeight)
|
||||
},
|
||||
BreakerTab n when n.GetType() == typeof(BreakerTab) => new BreakerTab
|
||||
{
|
||||
BreakerDepth = Dimension(n.BreakerDepth),
|
||||
BreakerLeadInLength = Dimension(n.BreakerLeadInLength),
|
||||
BreakerAngle = Angle(n.BreakerAngle)
|
||||
},
|
||||
MachineTab n when n.GetType() == typeof(MachineTab) => new MachineTab { MachineTabId = n.MachineTabId },
|
||||
_ => throw new NotSupportedException("Unsupported tab runtime type.")
|
||||
};
|
||||
copy.Size = Dimension(source.Size);
|
||||
copy.TabLeadIn = source.TabLeadIn == null ? null : CopyLeadIn(source.TabLeadIn);
|
||||
copy.TabLeadOut = source.TabLeadOut == null ? null : CopyLeadOut(source.TabLeadOut);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static void Exact<T>(T value) where T : class
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentException("Missing cutting settings.");
|
||||
if (value.GetType() != typeof(T))
|
||||
throw new NotSupportedException("Unsupported settings runtime type.");
|
||||
}
|
||||
|
||||
private static double Dimension(double value) => double.IsFinite(value) && value >= 0
|
||||
? value : throw new ArgumentException("Dimensions must be finite and nonnegative.");
|
||||
|
||||
// Approach angles are degrees, not lengths. Signed and periodic angles are valid.
|
||||
private static double Angle(double value) => double.IsFinite(value)
|
||||
? value : throw new ArgumentException("Angles must be finite.");
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingPlanning;
|
||||
|
||||
/// <summary>A nominal native entry owned by one preparation, not an actual emitted pierce.</summary>
|
||||
public sealed record ContourChoice(int ContourOrdinal, int EntityOrdinal, Vector Point)
|
||||
{
|
||||
internal PreparedContours Owner { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable owned clean contours and settings. Callers supply the clean, already-rotated
|
||||
/// program and keep it stable during Capture. This is not a topology or lead-path verdict.
|
||||
/// </summary>
|
||||
public sealed class PreparedContours
|
||||
{
|
||||
private readonly Shape[] shapes;
|
||||
private readonly CuttingParameters parameters;
|
||||
private readonly List<Entity> scribes;
|
||||
|
||||
private PreparedContours(Shape[] shapes, List<Entity> scribes, CuttingParameters parameters)
|
||||
{
|
||||
this.shapes = shapes;
|
||||
this.scribes = scribes;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public int Count => shapes.Length;
|
||||
public int PerimeterOrdinal => Count - 1;
|
||||
|
||||
public static PreparedContours Capture(Program program, CuttingParameters parameters, CancellationToken token = default)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var ownedParameters = OwnedCuttingParameters.Copy(parameters);
|
||||
ExecutionMotionReader.Read(program, Vector.Zero, null, token);
|
||||
ValidateProgramTypes(program, token);
|
||||
// ToGeometry constructs fresh native entities. Normalize an owned motion graph
|
||||
// to incremental mode so absolute subprograms retain their frame offsets too.
|
||||
// Retain execution boundaries: ShapeBuilder can join duplicate closed contours.
|
||||
var geometryProgram = CopyForGeometry(program, token);
|
||||
var contours = new List<Shape>();
|
||||
var scribes = new List<Entity>();
|
||||
var current = new Shape();
|
||||
foreach (var entity in geometryProgram.ToGeometry())
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (entity.GetType() != typeof(Line) && entity.GetType() != typeof(Arc) && entity.GetType() != typeof(Circle))
|
||||
throw new NotSupportedException("Unsupported native geometry.");
|
||||
if (entity.Layer == SpecialLayers.Rapid || entity.Layer == SpecialLayers.Scribe)
|
||||
{
|
||||
if (current.Entities.Count != 0)
|
||||
throw new ArgumentException("Preparation requires closed execution contours.");
|
||||
if (entity.Layer == SpecialLayers.Scribe)
|
||||
scribes.Add(entity);
|
||||
continue;
|
||||
}
|
||||
if (entity.Layer != SpecialLayers.Cut && entity.Layer != SpecialLayers.Display)
|
||||
throw new ArgumentException("Preparation requires clean cut/display contours.");
|
||||
if (entity.Length <= PostVerificationGeometry.Epsilon || !double.IsFinite(entity.Length))
|
||||
throw new ArgumentException("Degenerate contour entity.");
|
||||
if (entity is Circle)
|
||||
{
|
||||
if (current.Entities.Count != 0)
|
||||
throw new ArgumentException("Circle interrupts an open contour.");
|
||||
current.Entities.Add(entity);
|
||||
FinishContour();
|
||||
continue;
|
||||
}
|
||||
if (current.Entities.Count > 0 && End(current.Entities[^1]).DistanceTo(Start(entity)) > PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("Discontinuous native contour.");
|
||||
current.Entities.Add(entity);
|
||||
if (Start(current.Entities[0]).DistanceTo(End(entity)) <= PostVerificationGeometry.Epsilon)
|
||||
FinishContour();
|
||||
}
|
||||
if (current.Entities.Count != 0 || contours.Count == 0)
|
||||
throw new ArgumentException("Preparation requires nonempty closed contours.");
|
||||
var perimeter = 0;
|
||||
for (var i = 1; i < contours.Count; i++)
|
||||
{
|
||||
var box = contours[i].BoundingBox;
|
||||
var best = contours[perimeter].BoundingBox;
|
||||
if (box.Width * box.Length > best.Width * best.Length)
|
||||
perimeter = i;
|
||||
}
|
||||
var outer = contours[perimeter];
|
||||
contours.RemoveAt(perimeter);
|
||||
contours.Add(outer);
|
||||
return new(contours.ToArray(), scribes, ownedParameters);
|
||||
|
||||
void FinishContour()
|
||||
{
|
||||
current.UpdateBounds();
|
||||
contours.Add(current);
|
||||
current = new Shape();
|
||||
}
|
||||
}
|
||||
|
||||
public ContourChoice ClosestEntry(int contourOrdinal, Vector approach)
|
||||
{
|
||||
PostVerificationGeometry.Validate(approach);
|
||||
var shape = GetShape(contourOrdinal);
|
||||
var point = shape.ClosestPointTo(approach, out var entity);
|
||||
return Entry(contourOrdinal, shape.Entities.IndexOf(entity), point);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closest native point first, then source-order vertices/midpoints or eight circle
|
||||
/// angles (0 through 315 degrees). Geometrical duplicates keep their first entity.
|
||||
/// Emit before validating actual paths: circle rounding/clamping may move this point.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ContourChoice> Entries(int contourOrdinal, Vector approach, int maxEntries = 16, CancellationToken token = default)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (maxEntries <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxEntries));
|
||||
var shape = GetShape(contourOrdinal);
|
||||
var entries = new List<ContourChoice>();
|
||||
entries.Add(ClosestEntry(contourOrdinal, approach));
|
||||
for (var i = 0; i < shape.Entities.Count && entries.Count < maxEntries; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var entity = shape.Entities[i];
|
||||
if (entity is Circle circle)
|
||||
{
|
||||
for (var angle = 0; angle < 8 && entries.Count < maxEntries; angle++)
|
||||
Add(i, circle.Center + new Vector(System.Math.Cos(angle * System.Math.PI / 4),
|
||||
System.Math.Sin(angle * System.Math.PI / 4)) * circle.Radius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(i, Start(entity));
|
||||
Add(i, entity is Line line ? line.MidPoint : ((Arc)entity).MidPoint());
|
||||
Add(i, End(entity));
|
||||
}
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
return entries.AsReadOnly();
|
||||
|
||||
void Add(int entityOrdinal, Vector point)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (entries.Count < maxEntries && !entries.Any(e => e.Point.DistanceTo(point) <= PostVerificationGeometry.Epsilon))
|
||||
entries.Add(Entry(contourOrdinal, entityOrdinal, point));
|
||||
}
|
||||
}
|
||||
|
||||
public ContourChoice Entry(int contourOrdinal, int entityOrdinal, Vector point)
|
||||
{
|
||||
var choice = new ContourChoice(contourOrdinal, entityOrdinal, point) { Owner = this };
|
||||
ValidateChoice(choice);
|
||||
return choice;
|
||||
}
|
||||
|
||||
/// <summary>Emits every contour once in caller order, holes before perimeter, with scribes once.</summary>
|
||||
public Program Emit(IReadOnlyList<ContourChoice> choices)
|
||||
{
|
||||
Validate(choices, true);
|
||||
return EmitPrefix(choices);
|
||||
}
|
||||
|
||||
// Each prefix is a standalone owned program, including the same scribes once.
|
||||
internal Program EmitPrefix(IReadOnlyList<ContourChoice> choices)
|
||||
{
|
||||
Validate(choices, false);
|
||||
return new ContourCuttingStrategy { Parameters = parameters }.EmitPrepared(
|
||||
shapes.Select(s => (Shape)s.Clone()).ToArray(), scribes.Select(e => e.Clone()).ToList(), choices);
|
||||
}
|
||||
|
||||
private void Validate(IReadOnlyList<ContourChoice> choices, bool complete)
|
||||
{
|
||||
if (choices == null || (complete && choices.Count != Count) || choices.Count > Count)
|
||||
throw new ArgumentException("Every contour must be represented exactly once.");
|
||||
var visited = new HashSet<int>();
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
ValidateChoice(choice);
|
||||
if (!visited.Add(choice.ContourOrdinal) || choice.ContourOrdinal == PerimeterOrdinal && visited.Count != Count)
|
||||
throw new ArgumentException("Duplicate contour or perimeter before its internal contours.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateChoice(ContourChoice choice)
|
||||
{
|
||||
if (choice == null || !ReferenceEquals(choice.Owner, this))
|
||||
throw new ArgumentException("Foreign contour choice.");
|
||||
PostVerificationGeometry.Validate(choice.Point);
|
||||
var shape = GetShape(choice.ContourOrdinal);
|
||||
if (choice.EntityOrdinal < 0 || choice.EntityOrdinal >= shape.Entities.Count)
|
||||
throw new ArgumentException("Foreign entity ordinal.");
|
||||
var distance = shape.Entities[choice.EntityOrdinal].ClosestPointTo(choice.Point).DistanceTo(choice.Point);
|
||||
if (!double.IsFinite(distance) || distance > PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("Entry is not on the selected native entity.");
|
||||
}
|
||||
|
||||
private Shape GetShape(int ordinal) => ordinal < 0 || ordinal >= Count
|
||||
? throw new ArgumentException("Foreign contour ordinal.") : shapes[ordinal];
|
||||
|
||||
private static Vector Start(Entity entity) => entity is Line line ? line.StartPoint : ((Arc)entity).StartPoint();
|
||||
private static Vector End(Entity entity) => entity is Line line ? line.EndPoint : ((Arc)entity).EndPoint();
|
||||
|
||||
private static Program CopyForGeometry(Program source, CancellationToken token)
|
||||
{
|
||||
var copies = new Dictionary<Program, Program>(ReferenceEqualityComparer.Instance);
|
||||
return Copy(source);
|
||||
|
||||
Program Copy(Program current)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (copies.TryGetValue(current, out var existing))
|
||||
return existing;
|
||||
var copy = new Program(current.Mode);
|
||||
copies.Add(current, copy);
|
||||
foreach (var code in current.Codes)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var ownedCode = code.Clone();
|
||||
if (ownedCode is SubProgramCall call)
|
||||
call.BindProgram(Copy(((SubProgramCall)code).Program));
|
||||
copy.Codes.Add(ownedCode);
|
||||
}
|
||||
copy.Mode = Mode.Incremental;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateProgramTypes(Program program, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (program.GetType() != typeof(Program) || !Enum.IsDefined(program.Mode))
|
||||
throw new NotSupportedException("Unsupported program runtime type or mode.");
|
||||
foreach (var code in program.Codes)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var type = code.GetType();
|
||||
if (type != typeof(RapidMove) && type != typeof(LinearMove) && type != typeof(ArcMove)
|
||||
&& type != typeof(SubProgramCall) && type != typeof(Comment) && type != typeof(Feedrate) && type != typeof(Kerf))
|
||||
throw new NotSupportedException("Unsupported instruction runtime type.");
|
||||
if (code is SubProgramCall call)
|
||||
ValidateProgramTypes(call.Program, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,21 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
return null;
|
||||
}
|
||||
|
||||
internal Program EmitPrepared(Shape[] shapes, List<Entity> scribes,
|
||||
IReadOnlyList<CuttingPlanning.ContourChoice> choices)
|
||||
{
|
||||
var result = new Program(Mode.Absolute);
|
||||
EmitScribeContours(result, scribes);
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
var shape = shapes[choice.ContourOrdinal];
|
||||
EmitContour(result, shape, choice.Point, shape.Entities[choice.EntityOrdinal],
|
||||
choice.ContourOrdinal == shapes.Length - 1 ? ContourType.External : null);
|
||||
}
|
||||
result.Mode = Mode.Incremental;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EmitRawContour(Program program, Shape shape)
|
||||
{
|
||||
var startPoint = GetShapeStartPoint(shape);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="OpenNest.Tests" />
|
||||
<InternalsVisibleTo Include="OpenNest.Engine" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Clipper2" Version="2.0.0" />
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class OwnedCuttingParameterTests
|
||||
{
|
||||
public static IEnumerable<object[]> Styles()
|
||||
{
|
||||
foreach (var leadIn in new LeadIn[] { new NoLeadIn(), new LineLeadIn { Length = 0.3, ApproachAngle = 45 },
|
||||
new ArcLeadIn { Radius = 0.2 }, new LineArcLeadIn { LineLength = 0.1, ArcRadius = 0.2, ApproachAngle = 120 },
|
||||
new LineLineLeadIn { Length1 = 0.1, Length2 = 0.2, ApproachAngle1 = 40, ApproachAngle2 = -30 },
|
||||
new CleanHoleLeadIn { LineLength = 0.1, ArcRadius = 0.2, Kerf = 0.01 } })
|
||||
foreach (var leadOut in new LeadOut[] { new NoLeadOut(), new LineLeadOut { Length = 0.4, ApproachAngle = 80 }, new ArcLeadOut { Radius = 0.3 } })
|
||||
foreach (var tab in new Tab[] { new NormalTab { Size = 0.2, CutoutMinWidth = 1, CutoutMaxWidth = 5, CutoutMinHeight = 2, CutoutMaxHeight = 6 },
|
||||
new BreakerTab { Size = 0.3, BreakerDepth = 0.1, BreakerLeadInLength = 0.2, BreakerAngle = 30 },
|
||||
new MachineTab { Size = 0.4, MachineTabId = 7 } })
|
||||
yield return [leadIn, leadOut, tab];
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Styles))]
|
||||
public void Copy_PreservesEveryMemberAndBreaksEveryMutableAlias(LeadIn leadIn, LeadOut leadOut, Tab tab)
|
||||
{
|
||||
tab.TabLeadIn = leadIn; tab.TabLeadOut = leadOut;
|
||||
var source = new CuttingParameters
|
||||
{
|
||||
Id = 17,
|
||||
MachineName = "machine",
|
||||
MaterialName = "material",
|
||||
Grade = "grade",
|
||||
Thickness = 0.4,
|
||||
Kerf = 0.01,
|
||||
PartSpacing = 0.1,
|
||||
PierceClearance = 0.05,
|
||||
ExternalLeadIn = leadIn,
|
||||
InternalLeadIn = leadIn,
|
||||
ArcCircleLeadIn = leadIn,
|
||||
ExternalLeadOut = leadOut,
|
||||
InternalLeadOut = leadOut,
|
||||
ArcCircleLeadOut = leadOut,
|
||||
TabConfig = tab,
|
||||
TabsEnabled = true,
|
||||
AutoTabMinSize = 0.1,
|
||||
AutoTabMaxSize = 0.5,
|
||||
RoundLeadInAngles = true,
|
||||
LeadInAngleIncrement = 30,
|
||||
Sequencing = new SequenceParameters
|
||||
{
|
||||
Method = SequenceMethod.LeftSide,
|
||||
SmallCutoutWidth = 2,
|
||||
SmallCutoutHeight = 3,
|
||||
MediumCutoutWidth = 9,
|
||||
MediumCutoutHeight = 10,
|
||||
DistanceMediumSmall = 4,
|
||||
AlternateRowsColumns = false,
|
||||
AlternateCutoutsWithinRowColumn = false,
|
||||
MinDistanceBetweenRowsColumns = 0.3
|
||||
},
|
||||
Assignment = new AssignmentParameters { Method = SequenceMethod.EdgeStart, Preference = "test", MinGeometryLength = 0.04 }
|
||||
};
|
||||
var copy = OwnedCuttingParameters.Copy(source);
|
||||
AssertOwnedEqual(source, copy);
|
||||
var secondCopy = OwnedCuttingParameters.Copy(copy);
|
||||
Mutate(source, new HashSet<object>(ReferenceEqualityComparer.Instance));
|
||||
AssertOwnedEqual(secondCopy, copy);
|
||||
}
|
||||
|
||||
private static void AssertOwnedEqual(object? source, object? copy)
|
||||
{
|
||||
Assert.Equal(source?.GetType(), copy?.GetType());
|
||||
if (source == null) return;
|
||||
Assert.NotSame(source, copy);
|
||||
foreach (var property in source.GetType().GetProperties())
|
||||
{
|
||||
var a = property.GetValue(source)!; var b = property.GetValue(copy);
|
||||
if (property.PropertyType.IsValueType || property.PropertyType == typeof(string))
|
||||
Assert.Equal(a, b);
|
||||
else
|
||||
AssertOwnedEqual(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Mutate(object? source, HashSet<object> visited)
|
||||
{
|
||||
if (source == null || !visited.Add(source)) return;
|
||||
foreach (var property in source.GetType().GetProperties())
|
||||
{
|
||||
var value = property.GetValue(source)!;
|
||||
if (property.PropertyType == typeof(double)) property.SetValue(source, (double)value + 100);
|
||||
else if (property.PropertyType == typeof(bool)) property.SetValue(source, !(bool)value);
|
||||
else if (property.PropertyType == typeof(int)) property.SetValue(source, (int)value + 10);
|
||||
else if (property.PropertyType == typeof(string)) property.SetValue(source, "mutated");
|
||||
else if (property.PropertyType.IsEnum) property.SetValue(source, Enum.ToObject(property.PropertyType, 100));
|
||||
else Mutate(value, visited);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null-settings")]
|
||||
[InlineData("null-sequence")]
|
||||
[InlineData("null-assignment")]
|
||||
[InlineData("null-lead")]
|
||||
[InlineData("null-leadout")]
|
||||
[InlineData("null-tab")]
|
||||
[InlineData("dimension")]
|
||||
[InlineData("angle")]
|
||||
[InlineData("increment")]
|
||||
[InlineData("enum")]
|
||||
[InlineData("tab-lead")]
|
||||
[InlineData("custom-in")]
|
||||
[InlineData("custom-out")]
|
||||
[InlineData("custom-tab")]
|
||||
[InlineData("custom-settings")]
|
||||
[InlineData("custom-sequence")]
|
||||
[InlineData("custom-assignment")]
|
||||
public void Copy_RejectsInvalidOrUnsupportedSettings(string fault)
|
||||
{
|
||||
var source = new CuttingParameters();
|
||||
switch (fault)
|
||||
{
|
||||
case "null-settings": source = null; break;
|
||||
case "null-sequence": source.Sequencing = null; break;
|
||||
case "null-assignment": source.Assignment = null; break;
|
||||
case "null-lead": source.ArcCircleLeadIn = null; break;
|
||||
case "null-leadout": source.InternalLeadOut = null; break;
|
||||
case "null-tab": source.TabsEnabled = true; break;
|
||||
case "dimension": source.PartSpacing = -1; break;
|
||||
case "angle": source.ExternalLeadIn = new LineLeadIn { ApproachAngle = double.NaN }; break;
|
||||
case "increment": source.LeadInAngleIncrement = 0; break;
|
||||
case "enum": source.Assignment.Method = (SequenceMethod)6; break;
|
||||
case "tab-lead": source.TabConfig = new NormalTab { TabLeadIn = new ArcLeadIn { Radius = double.PositiveInfinity } }; break;
|
||||
case "custom-in": source.InternalLeadIn = new CustomLeadIn(); break;
|
||||
case "custom-out": source.ArcCircleLeadOut = new CustomLeadOut(); break;
|
||||
case "custom-tab": source.TabConfig = new CustomTab(); break;
|
||||
case "custom-settings": source = new CustomParameters(); break;
|
||||
case "custom-sequence": source.Sequencing = new CustomSequence(); break;
|
||||
case "custom-assignment": source.Assignment = new CustomAssignment(); break;
|
||||
}
|
||||
Assert.ThrowsAny<Exception>(() => OwnedCuttingParameters.Copy(source));
|
||||
Assert.ThrowsAny<Exception>(() => PreparedContours.Capture(ExplicitContourTests.Square(false), source));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public void Copy_RejectsEveryInvalidDimension(double bad)
|
||||
{
|
||||
foreach (var sample in Styles())
|
||||
{
|
||||
var source = new CuttingParameters { ExternalLeadIn = (LeadIn)sample[0], ExternalLeadOut = (LeadOut)sample[1], TabConfig = (Tab)sample[2] };
|
||||
foreach (var owner in new object[] { source, source.Sequencing, source.Assignment, source.ExternalLeadIn, source.ExternalLeadOut, source.TabConfig })
|
||||
foreach (var property in owner.GetType().GetProperties().Where(p => p.PropertyType == typeof(double) && !p.Name.Contains("Angle")))
|
||||
{
|
||||
var original = property.GetValue(owner);
|
||||
property.SetValue(owner, bad);
|
||||
Assert.Throws<ArgumentException>(() => OwnedCuttingParameters.Copy(source));
|
||||
property.SetValue(owner, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CustomLeadIn : LineLeadIn { }
|
||||
private sealed class CustomLeadOut : ArcLeadOut { }
|
||||
private sealed class CustomTab : NormalTab { }
|
||||
private sealed class CustomParameters : CuttingParameters { }
|
||||
private sealed class CustomSequence : SequenceParameters { }
|
||||
private sealed class CustomAssignment : AssignmentParameters { }
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class PreparedContourTests
|
||||
{
|
||||
[Fact]
|
||||
public void Emit_RespectsEveryExplicitEntryAndContourOrder()
|
||||
{
|
||||
var source = Holes();
|
||||
var prepared = PreparedContours.Capture(source, ExplicitContourTests.Parameters());
|
||||
var choices = new[]
|
||||
{
|
||||
prepared.ClosestEntry(1, new Vector(20, 3)),
|
||||
prepared.ClosestEntry(0, new Vector(20, 3)),
|
||||
prepared.ClosestEntry(2, new Vector(20, 5))
|
||||
};
|
||||
var emitted = prepared.Emit(choices);
|
||||
var calls = emitted.Codes.OfType<SubProgramCall>().ToArray();
|
||||
Assert.Equal(new[] { new Vector(7, 3), new Vector(3, 3) }, calls.Select(c => c.Offset));
|
||||
var cuts = ExecutionMotionReader.Read(emitted, Vector.Zero, null, default).Motions
|
||||
.Where(m => m.Layer == LayerType.Display && !m.Rapid).ToArray();
|
||||
Assert.Equal(new Vector(8, 3), cuts[0].Start);
|
||||
Assert.Equal(new Vector(4, 3), cuts[1].Start);
|
||||
Assert.Equal(source.ToString(), Holes().ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("duplicate")]
|
||||
[InlineData("omitted")]
|
||||
[InlineData("foreign")]
|
||||
[InlineData("perimeter-first")]
|
||||
public void Emit_RejectsIncompleteOrForeignAccounting(string fault)
|
||||
{
|
||||
var prepared = PreparedContours.Capture(Holes(), ExplicitContourTests.Parameters());
|
||||
var choices = Enumerable.Range(0, 3).Select(i => prepared.ClosestEntry(i, new Vector(20, 5))).ToArray();
|
||||
switch (fault)
|
||||
{
|
||||
case "duplicate": choices[1] = choices[0]; break;
|
||||
case "omitted": choices = choices[..2]; break;
|
||||
case "foreign": choices[0] = PreparedContours.Capture(Holes(), ExplicitContourTests.Parameters()).ClosestEntry(0, Vector.Zero); break;
|
||||
case "perimeter-first": Array.Reverse(choices); break;
|
||||
}
|
||||
Assert.Throws<ArgumentException>(() => prepared.Emit(choices));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prepared_OwnsGeometryAndDeepSettings()
|
||||
{
|
||||
var source = Holes();
|
||||
var settings = ExplicitContourTests.Parameters();
|
||||
var prepared = PreparedContours.Capture(source, settings);
|
||||
var choices = Enumerable.Range(0, 3).Select(i => prepared.ClosestEntry(i, new Vector(20, 5))).ToArray();
|
||||
var before = ExplicitContourTests.Fingerprint(prepared.Emit(choices));
|
||||
source.Codes.Clear();
|
||||
((LineLeadIn)settings.ExternalLeadIn).Length = 100;
|
||||
settings.TabsEnabled = true;
|
||||
Assert.Equal(before, ExplicitContourTests.Fingerprint(prepared.Emit(choices)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ExplicitContourTests.RetainedStyles), MemberType = typeof(ExplicitContourTests))]
|
||||
public void ExplicitEmission_OrdinarySingleContourMatchesLegacy(string style, bool reverse, bool internalContour)
|
||||
{
|
||||
var source = ExplicitContourTests.Square(reverse);
|
||||
var target = source.ToGeometry().First(e => e.Layer != SpecialLayers.Rapid);
|
||||
var parameters = ExplicitContourTests.Parameters(style);
|
||||
if (internalContour)
|
||||
{
|
||||
source.MoveTo(-5, -5); source.LineTo(-5, 15); source.LineTo(15, 15);
|
||||
source.LineTo(15, -5); source.LineTo(-5, -5);
|
||||
parameters.ExternalLeadIn = new NoLeadIn();
|
||||
}
|
||||
var prepared = PreparedContours.Capture(source, parameters);
|
||||
var choice = prepared.ClosestEntry(0, Vector.Zero);
|
||||
var choices = internalContour
|
||||
? new[] { choice, prepared.Entry(1, 0, new Vector(-5, -5)) }
|
||||
: new[] { choice };
|
||||
var legacy = new ContourCuttingStrategy { Parameters = parameters }.ApplySingle(source,
|
||||
Vector.Zero, target, internalContour ? ContourType.Internal : ContourType.External).Program;
|
||||
Assert.Equal(ExplicitContourTests.Fingerprint(legacy), ExplicitContourTests.Fingerprint(prepared.Emit(choices)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Entries_AreStableBoundedNativeAndGeometricallyUnique()
|
||||
{
|
||||
var prepared = PreparedContours.Capture(ExplicitContourTests.Square(false), new CuttingParameters());
|
||||
var approach = new Vector(-1, 5);
|
||||
var entries = prepared.Entries(0, approach);
|
||||
Assert.Equal(prepared.ClosestEntry(0, approach), entries[0]);
|
||||
Assert.Equal(new[] { new Vector(0, 5), new Vector(0, 0), new Vector(0, 10),
|
||||
new Vector(5, 10), new Vector(10, 10), new Vector(10, 5), new Vector(10, 0), new Vector(5, 0) },
|
||||
entries.Select(e => e.Point));
|
||||
Assert.Equal(entries, prepared.Entries(0, approach));
|
||||
Assert.Equal(entries.Take(3), prepared.Entries(0, approach, 3));
|
||||
Assert.Single(prepared.Entries(0, approach, 1));
|
||||
Assert.Throws<NotSupportedException>(() => ((IList<ContourChoice>)entries).Clear());
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => prepared.Entries(0, approach, 0));
|
||||
Assert.Throws<OperationCanceledException>(() => prepared.Entries(0, approach,
|
||||
token: new CancellationToken(true)));
|
||||
Assert.Throws<OperationCanceledException>(() => PreparedContours.Capture(
|
||||
ExplicitContourTests.Square(false), new CuttingParameters(), new CancellationToken(true)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Entries_IncludeNativeArcMidpointAndCircleAngles()
|
||||
{
|
||||
var source = new Program();
|
||||
source.MoveTo(1, 0); source.ArcTo(-1, 0, 0, 0, RotationType.CCW); source.LineTo(1, 0);
|
||||
var prepared = PreparedContours.Capture(source, new CuttingParameters());
|
||||
var entries = prepared.Entries(0, new Vector(0, -2));
|
||||
Assert.Contains(entries, e => e.Point.DistanceTo(new Vector(0, 1)) < 1e-8);
|
||||
Assert.Contains(entries, e => e.Point.DistanceTo(new Vector(-1, 0)) < 1e-8);
|
||||
prepared = PreparedContours.Capture(Holes(), new CuttingParameters());
|
||||
var circles = prepared.Entries(0, new Vector(5, 5));
|
||||
Assert.InRange(circles.Count, 8, 9);
|
||||
Assert.Equal(circles, prepared.Entries(0, new Vector(5, 5)));
|
||||
Assert.All(circles, e => Assert.Equal(1, e.Point.DistanceTo(new Vector(3, 3)), 8));
|
||||
Assert.Equal(circles.Count, circles.Select(e => e.Point).Distinct().Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1, 0, 0, 0)]
|
||||
[InlineData(3, 0, 0, 0)]
|
||||
[InlineData(0, -1, 0, 0)]
|
||||
[InlineData(0, 1, 3, 3)]
|
||||
[InlineData(0, 0, 3, 3)]
|
||||
[InlineData(0, 0, double.NaN, 3)]
|
||||
[InlineData(0, 0, double.PositiveInfinity, 3)]
|
||||
public void Entry_RejectsInvalidNativeChoices(int contour, int entity, double x, double y)
|
||||
{
|
||||
var prepared = PreparedContours.Capture(Holes(), new CuttingParameters());
|
||||
Assert.Throws<ArgumentException>(() => prepared.Entry(contour, entity, new Vector(x, y)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Emit_RevalidatesRecordCopiesAndRejectsUnownedRecords()
|
||||
{
|
||||
var prepared = PreparedContours.Capture(ExplicitContourTests.Square(false), new CuttingParameters());
|
||||
var choice = prepared.ClosestEntry(0, Vector.Zero);
|
||||
Assert.Throws<ArgumentException>(() => prepared.Emit([choice with { EntityOrdinal = 100 }]));
|
||||
Assert.Throws<ArgumentException>(() => prepared.Emit([choice with { Point = new Vector(5, 5) }]));
|
||||
Assert.Throws<ArgumentException>(() => prepared.Emit([new ContourChoice(0, 0, Vector.Zero)]));
|
||||
Assert.Throws<ArgumentException>(() => prepared.Emit([null]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_AccountsForDuplicateContoursWithoutJoiningOrDroppingThem()
|
||||
{
|
||||
var source = ExplicitContourTests.Square(false);
|
||||
foreach (var ignored in new[] { 0, 1 })
|
||||
{
|
||||
source.MoveTo(2, 2); source.LineTo(2, 4); source.LineTo(4, 4);
|
||||
source.LineTo(4, 2); source.LineTo(2, 2);
|
||||
source.MoveTo(7, 7); source.ArcTo(7, 7, 6, 7, RotationType.CCW);
|
||||
}
|
||||
var prepared = PreparedContours.Capture(source, new CuttingParameters());
|
||||
Assert.Equal(5, prepared.Count);
|
||||
var choices = Enumerable.Range(0, 5).Select(i => prepared.ClosestEntry(i, Vector.Zero)).ToArray();
|
||||
var motions = ExecutionMotionReader.Read(prepared.Emit(choices), Vector.Zero, null, default).Motions;
|
||||
Assert.Equal(14, motions.Count(m => !m.Rapid && m.Layer == LayerType.Display));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("open")]
|
||||
[InlineData("lead")]
|
||||
[InlineData("layer")]
|
||||
[InlineData("derived")]
|
||||
[InlineData("mode")]
|
||||
public void Capture_RefusesUnsupportedOrUncleanExecution(string fault)
|
||||
{
|
||||
var source = ExplicitContourTests.Square(false);
|
||||
if (fault == "open") source.Codes.RemoveAt(source.Codes.Count - 1);
|
||||
if (fault == "lead") ((LinearMove)source.Codes[1]).Layer = LayerType.Leadin;
|
||||
if (fault == "layer") ((LinearMove)source.Codes[1]).Layer = (LayerType)100;
|
||||
if (fault == "derived") source.Codes[1] = new CustomLinearMove();
|
||||
if (fault == "mode") typeof(Program).GetField("mode", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.SetValue(source, (Mode)100);
|
||||
Assert.ThrowsAny<Exception>(() => PreparedContours.Capture(source, new CuttingParameters()));
|
||||
}
|
||||
|
||||
private sealed class CustomLinearMove : LinearMove { }
|
||||
|
||||
[Fact]
|
||||
public void ExplicitCircle_RoundsActualEntryClampsAndSharesOwnedSubprograms()
|
||||
{
|
||||
var source = Holes();
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.ArcCircleLeadIn = new LineLeadIn { Length = 10 };
|
||||
parameters.RoundLeadInAngles = true;
|
||||
parameters.LeadInAngleIncrement = 90;
|
||||
var prepared = PreparedContours.Capture(source, parameters);
|
||||
var choices = new[] { prepared.ClosestEntry(0, new Vector(5, 5)),
|
||||
prepared.ClosestEntry(1, new Vector(9, 5)), prepared.ClosestEntry(2, Vector.Zero) };
|
||||
var result = prepared.Emit(choices);
|
||||
var calls = result.Codes.OfType<SubProgramCall>().ToArray();
|
||||
Assert.Equal(2, calls.Length);
|
||||
Assert.Same(calls[0].Program, calls[1].Program);
|
||||
var motions = ExecutionMotionReader.Read(result, Vector.Zero, null, default).Motions;
|
||||
var leads = motions.Where(m => m.Layer == LayerType.Leadin).Take(2).ToArray();
|
||||
Assert.All(leads, m => Assert.True(m.Length < 2));
|
||||
Assert.True(choices[0].Point.DistanceTo(leads[0].End) > 0.1);
|
||||
Assert.Equal(new Vector(4, 3), leads[0].End);
|
||||
Assert.True(leads[0].Start!.Value.DistanceTo(new Vector(3, 3)) <= 0.95 + 1e-8);
|
||||
var before = ExplicitContourTests.Fingerprint(prepared.Emit(choices));
|
||||
calls[0].Program.Codes.Clear(); result.Codes.Clear(); source.Codes.Clear();
|
||||
Assert.Equal(before, ExplicitContourTests.Fingerprint(prepared.Emit(choices)));
|
||||
Assert.NotSame(calls[0].Program, prepared.Emit(choices).Codes.OfType<SubProgramCall>().First().Program);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("normal")]
|
||||
[InlineData("breaker")]
|
||||
[InlineData("machine")]
|
||||
public void ExplicitTab_RetainsLegacyGapAndOwnsTabSize(string style)
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.TabsEnabled = true;
|
||||
parameters.TabConfig = style switch
|
||||
{
|
||||
"breaker" => new BreakerTab { Size = 0.2 },
|
||||
"machine" => new MachineTab { Size = 0.2 },
|
||||
_ => new NormalTab { Size = 0.2 }
|
||||
};
|
||||
var source = ExplicitContourTests.Square(false);
|
||||
var approach = new Vector(-1, 5);
|
||||
var prepared = PreparedContours.Capture(source, parameters);
|
||||
var choice = prepared.ClosestEntry(0, approach);
|
||||
var result = prepared.Emit([choice]);
|
||||
var legacy = new ContourCuttingStrategy { Parameters = parameters }.Apply(source, approach).Program;
|
||||
Assert.Equal(ExplicitContourTests.Fingerprint(legacy), ExplicitContourTests.Fingerprint(result));
|
||||
var cuts = ExecutionMotionReader.Read(result, Vector.Zero, null, default).Motions
|
||||
.Where(m => !m.Rapid && m.Layer == LayerType.Display).ToArray();
|
||||
Assert.True(cuts[0].Start!.Value.DistanceTo(cuts[^1].End) > 0.1);
|
||||
parameters.TabConfig.Size = 8; parameters.TabsEnabled = false;
|
||||
Assert.Equal(ExplicitContourTests.Fingerprint(result), ExplicitContourTests.Fingerprint(prepared.Emit([choice])));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefixAndCompletePrograms_EmitScribesExactlyOnceAndOwnThem()
|
||||
{
|
||||
var source = Holes();
|
||||
source.MoveTo(1, 1); source.Codes.Add(new LinearMove(new Vector(2, 1)) { Layer = LayerType.Scribe });
|
||||
var prepared = PreparedContours.Capture(source, new CuttingParameters());
|
||||
var choices = Enumerable.Range(0, 3).Select(i => prepared.ClosestEntry(i, Vector.Zero)).ToArray();
|
||||
Assert.Throws<ArgumentException>(() => prepared.EmitPrefix([choices[2]]));
|
||||
foreach (var count in new[] { 0, 1, 2, 3 })
|
||||
{
|
||||
var result = prepared.EmitPrefix(choices.Take(count).ToArray());
|
||||
var motions = ExecutionMotionReader.Read(result, Vector.Zero, null, default).Motions;
|
||||
Assert.Single(motions.Where(m => m.Layer == LayerType.Scribe));
|
||||
result.Codes.Clear();
|
||||
}
|
||||
Assert.Single(ExecutionMotionReader.Read(prepared.Emit(choices), Vector.Zero, null, default).Motions
|
||||
.Where(m => m.Layer == LayerType.Scribe));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Capture_ExpandsSharedSubprogramFramesAndOwnsTheirMotion(bool incremental)
|
||||
{
|
||||
var hole = new Program();
|
||||
hole.MoveTo(1, 0); hole.ArcTo(1, 0, 0, 0, RotationType.CCW);
|
||||
if (incremental) hole.Mode = Mode.Incremental;
|
||||
var source = ExplicitContourTests.Square(false);
|
||||
source.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(3, 3) });
|
||||
source.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(7, 3) });
|
||||
var prepared = PreparedContours.Capture(source, ExplicitContourTests.Parameters());
|
||||
Assert.Equal(3, prepared.Count);
|
||||
var choices = Enumerable.Range(0, 3).Select(i => prepared.ClosestEntry(i, new Vector(20, 3))).ToArray();
|
||||
Assert.Equal(new Vector(4, 3), choices[0].Point);
|
||||
Assert.Equal(new Vector(8, 3), choices[1].Point);
|
||||
var before = ExplicitContourTests.Fingerprint(prepared.Emit(choices));
|
||||
hole.Codes.Clear(); source.Codes.Clear();
|
||||
Assert.Equal(before, ExplicitContourTests.Fingerprint(prepared.Emit(choices)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prefix_RejectsDuplicateContoursEvenBeforeThePerimeter()
|
||||
{
|
||||
var prepared = PreparedContours.Capture(Holes(), new CuttingParameters());
|
||||
var choice = prepared.ClosestEntry(0, Vector.Zero);
|
||||
Assert.Throws<ArgumentException>(() => prepared.EmitPrefix([choice, choice]));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("none", false)]
|
||||
[InlineData("none", true)]
|
||||
[InlineData("line", false)]
|
||||
[InlineData("line", true)]
|
||||
[InlineData("arc", false)]
|
||||
[InlineData("arc", true)]
|
||||
public void ExplicitLeadOut_RetainsLegacyGeometryAndOwnership(string style, bool reversed)
|
||||
{
|
||||
var source = ExplicitContourTests.Square(reversed);
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.ExternalLeadOut = style switch
|
||||
{
|
||||
"line" => new LineLeadOut { Length = 0.2, ApproachAngle = 45 },
|
||||
"arc" => new ArcLeadOut { Radius = 0.2 },
|
||||
_ => new NoLeadOut()
|
||||
};
|
||||
var approach = new Vector(-1, 5);
|
||||
var prepared = PreparedContours.Capture(source, parameters);
|
||||
var choice = prepared.ClosestEntry(0, approach);
|
||||
var emitted = prepared.Emit([choice]);
|
||||
var legacy = new ContourCuttingStrategy { Parameters = parameters }.Apply(source, approach).Program;
|
||||
Assert.Equal(ExplicitContourTests.Fingerprint(legacy), ExplicitContourTests.Fingerprint(emitted));
|
||||
if (parameters.ExternalLeadOut is LineLeadOut line) line.Length = 9;
|
||||
if (parameters.ExternalLeadOut is ArcLeadOut arc) arc.Radius = 9;
|
||||
Assert.Equal(ExplicitContourTests.Fingerprint(emitted), ExplicitContourTests.Fingerprint(prepared.Emit([choice])));
|
||||
}
|
||||
|
||||
internal static Program Holes()
|
||||
{
|
||||
var p = ExplicitContourTests.Square(false);
|
||||
foreach (var x in new[] { 3.0, 7.0 })
|
||||
{
|
||||
p.MoveTo(x + 1, 3); p.ArcTo(x + 1, 3, x, 3, RotationType.CCW);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user