From 14b7c1cf32935ba9a407171643b310e8f7a01170 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 20 Apr 2026 09:13:37 -0400 Subject: [PATCH] feat(core): store Source.Angle; recompute when Program changes --- OpenNest.Core/Drawing.cs | 22 +++++++- OpenNest.Tests/Engine/CanonicalAngleTests.cs | 57 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/OpenNest.Core/Drawing.cs b/OpenNest.Core/Drawing.cs index 080569f..d266f7c 100644 --- a/OpenNest.Core/Drawing.cs +++ b/OpenNest.Core/Drawing.cs @@ -54,9 +54,9 @@ namespace OpenNest Id = Interlocked.Increment(ref nextId); Name = name; Material = new Material(); - Program = pgm; Constraints = new NestConstraints(); Source = new SourceInfo(); + Program = pgm; } public int Id { get; } @@ -78,9 +78,29 @@ namespace OpenNest { program = value; UpdateArea(); + RecomputeCanonicalAngle(); } } + /// + /// Recomputes and stores the canonical angle from the current Program. + /// Callers that mutate Program in place (rather than reassigning it) must invoke this explicitly. + /// Cut-off drawings are left with Angle=0. + /// + public void RecomputeCanonicalAngle() + { + if (Source == null) + Source = new SourceInfo(); + + if (program == null || IsCutOff) + { + Source.Angle = 0.0; + return; + } + + Source.Angle = CanonicalAngle.Compute(this); + } + public Color Color { get; set; } public bool IsCutOff { get; set; } diff --git a/OpenNest.Tests/Engine/CanonicalAngleTests.cs b/OpenNest.Tests/Engine/CanonicalAngleTests.cs index 3b23e98..d570c54 100644 --- a/OpenNest.Tests/Engine/CanonicalAngleTests.cs +++ b/OpenNest.Tests/Engine/CanonicalAngleTests.cs @@ -97,3 +97,60 @@ public class CanonicalAngleTests Assert.Equal(0.0, CanonicalAngle.Compute(d), precision: 6); } } + +public class DrawingCanonicalAngleWiringTests +{ + private static OpenNest.CNC.Program RotatedRectProgram(double w, double h, double theta) + { + 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 (!OpenNest.Math.Tolerance.IsEqualTo(theta, 0)) + pgm.Rotate(theta, pgm.BoundingBox().Center); + return pgm; + } + + [Fact] + public void Constructor_ComputesAngleOnProgramAssignment() + { + var pgm = RotatedRectProgram(100, 50, 0.5); + var d = new Drawing("r", pgm); + Assert.InRange(d.Source.Angle, -0.52, -0.48); + } + + [Fact] + public void SetProgram_RecomputesAngle() + { + var d = new Drawing("r", RotatedRectProgram(100, 50, 0.0)); + Assert.Equal(0.0, d.Source.Angle, precision: 6); + + d.Program = RotatedRectProgram(100, 50, 0.5); + Assert.InRange(d.Source.Angle, -0.52, -0.48); + } + + [Fact] + public void IsCutOff_SkipsAngleComputation() + { + var d = new Drawing("cut", RotatedRectProgram(100, 50, 0.5)) { IsCutOff = true }; + // Re-assign after flag is set so the setter observes IsCutOff. + d.Program = RotatedRectProgram(100, 50, 0.5); + Assert.Equal(0.0, d.Source.Angle, precision: 6); + } + + [Fact] + public void RecomputeCanonicalAngle_UpdatesAfterMutation() + { + var d = new Drawing("r", RotatedRectProgram(100, 50, 0.0)); + Assert.Equal(0.0, d.Source.Angle, precision: 6); + + // Mutate in-place (doesn't trigger setter). + d.Program.Rotate(0.5, d.Program.BoundingBox().Center); + Assert.Equal(0.0, d.Source.Angle, precision: 6); // still stale + + d.RecomputeCanonicalAngle(); + Assert.InRange(d.Source.Angle, -0.52, -0.48); + } +}