fix(cutting): share only exact prepared circle programs

This commit is contained in:
aj committed 2026-10-04 21:36:40 -04:00
1 parent 1597ee9069
commit d7204e8d07
2 files changed
+123 -3

No files matched your search

@@ -243,7 +243,8 @@ namespace OpenNest.CNC.CuttingStrategy
{
var shape = shapes[choice.ContourOrdinal];
EmitContour(result, shape, choice.Point, shape.Entities[choice.EntityOrdinal],
choice.ContourOrdinal == shapes.Length - 1 ? ContourType.External : null);
choice.ContourOrdinal == shapes.Length - 1 ? ContourType.External : null,
exactCirclePrograms: true);
}
result.Mode = Mode.Incremental;
return result;
@@ -315,12 +316,57 @@ namespace OpenNest.CNC.CuttingStrategy
return HashCode.Combine(r, a);
}
// Prepared emission compares the actual resolved, generated motions, not a
// rounded geometry hash. Labels are deterministic encounter-order identifiers;
// legacy Apply/ApplySingle retain their existing cache and labels unchanged.
private static int RegisterPreparedCircleProgram(Program owner, Program generated)
{
CuttingPlanning.ExecutionMotionReader.ReadSupported(generated, Vector.Zero, null);
if (generated.Variables.Count != 0 || generated.SubPrograms.Count != 0)
throw new NotSupportedException("Unsupported generated circle program metadata.");
foreach (var code in generated.Codes)
if (code is not (RapidMove or LinearMove or ArcMove) || ((Motion)code).VariableRefs != null)
throw new NotSupportedException("Unsupported generated circle instruction.");
foreach (var pair in owner.SubPrograms)
if (SameGeneratedCircleProgram(pair.Value, generated))
return pair.Key;
var key = checked(owner.SubPrograms.Count + 1);
owner.SubPrograms.Add(key, generated);
return key;
}
private static bool SameGeneratedCircleProgram(Program a, Program b)
{
if (a.Mode != b.Mode || !SameBits(a.Rotation, b.Rotation) || a.Codes.Count != b.Codes.Count)
return false;
for (var i = 0; i < a.Codes.Count; i++)
{
var left = (Motion)a.Codes[i];
var right = (Motion)b.Codes[i];
if (left.GetType() != right.GetType() || left.Suppressed != right.Suppressed
|| left.UseExactStop != right.UseExactStop || left.Feedrate != right.Feedrate
|| !SameVector(left.EndPoint, right.EndPoint))
return false;
if (left is LinearMove line && line.Layer != ((LinearMove)right).Layer)
return false;
if (left is ArcMove arc && (arc.Layer != ((ArcMove)right).Layer
|| arc.Rotation != ((ArcMove)right).Rotation
|| !SameVector(arc.CenterPoint, ((ArcMove)right).CenterPoint)))
return false;
}
return true;
}
private static bool SameVector(Vector a, Vector b) => SameBits(a.X, b.X) && SameBits(a.Y, b.Y);
private static bool SameBits(double a, double b) => BitConverter.DoubleToInt64Bits(a) == BitConverter.DoubleToInt64Bits(b);
private void EmitContour(
Program program,
Shape shape,
Vector point,
Entity entity,
ContourType? forceType = null
ContourType? forceType = null,
bool exactCirclePrograms = false
)
{
var contourType = forceType ?? DetectContourType(shape);
@@ -366,7 +412,9 @@ namespace OpenNest.CNC.CuttingStrategy
subPgm.Mode = Mode.Incremental;
// Deduplicate: check if an identical sub-program already exists
var key = ComputeSubProgramKey(circle.Radius, normal);
var key = exactCirclePrograms
? RegisterPreparedCircleProgram(program, subPgm)
: ComputeSubProgramKey(circle.Radius, normal);
if (!program.SubPrograms.ContainsKey(key))
program.SubPrograms[key] = subPgm;
@@ -0,0 +1,72 @@
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
public class ExplicitCircleIdentityTests
{
[Fact]
public void Emit_DistinctNearRoundedNormalsPreserveActualEntry()
{
var source = LeadPathValidationTests.Rectangle(0, 0, 10, 10);
foreach (var x in new[] { 3, 7 })
{
source.MoveTo(x + 1, 3);
source.ArcTo(x + 1, 3, x, 3, RotationType.CCW);
}
var prepared = PreparedContours.Capture(source, new CuttingParameters());
var angle = 0.0000001;
var choices = new[] { prepared.Entry(0, 0, new Vector(4, 3)),
prepared.Entry(1, 0, new Vector(7 + System.Math.Cos(angle), 3 + System.Math.Sin(angle))),
prepared.ClosestEntry(2, Vector.Zero) };
var emitted = prepared.Emit(choices);
var circles = ExecutionMotionReader.Read(emitted, Vector.Zero, null, default).Motions
.Where(m => !m.Rapid && m.Curve.ToEntity() is Circle).ToArray();
Assert.Equal(choices[1].Point.Y, circles[1].Start!.Value.Y);
var calls = emitted.Codes.OfType<SubProgramCall>().ToArray();
Assert.NotSame(calls[0].Program, calls[1].Program);
Assert.Equal(new[] { 1, 2 }, calls.Select(c => c.Id));
}
[Theory]
[InlineData(1.0000004, false, false)]
[InlineData(1, true, false)]
[InlineData(1, false, true)]
public void Emit_SharesOnlyExactlyEquivalentCirclePrograms(double secondRadius, bool reverse, bool shared)
{
var source = LeadPathValidationTests.Rectangle(0, 0, 10, 10);
source.MoveTo(4, 3);
source.ArcTo(4, 3, 3, 3, RotationType.CCW);
source.MoveTo(7 + secondRadius, 3);
source.ArcTo(7 + secondRadius, 3, 7, 3, reverse ? RotationType.CW : RotationType.CCW);
var prepared = PreparedContours.Capture(source, new CuttingParameters());
var choices = Enumerable.Range(0, prepared.Count)
.Select(i => prepared.ClosestEntry(i, new Vector(20, 3))).ToArray();
var emitted = prepared.Emit(choices);
var calls = emitted.Codes.OfType<SubProgramCall>().ToArray();
Assert.Equal(2, calls.Length);
var cuts = ExecutionMotionReader.Read(emitted, Vector.Zero, null, default).Motions
.Where(m => !m.Rapid && m.Curve.ToEntity() is Circle).ToArray();
Assert.Equal(2, cuts.Length);
Assert.Equal(choices[0].Point, cuts[0].Start);
Assert.Equal(choices[1].Point, cuts[1].Start);
Assert.Equal(1, ((Circle)cuts[0].Curve.ToEntity()).Radius, 12);
Assert.Equal(secondRadius, ((Circle)cuts[1].Curve.ToEntity()).Radius, 12);
Assert.Equal(RotationType.CCW, Assert.Single(calls[0].Program.Codes.OfType<ArcMove>()).Rotation);
Assert.Equal(reverse ? RotationType.CW : RotationType.CCW,
Assert.Single(calls[1].Program.Codes.OfType<ArcMove>()).Rotation);
Assert.Equal(shared, ReferenceEquals(calls[0].Program, calls[1].Program));
Assert.Equal(shared, calls[0].Id == calls[1].Id);
Assert.Equal(new[] { 1, shared ? 1 : 2 }, calls.Select(c => c.Id));
Assert.All(calls, c => Assert.Same(c.Program, emitted.SubPrograms[c.Id]));
var fresh = prepared.Emit(choices);
var freshCalls = fresh.Codes.OfType<SubProgramCall>().ToArray();
Assert.Equal(calls.Select(c => c.Id), freshCalls.Select(c => c.Id));
Assert.All(freshCalls, c => Assert.DoesNotContain(calls, old => ReferenceEquals(old.Program, c.Program)));
calls[0].Program.Codes.Clear();
Assert.NotEmpty(freshCalls[0].Program.Codes);
Assert.Equal(ExplicitContourTests.Fingerprint(fresh), ExplicitContourTests.Fingerprint(prepared.Emit(choices)));
}
}