mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 10:02:12 -04:00
fix(cutting): share only exact prepared circle programs
This commit is contained in:
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)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user