diff --git a/OpenNest.Core/Converters/DrawingMarkRecovery.cs b/OpenNest.Core/Converters/DrawingMarkRecovery.cs
index df92fc1a..bcb4c417 100644
--- a/OpenNest.Core/Converters/DrawingMarkRecovery.cs
+++ b/OpenNest.Core/Converters/DrawingMarkRecovery.cs
@@ -57,8 +57,7 @@ public static class DrawingMarkRecovery
return false;
AppendMarks(drawing.Program, marks);
- drawing.UpdateArea();
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
foreach (var part in placedParts.Where(part => ReferenceEquals(part.BaseDrawing, drawing)
&& !part.HasManualLeadIns))
diff --git a/OpenNest.Core/Drawing.cs b/OpenNest.Core/Drawing.cs
index de3fee1f..5280a8d3 100644
--- a/OpenNest.Core/Drawing.cs
+++ b/OpenNest.Core/Drawing.cs
@@ -79,17 +79,24 @@ namespace OpenNest
set
{
program = value;
- UpdateArea();
- RecomputeCanonicalAngle();
+ ProgramChanged();
}
}
///
- /// 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.
+ /// Refreshes everything derived from : and the
+ /// canonical angle in . Assigning
+ /// calls it; callers that mutate the program in place (rather than reassigning it) must
+ /// call it afterwards.
///
- public void RecomputeCanonicalAngle()
+ public void ProgramChanged()
+ {
+ UpdateArea();
+ RecomputeCanonicalAngle();
+ }
+
+ // Cut-off drawings are left with Angle=0.
+ private void RecomputeCanonicalAngle()
{
if (Source == null)
Source = new SourceInfo();
@@ -188,15 +195,25 @@ namespace OpenNest
public double Area { get; protected set; }
- public void UpdateArea()
+ private void UpdateArea()
{
+ if (program == null)
+ {
+ Area = 0;
+ return;
+ }
+
var geometry = ConvertProgram
- .ToGeometry(Program)
+ .ToGeometry(program)
.Where(entity => SpecialLayers.IsMaterial(entity.Layer));
var shapes = ShapeBuilder.GetShapes(geometry);
+ // No material left (e.g. only marks): a stale area would still count as material.
if (shapes.Count == 0)
+ {
+ Area = 0;
return;
+ }
var areas = new double[shapes.Count];
@@ -255,7 +272,7 @@ namespace OpenNest
/// setter via . A value of 0 means the drawing is
/// already canonical or is true. Callers that mutate
/// in place must invoke
- /// to refresh.
+ /// to refresh.
///
public double Angle { get; set; }
}
diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs
index e9d25b6d..e2710b7f 100644
--- a/OpenNest.IO/NestReader.cs
+++ b/OpenNest.IO/NestReader.cs
@@ -336,8 +336,7 @@ namespace OpenNest.IO
&& ScribeLayerRepair.Apply(drawing.Program, marks, drawing.Source.Offset) > 0
)
{
- drawing.UpdateArea();
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
}
}
diff --git a/OpenNest.Tests/BestFit/BestFitCacheStoreTests.cs b/OpenNest.Tests/BestFit/BestFitCacheStoreTests.cs
index f3ce506a..f853f9f8 100644
--- a/OpenNest.Tests/BestFit/BestFitCacheStoreTests.cs
+++ b/OpenNest.Tests/BestFit/BestFitCacheStoreTests.cs
@@ -194,7 +194,7 @@ public class BestFitCacheStoreTests
{
var drawing = MakeDrawing();
drawing.Program.Rotate(0.5, drawing.Program.BoundingBox().Center);
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
Assert.NotEqual(0.0, drawing.Source.Angle);
return drawing;
}
diff --git a/OpenNest.Tests/BestFit/BestFitCacheTests.cs b/OpenNest.Tests/BestFit/BestFitCacheTests.cs
index 8c8bc585..3c6c92c2 100644
--- a/OpenNest.Tests/BestFit/BestFitCacheTests.cs
+++ b/OpenNest.Tests/BestFit/BestFitCacheTests.cs
@@ -22,7 +22,7 @@ public class BestFitCacheTests
{
var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing();
drawing.Program.Rotate(Angle.ToRadians(30), drawing.Program.BoundingBox().Center);
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
return drawing;
}
diff --git a/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs b/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs
index c791c1a5..8871deff 100644
--- a/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs
+++ b/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs
@@ -35,7 +35,7 @@ public class BestFitResultFrameTests
{
var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing();
drawing.Program.Rotate(Angle.ToRadians(30), drawing.Program.BoundingBox().Center);
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
var result = EvaluateOffsetPair(canonical, new Vector(40, 30));
diff --git a/OpenNest.Tests/DrawingProgramChangedTests.cs b/OpenNest.Tests/DrawingProgramChangedTests.cs
new file mode 100644
index 00000000..4c5836d0
--- /dev/null
+++ b/OpenNest.Tests/DrawingProgramChangedTests.cs
@@ -0,0 +1,88 @@
+using System.Linq;
+using OpenNest.CNC;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests;
+
+public class DrawingProgramChangedTests
+{
+ private static Program Rectangle(double width, double height)
+ {
+ var program = new Program();
+ program.Codes.Add(new RapidMove(new Vector(0, 0)));
+ program.Codes.Add(new LinearMove(new Vector(width, 0)));
+ program.Codes.Add(new LinearMove(new Vector(width, height)));
+ program.Codes.Add(new LinearMove(new Vector(0, height)));
+ program.Codes.Add(new LinearMove(new Vector(0, 0)));
+ return program;
+ }
+
+ [Fact]
+ public void InPlaceEditIsStaleUntilProgramChangedRefreshesAreaAndAngle()
+ {
+ var drawing = new Drawing("r", Rectangle(100, 50));
+ Assert.Equal(5000, drawing.Area, 6);
+ Assert.Equal(0.0, drawing.Source.Angle, 6);
+
+ // Neither edit goes through the Program setter.
+ foreach (var move in drawing.Program.Codes.OfType())
+ move.EndPoint = new Vector(move.EndPoint.X * 2, move.EndPoint.Y);
+ drawing.Program.Rotate(0.5, drawing.Program.BoundingBox().Center);
+ Assert.Equal(5000, drawing.Area, 6);
+ Assert.Equal(0.0, drawing.Source.Angle, 6);
+
+ drawing.ProgramChanged();
+
+ Assert.Equal(10000, drawing.Area, 6);
+ Assert.InRange(drawing.Source.Angle, -0.52, -0.48);
+ }
+
+ [Fact]
+ public void ClearingTheMaterialZeroesTheArea()
+ {
+ var drawing = new Drawing("r", Rectangle(100, 50));
+
+ foreach (var move in drawing.Program.Codes.OfType())
+ move.Layer = LayerType.Scribe;
+ drawing.ProgramChanged();
+
+ Assert.Equal(0.0, drawing.Area);
+ }
+
+ [Fact]
+ public void AnEmptiedProgramZeroesTheArea()
+ {
+ var drawing = new Drawing("r", Rectangle(100, 50));
+
+ drawing.Program.Codes.Clear();
+ drawing.ProgramChanged();
+
+ Assert.Equal(0.0, drawing.Area);
+ }
+
+ [Fact]
+ public void ANullProgramZeroesAreaAndAngle()
+ {
+ var program = Rectangle(100, 50);
+ program.Rotate(0.5, program.BoundingBox().Center);
+ var drawing = new Drawing("r", program);
+ Assert.NotEqual(0.0, drawing.Source.Angle);
+
+ drawing.Program = null;
+
+ Assert.Equal(0.0, drawing.Area);
+ Assert.Equal(0.0, drawing.Source.Angle);
+ }
+
+ [Fact]
+ public void ACutOffDrawingKeepsAngleZero()
+ {
+ var drawing = new Drawing("cut", Rectangle(100, 50)) { IsCutOff = true };
+
+ drawing.Program.Rotate(0.5, drawing.Program.BoundingBox().Center);
+ drawing.ProgramChanged();
+
+ Assert.Equal(0.0, drawing.Source.Angle);
+ Assert.Equal(5000, drawing.Area, 6);
+ }
+}
diff --git a/OpenNest.Tests/Engine/CanonicalAngleTests.cs b/OpenNest.Tests/Engine/CanonicalAngleTests.cs
index 3d872f42..b834fb48 100644
--- a/OpenNest.Tests/Engine/CanonicalAngleTests.cs
+++ b/OpenNest.Tests/Engine/CanonicalAngleTests.cs
@@ -185,7 +185,7 @@ public class DrawingCanonicalAngleWiringTests
}
[Fact]
- public void RecomputeCanonicalAngle_UpdatesAfterMutation()
+ public void ProgramChanged_UpdatesAngleAfterMutation()
{
var d = new Drawing("r", RotatedRectProgram(100, 50, 0.0));
Assert.Equal(0.0, d.Source.Angle, precision: 6);
@@ -194,7 +194,7 @@ public class DrawingCanonicalAngleWiringTests
d.Program.Rotate(0.5, d.Program.BoundingBox().Center);
Assert.Equal(0.0, d.Source.Angle, precision: 6); // still stale
- d.RecomputeCanonicalAngle();
+ d.ProgramChanged();
Assert.InRange(d.Source.Angle, -0.52, -0.48);
}
}
diff --git a/OpenNest.Tests/IO/BestFitPersistenceTests.cs b/OpenNest.Tests/IO/BestFitPersistenceTests.cs
index 3c75a80c..52813b13 100644
--- a/OpenNest.Tests/IO/BestFitPersistenceTests.cs
+++ b/OpenNest.Tests/IO/BestFitPersistenceTests.cs
@@ -237,7 +237,7 @@ public class BestFitPersistenceTests
{
var drawing = MakeTShape();
drawing.Program.Rotate(Angle.ToRadians(30), drawing.Program.BoundingBox().Center);
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
return drawing;
}
diff --git a/OpenNest.Tests/Strategies/FillHelpersTests.cs b/OpenNest.Tests/Strategies/FillHelpersTests.cs
index e7c475fc..6cfb893d 100644
--- a/OpenNest.Tests/Strategies/FillHelpersTests.cs
+++ b/OpenNest.Tests/Strategies/FillHelpersTests.cs
@@ -34,7 +34,7 @@ public class FillHelpersTests
{
// Program.Rotate mutates in place; refresh the canonical angle the same way
// a rotated CAD import carries it, or AsCanonicalCopy takes the zero-angle path.
- drawing.RecomputeCanonicalAngle();
+ drawing.ProgramChanged();
canonicalAngle = drawing.Source.Angle;
Assert.False(OpenNest.Math.Tolerance.IsEqualTo(canonicalAngle, 0));
drawing = CanonicalFrame.AsCanonicalCopy(drawing);
diff --git a/OpenNest.Training/Program.cs b/OpenNest.Training/Program.cs
index 6345a830..2e93dae8 100644
--- a/OpenNest.Training/Program.cs
+++ b/OpenNest.Training/Program.cs
@@ -167,7 +167,6 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
continue;
}
- drawing.UpdateArea();
drawing.Color = PartColors[colorIndex % PartColors.Length];
colorIndex++;
diff --git a/OpenNest.Training/TrainingDatabase.cs b/OpenNest.Training/TrainingDatabase.cs
index a8b7accf..3bd56adf 100644
--- a/OpenNest.Training/TrainingDatabase.cs
+++ b/OpenNest.Training/TrainingDatabase.cs
@@ -4,10 +4,10 @@ using System.IO;
using System.Linq;
using System.Text;
using Microsoft.EntityFrameworkCore;
+using OpenNest.Engine;
using OpenNest.Engine.ML;
using OpenNest.IO;
using OpenNest.Training.Data;
-using OpenNest.Engine;
namespace OpenNest.Training
{
@@ -137,7 +137,6 @@ namespace OpenNest.Training
var program = programReader.Read();
var drawing = new Drawing("backfill") { Program = program };
- drawing.UpdateArea();
var features = FeatureExtractor.Extract(drawing);
if (features == null)
@@ -175,7 +174,7 @@ namespace OpenNest.Training
{
try
{
- _db.Database.ExecuteSqlRaw($"ALTER TABLE Runs ADD COLUMN {name} {type}");
+ _db.Database.ExecuteSql($"ALTER TABLE Runs ADD COLUMN {name} {type}");
}
catch
{