diff --git a/OpenNest.Core/Drawing.cs b/OpenNest.Core/Drawing.cs index 8a2e61e..080569f 100644 --- a/OpenNest.Core/Drawing.cs +++ b/OpenNest.Core/Drawing.cs @@ -163,5 +163,14 @@ namespace OpenNest /// Offset distances to the original location. /// public Vector Offset { get; set; } + + /// + /// Rotation (radians) that maps the source program geometry to its canonical + /// (MBR-axis-aligned) frame. A value of 0 means the drawing is already canonical. + /// Task 3 will wire setter to populate this via + /// ; for now it is assigned manually by + /// engine helpers (see OpenNest.Engine.CanonicalFrame). + /// + public double Angle { get; set; } } } diff --git a/OpenNest.Engine/CanonicalFrame.cs b/OpenNest.Engine/CanonicalFrame.cs new file mode 100644 index 0000000..8911d26 --- /dev/null +++ b/OpenNest.Engine/CanonicalFrame.cs @@ -0,0 +1,76 @@ +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.Math; +using System.Collections.Generic; + +namespace OpenNest.Engine +{ + /// + /// Produces transient canonical (MBR-axis-aligned) copies of drawings for engine consumption + /// and un-rotates placed parts back to the drawing's original frame. + /// + public static class CanonicalFrame + { + /// + /// Returns a new Drawing whose Program geometry is rotated to the canonical frame. + /// The source drawing is not mutated. + /// + public static Drawing AsCanonicalCopy(Drawing drawing) + { + if (drawing == null) + return null; + + var angle = drawing.Source?.Angle ?? 0.0; + + // Clone program (never mutate the source). + var pgm = (drawing.Program.Clone() as OpenNest.CNC.Program) + ?? new OpenNest.CNC.Program(); + + if (!Tolerance.IsEqualTo(angle, 0)) + pgm.Rotate(angle, pgm.BoundingBox().Center); + + var copy = new Drawing(drawing.Name ?? string.Empty, pgm) + { + Color = drawing.Color, + Constraints = drawing.Constraints, + Material = drawing.Material, + Priority = drawing.Priority, + Customer = drawing.Customer, + IsCutOff = drawing.IsCutOff, + Source = new SourceInfo + { + Path = drawing.Source?.Path, + Offset = drawing.Source?.Offset ?? new Vector(0, 0), + Angle = 0.0, + }, + }; + return copy; + } + + /// + /// Composes the source drawing's canonical angle onto each placed part so the + /// returned list is in the drawing's original (visible) frame. + /// + /// Derivation: let sourceAngle = S (rotation mapping source -> canonical). + /// Canonical part at rotation R shows visible orientation R. + /// Source part at rotation R' shows visible orientation R' + (-S), because the + /// source geometry is already rotated by -S relative to canonical. + /// Setting equal gives R' = R + S, so we ADD sourceAngle to each placed part. + /// + /// Rotation is performed around the part's Location so its placement position is preserved; + /// only the orientation composes. + /// + public static List FromCanonical(List placed, double sourceAngle) + { + if (placed == null || placed.Count == 0) + return placed; + if (Tolerance.IsEqualTo(sourceAngle, 0)) + return placed; + + foreach (var p in placed) + p.Rotate(sourceAngle, p.Location); + + return placed; + } + } +} diff --git a/OpenNest.Tests/Engine/CanonicalFrameTests.cs b/OpenNest.Tests/Engine/CanonicalFrameTests.cs new file mode 100644 index 0000000..9facb0e --- /dev/null +++ b/OpenNest.Tests/Engine/CanonicalFrameTests.cs @@ -0,0 +1,84 @@ +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 }, 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 }, 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); + } +}