mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 09:02:10 -04:00
fix(cutting): preserve clean geometry through shared graph rotation
This commit is contained in:
1 parent
fef93cbfa5
commit
6f7652ee83
3 files changed
+103
-3
No files matched your search
@@ -61,7 +61,11 @@ public static class CuttingPlanService
|
||||
LeadMaterialSnapshot material = null;
|
||||
if (request.ConfirmedParameters != null)
|
||||
{
|
||||
var ownedClean = OwnedProgramCopy.Copy(source.BaseDrawing.Program, token);
|
||||
// Geometry-only transform: original graphs were strictly validated and
|
||||
// clone expansion bounded above. Legacy Rotate visits per parent, so
|
||||
// retain Clone's per-parent sharing here instead of restoring diamonds.
|
||||
// Exact placed/proposed payloads still use lossless OwnedProgramCopy.
|
||||
var ownedClean = (Program)source.BaseDrawing.Program.Clone();
|
||||
ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation);
|
||||
material = LeadMaterialSnapshot.Capture(ownedClean, source.Location, token);
|
||||
if (!material.IsComplete)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class CuttingCaptureRotationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0.3, false)]
|
||||
[InlineData(-0.3, false)]
|
||||
[InlineData(0, false)]
|
||||
[InlineData(0.3, true)]
|
||||
public void SharedDiamond_CaptureAndReadyPreserveOnceRotatedCleanGeometry(double angle, bool incrementalRoot)
|
||||
{
|
||||
var leaf = Rectangle(1, 2);
|
||||
leaf.Mode = Mode.Incremental;
|
||||
var left = new Program();
|
||||
left.Codes.Add(new SubProgramCall(leaf, 0));
|
||||
left.Mode = Mode.Incremental;
|
||||
var right = new Program();
|
||||
right.Codes.Add(new SubProgramCall(leaf, 0));
|
||||
right.Mode = Mode.Incremental;
|
||||
var clean = Rectangle(10, 10);
|
||||
clean.Codes.Add(new SubProgramCall(left, 0) { Offset = new Vector(3, 3) });
|
||||
clean.Codes.Add(new SubProgramCall(right, 0) { Offset = new Vector(7, 3) });
|
||||
if (incrementalRoot) clean.Mode = Mode.Incremental;
|
||||
var part = new Part(new Drawing("shared diamond", clean));
|
||||
part.Rotate(angle);
|
||||
part.Location = new Vector(11, 13);
|
||||
var before = ExplicitContourTests.Fingerprint(clean);
|
||||
var beforeLeaf = ExplicitContourTests.Fingerprint(leaf);
|
||||
var beforePlaced = ExplicitContourTests.Fingerprint(part.Program);
|
||||
var bounds = part.BoundingBox;
|
||||
var quantity = part.BaseDrawing.Quantity.Nested;
|
||||
var settings = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.1 },
|
||||
InternalLeadIn = new LineLeadIn { Length = 0.1 },
|
||||
ExternalLeadOut = new NoLeadOut(),
|
||||
InternalLeadOut = new NoLeadOut(),
|
||||
PierceClearance = 0.01
|
||||
};
|
||||
var original = ExecutionMotionReader.ReadSupported(part.Program, part.Location, null);
|
||||
var expectedCuts = original.Motions.Where(IsCut).ToArray();
|
||||
var once = new Vector(3, 5).Rotate(angle) + part.Location;
|
||||
Assert.Contains(expectedCuts, m => m.End.DistanceTo(once) < 1e-8);
|
||||
// Retain the review's independent per-parent clone/once-rotation control.
|
||||
var legacy = (Program)clean.Clone();
|
||||
legacy.Rotate(angle);
|
||||
var legacyCuts = ExecutionMotionReader.ReadSupported(legacy, part.Location, null).Motions.Where(IsCut).ToArray();
|
||||
Assert.Equal(expectedCuts.Length, legacyCuts.Length);
|
||||
Assert.All(Enumerable.Range(0, expectedCuts.Length), i =>
|
||||
Assert.True(expectedCuts[i].End.DistanceTo(legacyCuts[i].End) < 1e-8));
|
||||
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], new Vector(9, 13), confirmedParameters: settings));
|
||||
var captured = Assert.Single(snapshot.Placements);
|
||||
var nominal = captured.Material.Rings.SelectMany(r => r).ToArray();
|
||||
Assert.Equal(expectedCuts.Length, nominal.Length);
|
||||
Assert.All(Enumerable.Range(0, nominal.Length), i =>
|
||||
Assert.True(nominal[i].End.DistanceTo(expectedCuts[i].End) < 1e-8,
|
||||
$"Nominal curve {i} moved from {expectedCuts[i].End} to {nominal[i].End}."));
|
||||
var ready = CuttingPlanService.Plan(snapshot);
|
||||
Assert.True(ready.Status == CuttingPlanStatus.Ready, string.Join("; ", ready.Findings.Select(f => f.Message)));
|
||||
Assert.True(ready.IndependentlyReplayed);
|
||||
var emitted = ExecutionMotionReader.ReadSupported(Assert.Single(ready.ProposedOrder).CopyProgram(), part.Location, null)
|
||||
.Motions.Where(IsCut).ToArray();
|
||||
Assert.Equal(expectedCuts.Sum(m => m.Length), emitted.Sum(m => m.Length), 8);
|
||||
Assert.All(emitted, move => Assert.Contains(expectedCuts, expected => expected.Curve.SameSupport(move.Curve)
|
||||
&& expected.Curve.Contains(move.Curve.Start) && expected.Curve.Contains(move.End)));
|
||||
Assert.Equal(before, ExplicitContourTests.Fingerprint(clean));
|
||||
Assert.Equal(beforeLeaf, ExplicitContourTests.Fingerprint(leaf));
|
||||
Assert.Equal(beforePlaced, ExplicitContourTests.Fingerprint(part.Program));
|
||||
Assert.Same(leaf, ((SubProgramCall)left.Codes[0]).Program);
|
||||
Assert.Same(leaf, ((SubProgramCall)right.Codes[0]).Program);
|
||||
Assert.Same(bounds, part.BoundingBox);
|
||||
Assert.Equal(quantity, part.BaseDrawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
private static bool IsCut(ExecutionMotion motion) => !motion.Rapid && motion.Layer is LayerType.Cut or LayerType.Display;
|
||||
|
||||
private static Program Rectangle(double width, double height)
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0);
|
||||
program.LineTo(0, height);
|
||||
program.LineTo(width, height);
|
||||
program.LineTo(width, 0);
|
||||
program.LineTo(0, 0);
|
||||
return program;
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,10 @@ must not rotate shared programs through their property setters. No live drawings
|
||||
are attached to preview plates, so capture/search do not change quantity accounting.
|
||||
The snapshot is historical, not a freshness check against later edits. Original
|
||||
clean and executable graphs are type/mode-checked before cloning can erase unknown
|
||||
semantics. Owned copies preserve authored motion feed/exact-stop flags, symbolic
|
||||
bindings and shared subprogram identity; unsupported graphs are refused.
|
||||
semantics. Exact placed/proposed copies preserve authored motion feed/exact-stop
|
||||
flags, symbolic bindings and shared subprogram identity; unsupported graphs are
|
||||
refused. The geometry-only clean transform uses per-parent copies so legacy rotation
|
||||
does not visit a globally shared descendant twice; it never changes the fixed payload.
|
||||
|
||||
## Search and exact output
|
||||
|
||||
|
||||
Reference in new issue
Block a user