Part.Rotation is cumulative, so rebinding canonical parts with CreateAtOrigin(original, p.Rotation) double-counted the drawing's own rotation, and FromCanonical rotated each part about its Location, which moved it off its slot and out of the work area. Add CanonicalFrame.RebindToOriginal (rotation = part - original program rotation, footprint aligned to the canonical part) and use it in the three places that duplicated the old logic. The MBR only fixes the frame modulo 90 degrees and nest results are not 90-degree symmetric (an L gave 56/43/42/42 parts by orientation). CanonicalAngle.Compute now picks one of the four orientations from the centroid offset; symmetric shapes keep the MBR orientation. Fixes the three NestInvarianceTests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
4.6 KiB
C#
116 lines
4.6 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Tests.Engine;
|
|
|
|
public class CanonicalFrameTests
|
|
{
|
|
private static Drawing MakeRect(double w, double h, double rotation)
|
|
{
|
|
var pgm = new OpenNest.CNC.Program();
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
|
if (!Tolerance.IsEqualTo(rotation, 0))
|
|
pgm.Rotate(rotation, pgm.BoundingBox().Center);
|
|
return new Drawing("rect", pgm) { Source = new SourceInfo { Angle = -rotation } };
|
|
}
|
|
|
|
[Fact]
|
|
public void AsCanonicalCopy_AxisAlignsMbr()
|
|
{
|
|
var d = MakeRect(100, 50, 0.6);
|
|
var canonical = CanonicalFrame.AsCanonicalCopy(d);
|
|
|
|
var bb = canonical.Program.BoundingBox();
|
|
var longer = System.Math.Max(bb.Length, bb.Width);
|
|
var shorter = System.Math.Min(bb.Length, bb.Width);
|
|
Assert.InRange(longer, 100 - 0.1, 100 + 0.1);
|
|
Assert.InRange(shorter, 50 - 0.1, 50 + 0.1);
|
|
Assert.Equal(0.0, canonical.Source.Angle, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void AsCanonicalCopy_DoesNotMutateSource()
|
|
{
|
|
var d = MakeRect(100, 50, 0.6);
|
|
var originalBbox = d.Program.BoundingBox();
|
|
var originalAngle = d.Source.Angle;
|
|
|
|
CanonicalFrame.AsCanonicalCopy(d);
|
|
|
|
var afterBbox = d.Program.BoundingBox();
|
|
Assert.Equal(originalBbox.Width, afterBbox.Width, precision: 6);
|
|
Assert.Equal(originalBbox.Length, afterBbox.Length, precision: 6);
|
|
Assert.Equal(originalAngle, d.Source.Angle, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromCanonical_ComposesSourceAngleOntoRotation()
|
|
{
|
|
var d = MakeRect(100, 50, 0.0);
|
|
var part = new Part(d);
|
|
part.Rotate(0.2); // engine returned a canonical-frame part at R = 0.2
|
|
|
|
var placed = CanonicalFrame.FromCanonical(new List<Part> { part }, sourceAngle: -0.5);
|
|
|
|
// R' = R + sourceAngle = 0.2 + (-0.5) = -0.3
|
|
// Part.Rotation comes from Program.Rotation which is normalized to [0, 2PI),
|
|
// so compare after normalizing the expected value as well.
|
|
Assert.Single(placed);
|
|
Assert.Equal(Angle.NormalizeRad(-0.3), placed[0].Rotation, precision: 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void RoundTrip_RestoresGeometry()
|
|
{
|
|
var d = MakeRect(100, 50, 0.4);
|
|
var canonical = CanonicalFrame.AsCanonicalCopy(d);
|
|
|
|
// Place a part at origin in the canonical frame.
|
|
var part = Part.CreateAtOrigin(canonical);
|
|
var canonicalBbox = part.BoundingBox;
|
|
|
|
var placed = CanonicalFrame.FromCanonical(new List<Part> { part }, d.Source.Angle);
|
|
|
|
var originalBbox = d.Program.BoundingBox();
|
|
Assert.Equal(originalBbox.Width, placed[0].BoundingBox.Width, precision: 2);
|
|
Assert.Equal(originalBbox.Length, placed[0].BoundingBox.Length, precision: 2);
|
|
}
|
|
|
|
[Fact]
|
|
public void RebindToOriginal_PreservesWorldFootprint_ForRotatedImport()
|
|
{
|
|
// An L-shape imported at an angle: the canonical copy is rotated, so a canonical part and
|
|
// the rebound part must cover exactly the same footprint even though their programs differ.
|
|
var pgm = new OpenNest.CNC.Program();
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(100, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(100, 20)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(50, 20)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(50, 50)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 50)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
|
pgm.Rotate(0.8, pgm.BoundingBox().Center);
|
|
var original = new Drawing("L", pgm);
|
|
var canonical = CanonicalFrame.AsCanonicalCopy(original);
|
|
|
|
var placed = Part.CreateAtOrigin(canonical, 0.35);
|
|
placed.Offset(new Vector(40, 25));
|
|
placed.UpdateBounds();
|
|
var expected = placed.BoundingBox;
|
|
|
|
var rebound = CanonicalFrame.RebindToOriginal(new List<Part> { placed }, original);
|
|
|
|
Assert.Same(original, rebound[0].BaseDrawing);
|
|
Assert.Equal(expected.Left, rebound[0].BoundingBox.Left, precision: 6);
|
|
Assert.Equal(expected.Bottom, rebound[0].BoundingBox.Bottom, precision: 6);
|
|
Assert.Equal(expected.Right, rebound[0].BoundingBox.Right, precision: 6);
|
|
Assert.Equal(expected.Top, rebound[0].BoundingBox.Top, precision: 6);
|
|
}
|
|
}
|