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 {