refactor(core): refresh Drawing's derived state through ProgramChanged

Area and the canonical angle were refreshed together only by the Program setter; an in-place program edit needed two separate calls, and UpdateArea kept a stale Area when no material was left. ProgramChanged is now the single entry point (the setter calls it), a program with no material or a null program gives Area 0, and UpdateArea/RecomputeCanonicalAngle are private.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Fable 5.2 committed 2026-11-12 19:20:22 -05:00
1 parent dec685361c
commit 986167a411
12 files changed
+125 -24

No files matched your search

@@ -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))
+26 -9
View File
@@ -79,17 +79,24 @@ namespace OpenNest
set
{
program = value;
UpdateArea();
RecomputeCanonicalAngle();
ProgramChanged();
}
}
/// <summary>
/// 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 <see cref="Program"/>: <see cref="Area"/> and the
/// canonical angle in <see cref="SourceInfo.Angle"/>. Assigning <see cref="Program"/>
/// calls it; callers that mutate the program in place (rather than reassigning it) must
/// call it afterwards.
/// </summary>
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 <see cref="CanonicalAngle.Compute"/>. A value of 0 means the drawing is
/// already canonical or <see cref="Drawing.IsCutOff"/> is true. Callers that mutate
/// <see cref="Drawing.Program"/> in place must invoke
/// <see cref="Drawing.RecomputeCanonicalAngle"/> to refresh.
/// <see cref="Drawing.ProgramChanged"/> to refresh.
/// </summary>
public double Angle { get; set; }
}
+1 -2
View File
@@ -336,8 +336,7 @@ namespace OpenNest.IO
&& ScribeLayerRepair.Apply(drawing.Program, marks, drawing.Source.Offset) > 0
)
{
drawing.UpdateArea();
drawing.RecomputeCanonicalAngle();
drawing.ProgramChanged();
}
}
@@ -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;
}
+1 -1
View File
@@ -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;
}
@@ -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));
@@ -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<LinearMove>())
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<LinearMove>())
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);
}
}
+2 -2
View File
@@ -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);
}
}
+1 -1
View File
@@ -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;
}
@@ -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);
-1
View File
@@ -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++;
+2 -3
View File
@@ -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
{