diff --git a/CLAUDE.md b/CLAUDE.md index bf99449..9ffef3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,16 +37,15 @@ Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the - **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. - **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency. -- **BestFit/**: NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups. -- **RectanglePacking/**: `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions. -- **CirclePacking/**: Alternative packing for circular parts. -- **ML/**: `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data). -- `FillLinear`: Grid-based fill with directional sliding. -- `Compactor`: Post-fill gravity compaction — pushes parts toward a plate edge to close gaps. -- `FillScore`: Lexicographic comparison struct for fill results (count > utilization > compactness). +- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`. +- **Strategies/** (`namespace OpenNest.Engine.Strategies`): Pluggable fill strategy layer — `IFillStrategy` interface, `FillContext`, `FillStrategyRegistry` (auto-discovers strategies via reflection, supports plugin DLLs), `FillHelpers`. Built-in strategies: `LinearFillStrategy`, `PairsFillStrategy`, `RectBestFitStrategy`, `ExtentsFillStrategy`. +- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups. +- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions. +- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts. +- **Nfp/** (`namespace OpenNest.Engine.Nfp`): NFP-based nesting (not yet integrated) — `AutoNester` (mixed-part nesting with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`NestResult`. +- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data). - `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints. - `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback. -- `RotationAnalysis`: Analyzes part geometry to determine valid rotation angles. ### OpenNest.IO (class library, depends on Core) File I/O and format conversion. Uses ACadSharp for DXF/DWG support. @@ -99,9 +98,14 @@ Always use Roslyn Bridge MCP tools (`mcp__RoslynBridge__*`) as the primary metho - Always use `var` instead of explicit types (e.g., `var parts = new List();` not `List parts = new List();`). +## Documentation Maintenance + +Always keep `README.md` and `CLAUDE.md` up to date when making changes that affect project structure, architecture, build instructions, dependencies, or key patterns. If you add a new project, change a namespace, modify the build process, or alter significant behavior, update both files as part of the same change. + ## Key Patterns - OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`. +- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Nfp` (NFP-based nesting, not yet integrated), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`. - `ObservableList` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings. - Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion). - `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations. diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index a7cbf7b..dd89e29 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -1,13 +1,13 @@ +using OpenNest; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.IO; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; -using OpenNest; -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.IO; return NestConsole.Run(args); diff --git a/OpenNest.Core/Align.cs b/OpenNest.Core/Align.cs index 3de4469..40f36e4 100644 --- a/OpenNest.Core/Align.cs +++ b/OpenNest.Core/Align.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using OpenNest.Geometry; +using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest { diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs index 30455be..8812b49 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs b/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs index 933db14..682936a 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs @@ -1,4 +1,3 @@ -using OpenNest.CNC; using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs index e76c9ce..67af1b9 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs index d30f0e6..b0750ff 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs index 83be504..bc9eff4 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs index a816593..2fac024 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs index e7922cf..c20e8d9 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs index f5700cb..d646694 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs index 7100487..a751727 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs index 95d8724..ed47977 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs index 6915c5f..804427b 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs index c72847b..954858a 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/MicrotabLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/MicrotabLeadOut.cs index 13dc799..1b75e6f 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/MicrotabLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/MicrotabLeadOut.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs index 8a45cc8..0e3b53f 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs index 3921491..bb306a6 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs index b6e5fbd..10d2954 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs index de3e16c..908746a 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs index 504eec5..87a8d85 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { diff --git a/OpenNest.Core/CNC/Program.cs b/OpenNest.Core/CNC/Program.cs index 6f61c1b..43d6237 100644 --- a/OpenNest.Core/CNC/Program.cs +++ b/OpenNest.Core/CNC/Program.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.CNC { diff --git a/OpenNest.Core/Converters/ConvertGeometry.cs b/OpenNest.Core/Converters/ConvertGeometry.cs index 6c95e56..8c89945 100644 --- a/OpenNest.Core/Converters/ConvertGeometry.cs +++ b/OpenNest.Core/Converters/ConvertGeometry.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; -using OpenNest; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Converters { diff --git a/OpenNest.Core/Converters/ConvertProgram.cs b/OpenNest.Core/Converters/ConvertProgram.cs index 97efe77..f7dff54 100644 --- a/OpenNest.Core/Converters/ConvertProgram.cs +++ b/OpenNest.Core/Converters/ConvertProgram.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using OpenNest; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.Converters { diff --git a/OpenNest.Core/Drawing.cs b/OpenNest.Core/Drawing.cs index 2adc038..a55cd80 100644 --- a/OpenNest.Core/Drawing.cs +++ b/OpenNest.Core/Drawing.cs @@ -1,9 +1,9 @@ -using System.Drawing; -using System.Linq; -using System.Threading; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; +using System.Drawing; +using System.Linq; +using System.Threading; namespace OpenNest { diff --git a/OpenNest.Core/DwgQty.cs b/OpenNest.Core/DwgQty.cs index 6d4b60f..6fa3e07 100644 --- a/OpenNest.Core/DwgQty.cs +++ b/OpenNest.Core/DwgQty.cs @@ -8,10 +8,10 @@ public int Remaining { - get + get { var x = Required - Nested; - return x < 0 ? 0: x; + return x < 0 ? 0 : x; } } } diff --git a/OpenNest.Core/Geometry/Arc.cs b/OpenNest.Core/Geometry/Arc.cs index bbd2d3f..002cd09 100644 --- a/OpenNest.Core/Geometry/Arc.cs +++ b/OpenNest.Core/Geometry/Arc.cs @@ -1,6 +1,6 @@ -using System; +using OpenNest.Math; +using System; using System.Collections.Generic; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/Circle.cs b/OpenNest.Core/Geometry/Circle.cs index 82a0b3e..dd0148f 100644 --- a/OpenNest.Core/Geometry/Circle.cs +++ b/OpenNest.Core/Geometry/Circle.cs @@ -1,6 +1,5 @@ -using System; +using OpenNest.Math; using System.Collections.Generic; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/Entity.cs b/OpenNest.Core/Geometry/Entity.cs index 2f055d6..d32702d 100644 --- a/OpenNest.Core/Geometry/Entity.cs +++ b/OpenNest.Core/Geometry/Entity.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using OpenNest.Math; +using System.Collections.Generic; using System.Drawing; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/GeometryOptimizer.cs b/OpenNest.Core/Geometry/GeometryOptimizer.cs index db9eedd..dfe5b9a 100644 --- a/OpenNest.Core/Geometry/GeometryOptimizer.cs +++ b/OpenNest.Core/Geometry/GeometryOptimizer.cs @@ -1,7 +1,7 @@ +using OpenNest.Math; using System; using System.Collections.Generic; using System.Threading.Tasks; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/Intersect.cs b/OpenNest.Core/Geometry/Intersect.cs index 8af27e1..bdbcb1c 100644 --- a/OpenNest.Core/Geometry/Intersect.cs +++ b/OpenNest.Core/Geometry/Intersect.cs @@ -1,6 +1,6 @@ +using OpenNest.Math; using System.Collections.Generic; using System.Linq; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/Line.cs b/OpenNest.Core/Geometry/Line.cs index d4c5cc2..696ee97 100644 --- a/OpenNest.Core/Geometry/Line.cs +++ b/OpenNest.Core/Geometry/Line.cs @@ -1,6 +1,6 @@ -using System; +using OpenNest.Math; +using System; using System.Collections.Generic; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/NoFitPolygon.cs b/OpenNest.Core/Geometry/NoFitPolygon.cs index c4effa0..3e89222 100644 --- a/OpenNest.Core/Geometry/NoFitPolygon.cs +++ b/OpenNest.Core/Geometry/NoFitPolygon.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; -using System.Linq; using Clipper2Lib; +using System.Collections.Generic; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/PolyLabel.cs b/OpenNest.Core/Geometry/PolyLabel.cs index 9f3af4b..e6b2c9d 100644 --- a/OpenNest.Core/Geometry/PolyLabel.cs +++ b/OpenNest.Core/Geometry/PolyLabel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace OpenNest.Geometry diff --git a/OpenNest.Core/Geometry/Polygon.cs b/OpenNest.Core/Geometry/Polygon.cs index 2f09e65..4ff2a37 100644 --- a/OpenNest.Core/Geometry/Polygon.cs +++ b/OpenNest.Core/Geometry/Polygon.cs @@ -1,7 +1,7 @@ -using System; +using OpenNest.Math; +using System; using System.Collections.Generic; using System.Linq; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/RotatingCalipers.cs b/OpenNest.Core/Geometry/RotatingCalipers.cs index 15742fd..cc7ff08 100644 --- a/OpenNest.Core/Geometry/RotatingCalipers.cs +++ b/OpenNest.Core/Geometry/RotatingCalipers.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/ShapeBuilder.cs b/OpenNest.Core/Geometry/ShapeBuilder.cs index 8ae4cae..a4311f8 100644 --- a/OpenNest.Core/Geometry/ShapeBuilder.cs +++ b/OpenNest.Core/Geometry/ShapeBuilder.cs @@ -1,6 +1,6 @@ +using OpenNest.Math; using System.Collections.Generic; using System.Diagnostics; -using OpenNest.Math; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Geometry/Size.cs b/OpenNest.Core/Geometry/Size.cs index e5aead9..ca225e7 100644 --- a/OpenNest.Core/Geometry/Size.cs +++ b/OpenNest.Core/Geometry/Size.cs @@ -43,7 +43,7 @@ namespace OpenNest.Geometry } public override string ToString() => $"{Width} x {Length}"; - + public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}"; } } diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs index 39a226b..e34d345 100644 --- a/OpenNest.Core/Geometry/SpatialQuery.cs +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -1,7 +1,6 @@ -using System; +using OpenNest.Math; using System.Collections.Generic; using System.Linq; -using OpenNest.Math; namespace OpenNest.Geometry { @@ -30,41 +29,41 @@ namespace OpenNest.Geometry { case PushDirection.Left: case PushDirection.Right: - { - var dy = p2y - p1y; - if (System.Math.Abs(dy) < Tolerance.Epsilon) + { + var dy = p2y - p1y; + if (System.Math.Abs(dy) < Tolerance.Epsilon) + return double.MaxValue; + + var t = (vy - p1y) / dy; + if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + var ix = p1x + t * (p2x - p1x); + var dist = direction == PushDirection.Left ? vx - ix : ix - vx; + + if (dist > Tolerance.Epsilon) return dist; + if (dist >= -Tolerance.Epsilon) return 0; return double.MaxValue; - - var t = (vy - p1y) / dy; - if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) - return double.MaxValue; - - var ix = p1x + t * (p2x - p1x); - var dist = direction == PushDirection.Left ? vx - ix : ix - vx; - - if (dist > Tolerance.Epsilon) return dist; - if (dist >= -Tolerance.Epsilon) return 0; - return double.MaxValue; - } + } case PushDirection.Down: case PushDirection.Up: - { - var dx = p2x - p1x; - if (System.Math.Abs(dx) < Tolerance.Epsilon) + { + var dx = p2x - p1x; + if (System.Math.Abs(dx) < Tolerance.Epsilon) + return double.MaxValue; + + var t = (vx - p1x) / dx; + if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + var iy = p1y + t * (p2y - p1y); + var dist = direction == PushDirection.Down ? vy - iy : iy - vy; + + if (dist > Tolerance.Epsilon) return dist; + if (dist >= -Tolerance.Epsilon) return 0; return double.MaxValue; - - var t = (vx - p1x) / dx; - if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) - return double.MaxValue; - - var iy = p1y + t * (p2y - p1y); - var dist = direction == PushDirection.Down ? vy - iy : iy - vy; - - if (dist > Tolerance.Epsilon) return dist; - if (dist >= -Tolerance.Epsilon) return 0; - return double.MaxValue; - } + } default: return double.MaxValue; @@ -363,10 +362,10 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return box.Left - boundary.Left; + case PushDirection.Left: return box.Left - boundary.Left; case PushDirection.Right: return boundary.Right - box.Right; - case PushDirection.Up: return boundary.Top - box.Top; - case PushDirection.Down: return box.Bottom - boundary.Bottom; + case PushDirection.Up: return boundary.Top - box.Top; + case PushDirection.Down: return box.Bottom - boundary.Bottom; default: return double.MaxValue; } } @@ -375,10 +374,10 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return new Vector(-distance, 0); + case PushDirection.Left: return new Vector(-distance, 0); case PushDirection.Right: return new Vector(distance, 0); - case PushDirection.Up: return new Vector(0, distance); - case PushDirection.Down: return new Vector(0, -distance); + case PushDirection.Up: return new Vector(0, distance); + case PushDirection.Down: return new Vector(0, -distance); default: return new Vector(); } } @@ -387,10 +386,10 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return from.Left - to.Right; + case PushDirection.Left: return from.Left - to.Right; case PushDirection.Right: return to.Left - from.Right; - case PushDirection.Up: return to.Bottom - from.Top; - case PushDirection.Down: return from.Bottom - to.Top; + case PushDirection.Up: return to.Bottom - from.Top; + case PushDirection.Down: return from.Bottom - to.Top; default: return double.MaxValue; } } diff --git a/OpenNest.Core/Geometry/Vector.cs b/OpenNest.Core/Geometry/Vector.cs index 22eccda..ebae254 100644 --- a/OpenNest.Core/Geometry/Vector.cs +++ b/OpenNest.Core/Geometry/Vector.cs @@ -1,5 +1,5 @@ -using System; -using OpenNest.Math; +using OpenNest.Math; +using System; namespace OpenNest.Geometry { diff --git a/OpenNest.Core/Math/Angle.cs b/OpenNest.Core/Math/Angle.cs index d052660..7b2aaea 100644 --- a/OpenNest.Core/Math/Angle.cs +++ b/OpenNest.Core/Math/Angle.cs @@ -1,6 +1,4 @@ -using System; - -namespace OpenNest.Math +namespace OpenNest.Math { public static class Angle { diff --git a/OpenNest.Core/Math/Tolerance.cs b/OpenNest.Core/Math/Tolerance.cs index 105b529..4a5d150 100644 --- a/OpenNest.Core/Math/Tolerance.cs +++ b/OpenNest.Core/Math/Tolerance.cs @@ -1,6 +1,4 @@ -using System; - -namespace OpenNest.Math +namespace OpenNest.Math { public static class Tolerance { diff --git a/OpenNest.Core/Math/Trigonometry.cs b/OpenNest.Core/Math/Trigonometry.cs index 4349f5f..117a8ae 100644 --- a/OpenNest.Core/Math/Trigonometry.cs +++ b/OpenNest.Core/Math/Trigonometry.cs @@ -1,6 +1,4 @@ -using System; - -namespace OpenNest.Math +namespace OpenNest.Math { public static class Trigonometry { diff --git a/OpenNest.Core/Nest.cs b/OpenNest.Core/Nest.cs index 00a61b7..7169100 100644 --- a/OpenNest.Core/Nest.cs +++ b/OpenNest.Core/Nest.cs @@ -1,6 +1,6 @@ -using System; -using OpenNest.Collections; +using OpenNest.Collections; using OpenNest.Geometry; +using System; namespace OpenNest { diff --git a/OpenNest.Core/NestConstraints.cs b/OpenNest.Core/NestConstraints.cs index 89827ed..86ccca5 100644 --- a/OpenNest.Core/NestConstraints.cs +++ b/OpenNest.Core/NestConstraints.cs @@ -1,5 +1,4 @@ -using System; -using OpenNest.Math; +using OpenNest.Math; namespace OpenNest { diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs index 37b12f0..e0c3b0d 100644 --- a/OpenNest.Core/Part.cs +++ b/OpenNest.Core/Part.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; -using System.Linq; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; +using System.Collections.Generic; +using System.Linq; namespace OpenNest { diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs index 3b9234f..6ff64ab 100644 --- a/OpenNest.Core/PartGeometry.cs +++ b/OpenNest.Core/PartGeometry.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; -using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; +using System.Collections.Generic; +using System.Linq; namespace OpenNest { @@ -174,10 +174,10 @@ namespace OpenNest switch (facingDirection) { - case PushDirection.Left: keep = -sign * dy > 0; break; - case PushDirection.Right: keep = sign * dy > 0; break; - case PushDirection.Up: keep = -sign * dx > 0; break; - case PushDirection.Down: keep = sign * dx > 0; break; + case PushDirection.Left: keep = -sign * dy > 0; break; + case PushDirection.Right: keep = sign * dy > 0; break; + case PushDirection.Up: keep = -sign * dx > 0; break; + case PushDirection.Down: keep = sign * dx > 0; break; default: keep = true; break; } diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index 87c257a..6186cd1 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using OpenNest.Collections; +using OpenNest.Collections; using OpenNest.Geometry; using OpenNest.Math; +using System; +using System.Collections.Generic; +using System.Linq; namespace OpenNest { diff --git a/OpenNest.Core/Sequence.cs b/OpenNest.Core/Sequence.cs index 250bd76..f45e5d6 100644 --- a/OpenNest.Core/Sequence.cs +++ b/OpenNest.Core/Sequence.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using OpenNest.Geometry; +using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest { diff --git a/OpenNest.Core/Shapes/CircleShape.cs b/OpenNest.Core/Shapes/CircleShape.cs index 6288cef..1b3ac64 100644 --- a/OpenNest.Core/Shapes/CircleShape.cs +++ b/OpenNest.Core/Shapes/CircleShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/FlangeShape.cs b/OpenNest.Core/Shapes/FlangeShape.cs index e9183d7..b08d87f 100644 --- a/OpenNest.Core/Shapes/FlangeShape.cs +++ b/OpenNest.Core/Shapes/FlangeShape.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs b/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs index 125127f..c4d65f3 100644 --- a/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs +++ b/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/LShape.cs b/OpenNest.Core/Shapes/LShape.cs index 3712aa2..701e54f 100644 --- a/OpenNest.Core/Shapes/LShape.cs +++ b/OpenNest.Core/Shapes/LShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/OctagonShape.cs b/OpenNest.Core/Shapes/OctagonShape.cs index 66a99ed..c73f392 100644 --- a/OpenNest.Core/Shapes/OctagonShape.cs +++ b/OpenNest.Core/Shapes/OctagonShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/RectangleShape.cs b/OpenNest.Core/Shapes/RectangleShape.cs index ad70466..c56ec56 100644 --- a/OpenNest.Core/Shapes/RectangleShape.cs +++ b/OpenNest.Core/Shapes/RectangleShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/RightTriangleShape.cs b/OpenNest.Core/Shapes/RightTriangleShape.cs index e7549ca..5aa1c01 100644 --- a/OpenNest.Core/Shapes/RightTriangleShape.cs +++ b/OpenNest.Core/Shapes/RightTriangleShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/RingShape.cs b/OpenNest.Core/Shapes/RingShape.cs index 2b22783..1cb1e66 100644 --- a/OpenNest.Core/Shapes/RingShape.cs +++ b/OpenNest.Core/Shapes/RingShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/RoundedRectangleShape.cs b/OpenNest.Core/Shapes/RoundedRectangleShape.cs index 1294ffa..dcc5726 100644 --- a/OpenNest.Core/Shapes/RoundedRectangleShape.cs +++ b/OpenNest.Core/Shapes/RoundedRectangleShape.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/ShapeDefinition.cs b/OpenNest.Core/Shapes/ShapeDefinition.cs index 4327935..b0890c4 100644 --- a/OpenNest.Core/Shapes/ShapeDefinition.cs +++ b/OpenNest.Core/Shapes/ShapeDefinition.cs @@ -1,9 +1,9 @@ +using OpenNest.Converters; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.IO; using System.Text.Json; -using OpenNest.Converters; -using OpenNest.Geometry; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/TShape.cs b/OpenNest.Core/Shapes/TShape.cs index 15a0c95..4231de0 100644 --- a/OpenNest.Core/Shapes/TShape.cs +++ b/OpenNest.Core/Shapes/TShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Shapes/TrapezoidShape.cs b/OpenNest.Core/Shapes/TrapezoidShape.cs index 1c56eeb..b50e0a2 100644 --- a/OpenNest.Core/Shapes/TrapezoidShape.cs +++ b/OpenNest.Core/Shapes/TrapezoidShape.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Shapes { diff --git a/OpenNest.Core/Timing.cs b/OpenNest.Core/Timing.cs index bd20d5d..ee8479a 100644 --- a/OpenNest.Core/Timing.cs +++ b/OpenNest.Core/Timing.cs @@ -1,9 +1,9 @@ -using System; -using System.Linq; -using OpenNest.Api; +using OpenNest.Api; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; +using System; +using System.Linq; namespace OpenNest { @@ -84,7 +84,7 @@ namespace OpenNest time += TimeSpan.FromSeconds(info.TravelDistance / cutParams.RapidTravelRate); break; } - + time += TimeSpan.FromTicks(info.PierceCount * cutParams.PierceTime.Ticks); return time; diff --git a/OpenNest.Core/Units.cs b/OpenNest.Core/Units.cs index 3b538b4..c695143 100644 --- a/OpenNest.Core/Units.cs +++ b/OpenNest.Core/Units.cs @@ -19,7 +19,7 @@ namespace OpenNest case Units.Millimeters: return "mm"; - default: + default: return string.Empty; } } @@ -34,7 +34,7 @@ namespace OpenNest case Units.Millimeters: return "millimeters"; - default: + default: return string.Empty; } } @@ -49,7 +49,7 @@ namespace OpenNest case Units.Millimeters: return "sec"; - default: + default: return string.Empty; } } diff --git a/OpenNest.Engine/AngleCandidateBuilder.cs b/OpenNest.Engine/AngleCandidateBuilder.cs deleted file mode 100644 index 8c09ce0..0000000 --- a/OpenNest.Engine/AngleCandidateBuilder.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using OpenNest.Engine.ML; -using OpenNest.Geometry; -using OpenNest.Math; - -namespace OpenNest -{ - /// - /// Builds candidate rotation angles for single-item fill. Encapsulates the - /// full pipeline: base angles, narrow-area sweep, ML prediction, and - /// known-good pruning across fills. - /// - public class AngleCandidateBuilder - { - private readonly HashSet knownGoodAngles = new(); - - public bool ForceFullSweep { get; set; } - - public List Build(NestItem item, double bestRotation, Box workArea) - { - var angles = new List { bestRotation, bestRotation + Angle.HalfPI }; - - var testPart = new Part(item.Drawing); - if (!bestRotation.IsEqualTo(0)) - testPart.Rotate(bestRotation); - testPart.UpdateBounds(); - - var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length); - var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length); - var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep; - - if (needsSweep) - { - var step = Angle.ToRadians(5); - for (var a = 0.0; a < System.Math.PI; a += step) - { - if (!angles.Any(existing => existing.IsEqualTo(a))) - angles.Add(a); - } - } - - if (!ForceFullSweep && angles.Count > 2) - { - var features = FeatureExtractor.Extract(item.Drawing); - if (features != null) - { - var predicted = AnglePredictor.PredictAngles( - features, workArea.Width, workArea.Length); - - if (predicted != null) - { - var mlAngles = new List(predicted); - - if (!mlAngles.Any(a => a.IsEqualTo(bestRotation))) - mlAngles.Add(bestRotation); - if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI))) - mlAngles.Add(bestRotation + Angle.HalfPI); - - Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted"); - angles = mlAngles; - } - } - } - - if (knownGoodAngles.Count > 0 && !ForceFullSweep) - { - var pruned = new List { bestRotation, bestRotation + Angle.HalfPI }; - - foreach (var a in knownGoodAngles) - { - if (!pruned.Any(existing => existing.IsEqualTo(a))) - pruned.Add(a); - } - - Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)"); - return pruned; - } - - return angles; - } - - /// - /// Records angles that produced results. These are used to prune - /// subsequent Build() calls. - /// - public void RecordProductive(List angleResults) - { - foreach (var ar in angleResults) - { - if (ar.PartCount > 0) - knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg)); - } - } - } -} diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index 49a4121..95861b5 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -1,11 +1,11 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using OpenNest.Converters; using OpenNest.Engine.BestFit.Tiling; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace OpenNest.Engine.BestFit { diff --git a/OpenNest.Engine/BestFit/BestFitResult.cs b/OpenNest.Engine/BestFit/BestFitResult.cs index 97ce07c..60fc093 100644 --- a/OpenNest.Engine/BestFit/BestFitResult.cs +++ b/OpenNest.Engine/BestFit/BestFitResult.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.Engine.BestFit { diff --git a/OpenNest.Engine/BestFit/PairEvaluator.cs b/OpenNest.Engine/BestFit/PairEvaluator.cs index c48f1f0..591b4b4 100644 --- a/OpenNest.Engine/BestFit/PairEvaluator.cs +++ b/OpenNest.Engine/BestFit/PairEvaluator.cs @@ -1,9 +1,10 @@ +using OpenNest.Converters; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using OpenNest.Converters; -using OpenNest.Geometry; namespace OpenNest.Engine.BestFit { diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 9308ec3..7ad834d 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -1,6 +1,6 @@ +using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; -using OpenNest.Geometry; namespace OpenNest.Engine.BestFit { diff --git a/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs b/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs index 8db666e..c0aa95e 100644 --- a/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs +++ b/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.Engine.BestFit.Tiling { diff --git a/OpenNest.Engine/BestFit/Tiling/TileResult.cs b/OpenNest.Engine/BestFit/Tiling/TileResult.cs index a3f1181..7464d4e 100644 --- a/OpenNest.Engine/BestFit/Tiling/TileResult.cs +++ b/OpenNest.Engine/BestFit/Tiling/TileResult.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Engine.BestFit.Tiling { diff --git a/OpenNest.Engine/CirclePacking/Bin.cs b/OpenNest.Engine/CirclePacking/Bin.cs index f191714..9dc3ab3 100644 --- a/OpenNest.Engine/CirclePacking/Bin.cs +++ b/OpenNest.Engine/CirclePacking/Bin.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using OpenNest.Geometry; +using System.Collections.Generic; using System.Linq; -using OpenNest.Geometry; namespace OpenNest.CirclePacking { diff --git a/OpenNest.Engine/CirclePacking/FillEndEven.cs b/OpenNest.Engine/CirclePacking/FillEndEven.cs index b343301..62ece31 100644 --- a/OpenNest.Engine/CirclePacking/FillEndEven.cs +++ b/OpenNest.Engine/CirclePacking/FillEndEven.cs @@ -1,6 +1,6 @@ -using System; -using OpenNest.Geometry; +using OpenNest.Geometry; using OpenNest.Math; +using System; namespace OpenNest.CirclePacking { diff --git a/OpenNest.Engine/CirclePacking/FillEndOdd.cs b/OpenNest.Engine/CirclePacking/FillEndOdd.cs index 44c8321..08415c7 100644 --- a/OpenNest.Engine/CirclePacking/FillEndOdd.cs +++ b/OpenNest.Engine/CirclePacking/FillEndOdd.cs @@ -1,6 +1,6 @@ -using System; -using OpenNest.Geometry; +using OpenNest.Geometry; using OpenNest.Math; +using System; namespace OpenNest.CirclePacking { diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs deleted file mode 100644 index 90dd994..0000000 --- a/OpenNest.Engine/Compactor.cs +++ /dev/null @@ -1,363 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using OpenNest.Geometry; - -namespace OpenNest -{ - /// - /// Pushes a group of parts left and down to close gaps after placement. - /// Uses the same directional-distance logic as PlateView.PushSelected - /// but operates on Part objects directly. - /// - public static class Compactor - { - private const double ChordTolerance = 0.001; - - /// - /// Compacts movingParts toward the bottom-left of the plate work area. - /// Everything already on the plate (excluding movingParts) is treated - /// as stationary obstacles. - /// - private const double RepeatThreshold = 0.01; - private const int MaxIterations = 20; - - public static void Compact(List movingParts, Plate plate) - { - if (movingParts == null || movingParts.Count == 0) - return; - - var savedPositions = SavePositions(movingParts); - - // Try left-first. - var leftFirst = CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down); - - // Restore and try down-first. - RestorePositions(movingParts, savedPositions); - var downFirst = CompactLoop(movingParts, plate, PushDirection.Down, PushDirection.Left); - - // Keep left-first if it traveled further. - if (leftFirst > downFirst) - { - RestorePositions(movingParts, savedPositions); - CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down); - } - } - - private static double CompactLoop(List parts, Plate plate, - PushDirection first, PushDirection second) - { - var total = 0.0; - - for (var i = 0; i < MaxIterations; i++) - { - var a = Push(parts, plate, first); - var b = Push(parts, plate, second); - total += a + b; - - if (a <= RepeatThreshold && b <= RepeatThreshold) - break; - } - - return total; - } - - private static Vector[] SavePositions(List parts) - { - var positions = new Vector[parts.Count]; - for (var i = 0; i < parts.Count; i++) - positions[i] = parts[i].Location; - return positions; - } - - private static void RestorePositions(List parts, Vector[] positions) - { - for (var i = 0; i < parts.Count; i++) - parts[i].Location = positions[i]; - } - - public static double Push(List movingParts, Plate plate, PushDirection direction) - { - var obstacleParts = plate.Parts - .Where(p => !movingParts.Contains(p)) - .ToList(); - - return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); - } - - /// - /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). - /// - public static double Push(List movingParts, Plate plate, double angle) - { - var obstacleParts = plate.Parts - .Where(p => !movingParts.Contains(p)) - .ToList(); - - return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle); - } - - /// - /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). - /// - public static double Push(List movingParts, List obstacleParts, - Box workArea, double partSpacing, double angle) - { - var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); - var opposite = -direction; - - var obstacleBoxes = new Box[obstacleParts.Count]; - var obstacleLines = new List[obstacleParts.Count]; - - for (var i = 0; i < obstacleParts.Count; i++) - obstacleBoxes[i] = obstacleParts[i].BoundingBox; - - var halfSpacing = partSpacing / 2; - var distance = double.MaxValue; - - foreach (var moving in movingParts) - { - var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); - if (edgeDist <= 0) - distance = 0; - else if (edgeDist < distance) - distance = edgeDist; - - var movingBox = moving.BoundingBox; - List movingLines = null; - - for (var i = 0; i < obstacleBoxes.Length; i++) - { - var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); - if (reverseGap > 0) - continue; - - var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); - if (gap >= distance) - continue; - - if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction)) - continue; - - movingLines ??= halfSpacing > 0 - ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) - : PartGeometry.GetPartLines(moving, direction, ChordTolerance); - - obstacleLines[i] ??= halfSpacing > 0 - ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) - : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); - - var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); - if (d < distance) - distance = d; - } - } - - if (distance < double.MaxValue && distance > 0) - { - var offset = direction * distance; - foreach (var moving in movingParts) - moving.Offset(offset); - return distance; - } - - return 0; - } - - public static double Push(List movingParts, List obstacleParts, - Box workArea, double partSpacing, PushDirection direction) - { - var obstacleBoxes = new Box[obstacleParts.Count]; - var obstacleLines = new List[obstacleParts.Count]; - - for (var i = 0; i < obstacleParts.Count; i++) - obstacleBoxes[i] = obstacleParts[i].BoundingBox; - - var opposite = SpatialQuery.OppositeDirection(direction); - var halfSpacing = partSpacing / 2; - var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); - var distance = double.MaxValue; - - // BB gap at which offset geometries are expected to be touching. - var contactGap = (halfSpacing + ChordTolerance) * 2; - - foreach (var moving in movingParts) - { - var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); - if (edgeDist <= 0) - distance = 0; - else if (edgeDist < distance) - distance = edgeDist; - - var movingBox = moving.BoundingBox; - List movingLines = null; - - for (var i = 0; i < obstacleBoxes.Length; i++) - { - // Use the reverse-direction gap to check if the obstacle is entirely - // behind the moving part. The forward gap (gap < 0) is unreliable for - // irregular shapes whose bounding boxes overlap even when the actual - // geometry still has a valid contact in the push direction. - var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); - if (reverseGap > 0) - continue; - - var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); - if (gap >= distance) - continue; - - var perpOverlap = isHorizontal - ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _) - : movingBox.IsVerticalTo(obstacleBoxes[i], out _); - - if (!perpOverlap) - continue; - - movingLines ??= halfSpacing > 0 - ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) - : PartGeometry.GetPartLines(moving, direction, ChordTolerance); - - obstacleLines[i] ??= halfSpacing > 0 - ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) - : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); - - var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); - if (d < distance) - distance = d; - } - } - - if (distance < double.MaxValue && distance > 0) - { - var offset = SpatialQuery.DirectionToOffset(direction, distance); - foreach (var moving in movingParts) - moving.Offset(offset); - return distance; - } - - return 0; - } - - /// - /// Pushes movingParts using bounding-box distances only (no geometry lines). - /// Much faster but less precise — use as a coarse positioning pass before - /// a full geometry Push. - /// - public static double PushBoundingBox(List movingParts, Plate plate, PushDirection direction) - { - var obstacleParts = plate.Parts - .Where(p => !movingParts.Contains(p)) - .ToList(); - - return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); - } - - public static double PushBoundingBox(List movingParts, List obstacleParts, - Box workArea, double partSpacing, PushDirection direction) - { - var obstacleBoxes = new Box[obstacleParts.Count]; - for (var i = 0; i < obstacleParts.Count; i++) - obstacleBoxes[i] = obstacleParts[i].BoundingBox; - - var opposite = SpatialQuery.OppositeDirection(direction); - var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); - var distance = double.MaxValue; - - foreach (var moving in movingParts) - { - var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); - if (edgeDist <= 0) - distance = 0; - else if (edgeDist < distance) - distance = edgeDist; - - var movingBox = moving.BoundingBox; - - for (var i = 0; i < obstacleBoxes.Length; i++) - { - var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); - if (reverseGap > 0) - continue; - - var perpOverlap = isHorizontal - ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _) - : movingBox.IsVerticalTo(obstacleBoxes[i], out _); - - if (!perpOverlap) - continue; - - var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); - var d = gap - partSpacing; - if (d < 0) d = 0; - if (d < distance) - distance = d; - } - } - - if (distance < double.MaxValue && distance > 0) - { - var offset = SpatialQuery.DirectionToOffset(direction, distance); - foreach (var moving in movingParts) - moving.Offset(offset); - return distance; - } - - return 0; - } - - /// - /// Compacts parts individually toward the bottom-left of the work area. - /// Each part is pushed against all others as obstacles, closing geometry-based gaps. - /// Does not require parts to be on a plate. - /// - public static void CompactIndividual(List parts, Box workArea, double partSpacing) - { - if (parts == null || parts.Count < 2) - return; - - var savedPositions = SavePositions(parts); - - var leftFirst = CompactIndividualLoop(parts, workArea, partSpacing, - PushDirection.Left, PushDirection.Down); - - RestorePositions(parts, savedPositions); - var downFirst = CompactIndividualLoop(parts, workArea, partSpacing, - PushDirection.Down, PushDirection.Left); - - if (leftFirst > downFirst) - { - RestorePositions(parts, savedPositions); - CompactIndividualLoop(parts, workArea, partSpacing, - PushDirection.Left, PushDirection.Down); - } - } - - private static double CompactIndividualLoop(List parts, Box workArea, - double partSpacing, PushDirection first, PushDirection second) - { - var total = 0.0; - - for (var pass = 0; pass < MaxIterations; pass++) - { - var moved = 0.0; - - foreach (var part in parts) - { - var single = new List(1) { part }; - var obstacles = new List(parts.Count - 1); - foreach (var p in parts) - if (p != part) obstacles.Add(p); - - moved += Push(single, obstacles, workArea, partSpacing, first); - moved += Push(single, obstacles, workArea, partSpacing, second); - } - - total += moved; - if (moved <= RepeatThreshold) - break; - } - - return total; - } - } -} diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index 69ac4f1..e630392 100644 --- a/OpenNest.Engine/DefaultNestEngine.cs +++ b/OpenNest.Engine/DefaultNestEngine.cs @@ -1,13 +1,12 @@ +using OpenNest.Engine.Fill; +using OpenNest.Engine.Strategies; +using OpenNest.Geometry; +using OpenNest.RectanglePacking; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; -using OpenNest.Engine.BestFit; -using OpenNest.Geometry; -using OpenNest.Math; -using OpenNest.RectanglePacking; namespace OpenNest { @@ -56,7 +55,8 @@ namespace OpenNest if (item.Quantity > 0 && best.Count > item.Quantity) best = best.Take(item.Quantity).ToList(); - ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary()); + ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(), + isOverallBest: true); return best; } @@ -67,89 +67,24 @@ namespace OpenNest if (groupParts == null || groupParts.Count == 0) return new List(); + // Single part: delegate to the strategy pipeline. + if (groupParts.Count == 1) + { + var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing }; + return Fill(nestItem, workArea, progress, token); + } + + // Multi-part group: linear pattern fill only. PhaseResults.Clear(); var engine = new FillLinear(workArea, Plate.PartSpacing); var angles = RotationAnalysis.FindHullEdgeAngles(groupParts); - var best = FillPattern(engine, groupParts, angles, workArea); + var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea); PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0)); - Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}"); + Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}"); - ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary()); - - if (groupParts.Count == 1) - { - try - { - token.ThrowIfCancellationRequested(); - - var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing }; - var binItem = BinConverter.ToItem(nestItem, Plate.PartSpacing); - var bin = BinConverter.CreateBin(workArea, Plate.PartSpacing); - var rectEngine = new FillBestFit(bin); - rectEngine.Fill(binItem); - var rectResult = BinConverter.ToParts(bin, new List { nestItem }); - PhaseResults.Add(new PhaseResult(NestPhase.RectBestFit, rectResult?.Count ?? 0, 0)); - - Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts"); - - if (IsBetterFill(rectResult, best, workArea)) - { - best = rectResult; - ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary()); - } - - token.ThrowIfCancellationRequested(); - - var pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing); - var pairResult = pairFiller.Fill(nestItem, workArea, PlateNumber, token, progress); - PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, 0)); - - Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}"); - - if (IsBetterFill(pairResult, best, workArea)) - { - best = pairResult; - ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary()); - } - - token.ThrowIfCancellationRequested(); - - var extentsFiller = new FillExtents(workArea, Plate.PartSpacing); - var bestFits2 = BestFitCache.GetOrCompute( - groupParts[0].BaseDrawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing); - var extentsAngles2 = new[] { groupParts[0].Rotation, groupParts[0].Rotation + Angle.HalfPI }; - List bestExtents2 = null; - - foreach (var angle in extentsAngles2) - { - token.ThrowIfCancellationRequested(); - var result = extentsFiller.Fill(groupParts[0].BaseDrawing, angle, PlateNumber, token, progress, bestFits2); - if (result != null && result.Count > (bestExtents2?.Count ?? 0)) - bestExtents2 = result; - } - - PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents2?.Count ?? 0, 0)); - Debug.WriteLine($"[Fill(groupParts,Box)] Extents: {bestExtents2?.Count ?? 0} parts"); - - if (IsBetterFill(bestExtents2, best, workArea)) - { - best = bestExtents2; - ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary()); - } - } - catch (OperationCanceledException) - { - Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best"); - } - } - - // Always report the final winner so the UI's temporary parts - // match the returned result. - var winPhase = PhaseResults.Count > 0 - ? PhaseResults.OrderByDescending(r => r.PartCount).First().Phase - : NestPhase.Linear; - ReportProgress(progress, winPhase, PlateNumber, best, workArea, BuildProgressSummary()); + ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(), + isOverallBest: true); return best ?? new List(); } @@ -201,8 +136,13 @@ namespace OpenNest context.CurrentBest = result; context.CurrentBestScore = FillScore.Compute(result, context.WorkArea); context.WinnerPhase = strategy.Phase; - ReportProgress(context.Progress, strategy.Phase, PlateNumber, - result, context.WorkArea, BuildProgressSummary()); + } + + if (context.CurrentBest != null && context.CurrentBest.Count > 0) + { + ReportProgress(context.Progress, context.WinnerPhase, PlateNumber, + context.CurrentBest, context.WorkArea, BuildProgressSummary(), + isOverallBest: true); } } } @@ -214,13 +154,5 @@ namespace OpenNest angleBuilder.RecordProductive(context.AngleResults); } - // --- Pattern helpers --- - - internal static Pattern BuildRotatedPattern(List groupParts, double angle) - => FillHelpers.BuildRotatedPattern(groupParts, angle); - - internal static List FillPattern(FillLinear engine, List groupParts, List angles, Box workArea) - => FillHelpers.FillPattern(engine, groupParts, angles, workArea); - } } diff --git a/OpenNest.Engine/AccumulatingProgress.cs b/OpenNest.Engine/Fill/AccumulatingProgress.cs similarity index 97% rename from OpenNest.Engine/AccumulatingProgress.cs rename to OpenNest.Engine/Fill/AccumulatingProgress.cs index 7b84803..051ec22 100644 --- a/OpenNest.Engine/AccumulatingProgress.cs +++ b/OpenNest.Engine/Fill/AccumulatingProgress.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Fill { /// /// Wraps an IProgress to prepend previously placed parts to each report, diff --git a/OpenNest.Engine/Fill/AngleCandidateBuilder.cs b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs new file mode 100644 index 0000000..670c459 --- /dev/null +++ b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs @@ -0,0 +1,114 @@ +using OpenNest.Engine.ML; +using OpenNest.Geometry; +using OpenNest.Math; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace OpenNest.Engine.Fill +{ + /// + /// Builds candidate rotation angles for single-item fill. Encapsulates the + /// full pipeline: base angles, narrow-area sweep, ML prediction, and + /// known-good pruning across fills. + /// + public class AngleCandidateBuilder + { + private readonly HashSet knownGoodAngles = new(); + + public bool ForceFullSweep { get; set; } + + public List Build(NestItem item, double bestRotation, Box workArea) + { + var baseAngles = new[] { bestRotation, bestRotation + Angle.HalfPI }; + + if (knownGoodAngles.Count > 0 && !ForceFullSweep) + return BuildPrunedList(baseAngles); + + var angles = new List(baseAngles); + + if (NeedsSweep(item, bestRotation, workArea)) + AddSweepAngles(angles); + + if (!ForceFullSweep && angles.Count > 2) + angles = ApplyMlPrediction(item, workArea, baseAngles, angles); + + return angles; + } + + private bool NeedsSweep(NestItem item, double bestRotation, Box workArea) + { + var testPart = new Part(item.Drawing); + if (!bestRotation.IsEqualTo(0)) + testPart.Rotate(bestRotation); + testPart.UpdateBounds(); + + var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length); + var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length); + return workAreaShortSide < partLongestSide || ForceFullSweep; + } + + private static void AddSweepAngles(List angles) + { + var step = Angle.ToRadians(5); + for (var a = 0.0; a < System.Math.PI; a += step) + { + if (!ContainsAngle(angles, a)) + angles.Add(a); + } + } + + private static List ApplyMlPrediction( + NestItem item, Box workArea, double[] baseAngles, List fallback) + { + var features = FeatureExtractor.Extract(item.Drawing); + if (features == null) + return fallback; + + var predicted = AnglePredictor.PredictAngles(features, workArea.Width, workArea.Length); + if (predicted == null) + return fallback; + + var mlAngles = new List(predicted); + foreach (var b in baseAngles) + { + if (!ContainsAngle(mlAngles, b)) + mlAngles.Add(b); + } + + Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} angles -> {mlAngles.Count} predicted"); + return mlAngles; + } + + private List BuildPrunedList(double[] baseAngles) + { + var pruned = new List(baseAngles); + foreach (var a in knownGoodAngles) + { + if (!ContainsAngle(pruned, a)) + pruned.Add(a); + } + + Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)"); + return pruned; + } + + private static bool ContainsAngle(List angles, double angle) + { + return angles.Any(existing => existing.IsEqualTo(angle)); + } + + /// + /// Records angles that produced results. These are used to prune + /// subsequent Build() calls. + /// + public void RecordProductive(List angleResults) + { + foreach (var ar in angleResults) + { + if (ar.PartCount > 0) + knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg)); + } + } + } +} diff --git a/OpenNest.Engine/BestCombination.cs b/OpenNest.Engine/Fill/BestCombination.cs similarity index 98% rename from OpenNest.Engine/BestCombination.cs rename to OpenNest.Engine/Fill/BestCombination.cs index f0bc2ab..69bb186 100644 --- a/OpenNest.Engine/BestCombination.cs +++ b/OpenNest.Engine/Fill/BestCombination.cs @@ -1,5 +1,4 @@ -using System; -using OpenNest.Math; +using OpenNest.Math; namespace OpenNest { diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs new file mode 100644 index 0000000..66af511 --- /dev/null +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -0,0 +1,178 @@ +using OpenNest.Geometry; +using System.Collections.Generic; +using System.Linq; + +namespace OpenNest.Engine.Fill +{ + /// + /// Pushes a group of parts left and down to close gaps after placement. + /// Uses the same directional-distance logic as PlateView.PushSelected + /// but operates on Part objects directly. + /// + public static class Compactor + { + private const double ChordTolerance = 0.001; + + public static double Push(List movingParts, Plate plate, PushDirection direction) + { + var obstacleParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + } + + /// + /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). + /// + public static double Push(List movingParts, Plate plate, double angle) + { + var obstacleParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); + return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + } + + /// + /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). + /// + public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, Vector direction) + { + var opposite = -direction; + + var obstacleBoxes = new Box[obstacleParts.Count]; + var obstacleLines = new List[obstacleParts.Count]; + + for (var i = 0; i < obstacleParts.Count; i++) + obstacleBoxes[i] = obstacleParts[i].BoundingBox; + + var halfSpacing = partSpacing / 2; + var distance = double.MaxValue; + + foreach (var moving in movingParts) + { + var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); + if (edgeDist <= 0) + distance = 0; + else if (edgeDist < distance) + distance = edgeDist; + + var movingBox = moving.BoundingBox; + List movingLines = null; + + for (var i = 0; i < obstacleBoxes.Length; i++) + { + var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + if (reverseGap > 0) + continue; + + var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); + if (gap >= distance) + continue; + + if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction)) + continue; + + movingLines ??= halfSpacing > 0 + ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) + : PartGeometry.GetPartLines(moving, direction, ChordTolerance); + + obstacleLines[i] ??= halfSpacing > 0 + ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) + : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); + + var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); + if (d < distance) + distance = d; + } + } + + if (distance < double.MaxValue && distance > 0) + { + var offset = direction * distance; + foreach (var moving in movingParts) + moving.Offset(offset); + return distance; + } + + return 0; + } + + public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, PushDirection direction) + { + var vector = SpatialQuery.DirectionToOffset(direction, 1.0); + return Push(movingParts, obstacleParts, workArea, partSpacing, vector); + } + + /// + /// Pushes movingParts using bounding-box distances only (no geometry lines). + /// Much faster but less precise — use as a coarse positioning pass before + /// a full geometry Push. + /// + public static double PushBoundingBox(List movingParts, Plate plate, PushDirection direction) + { + var obstacleParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + } + + public static double PushBoundingBox(List movingParts, List obstacleParts, + Box workArea, double partSpacing, PushDirection direction) + { + var obstacleBoxes = new Box[obstacleParts.Count]; + for (var i = 0; i < obstacleParts.Count; i++) + obstacleBoxes[i] = obstacleParts[i].BoundingBox; + + var opposite = SpatialQuery.OppositeDirection(direction); + var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); + var distance = double.MaxValue; + + foreach (var moving in movingParts) + { + var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); + if (edgeDist <= 0) + distance = 0; + else if (edgeDist < distance) + distance = edgeDist; + + var movingBox = moving.BoundingBox; + + for (var i = 0; i < obstacleBoxes.Length; i++) + { + var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + if (reverseGap > 0) + continue; + + var perpOverlap = isHorizontal + ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _) + : movingBox.IsVerticalTo(obstacleBoxes[i], out _); + + if (!perpOverlap) + continue; + + var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); + var d = gap - partSpacing; + if (d < 0) d = 0; + if (d < distance) + distance = d; + } + } + + if (distance < double.MaxValue && distance > 0) + { + var offset = SpatialQuery.DirectionToOffset(direction, distance); + foreach (var moving in movingParts) + moving.Offset(offset); + return distance; + } + + return 0; + } + } +} diff --git a/OpenNest.Engine/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs similarity index 95% rename from OpenNest.Engine/FillExtents.cs rename to OpenNest.Engine/Fill/FillExtents.cs index 3d4c324..185c346 100644 --- a/OpenNest.Engine/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,11 +1,11 @@ +using OpenNest.Geometry; +using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; -using OpenNest.Geometry; -using OpenNest.Math; -namespace OpenNest +namespace OpenNest.Engine.Fill { public class FillExtents { @@ -173,18 +173,12 @@ namespace OpenNest if (minSlide >= double.MaxValue || minSlide < 0) return pairHeight + partSpacing; - // Boundaries are inflated by halfSpacing, so when inflated edges touch - // the actual parts have partSpacing gap. Match FillLinear's pattern: - // startOffset = pairHeight (no extra spacing), copyDist = height - slide. + // Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide, + // clamped so it never goes below pairHeight + partSpacing to prevent + // bounding-box overlap from spurious slide values. var copyDist = pairHeight - minSlide; - // Boundaries are inflated by halfSpacing, so the geometry-aware - // distance already guarantees partSpacing gap. Only fall back to - // bounding-box distance if the calculation produced a non-positive value. - if (copyDist <= Tolerance.Epsilon) - return pairHeight + partSpacing; - - return copyDist; + return System.Math.Max(copyDist, pairHeight + partSpacing); } private static double SlideDistance( diff --git a/OpenNest.Engine/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs similarity index 93% rename from OpenNest.Engine/FillLinear.cs rename to OpenNest.Engine/Fill/FillLinear.cs index 8fe1ef0..4a09190 100644 --- a/OpenNest.Engine/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; +using System.Threading.Tasks; -namespace OpenNest +namespace OpenNest.Engine.Fill { public class FillLinear { @@ -16,7 +16,7 @@ namespace OpenNest public Box WorkArea { get; } public double PartSpacing { get; } - + public double HalfSpacing => PartSpacing / 2; /// @@ -110,47 +110,40 @@ namespace OpenNest var pushDir = GetPushDirection(direction); var opposite = SpatialQuery.OppositeDirection(pushDir); - // Compute a starting offset large enough that every part-pair in - // patternB has its offset geometry beyond patternA's offset geometry. - var maxUpper = double.MinValue; - var minLower = double.MaxValue; - - for (var i = 0; i < patternA.Parts.Count; i++) - { - var bb = patternA.Parts[i].BoundingBox; - var upper = direction == NestDirection.Horizontal ? bb.Right : bb.Top; - var lower = direction == NestDirection.Horizontal ? bb.Left : bb.Bottom; - - if (upper > maxUpper) maxUpper = upper; - if (lower < minLower) minLower = lower; - } - - var startOffset = System.Math.Max(bboxDim, - maxUpper - minLower + PartSpacing + Tolerance.Epsilon); - + // bboxDim already spans max(upper) - min(lower) across all parts, + // so the start offset just needs to push beyond that plus spacing. + var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon; var offset = MakeOffset(direction, startOffset); - // Pre-cache edge arrays. - var movingEdges = new (Vector start, Vector end)[patternA.Parts.Count][]; - var stationaryEdges = new (Vector start, Vector end)[patternA.Parts.Count][]; + var maxCopyDistance = FindMaxPairDistance( + patternA.Parts, boundaries, offset, pushDir, opposite, startOffset); - for (var i = 0; i < patternA.Parts.Count; i++) - { - movingEdges[i] = boundaries[i].GetEdges(pushDir); - stationaryEdges[i] = boundaries[i].GetEdges(opposite); - } + if (maxCopyDistance < Tolerance.Epsilon) + return bboxDim + PartSpacing; + return maxCopyDistance; + } + + /// + /// Tests every pair of parts across adjacent pattern copies and returns the + /// maximum copy distance found. Returns 0 if no valid slide was found. + /// + private static double FindMaxPairDistance( + List parts, PartBoundary[] boundaries, Vector offset, + PushDirection pushDir, PushDirection opposite, double startOffset) + { var maxCopyDistance = 0.0; - for (var j = 0; j < patternA.Parts.Count; j++) + for (var j = 0; j < parts.Count; j++) { - var locationB = patternA.Parts[j].Location + offset; + var movingEdges = boundaries[j].GetEdges(pushDir); + var locationB = parts[j].Location + offset; - for (var i = 0; i < patternA.Parts.Count; i++) + for (var i = 0; i < parts.Count; i++) { var slideDistance = SpatialQuery.DirectionalDistance( - movingEdges[j], locationB, - stationaryEdges[i], patternA.Parts[i].Location, + movingEdges, locationB, + boundaries[i].GetEdges(opposite), parts[i].Location, pushDir); if (slideDistance >= double.MaxValue || slideDistance < 0) @@ -163,9 +156,6 @@ namespace OpenNest } } - if (maxCopyDistance < Tolerance.Epsilon) - return bboxDim + PartSpacing; - return maxCopyDistance; } diff --git a/OpenNest.Engine/FillScore.cs b/OpenNest.Engine/Fill/FillScore.cs similarity index 98% rename from OpenNest.Engine/FillScore.cs rename to OpenNest.Engine/Fill/FillScore.cs index 7c31e4d..957768c 100644 --- a/OpenNest.Engine/FillScore.cs +++ b/OpenNest.Engine/Fill/FillScore.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Fill { public readonly struct FillScore : System.IComparable { diff --git a/OpenNest.Engine/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs similarity index 68% rename from OpenNest.Engine/PairFiller.cs rename to OpenNest.Engine/Fill/PairFiller.cs index f1a30aa..1a2b2fe 100644 --- a/OpenNest.Engine/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -1,23 +1,35 @@ +using OpenNest.Engine.BestFit; +using OpenNest.Engine.Strategies; +using OpenNest.Geometry; +using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using OpenNest.Engine.BestFit; -using OpenNest.Geometry; -using OpenNest.Math; -namespace OpenNest +namespace OpenNest.Engine.Fill { /// /// Fills a work area using interlocking part pairs from BestFitCache. - /// Extracted from DefaultNestEngine.FillWithPairs. /// public class PairFiller { + private const int MaxTopCandidates = 50; + private const int MaxStripCandidates = 100; + private const double MinStripUtilization = 0.3; + private const int EarlyExitMinTried = 10; + private const int EarlyExitStaleLimit = 10; + private readonly Size plateSize; private readonly double partSpacing; + /// + /// The best-fit results computed during the last Fill call. + /// Available after Fill returns so callers can reuse without recomputing. + /// + public List BestFits { get; private set; } + public PairFiller(Size plateSize, double partSpacing) { this.plateSize = plateSize; @@ -29,11 +41,11 @@ namespace OpenNest CancellationToken token = default, IProgress progress = null) { - var bestFits = BestFitCache.GetOrCompute( + BestFits = BestFitCache.GetOrCompute( item.Drawing, plateSize.Length, plateSize.Width, partSpacing); - var candidates = SelectPairCandidates(bestFits, workArea); - Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); + var candidates = SelectPairCandidates(BestFits, workArea); + Debug.WriteLine($"[PairFiller] Total: {BestFits.Count}, Kept: {BestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"); List best = null; @@ -46,17 +58,7 @@ namespace OpenNest { token.ThrowIfCancellationRequested(); - var result = candidates[i]; - var pairParts = result.BuildParts(item.Drawing); - var angles = result.HullAngles; - var engine = new FillLinear(workArea, partSpacing); - - // Let the remainder strip try pair-based filling too. - var p0 = DefaultNestEngine.BuildRotatedPattern(pairParts, 0); - var p90 = DefaultNestEngine.BuildRotatedPattern(pairParts, Angle.HalfPI); - engine.RemainderPatterns = new List { p0, p90 }; - - var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea); + var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea); if (filled != null && filled.Count > 0) { @@ -80,8 +82,7 @@ namespace OpenNest NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea, $"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts"); - // Early exit: stop if we've tried enough candidates without improvement. - if (i >= 9 && sinceImproved >= 10) + if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit) { Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates"); break; @@ -97,10 +98,22 @@ namespace OpenNest return best ?? new List(); } + private List EvaluateCandidate(BestFitResult candidate, Drawing drawing, Box workArea) + { + var pairParts = candidate.BuildParts(drawing); + var engine = new FillLinear(workArea, partSpacing); + + var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0); + var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI); + engine.RemainderPatterns = new List { p0, p90 }; + + return FillHelpers.FillPattern(engine, pairParts, candidate.HullAngles, workArea); + } + private List SelectPairCandidates(List bestFits, Box workArea) { var kept = bestFits.Where(r => r.Keep).ToList(); - var top = kept.Take(50).ToList(); + var top = kept.Take(MaxTopCandidates).ToList(); var workShortSide = System.Math.Min(workArea.Width, workArea.Length); var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length); @@ -109,14 +122,14 @@ namespace OpenNest { var stripCandidates = bestFits .Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon - && r.Utilization >= 0.3) + && r.Utilization >= MinStripUtilization) .OrderByDescending(r => r.Utilization); var existing = new HashSet(top); foreach (var r in stripCandidates) { - if (top.Count >= 100) + if (top.Count >= MaxStripCandidates) break; if (existing.Add(r)) diff --git a/OpenNest.Engine/PartBoundary.cs b/OpenNest.Engine/Fill/PartBoundary.cs similarity index 93% rename from OpenNest.Engine/PartBoundary.cs rename to OpenNest.Engine/Fill/PartBoundary.cs index 44c14bc..e39dbdd 100644 --- a/OpenNest.Engine/PartBoundary.cs +++ b/OpenNest.Engine/Fill/PartBoundary.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; +using System.Collections.Generic; +using System.Linq; -namespace OpenNest +namespace OpenNest.Engine.Fill { /// /// Pre-computed offset boundary polygons for a part's geometry. @@ -87,9 +87,9 @@ namespace OpenNest var edge = (verts[i - 1], verts[i]); if (-sign * dy > 0) left.Add(edge); - if ( sign * dy > 0) right.Add(edge); + if (sign * dy > 0) right.Add(edge); if (-sign * dx > 0) up.Add(edge); - if ( sign * dx > 0) down.Add(edge); + if (sign * dx > 0) down.Add(edge); } } @@ -145,11 +145,11 @@ namespace OpenNest { switch (direction) { - case PushDirection.Left: return _leftEdges; + case PushDirection.Left: return _leftEdges; case PushDirection.Right: return _rightEdges; - case PushDirection.Up: return _upEdges; - case PushDirection.Down: return _downEdges; - default: return _leftEdges; + case PushDirection.Up: return _upEdges; + case PushDirection.Down: return _downEdges; + default: return _leftEdges; } } diff --git a/OpenNest.Engine/Pattern.cs b/OpenNest.Engine/Fill/Pattern.cs similarity index 95% rename from OpenNest.Engine/Pattern.cs rename to OpenNest.Engine/Fill/Pattern.cs index 67f40e4..1432fd4 100644 --- a/OpenNest.Engine/Pattern.cs +++ b/OpenNest.Engine/Fill/Pattern.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Fill { public class Pattern { diff --git a/OpenNest.Engine/PatternTiler.cs b/OpenNest.Engine/Fill/PatternTiler.cs similarity index 98% rename from OpenNest.Engine/PatternTiler.cs rename to OpenNest.Engine/Fill/PatternTiler.cs index 39363fd..b9260d3 100644 --- a/OpenNest.Engine/PatternTiler.cs +++ b/OpenNest.Engine/Fill/PatternTiler.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; -namespace OpenNest.Engine +namespace OpenNest.Engine.Fill { public static class PatternTiler { diff --git a/OpenNest.Engine/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs similarity index 99% rename from OpenNest.Engine/RemnantFiller.cs rename to OpenNest.Engine/Fill/RemnantFiller.cs index 44036d8..94d8b98 100644 --- a/OpenNest.Engine/RemnantFiller.cs +++ b/OpenNest.Engine/Fill/RemnantFiller.cs @@ -1,9 +1,9 @@ +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Fill { /// /// Iteratively fills remnant boxes with items using a RemnantFinder. diff --git a/OpenNest.Engine/RemnantFinder.cs b/OpenNest.Engine/Fill/RemnantFinder.cs similarity index 99% rename from OpenNest.Engine/RemnantFinder.cs rename to OpenNest.Engine/Fill/RemnantFinder.cs index a6ad082..fabd193 100644 --- a/OpenNest.Engine/RemnantFinder.cs +++ b/OpenNest.Engine/Fill/RemnantFinder.cs @@ -1,9 +1,8 @@ -using System; +using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Fill { /// /// A remnant box with a priority tier. diff --git a/OpenNest.Engine/RotationAnalysis.cs b/OpenNest.Engine/Fill/RotationAnalysis.cs similarity index 99% rename from OpenNest.Engine/RotationAnalysis.cs rename to OpenNest.Engine/Fill/RotationAnalysis.cs index e79af6f..5ce61ce 100644 --- a/OpenNest.Engine/RotationAnalysis.cs +++ b/OpenNest.Engine/Fill/RotationAnalysis.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; -using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; +using System.Linq; -namespace OpenNest +namespace OpenNest.Engine.Fill { internal static class RotationAnalysis { diff --git a/OpenNest.Engine/ShrinkFiller.cs b/OpenNest.Engine/Fill/ShrinkFiller.cs similarity index 98% rename from OpenNest.Engine/ShrinkFiller.cs rename to OpenNest.Engine/Fill/ShrinkFiller.cs index 3b62085..dcff9e7 100644 --- a/OpenNest.Engine/ShrinkFiller.cs +++ b/OpenNest.Engine/Fill/ShrinkFiller.cs @@ -1,10 +1,10 @@ +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Fill { public enum ShrinkAxis { Width, Height } diff --git a/OpenNest.Engine/ML/AnglePredictor.cs b/OpenNest.Engine/ML/AnglePredictor.cs index da864a8..37cb749 100644 --- a/OpenNest.Engine/ML/AnglePredictor.cs +++ b/OpenNest.Engine/ML/AnglePredictor.cs @@ -1,11 +1,11 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using Microsoft.ML.OnnxRuntime; -using Microsoft.ML.OnnxRuntime.Tensors; -using OpenNest.Math; namespace OpenNest.Engine.ML { diff --git a/OpenNest.Engine/ML/BruteForceRunner.cs b/OpenNest.Engine/ML/BruteForceRunner.cs index a29e272..7aaaa32 100644 --- a/OpenNest.Engine/ML/BruteForceRunner.cs +++ b/OpenNest.Engine/ML/BruteForceRunner.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using OpenNest.Geometry; namespace OpenNest.Engine.ML { diff --git a/OpenNest.Engine/ML/FeatureExtractor.cs b/OpenNest.Engine/ML/FeatureExtractor.cs index c940105..7103065 100644 --- a/OpenNest.Engine/ML/FeatureExtractor.cs +++ b/OpenNest.Engine/ML/FeatureExtractor.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using OpenNest.Geometry; +using System.Linq; namespace OpenNest.Engine.ML { diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index 20c3e84..d7b7362 100644 --- a/OpenNest.Engine/NestEngineBase.cs +++ b/OpenNest.Engine/NestEngineBase.cs @@ -1,9 +1,10 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using OpenNest.Geometry; namespace OpenNest { @@ -190,7 +191,8 @@ namespace OpenNest int plateNumber, List best, Box workArea, - string description) + string description, + bool isOverallBest = false) { if (progress == null || best == null || best.Count == 0) return; @@ -212,9 +214,13 @@ namespace OpenNest $"PartArea={totalPartArea:F0}, Remnant={workArea.Area() - totalPartArea:F0}, " + $"WorkArea={workArea.Width:F1}x{workArea.Length:F1} | {description}"; Debug.WriteLine(msg); - try { System.IO.File.AppendAllText( + try + { + System.IO.File.AppendAllText( System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"), - $"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); } catch { } + $"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); + } + catch { } progress.Report(new NestProgress { @@ -228,6 +234,7 @@ namespace OpenNest BestParts = clonedParts, Description = description, ActiveWorkArea = workArea, + IsOverallBest = isOverallBest, }); } diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index 49ae0ba..a8ff655 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest { @@ -46,5 +46,6 @@ namespace OpenNest public List BestParts { get; set; } public string Description { get; set; } public Box ActiveWorkArea { get; set; } + public bool IsOverallBest { get; set; } } } diff --git a/OpenNest.Engine/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs similarity index 99% rename from OpenNest.Engine/AutoNester.cs rename to OpenNest.Engine/Nfp/AutoNester.cs index 0140544..55d1d38 100644 --- a/OpenNest.Engine/AutoNester.cs +++ b/OpenNest.Engine/Nfp/AutoNester.cs @@ -1,13 +1,12 @@ -using System; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.Math; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.Math; -namespace OpenNest +namespace OpenNest.Engine.Nfp { /// /// Mixed-part geometry-aware nesting using NFP-based collision avoidance diff --git a/OpenNest.Engine/BottomLeftFill.cs b/OpenNest.Engine/Nfp/BottomLeftFill.cs similarity index 99% rename from OpenNest.Engine/BottomLeftFill.cs rename to OpenNest.Engine/Nfp/BottomLeftFill.cs index 192560b..48ecd1a 100644 --- a/OpenNest.Engine/BottomLeftFill.cs +++ b/OpenNest.Engine/Nfp/BottomLeftFill.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Nfp { /// /// NFP-based Bottom-Left Fill (BLF) placement engine. diff --git a/OpenNest.Engine/INestOptimizer.cs b/OpenNest.Engine/Nfp/INestOptimizer.cs similarity index 95% rename from OpenNest.Engine/INestOptimizer.cs rename to OpenNest.Engine/Nfp/INestOptimizer.cs index 29fde8f..cad4304 100644 --- a/OpenNest.Engine/INestOptimizer.cs +++ b/OpenNest.Engine/Nfp/INestOptimizer.cs @@ -1,8 +1,9 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System.Collections.Generic; using System.Threading; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Nfp { /// /// Result of a nest optimization run. diff --git a/OpenNest.Engine/NfpCache.cs b/OpenNest.Engine/Nfp/NfpCache.cs similarity index 99% rename from OpenNest.Engine/NfpCache.cs rename to OpenNest.Engine/Nfp/NfpCache.cs index bfcabeb..90076b2 100644 --- a/OpenNest.Engine/NfpCache.cs +++ b/OpenNest.Engine/Nfp/NfpCache.cs @@ -1,8 +1,8 @@ +using OpenNest.Geometry; using System; using System.Collections.Generic; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Nfp { /// /// Caches computed No-Fit Polygons keyed by (DrawingA.Id, RotationA, DrawingB.Id, RotationB). diff --git a/OpenNest.Engine/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs similarity index 99% rename from OpenNest.Engine/SimulatedAnnealing.cs rename to OpenNest.Engine/Nfp/SimulatedAnnealing.cs index f36a08a..45462b6 100644 --- a/OpenNest.Engine/SimulatedAnnealing.cs +++ b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs @@ -1,11 +1,12 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Nfp { /// /// Simulated annealing optimizer for NFP-based nesting. diff --git a/OpenNest.Engine/PlateProcessor.cs b/OpenNest.Engine/PlateProcessor.cs index 9601933..99391b8 100644 --- a/OpenNest.Engine/PlateProcessor.cs +++ b/OpenNest.Engine/PlateProcessor.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; -using System.Linq; using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine.RapidPlanning; using OpenNest.Engine.Sequencing; using OpenNest.Geometry; +using System.Collections.Generic; +using System.Linq; namespace OpenNest.Engine { diff --git a/OpenNest.Engine/PlateResult.cs b/OpenNest.Engine/PlateResult.cs index 7209be7..cc7a67a 100644 --- a/OpenNest.Engine/PlateResult.cs +++ b/OpenNest.Engine/PlateResult.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Engine.RapidPlanning; +using System.Collections.Generic; namespace OpenNest.Engine { diff --git a/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs index 154e525..7784c48 100644 --- a/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs +++ b/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Engine.RapidPlanning { diff --git a/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs index edae37c..c33f36e 100644 --- a/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs +++ b/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Engine.RapidPlanning { diff --git a/OpenNest.Engine/RapidPlanning/RapidPath.cs b/OpenNest.Engine/RapidPlanning/RapidPath.cs index 8ff6eb4..f62b8d7 100644 --- a/OpenNest.Engine/RapidPlanning/RapidPath.cs +++ b/OpenNest.Engine/RapidPlanning/RapidPath.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Engine.RapidPlanning { diff --git a/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs index 6de4db6..c090224 100644 --- a/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs +++ b/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.Engine.RapidPlanning { diff --git a/OpenNest.Engine/RectanglePacking/Bin.cs b/OpenNest.Engine/RectanglePacking/Bin.cs index 5ecff28..815913d 100644 --- a/OpenNest.Engine/RectanglePacking/Bin.cs +++ b/OpenNest.Engine/RectanglePacking/Bin.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using OpenNest.Geometry; +using System.Collections.Generic; using System.Linq; -using OpenNest.Geometry; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/BinConverter.cs b/OpenNest.Engine/RectanglePacking/BinConverter.cs index bf9f4cd..1b5c7d9 100644 --- a/OpenNest.Engine/RectanglePacking/BinConverter.cs +++ b/OpenNest.Engine/RectanglePacking/BinConverter.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/FillBestFit.cs b/OpenNest.Engine/RectanglePacking/FillBestFit.cs index fb37247..6a6df2c 100644 --- a/OpenNest.Engine/RectanglePacking/FillBestFit.cs +++ b/OpenNest.Engine/RectanglePacking/FillBestFit.cs @@ -1,6 +1,5 @@ -using System; -using OpenNest.Geometry; -using OpenNest.Math; +using OpenNest.Math; +using System; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/FillEngine.cs b/OpenNest.Engine/RectanglePacking/FillEngine.cs index 5cca401..ac2ae46 100644 --- a/OpenNest.Engine/RectanglePacking/FillEngine.cs +++ b/OpenNest.Engine/RectanglePacking/FillEngine.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using OpenNest.Geometry; +using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/FillNoRotation.cs b/OpenNest.Engine/RectanglePacking/FillNoRotation.cs index f56727a..3fe6443 100644 --- a/OpenNest.Engine/RectanglePacking/FillNoRotation.cs +++ b/OpenNest.Engine/RectanglePacking/FillNoRotation.cs @@ -1,5 +1,4 @@ -using System; -using OpenNest.Geometry; +using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.RectanglePacking diff --git a/OpenNest.Engine/RectanglePacking/FillSameRotation.cs b/OpenNest.Engine/RectanglePacking/FillSameRotation.cs index a7c1504..a7b5a64 100644 --- a/OpenNest.Engine/RectanglePacking/FillSameRotation.cs +++ b/OpenNest.Engine/RectanglePacking/FillSameRotation.cs @@ -1,5 +1,4 @@ -using OpenNest.Geometry; -using OpenNest.Math; +using OpenNest.Math; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/Item.cs b/OpenNest.Engine/RectanglePacking/Item.cs index a78c9d5..5eccda9 100644 --- a/OpenNest.Engine/RectanglePacking/Item.cs +++ b/OpenNest.Engine/RectanglePacking/Item.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; -using OpenNest.Geometry; +using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs b/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs index 7ce8a6a..6548432 100644 --- a/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs +++ b/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; -using System.Linq; -using OpenNest.Geometry; +using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; +using System.Linq; namespace OpenNest.RectanglePacking { @@ -71,7 +71,7 @@ namespace OpenNest.RectanglePacking if (pt.X != double.MaxValue && pt.Y != double.MaxValue) return pt; - + return null; } diff --git a/OpenNest.Engine/RectanglePacking/PackEngine.cs b/OpenNest.Engine/RectanglePacking/PackEngine.cs index c8459a0..9c4f7b3 100644 --- a/OpenNest.Engine/RectanglePacking/PackEngine.cs +++ b/OpenNest.Engine/RectanglePacking/PackEngine.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using OpenNest.Geometry; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs b/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs index b290d4d..6c1a078 100644 --- a/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs +++ b/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using OpenNest.Geometry; +using System.Collections.Generic; using System.Linq; -using OpenNest.Geometry; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs index 106b60c..862d50b 100644 --- a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs +++ b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; -using System.Linq; using OpenNest.CNC.CuttingStrategy; using OpenNest.Math; +using System.Collections.Generic; +using System.Linq; namespace OpenNest.Engine.Sequencing { diff --git a/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs b/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs index 187a599..3ce47a9 100644 --- a/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs +++ b/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs @@ -25,10 +25,10 @@ namespace OpenNest.Engine.Sequencing private static double MinEdgeDistance(OpenNest.Geometry.Vector center, OpenNest.Geometry.Box plateBox) { - var distLeft = center.X - plateBox.Left; - var distRight = plateBox.Right - center.X; + var distLeft = center.X - plateBox.Left; + var distRight = plateBox.Right - center.X; var distBottom = center.Y - plateBox.Bottom; - var distTop = plateBox.Top - center.Y; + var distTop = plateBox.Top - center.Y; return System.Math.Min(System.Math.Min(distLeft, distRight), System.Math.Min(distBottom, distTop)); } diff --git a/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs b/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs index 63b0e2a..045cb1e 100644 --- a/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs +++ b/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; using OpenNest.Math; +using System.Collections.Generic; namespace OpenNest.Engine.Sequencing { diff --git a/OpenNest.Engine/Sequencing/PartSequencerFactory.cs b/OpenNest.Engine/Sequencing/PartSequencerFactory.cs index 0e29d1e..57444eb 100644 --- a/OpenNest.Engine/Sequencing/PartSequencerFactory.cs +++ b/OpenNest.Engine/Sequencing/PartSequencerFactory.cs @@ -1,5 +1,5 @@ -using System; using OpenNest.CNC.CuttingStrategy; +using System; namespace OpenNest.Engine.Sequencing { @@ -9,12 +9,12 @@ namespace OpenNest.Engine.Sequencing { return parameters.Method switch { - SequenceMethod.RightSide => new RightSideSequencer(), - SequenceMethod.LeftSide => new LeftSideSequencer(), + SequenceMethod.RightSide => new RightSideSequencer(), + SequenceMethod.LeftSide => new LeftSideSequencer(), SequenceMethod.BottomSide => new BottomSideSequencer(), - SequenceMethod.EdgeStart => new EdgeStartSequencer(), - SequenceMethod.LeastCode => new LeastCodeSequencer(), - SequenceMethod.Advanced => new AdvancedSequencer(parameters), + SequenceMethod.EdgeStart => new EdgeStartSequencer(), + SequenceMethod.LeastCode => new LeastCodeSequencer(), + SequenceMethod.Advanced => new AdvancedSequencer(parameters), _ => throw new NotSupportedException( $"Sequence method '{parameters.Method}' is not supported.") }; diff --git a/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs b/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs index 67f762e..cc202bb 100644 --- a/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; -using OpenNest.Engine.BestFit; +using OpenNest.Engine.Fill; using OpenNest.Math; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public class ExtentsFillStrategy : IFillStrategy { @@ -20,10 +20,6 @@ namespace OpenNest var angles = new[] { bestRotation, bestRotation + Angle.HalfPI }; - var bestFits = context.SharedState.TryGetValue("BestFits", out var cached) - ? (List)cached - : null; - List best = null; var bestScore = default(FillScore); @@ -31,7 +27,7 @@ namespace OpenNest { context.Token.ThrowIfCancellationRequested(); var result = filler.Fill(context.Item.Drawing, angle, - context.PlateNumber, context.Token, context.Progress, bestFits); + context.PlateNumber, context.Token, context.Progress); if (result != null && result.Count > 0) { var score = FillScore.Compute(result, context.WorkArea); diff --git a/OpenNest.Engine/Strategies/FillContext.cs b/OpenNest.Engine/Strategies/FillContext.cs index 7899668..26b3159 100644 --- a/OpenNest.Engine/Strategies/FillContext.cs +++ b/OpenNest.Engine/Strategies/FillContext.cs @@ -1,9 +1,10 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; -using OpenNest.Geometry; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public class FillContext { diff --git a/OpenNest.Engine/Strategies/FillHelpers.cs b/OpenNest.Engine/Strategies/FillHelpers.cs index 1a3c005..6533568 100644 --- a/OpenNest.Engine/Strategies/FillHelpers.cs +++ b/OpenNest.Engine/Strategies/FillHelpers.cs @@ -1,10 +1,11 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using OpenNest.Math; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; -using OpenNest.Geometry; -using OpenNest.Math; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public static class FillHelpers { diff --git a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs index f146cdf..581b4bf 100644 --- a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs +++ b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs @@ -5,12 +5,13 @@ using System.IO; using System.Linq; using System.Reflection; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public static class FillStrategyRegistry { private static readonly List strategies = new(); private static List sorted; + private static HashSet enabledFilter; static FillStrategyRegistry() { @@ -18,7 +19,21 @@ namespace OpenNest } public static IReadOnlyList Strategies => - sorted ??= strategies.OrderBy(s => s.Order).ToList(); + sorted ??= (enabledFilter != null + ? strategies.Where(s => enabledFilter.Contains(s.Name)).OrderBy(s => s.Order).ToList() + : strategies.OrderBy(s => s.Order).ToList()); + + /// + /// Restricts the active strategies to only those whose names are listed. + /// Pass null to restore all strategies. + /// + public static void SetEnabled(params string[] names) + { + enabledFilter = names != null && names.Length > 0 + ? new HashSet(names, StringComparer.OrdinalIgnoreCase) + : null; + sorted = null; + } public static void LoadFrom(Assembly assembly) { diff --git a/OpenNest.Engine/Strategies/IFillStrategy.cs b/OpenNest.Engine/Strategies/IFillStrategy.cs index 86170f0..5dc7514 100644 --- a/OpenNest.Engine/Strategies/IFillStrategy.cs +++ b/OpenNest.Engine/Strategies/IFillStrategy.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public interface IFillStrategy { diff --git a/OpenNest.Engine/Strategies/LinearFillStrategy.cs b/OpenNest.Engine/Strategies/LinearFillStrategy.cs index dfe84ef..1d733c8 100644 --- a/OpenNest.Engine/Strategies/LinearFillStrategy.cs +++ b/OpenNest.Engine/Strategies/LinearFillStrategy.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using OpenNest.Engine.Fill; using OpenNest.Math; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public class LinearFillStrategy : IFillStrategy { diff --git a/OpenNest.Engine/Strategies/PairsFillStrategy.cs b/OpenNest.Engine/Strategies/PairsFillStrategy.cs index 904c220..118c732 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -1,7 +1,7 @@ +using OpenNest.Engine.Fill; using System.Collections.Generic; -using OpenNest.Engine.BestFit; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public class PairsFillStrategy : IFillStrategy { @@ -15,11 +15,7 @@ namespace OpenNest var result = filler.Fill(context.Item, context.WorkArea, context.PlateNumber, context.Token, context.Progress); - // Cache hit — PairFiller already called GetOrCompute internally. - var bestFits = BestFitCache.GetOrCompute( - context.Item.Drawing, context.Plate.Size.Length, - context.Plate.Size.Width, context.Plate.PartSpacing); - context.SharedState["BestFits"] = bestFits; + context.SharedState["BestFits"] = filler.BestFits; return result; } diff --git a/OpenNest.Engine/Strategies/RectBestFitStrategy.cs b/OpenNest.Engine/Strategies/RectBestFitStrategy.cs index 81d1011..d045b2c 100644 --- a/OpenNest.Engine/Strategies/RectBestFitStrategy.cs +++ b/OpenNest.Engine/Strategies/RectBestFitStrategy.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using OpenNest.RectanglePacking; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public class RectBestFitStrategy : IFillStrategy { diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index a2da5f1..d114f11 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -1,8 +1,9 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using OpenNest.Geometry; namespace OpenNest { @@ -182,16 +183,6 @@ namespace OpenNest var bestParts = shrinkResult.Parts; var bestDim = shrinkResult.Dimension; - // TODO: Compact strip parts individually to close geometry-based gaps. - // Disabled pending investigation — remnant finder picks up gaps created - // by compaction and scatters parts into them. - // Compactor.CompactIndividual(bestParts, workArea, Plate.PartSpacing); - // - // var compactedBox = bestParts.Cast().GetBoundingBox(); - // bestDim = direction == StripDirection.Bottom - // ? compactedBox.Top - workArea.Y - // : compactedBox.Right - workArea.X; - // Build remnant box with spacing gap. var spacing = Plate.PartSpacing; var remnantBox = direction == StripDirection.Bottom diff --git a/OpenNest.Engine/StripNestResult.cs b/OpenNest.Engine/StripNestResult.cs index 44181ca..46cc386 100644 --- a/OpenNest.Engine/StripNestResult.cs +++ b/OpenNest.Engine/StripNestResult.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using OpenNest.Engine.Fill; using OpenNest.Geometry; +using System.Collections.Generic; namespace OpenNest { diff --git a/OpenNest.Gpu/GpuEvaluatorFactory.cs b/OpenNest.Gpu/GpuEvaluatorFactory.cs index 9ee04f4..22c2e5e 100644 --- a/OpenNest.Gpu/GpuEvaluatorFactory.cs +++ b/OpenNest.Gpu/GpuEvaluatorFactory.cs @@ -1,8 +1,8 @@ -using System; -using System.Diagnostics; using ILGPU; using ILGPU.Runtime; using OpenNest.Engine.BestFit; +using System; +using System.Diagnostics; namespace OpenNest.Gpu { diff --git a/OpenNest.Gpu/GpuPairEvaluator.cs b/OpenNest.Gpu/GpuPairEvaluator.cs index 4a6e502..7ff20f7 100644 --- a/OpenNest.Gpu/GpuPairEvaluator.cs +++ b/OpenNest.Gpu/GpuPairEvaluator.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using ILGPU; using ILGPU.Runtime; using OpenNest.Converters; using OpenNest.Engine.BestFit; using OpenNest.Geometry; -using OpenNest.Math; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace OpenNest.Gpu { diff --git a/OpenNest.Gpu/GpuSlideComputer.cs b/OpenNest.Gpu/GpuSlideComputer.cs index b6c2ea0..4e00511 100644 --- a/OpenNest.Gpu/GpuSlideComputer.cs +++ b/OpenNest.Gpu/GpuSlideComputer.cs @@ -1,8 +1,8 @@ -using System; using ILGPU; -using ILGPU.Runtime; using ILGPU.Algorithms; +using ILGPU.Runtime; using OpenNest.Engine.BestFit; +using System; namespace OpenNest.Gpu { @@ -13,7 +13,7 @@ namespace OpenNest.Gpu private readonly object _lock = new object(); // ── Kernels ────────────────────────────────────────────────── - + private readonly Action, // stationaryPrep ArrayView1D, // movingPrep @@ -152,12 +152,12 @@ namespace OpenNest.Gpu private void EnsureStationary(double[] data, int count) { // Fast check: if same object or content is identical, skip upload - if (_gpuStationaryPrep != null && - _lastStationaryData != null && + if (_gpuStationaryPrep != null && + _lastStationaryData != null && _lastStationaryData.Length == data.Length) { // Reference equality or content equality - if (_lastStationaryData == data || + if (_lastStationaryData == data || new ReadOnlySpan(_lastStationaryData).SequenceEqual(new ReadOnlySpan(data))) { return; @@ -178,11 +178,11 @@ namespace OpenNest.Gpu private void EnsureMoving(double[] data, int count) { - if (_gpuMovingPrep != null && - _lastMovingData != null && - _lastMovingData.Length == data.Length) + if (_gpuMovingPrep != null && + _lastMovingData != null && + _lastMovingData.Length == data.Length) { - if (_lastMovingData == data || + if (_lastMovingData == data || new ReadOnlySpan(_lastMovingData).SequenceEqual(new ReadOnlySpan(data))) { return; @@ -240,7 +240,7 @@ namespace OpenNest.Gpu var dx = x2 - x1; var dy = y2 - y1; - + // invD is used for parameter 't'. We use a small epsilon for stability. prepared[index * 10 + 4] = (XMath.Abs(dx) < 1e-9) ? 0 : 1.0 / dx; prepared[index * 10 + 5] = (XMath.Abs(dy) < 1e-9) ? 0 : 1.0 / dy; @@ -265,7 +265,7 @@ namespace OpenNest.Gpu var dx = offsets[index * 2]; var dy = offsets[index * 2 + 1]; - + results[index] = ComputeSlideLean( stationaryPrep, movingPrep, dx, dy, sCount, mCount, direction); } diff --git a/OpenNest.Gpu/PartBitmap.cs b/OpenNest.Gpu/PartBitmap.cs index 9b729ef..f75c2b3 100644 --- a/OpenNest.Gpu/PartBitmap.cs +++ b/OpenNest.Gpu/PartBitmap.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; +using System; +using System.Collections.Generic; +using System.Linq; namespace OpenNest.Gpu { diff --git a/OpenNest.IO/DxfExporter.cs b/OpenNest.IO/DxfExporter.cs index 2cfa222..ff7faf7 100644 --- a/OpenNest.IO/DxfExporter.cs +++ b/OpenNest.IO/DxfExporter.cs @@ -1,13 +1,10 @@ -using System; -using System.Diagnostics; -using System.IO; using ACadSharp; -using ACadSharp.Entities; using ACadSharp.IO; -using ACadSharp.Tables; using CSMath; using OpenNest.CNC; using OpenNest.Math; +using System.Diagnostics; +using System.IO; namespace OpenNest.IO { diff --git a/OpenNest.IO/DxfImporter.cs b/OpenNest.IO/DxfImporter.cs index c604a69..3aca509 100644 --- a/OpenNest.IO/DxfImporter.cs +++ b/OpenNest.IO/DxfImporter.cs @@ -1,10 +1,10 @@ +using ACadSharp; +using ACadSharp.IO; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using ACadSharp; -using ACadSharp.IO; -using OpenNest.Geometry; namespace OpenNest.IO { diff --git a/OpenNest.IO/Extensions.cs b/OpenNest.IO/Extensions.cs index faa1f71..a7d4be3 100644 --- a/OpenNest.IO/Extensions.cs +++ b/OpenNest.IO/Extensions.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using ACadSharp.Entities; using CSMath; using OpenNest.Geometry; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; namespace OpenNest.IO { @@ -26,9 +25,9 @@ namespace OpenNest.IO arc.Center.X, arc.Center.Y, arc.Radius, arc.StartAngle, arc.EndAngle) - { - Layer = arc.Layer.ToOpenNest() - }; + { + Layer = arc.Layer.ToOpenNest() + }; result.ApplyDxfProperties(arc); return result; } @@ -38,9 +37,9 @@ namespace OpenNest.IO var result = new Geometry.Circle( circle.Center.X, circle.Center.Y, circle.Radius) - { - Layer = circle.Layer.ToOpenNest() - }; + { + Layer = circle.Layer.ToOpenNest() + }; result.ApplyDxfProperties(circle); return result; } @@ -50,9 +49,9 @@ namespace OpenNest.IO var result = new Geometry.Line( line.StartPoint.X, line.StartPoint.Y, line.EndPoint.X, line.EndPoint.Y) - { - Layer = line.Layer.ToOpenNest() - }; + { + Layer = line.Layer.ToOpenNest() + }; result.ApplyDxfProperties(line); return result; } @@ -76,7 +75,9 @@ namespace OpenNest.IO lines.Add(new Geometry.Line(lastPoint, nextPoint) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); lastPoint = nextPoint; @@ -85,7 +86,9 @@ namespace OpenNest.IO if (spline.IsClosed) lines.Add(new Geometry.Line(lastPoint, pts[0].ToOpenNest()) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); return lines; @@ -109,7 +112,9 @@ namespace OpenNest.IO lines.Add(new Geometry.Line(lastPoint, nextPoint) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); lastPoint = nextPoint; @@ -120,7 +125,9 @@ namespace OpenNest.IO if (isClosed) lines.Add(new Geometry.Line(lastPoint, polyline.Vertices[0].Location.ToOpenNest()) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); return lines; @@ -144,7 +151,9 @@ namespace OpenNest.IO lines.Add(new Geometry.Line(lastPoint, nextPoint) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); lastPoint = nextPoint; @@ -155,7 +164,9 @@ namespace OpenNest.IO if (isClosed) lines.Add(new Geometry.Line(lastPoint, polyline.Vertices[0].ToOpenNest()) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); return lines; @@ -204,7 +215,9 @@ namespace OpenNest.IO { lines.Add(new Geometry.Line(points[i], points[i + 1]) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); } @@ -215,7 +228,9 @@ namespace OpenNest.IO var last = lines.Last(); lines.Add(new Geometry.Line(last.EndPoint, first.StartPoint) { - Layer = layer, Color = color, LineTypeName = lineTypeName + Layer = layer, + Color = color, + LineTypeName = lineTypeName }); } diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs index e88889e..99efcec 100644 --- a/OpenNest.IO/NestReader.cs +++ b/OpenNest.IO/NestReader.cs @@ -1,3 +1,6 @@ +using OpenNest.CNC; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; @@ -5,9 +8,6 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Text.Json; -using OpenNest.CNC; -using OpenNest.Engine.BestFit; -using OpenNest.Geometry; using static OpenNest.IO.NestFormat; namespace OpenNest.IO diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index 4ad324b..23cf581 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -1,3 +1,5 @@ +using OpenNest.CNC; +using OpenNest.Engine.BestFit; using System; using System.Collections.Generic; using System.IO; @@ -5,10 +7,6 @@ using System.IO.Compression; using System.Linq; using System.Text; using System.Text.Json; -using OpenNest.CNC; -using OpenNest.Engine.BestFit; -using OpenNest.Geometry; -using OpenNest.Math; using static OpenNest.IO.NestFormat; namespace OpenNest.IO diff --git a/OpenNest.IO/ProgramReader.cs b/OpenNest.IO/ProgramReader.cs index 6776d22..316f42a 100644 --- a/OpenNest.IO/ProgramReader.cs +++ b/OpenNest.IO/ProgramReader.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.Geometry; +using System.Collections.Generic; using System.IO; using System.Text; -using OpenNest.CNC; -using OpenNest.Geometry; namespace OpenNest.IO { diff --git a/OpenNest.Mcp/Program.cs b/OpenNest.Mcp/Program.cs index c924f9f..3ff768a 100644 --- a/OpenNest.Mcp/Program.cs +++ b/OpenNest.Mcp/Program.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using ModelContextProtocol.Server; using OpenNest.Mcp; var builder = Host.CreateApplicationBuilder(args); diff --git a/OpenNest.Mcp/Tools/InputTools.cs b/OpenNest.Mcp/Tools/InputTools.cs index 55d4d4b..729c08c 100644 --- a/OpenNest.Mcp/Tools/InputTools.cs +++ b/OpenNest.Mcp/Tools/InputTools.cs @@ -1,11 +1,10 @@ +using ModelContextProtocol.Server; +using OpenNest.Converters; +using OpenNest.IO; +using OpenNest.Shapes; using System.ComponentModel; using System.IO; using System.Text; -using ModelContextProtocol.Server; -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.IO; -using OpenNest.Shapes; using CncProgram = OpenNest.CNC.Program; namespace OpenNest.Mcp.Tools diff --git a/OpenNest.Mcp/Tools/InspectionTools.cs b/OpenNest.Mcp/Tools/InspectionTools.cs index b2f4eaa..f72a1b9 100644 --- a/OpenNest.Mcp/Tools/InspectionTools.cs +++ b/OpenNest.Mcp/Tools/InspectionTools.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; +using ModelContextProtocol.Server; +using OpenNest.Engine.Fill; +using OpenNest.Math; using System.ComponentModel; using System.Linq; using System.Text; -using ModelContextProtocol.Server; -using OpenNest.Geometry; -using OpenNest.Math; namespace OpenNest.Mcp.Tools { diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs index de555cf..78c0c8f 100644 --- a/OpenNest.Mcp/Tools/NestingTools.cs +++ b/OpenNest.Mcp/Tools/NestingTools.cs @@ -1,10 +1,11 @@ +using ModelContextProtocol.Server; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading; -using ModelContextProtocol.Server; -using OpenNest.Geometry; namespace OpenNest.Mcp.Tools { diff --git a/OpenNest.Mcp/Tools/SetupTools.cs b/OpenNest.Mcp/Tools/SetupTools.cs index 154503d..fa0bf71 100644 --- a/OpenNest.Mcp/Tools/SetupTools.cs +++ b/OpenNest.Mcp/Tools/SetupTools.cs @@ -1,7 +1,7 @@ -using System.ComponentModel; -using System.Text; using ModelContextProtocol.Server; using OpenNest.Geometry; +using System.ComponentModel; +using System.Text; namespace OpenNest.Mcp.Tools { diff --git a/OpenNest.Mcp/Tools/TestTools.cs b/OpenNest.Mcp/Tools/TestTools.cs index bcaf299..05401e6 100644 --- a/OpenNest.Mcp/Tools/TestTools.cs +++ b/OpenNest.Mcp/Tools/TestTools.cs @@ -1,8 +1,8 @@ +using ModelContextProtocol.Server; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Text; -using ModelContextProtocol.Server; namespace OpenNest.Mcp.Tools { diff --git a/OpenNest.Tests/AccumulatingProgressTests.cs b/OpenNest.Tests/AccumulatingProgressTests.cs index 2df42c5..870e859 100644 --- a/OpenNest.Tests/AccumulatingProgressTests.cs +++ b/OpenNest.Tests/AccumulatingProgressTests.cs @@ -1,3 +1,5 @@ +using OpenNest.Engine.Fill; + namespace OpenNest.Tests; public class AccumulatingProgressTests diff --git a/OpenNest.Tests/AngleCandidateBuilderTests.cs b/OpenNest.Tests/AngleCandidateBuilderTests.cs index 29d26bf..a76313d 100644 --- a/OpenNest.Tests/AngleCandidateBuilderTests.cs +++ b/OpenNest.Tests/AngleCandidateBuilderTests.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Fill; using OpenNest.Geometry; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs new file mode 100644 index 0000000..8d3e2c5 --- /dev/null +++ b/OpenNest.Tests/CompactorTests.cs @@ -0,0 +1,165 @@ +using OpenNest; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using Xunit; +using System.Collections.Generic; + +namespace OpenNest.Tests +{ + public class CompactorTests + { + private static Drawing MakeRectDrawing(double w, double h) + { + var pgm = new OpenNest.CNC.Program(); + pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0))); + return new Drawing("rect", pgm); + } + + private static Part MakeRectPart(double x, double y, double w, double h) + { + var drawing = MakeRectDrawing(w, h); + var part = new Part(drawing) { Location = new Vector(x, y) }; + part.UpdateBounds(); + return part; + } + + [Fact] + public void Push_Left_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Left < 1); + } + + [Fact] + public void Push_Left_StopsAtObstacle() + { + var workArea = new Box(0, 0, 100, 100); + var obstacle = MakeRectPart(0, 0, 10, 10); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List { obstacle }; + + Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1); + } + + [Fact] + public void Push_Down_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(0, 50, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Down); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Bottom < 1); + } + + [Fact] + public void Push_ReturnsZero_WhenAlreadyAtEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(0, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.Equal(0, distance); + } + + [Fact] + public void Push_WithSpacing_MovesLessThanWithout() + { + var workArea = new Box(0, 0, 100, 100); + + // Push without spacing. + var obstacle1 = MakeRectPart(0, 0, 10, 10); + var part1 = MakeRectPart(50, 0, 10, 10); + var distNoSpacing = Compactor.Push(new List { part1 }, new List { obstacle1 }, workArea, 0, PushDirection.Left); + + // Push with spacing. + var obstacle2 = MakeRectPart(0, 0, 10, 10); + var part2 = MakeRectPart(50, 0, 10, 10); + var distWithSpacing = Compactor.Push(new List { part2 }, new List { obstacle2 }, workArea, 2, PushDirection.Left); + + // Spacing should cause the part to stop at a different position than without spacing. + Assert.NotEqual(distNoSpacing, distWithSpacing); + } + + [Fact] + public void Push_AngleLeft_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + // direction = left + var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI)); + var distance = Compactor.Push(moving, obstacles, workArea, 0, direction); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Left < 1); + } + + [Fact] + public void Push_AngleDown_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(0, 50, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + // direction = down + var angle = 3 * System.Math.PI / 2; + var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); + var distance = Compactor.Push(moving, obstacles, workArea, 0, direction); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Bottom < 1); + } + + [Fact] + public void PushBoundingBox_Left_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Left < 1); + } + + [Fact] + public void PushBoundingBox_StopsAtObstacle() + { + var workArea = new Box(0, 0, 100, 100); + var obstacle = MakeRectPart(0, 0, 10, 10); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List { obstacle }; + + Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1); + } + } +} diff --git a/OpenNest.Tests/CuttingResultTests.cs b/OpenNest.Tests/CuttingResultTests.cs index 58cbefc..389985d 100644 --- a/OpenNest.Tests/CuttingResultTests.cs +++ b/OpenNest.Tests/CuttingResultTests.cs @@ -1,7 +1,6 @@ using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/FillExtentsTests.cs b/OpenNest.Tests/FillExtentsTests.cs index 3271ec6..d379778 100644 --- a/OpenNest.Tests/FillExtentsTests.cs +++ b/OpenNest.Tests/FillExtentsTests.cs @@ -1,4 +1,5 @@ using OpenNest.CNC; +using OpenNest.Engine.Fill; using OpenNest.Geometry; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/FillScoreTests.cs b/OpenNest.Tests/FillScoreTests.cs index a069675..1619a16 100644 --- a/OpenNest.Tests/FillScoreTests.cs +++ b/OpenNest.Tests/FillScoreTests.cs @@ -1,3 +1,5 @@ +using OpenNest.Engine.Fill; + namespace OpenNest.Tests; public class FillScoreTests diff --git a/OpenNest.Tests/PairFillerTests.cs b/OpenNest.Tests/PairFillerTests.cs index d2199b7..06c654d 100644 --- a/OpenNest.Tests/PairFillerTests.cs +++ b/OpenNest.Tests/PairFillerTests.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Fill; using OpenNest.Geometry; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/PartFlagTests.cs b/OpenNest.Tests/PartFlagTests.cs index f140dd2..43a9e8f 100644 --- a/OpenNest.Tests/PartFlagTests.cs +++ b/OpenNest.Tests/PartFlagTests.cs @@ -1,6 +1,5 @@ using OpenNest.CNC; using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/PatternTilerTests.cs b/OpenNest.Tests/PatternTilerTests.cs index 8b704b5..efaa916 100644 --- a/OpenNest.Tests/PatternTilerTests.cs +++ b/OpenNest.Tests/PatternTilerTests.cs @@ -1,5 +1,4 @@ -using OpenNest; -using OpenNest.Engine; +using OpenNest.Engine.Fill; using OpenNest.Geometry; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/PlateProcessorTests.cs b/OpenNest.Tests/PlateProcessorTests.cs index 73d5a98..3c891ff 100644 --- a/OpenNest.Tests/PlateProcessorTests.cs +++ b/OpenNest.Tests/PlateProcessorTests.cs @@ -1,12 +1,7 @@ -using System.Collections.Generic; -using System.Linq; -using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine; using OpenNest.Engine.RapidPlanning; using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs b/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs index ca6a00b..793876e 100644 --- a/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs +++ b/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using OpenNest.Engine.RapidPlanning; using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests.RapidPlanning; diff --git a/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs b/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs index 3db408f..1fa0c41 100644 --- a/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs +++ b/OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using OpenNest.Engine.RapidPlanning; using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests.RapidPlanning; diff --git a/OpenNest.Tests/RemnantFillerTests2.cs b/OpenNest.Tests/RemnantFillerTests2.cs index 2ee8c6a..d7b9037 100644 --- a/OpenNest.Tests/RemnantFillerTests2.cs +++ b/OpenNest.Tests/RemnantFillerTests2.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Fill; using OpenNest.Geometry; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/RemnantFinderTests.cs b/OpenNest.Tests/RemnantFinderTests.cs index 45734c5..309e46b 100644 --- a/OpenNest.Tests/RemnantFinderTests.cs +++ b/OpenNest.Tests/RemnantFinderTests.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Fill; using OpenNest.Geometry; using OpenNest.IO; diff --git a/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs b/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs index 69f540f..1ddf277 100644 --- a/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs +++ b/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; -using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests.Sequencing; diff --git a/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs b/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs index 0a64a98..13dfc8f 100644 --- a/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs +++ b/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using OpenNest.CNC; using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests.Sequencing; diff --git a/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs b/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs index 3f002de..b6a07d1 100644 --- a/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs +++ b/OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using OpenNest.CNC; using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests.Sequencing; diff --git a/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs b/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs index a5929c5..51e9f25 100644 --- a/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs +++ b/OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using OpenNest.CNC; using OpenNest.Engine.Sequencing; -using OpenNest.Geometry; -using Xunit; namespace OpenNest.Tests.Sequencing; diff --git a/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs b/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs index 1c6a22e..61e920f 100644 --- a/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs +++ b/OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs @@ -1,7 +1,5 @@ -using System; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine.Sequencing; -using Xunit; namespace OpenNest.Tests.Sequencing; diff --git a/OpenNest.Tests/Shapes/FlangeShapeTests.cs b/OpenNest.Tests/Shapes/FlangeShapeTests.cs index eca87c4..931582d 100644 --- a/OpenNest.Tests/Shapes/FlangeShapeTests.cs +++ b/OpenNest.Tests/Shapes/FlangeShapeTests.cs @@ -1,4 +1,3 @@ -using OpenNest.CNC; using OpenNest.Shapes; namespace OpenNest.Tests.Shapes; diff --git a/OpenNest.Tests/ShrinkFillerTests.cs b/OpenNest.Tests/ShrinkFillerTests.cs index 5368d51..8d50f32 100644 --- a/OpenNest.Tests/ShrinkFillerTests.cs +++ b/OpenNest.Tests/ShrinkFillerTests.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Fill; using OpenNest.Geometry; namespace OpenNest.Tests; diff --git a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs index 497b533..0458632 100644 --- a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs +++ b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs @@ -1,3 +1,5 @@ +using OpenNest.Engine.Strategies; + namespace OpenNest.Tests.Strategies; public class FillStrategyRegistryTests diff --git a/OpenNest.Training/Program.cs b/OpenNest.Training/Program.cs index 75f94b6..a584169 100644 --- a/OpenNest.Training/Program.cs +++ b/OpenNest.Training/Program.cs @@ -1,15 +1,15 @@ +using OpenNest; +using OpenNest.Engine.BestFit; +using OpenNest.Engine.ML; +using OpenNest.Geometry; +using OpenNest.Gpu; +using OpenNest.IO; +using OpenNest.Training; using System; using System.Diagnostics; using System.IO; using System.Linq; -using OpenNest; -using OpenNest.Geometry; -using OpenNest.IO; using Color = System.Drawing.Color; -using OpenNest.Engine.BestFit; -using OpenNest.Engine.ML; -using OpenNest.Gpu; -using OpenNest.Training; // Parse arguments. var dbPath = "OpenNestTraining"; diff --git a/OpenNest.Training/TrainingDatabase.cs b/OpenNest.Training/TrainingDatabase.cs index 97eb46e..a89e025 100644 --- a/OpenNest.Training/TrainingDatabase.cs +++ b/OpenNest.Training/TrainingDatabase.cs @@ -1,12 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using OpenNest.Engine.ML; +using OpenNest.IO; +using OpenNest.Training.Data; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using Microsoft.EntityFrameworkCore; -using OpenNest.Engine.ML; -using OpenNest.IO; -using OpenNest.Training.Data; namespace OpenNest.Training { diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index 669ccf5..7c3ab96 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using OpenNest.Controls; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Geometry; namespace OpenNest.Actions { @@ -71,7 +72,7 @@ namespace OpenNest.Actions { if (plateView.ViewScale != lastScale) { - parts.ForEach(p => + parts.ForEach(p => { p.Update(plateView); p.Draw(e.Graphics); @@ -146,11 +147,11 @@ namespace OpenNest.Actions PushDirection hDir, vDir; switch (plateView.Plate.Quadrant) { - case 1: hDir = PushDirection.Left; vDir = PushDirection.Down; break; - case 2: hDir = PushDirection.Right; vDir = PushDirection.Down; break; - case 3: hDir = PushDirection.Right; vDir = PushDirection.Up; break; - case 4: hDir = PushDirection.Left; vDir = PushDirection.Up; break; - default: hDir = PushDirection.Left; vDir = PushDirection.Down; break; + case 1: hDir = PushDirection.Left; vDir = PushDirection.Down; break; + case 2: hDir = PushDirection.Right; vDir = PushDirection.Down; break; + case 3: hDir = PushDirection.Right; vDir = PushDirection.Up; break; + case 4: hDir = PushDirection.Left; vDir = PushDirection.Up; break; + default: hDir = PushDirection.Left; vDir = PushDirection.Down; break; } // Phase 1: BB-only push to get past irregular geometry quickly. diff --git a/OpenNest/Actions/ActionFillArea.cs b/OpenNest/Actions/ActionFillArea.cs index 4101232..8307184 100644 --- a/OpenNest/Actions/ActionFillArea.cs +++ b/OpenNest/Actions/ActionFillArea.cs @@ -1,10 +1,10 @@ +using OpenNest.Controls; using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using OpenNest.Controls; namespace OpenNest.Actions { diff --git a/OpenNest/Actions/ActionSelect.cs b/OpenNest/Actions/ActionSelect.cs index 4ed0eff..228cfec 100644 --- a/OpenNest/Actions/ActionSelect.cs +++ b/OpenNest/Actions/ActionSelect.cs @@ -1,8 +1,8 @@ -using System.ComponentModel; +using OpenNest.Controls; +using OpenNest.Geometry; +using System.ComponentModel; using System.Drawing; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Geometry; namespace OpenNest.Actions { diff --git a/OpenNest/Actions/ActionSelectArea.cs b/OpenNest/Actions/ActionSelectArea.cs index 77b3ff3..542ed8c 100644 --- a/OpenNest/Actions/ActionSelectArea.cs +++ b/OpenNest/Actions/ActionSelectArea.cs @@ -1,11 +1,9 @@ -using System; +using OpenNest.Controls; +using OpenNest.Geometry; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; -using System.Linq; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Geometry; namespace OpenNest.Actions { @@ -94,12 +92,12 @@ namespace OpenNest.Actions var location = plateView.PointWorldToGraph(SelectedArea.Location); var size = new SizeF( - plateView.LengthWorldToGui(SelectedArea.Width), + plateView.LengthWorldToGui(SelectedArea.Width), plateView.LengthWorldToGui(SelectedArea.Length)); var rect = new System.Drawing.RectangleF(location.X, location.Y - size.Height, size.Width, size.Height); - e.Graphics.DrawRectangle(pen, + e.Graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, @@ -109,9 +107,9 @@ namespace OpenNest.Actions e.Graphics.DrawString( SelectedArea.Size.ToString(2), - font, - Brushes.Green, - rect, + font, + Brushes.Green, + rect, stringFormat); } diff --git a/OpenNest/Actions/ActionSetSequence.cs b/OpenNest/Actions/ActionSetSequence.cs index fb2ed33..eaf87d0 100644 --- a/OpenNest/Actions/ActionSetSequence.cs +++ b/OpenNest/Actions/ActionSetSequence.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using OpenNest.Controls; +using OpenNest.Converters; +using OpenNest.Forms; +using OpenNest.Geometry; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Converters; -using OpenNest.Forms; -using OpenNest.Geometry; namespace OpenNest.Actions { diff --git a/OpenNest/Actions/ActionZoomWindow.cs b/OpenNest/Actions/ActionZoomWindow.cs index 51736fb..e9843f0 100644 --- a/OpenNest/Actions/ActionZoomWindow.cs +++ b/OpenNest/Actions/ActionZoomWindow.cs @@ -1,8 +1,8 @@ -using System.ComponentModel; +using OpenNest.Controls; +using OpenNest.Geometry; +using System.ComponentModel; using System.Drawing; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Geometry; namespace OpenNest.Actions { diff --git a/OpenNest/ColorScheme.cs b/OpenNest/ColorScheme.cs index f525862..b0911d0 100644 --- a/OpenNest/ColorScheme.cs +++ b/OpenNest/ColorScheme.cs @@ -59,6 +59,10 @@ namespace OpenNest public Brush PreviewPartBrush { get; private set; } + public Pen ActivePreviewPartPen { get; private set; } + + public Brush ActivePreviewPartBrush { get; private set; } + #endregion Pens/Brushes #region Colors @@ -170,8 +174,16 @@ namespace OpenNest if (PreviewPartBrush != null) PreviewPartBrush.Dispose(); + if (ActivePreviewPartPen != null) + ActivePreviewPartPen.Dispose(); + + if (ActivePreviewPartBrush != null) + ActivePreviewPartBrush.Dispose(); + PreviewPartPen = new Pen(value, 1); PreviewPartBrush = new SolidBrush(Color.FromArgb(60, value)); + ActivePreviewPartPen = new Pen(Color.FromArgb(128, value), 1); + ActivePreviewPartBrush = new SolidBrush(Color.FromArgb(30, value)); } } diff --git a/OpenNest/Controls/BestFitCell.cs b/OpenNest/Controls/BestFitCell.cs index 39dc192..eed1fab 100644 --- a/OpenNest/Controls/BestFitCell.cs +++ b/OpenNest/Controls/BestFitCell.cs @@ -1,8 +1,8 @@ +using OpenNest.Engine.BestFit; +using OpenNest.Math; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; -using OpenNest.Engine.BestFit; -using OpenNest.Math; namespace OpenNest.Controls { diff --git a/OpenNest/Controls/DensityBar.cs b/OpenNest/Controls/DensityBar.cs new file mode 100644 index 0000000..88eb476 --- /dev/null +++ b/OpenNest/Controls/DensityBar.cs @@ -0,0 +1,71 @@ +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace OpenNest.Controls +{ + public class DensityBar : Control + { + private static readonly Color TrackColor = Color.FromArgb(224, 224, 224); + private static readonly Color LowColor = Color.FromArgb(245, 166, 35); + private static readonly Color HighColor = Color.FromArgb(76, 175, 80); + + private double value; + + public DensityBar() + { + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + Size = new Size(60, 8); + } + + public double Value + { + get => value; + set + { + this.value = System.Math.Clamp(value, 0.0, 1.0); + Invalidate(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + var rect = new Rectangle(0, 0, Width - 1, Height - 1); + + // Track background + using var trackPath = CreateRoundedRect(rect, 4); + using var trackBrush = new SolidBrush(TrackColor); + g.FillPath(trackBrush, trackPath); + + // Fill + var fillWidth = (int)(rect.Width * value); + if (fillWidth > 0) + { + var fillRadius = System.Math.Min(4, fillWidth / 2); + var fillRect = new Rectangle(rect.X, rect.Y, fillWidth, rect.Height); + using var fillPath = CreateRoundedRect(fillRect, fillRadius); + using var gradientBrush = new LinearGradientBrush( + new Point(rect.X, 0), new Point(rect.Right, 0), + LowColor, HighColor); + g.FillPath(gradientBrush, fillPath); + } + } + + private static GraphicsPath CreateRoundedRect(Rectangle rect, int radius) + { + var path = new GraphicsPath(); + var d = radius * 2; + path.AddArc(rect.X, rect.Y, d, d, 180, 90); + path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); + path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90); + path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); + path.CloseFigure(); + return path; + } + } +} diff --git a/OpenNest/Controls/DrawControl.cs b/OpenNest/Controls/DrawControl.cs index 3d9f2fc..6f0b271 100644 --- a/OpenNest/Controls/DrawControl.cs +++ b/OpenNest/Controls/DrawControl.cs @@ -1,8 +1,8 @@ -using System; +using OpenNest.Geometry; +using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; -using OpenNest.Geometry; namespace OpenNest.Controls { diff --git a/OpenNest/Controls/DrawingListBox.cs b/OpenNest/Controls/DrawingListBox.cs index 2a1b444..916ed00 100644 --- a/OpenNest/Controls/DrawingListBox.cs +++ b/OpenNest/Controls/DrawingListBox.cs @@ -1,5 +1,4 @@ -using System; -using System.Drawing; +using System.Drawing; using System.Windows.Forms; namespace OpenNest.Controls diff --git a/OpenNest/Controls/EntityView.cs b/OpenNest/Controls/EntityView.cs index 98db8dd..485c3f5 100644 --- a/OpenNest/Controls/EntityView.cs +++ b/OpenNest/Controls/EntityView.cs @@ -1,10 +1,9 @@ -using System; +using OpenNest.Geometry; +using OpenNest.Math; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; -using OpenNest.Geometry; -using OpenNest.Math; namespace OpenNest.Controls { @@ -137,7 +136,7 @@ namespace OpenNest.Controls var diameter = radius * 2.0f; var startAngle = arc.IsReversed - ? -(float)Angle.ToDegrees(arc.EndAngle) + ? -(float)Angle.ToDegrees(arc.EndAngle) : -(float)Angle.ToDegrees(arc.StartAngle); g.DrawArc( diff --git a/OpenNest/Controls/NumericUpDown.cs b/OpenNest/Controls/NumericUpDown.cs index 3518fb8..bc9557a 100644 --- a/OpenNest/Controls/NumericUpDown.cs +++ b/OpenNest/Controls/NumericUpDown.cs @@ -6,7 +6,7 @@ namespace OpenNest.Controls { private string suffix; - + public NumericUpDown() { @@ -16,8 +16,8 @@ namespace OpenNest.Controls public string Suffix { get { return suffix; } - set - { + set + { suffix = value; UpdateEditText(); } diff --git a/OpenNest/Controls/PhaseStepperControl.cs b/OpenNest/Controls/PhaseStepperControl.cs new file mode 100644 index 0000000..a0f8e92 --- /dev/null +++ b/OpenNest/Controls/PhaseStepperControl.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace OpenNest.Controls +{ + public class PhaseStepperControl : UserControl + { + private static readonly Color AccentColor = Color.FromArgb(0, 120, 212); + private static readonly Color GlowColor = Color.FromArgb(60, 0, 120, 212); + private static readonly Color PendingBorder = Color.FromArgb(192, 192, 192); + private static readonly Color LineColor = Color.FromArgb(208, 208, 208); + private static readonly Color PendingTextColor = Color.FromArgb(153, 153, 153); + private static readonly Color ActiveTextColor = Color.FromArgb(51, 51, 51); + + private static readonly Font LabelFont = new Font("Segoe UI", 8f, FontStyle.Regular); + private static readonly Font BoldLabelFont = new Font("Segoe UI", 8f, FontStyle.Bold); + + private static readonly NestPhase[] Phases = (NestPhase[])Enum.GetValues(typeof(NestPhase)); + + private readonly HashSet visitedPhases = new(); + private NestPhase? activePhase; + private bool isComplete; + + public PhaseStepperControl() + { + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + Height = 60; + } + + public NestPhase? ActivePhase + { + get => activePhase; + set + { + activePhase = value; + if (value.HasValue) + visitedPhases.Add(value.Value); + Invalidate(); + } + } + + public bool IsComplete + { + get => isComplete; + set + { + isComplete = value; + if (value) + { + foreach (var phase in Phases) + visitedPhases.Add(phase); + activePhase = null; + } + Invalidate(); + } + } + + private static string GetDisplayName(NestPhase phase) + { + switch (phase) + { + case NestPhase.RectBestFit: return "BestFit"; + case NestPhase.Nfp: return "NFP"; + default: return phase.ToString(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + + var count = Phases.Length; + if (count == 0) return; + + var padding = 30; + var usableWidth = Width - padding * 2; + var spacing = (float)usableWidth / (count - 1); + var circleY = 18; + var normalRadius = 9; + var activeRadius = 11; + + using var linePen = new Pen(LineColor, 2f); + using var accentBrush = new SolidBrush(AccentColor); + using var glowBrush = new SolidBrush(GlowColor); + using var pendingPen = new Pen(PendingBorder, 2f); + using var activeTextBrush = new SolidBrush(ActiveTextColor); + using var pendingTextBrush = new SolidBrush(PendingTextColor); + + // Draw connecting lines + for (var i = 0; i < count - 1; i++) + { + var x1 = (int)(padding + i * spacing); + var x2 = (int)(padding + (i + 1) * spacing); + g.DrawLine(linePen, x1, circleY, x2, circleY); + } + + // Draw circles and labels + for (var i = 0; i < count; i++) + { + var phase = Phases[i]; + var cx = (int)(padding + i * spacing); + var isActive = activePhase == phase && !isComplete; + var isVisited = visitedPhases.Contains(phase) || isComplete; + + if (isActive) + { + // Glow + g.FillEllipse(glowBrush, + cx - activeRadius - 3, circleY - activeRadius - 3, + (activeRadius + 3) * 2, (activeRadius + 3) * 2); + // Filled circle + g.FillEllipse(accentBrush, + cx - activeRadius, circleY - activeRadius, + activeRadius * 2, activeRadius * 2); + } + else if (isVisited) + { + g.FillEllipse(accentBrush, + cx - normalRadius, circleY - normalRadius, + normalRadius * 2, normalRadius * 2); + } + else + { + g.DrawEllipse(pendingPen, + cx - normalRadius, circleY - normalRadius, + normalRadius * 2, normalRadius * 2); + } + + // Label + var label = GetDisplayName(phase); + var font = isVisited || isActive ? BoldLabelFont : LabelFont; + var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush; + var labelSize = g.MeasureString(label, font); + g.DrawString(label, font, brush, + cx - labelSize.Width / 2, circleY + activeRadius + 5); + } + } + } +} diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index c61d6b4..d4f545f 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -1,4 +1,11 @@ -using System; +using OpenNest.Actions; +using OpenNest.CNC; +using OpenNest.Collections; +using OpenNest.Engine.Fill; +using OpenNest.Forms; +using OpenNest.Geometry; +using OpenNest.Math; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; @@ -9,12 +16,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using OpenNest.Actions; -using OpenNest.CNC; -using OpenNest.Collections; -using OpenNest.Forms; -using OpenNest.Geometry; -using OpenNest.Math; using Action = OpenNest.Actions.Action; using Timer = System.Timers.Timer; @@ -30,7 +31,8 @@ namespace OpenNest.Controls private Action currentAction; private Action previousAction; private List parts; - private List temporaryParts = new List(); + private List stationaryParts = new List(); + private List activeParts = new List(); private Point middleMouseDownPoint; private Box activeWorkArea; private List debugRemnants; @@ -59,7 +61,7 @@ namespace OpenNest.Controls public List SelectedParts; public ReadOnlyCollection Parts; - + public event EventHandler> PartAdded; public event EventHandler> PartRemoved; public event EventHandler StatusChanged; @@ -148,7 +150,8 @@ namespace OpenNest.Controls plate.PartAdded -= plate_PartAdded; plate.PartRemoved -= plate_PartRemoved; parts.Clear(); - temporaryParts.Clear(); + stationaryParts.Clear(); + activeParts.Clear(); SelectedParts.Clear(); } @@ -381,7 +384,7 @@ namespace OpenNest.Controls e.Graphics.DrawLine(ColorScheme.OriginPen, origin.X, 0, origin.X, Height); e.Graphics.DrawLine(ColorScheme.OriginPen, 0, origin.Y, Width, origin.Y); } - + e.Graphics.TranslateTransform(origin.X, origin.Y); DrawPlate(e.Graphics); @@ -407,7 +410,8 @@ namespace OpenNest.Controls public override void Refresh() { parts.ForEach(p => p.Update(this)); - temporaryParts.ForEach(p => p.Update(this)); + stationaryParts.ForEach(p => p.Update(this)); + activeParts.ForEach(p => p.Update(this)); Invalidate(); } @@ -502,24 +506,38 @@ namespace OpenNest.Controls part.Draw(g, (i + 1).ToString()); } - // Draw temporary (preview) parts - for (var i = 0; i < temporaryParts.Count; i++) + // Draw stationary preview parts (overall best — full opacity) + for (var i = 0; i < stationaryParts.Count; i++) { - var temp = temporaryParts[i]; + var part = stationaryParts[i]; - if (temp.IsDirty) - temp.Update(this); + if (part.IsDirty) + part.Update(this); - var path = temp.Path; - var pathBounds = path.GetBounds(); - - if (!pathBounds.IntersectsWith(viewBounds)) + var path = part.Path; + if (!path.GetBounds().IntersectsWith(viewBounds)) continue; g.FillPath(ColorScheme.PreviewPartBrush, path); g.DrawPath(ColorScheme.PreviewPartPen, path); } + // Draw active preview parts (current strategy — reduced opacity) + for (var i = 0; i < activeParts.Count; i++) + { + var part = activeParts[i]; + + if (part.IsDirty) + part.Update(this); + + var path = part.Path; + if (!path.GetBounds().IntersectsWith(viewBounds)) + continue; + + g.FillPath(ColorScheme.ActivePreviewPartBrush, path); + g.DrawPath(ColorScheme.ActivePreviewPartPen, path); + } + if (DrawOffset && Plate.PartSpacing > 0) DrawOffsetGeometry(g); @@ -878,34 +896,49 @@ namespace OpenNest.Controls Plate.Parts.Add(part); } - public void SetTemporaryParts(List parts) + public void SetStationaryParts(List parts) { - temporaryParts.Clear(); + stationaryParts.Clear(); if (parts != null) { foreach (var part in parts) - temporaryParts.Add(LayoutPart.Create(part, this)); + stationaryParts.Add(LayoutPart.Create(part, this)); } Invalidate(); } - public void ClearTemporaryParts() + public void SetActiveParts(List parts) { - temporaryParts.Clear(); + activeParts.Clear(); + + if (parts != null) + { + foreach (var part in parts) + activeParts.Add(LayoutPart.Create(part, this)); + } + Invalidate(); } - public int AcceptTemporaryParts() + public void ClearPreviewParts() { - var count = temporaryParts.Count; + stationaryParts.Clear(); + activeParts.Clear(); + Invalidate(); + } - foreach (var layoutPart in temporaryParts) - Plate.Parts.Add(layoutPart.BasePart); + public void AcceptPreviewParts(List parts) + { + if (parts != null) + { + foreach (var part in parts) + Plate.Parts.Add(part); + } - temporaryParts.Clear(); - return count; + stationaryParts.Clear(); + activeParts.Clear(); } public async void FillWithProgress(List groupParts, Box workArea) @@ -917,7 +950,12 @@ namespace OpenNest.Controls var progress = new Progress(p => { progressForm.UpdateProgress(p); - SetTemporaryParts(p.BestParts); + + if (p.IsOverallBest) + SetStationaryParts(p.BestParts); + else + SetActiveParts(p.BestParts); + ActiveWorkArea = p.ActiveWorkArea; }); @@ -929,22 +967,22 @@ namespace OpenNest.Controls var parts = await Task.Run(() => engine.Fill(groupParts, workArea, progress, cts.Token)); - if (parts.Count > 0 && !cts.IsCancellationRequested) + if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted)) { - AcceptTemporaryParts(); + AcceptPreviewParts(parts); sw.Stop(); Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms"; } else { - ClearTemporaryParts(); + ClearPreviewParts(); } progressForm.ShowCompleted(); } catch (Exception) { - ClearTemporaryParts(); + ClearPreviewParts(); } finally { @@ -1001,7 +1039,7 @@ namespace OpenNest.Controls { base.ZoomToPoint(pt, zoomFactor, false); - if (redraw) + if (redraw) Invalidate(); } @@ -1081,7 +1119,8 @@ namespace OpenNest.Controls { base.UpdateMatrix(); parts.ForEach(p => p.Update(this)); - temporaryParts.ForEach(p => p.Update(this)); + stationaryParts.ForEach(p => p.Update(this)); + activeParts.ForEach(p => p.Update(this)); } } } diff --git a/OpenNest/Document.cs b/OpenNest/Document.cs index 6b4b256..90c4f14 100644 --- a/OpenNest/Document.cs +++ b/OpenNest/Document.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using OpenNest.IO; +using System; using System.IO; -using System.Linq; -using System.Text; -using OpenNest.IO; namespace OpenNest { diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs index e168c50..15eff4d 100644 --- a/OpenNest/Forms/BestFitViewerForm.cs +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -1,9 +1,9 @@ +using OpenNest.Controls; +using OpenNest.Engine.BestFit; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Engine.BestFit; namespace OpenNest.Forms { diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index 9a7ad83..240f047 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Forms; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.IO; using OpenNest.Properties; -using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; namespace OpenNest.Forms { @@ -101,7 +100,7 @@ namespace OpenNest.Forms if (entities.Count == 0) continue; - + var drawing = new Drawing(item.Name); drawing.Color = GetNextColor(); drawing.Customer = item.Customer; diff --git a/OpenNest/Forms/CutParametersForm.cs b/OpenNest/Forms/CutParametersForm.cs index 9d1b860..625e2b8 100644 --- a/OpenNest/Forms/CutParametersForm.cs +++ b/OpenNest/Forms/CutParametersForm.cs @@ -48,7 +48,7 @@ namespace OpenNest.Forms var suffix = UnitsHelper.GetShortTimeUnitPair(units); numericUpDown1.Suffix = " " + suffix; - numericUpDown2.Suffix = " " + suffix; + numericUpDown2.Suffix = " " + suffix; } public CutParameters GetCutParameters() diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index e964abf..8d39961 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1,4 +1,12 @@ -using System; +using OpenNest.Actions; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Collections; +using OpenNest.Controls; +using OpenNest.Engine.Sequencing; +using OpenNest.IO; +using OpenNest.Math; +using OpenNest.Properties; +using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; @@ -6,16 +14,6 @@ using System.IO; using System.Linq; using System.Windows.Forms; using OpenNest.Api; -using OpenNest.Actions; -using OpenNest.CNC.CuttingStrategy; -using OpenNest.Collections; -using OpenNest.Controls; -using OpenNest.Engine; -using OpenNest.Engine.RapidPlanning; -using OpenNest.Engine.Sequencing; -using OpenNest.IO; -using OpenNest.Math; -using OpenNest.Properties; using Timer = System.Timers.Timer; namespace OpenNest.Forms @@ -32,6 +30,7 @@ namespace OpenNest.Forms private Panel plateHeaderPanel; private Label plateInfoLabel; private Button btnFirstPlate; + private Button btnRemovePlate; private Button btnPreviousPlate; private Button btnNextPlate; @@ -122,7 +121,7 @@ namespace OpenNest.Forms navPanel.Controls.AddRange(new Control[] { btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate }); - var btnRemovePlate = CreateNavButton(Resources.remove); + btnRemovePlate = CreateNavButton(Resources.remove); btnRemovePlate.Dock = DockStyle.Right; btnRemovePlate.Click += (s, e) => RemoveCurrentPlate(); @@ -178,6 +177,7 @@ namespace OpenNest.Forms UpdatePlateList(); UpdateDrawingList(); + UpdateRemovePlateButton(); LoadFirstPlate(); @@ -731,6 +731,7 @@ namespace OpenNest.Forms PlateView.Plate = Nest.Plates[CurrentPlateIndex]; UpdatePlateList(); + UpdateRemovePlateButton(); PlateView.ZoomToFit(); } @@ -738,10 +739,16 @@ namespace OpenNest.Forms { tabControl1.SelectedIndex = 0; UpdatePlateList(); + UpdateRemovePlateButton(); LoadLastPlate(); PlateView.ZoomToFit(); } + private void UpdateRemovePlateButton() + { + btnRemovePlate.Enabled = Nest.Plates.Count > 1; + } + #endregion private static ListViewItem GetListViewItem(Plate plate, int id) diff --git a/OpenNest/Forms/EditNestInfoForm.cs b/OpenNest/Forms/EditNestInfoForm.cs index 5143fbf..1b28f7d 100644 --- a/OpenNest/Forms/EditNestInfoForm.cs +++ b/OpenNest/Forms/EditNestInfoForm.cs @@ -1,6 +1,6 @@ -using System; +using OpenNest.Geometry; +using System; using System.Windows.Forms; -using OpenNest.Geometry; using Timer = System.Timers.Timer; namespace OpenNest.Forms diff --git a/OpenNest/Forms/EditPlateForm.cs b/OpenNest/Forms/EditPlateForm.cs index 1409a18..5781329 100644 --- a/OpenNest/Forms/EditPlateForm.cs +++ b/OpenNest/Forms/EditPlateForm.cs @@ -1,6 +1,5 @@ using System; using System.Windows.Forms; -using OpenNest.Geometry; using Timer = System.Timers.Timer; namespace OpenNest.Forms @@ -15,7 +14,7 @@ namespace OpenNest.Forms public EditPlateForm(Plate plate) { InitializeComponent(); - + this.plate = plate; timer = new Timer @@ -57,7 +56,7 @@ namespace OpenNest.Forms else increment = 1; - var controls = new [] + var controls = new[] { numericUpDownThickness, numericUpDownPartSpacing, diff --git a/OpenNest/Forms/FillPlateForm.cs b/OpenNest/Forms/FillPlateForm.cs index f838b4e..c51d1b8 100644 --- a/OpenNest/Forms/FillPlateForm.cs +++ b/OpenNest/Forms/FillPlateForm.cs @@ -1,8 +1,8 @@ -using System.Drawing; -using System.Windows.Forms; -using OpenNest.Collections; +using OpenNest.Collections; using OpenNest.Controls; using OpenNest.Geometry; +using System.Drawing; +using System.Windows.Forms; namespace OpenNest.Forms { diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 1086763..463dff2 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -1,4 +1,12 @@ -using System; +using OpenNest.Actions; +using OpenNest.Collections; +using OpenNest.Engine.BestFit; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using OpenNest.Gpu; +using OpenNest.IO; +using OpenNest.Properties; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; @@ -7,13 +15,6 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using OpenNest.Actions; -using OpenNest.Collections; -using OpenNest.Engine.BestFit; -using OpenNest.Gpu; -using OpenNest.Geometry; -using OpenNest.IO; -using OpenNest.Properties; namespace OpenNest.Forms { @@ -812,7 +813,7 @@ namespace OpenNest.Forms if (form.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; - + var items = form.GetNestItems(); if (!items.Any(it => it.Quantity > 0)) @@ -826,7 +827,12 @@ namespace OpenNest.Forms var progress = new Progress(p => { progressForm.UpdateProgress(p); - activeForm.PlateView.SetTemporaryParts(p.BestParts); + + if (p.IsOverallBest) + activeForm.PlateView.SetStationaryParts(p.BestParts); + else + activeForm.PlateView.SetActiveParts(p.BestParts); + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; }); @@ -862,9 +868,9 @@ namespace OpenNest.Forms var nestParts = await Task.Run(() => engine.Nest(remaining, progress, token)); - activeForm.PlateView.ClearTemporaryParts(); + activeForm.PlateView.ClearPreviewParts(); - if (nestParts.Count > 0 && !token.IsCancellationRequested) + if (nestParts.Count > 0 && (!token.IsCancellationRequested || progressForm.Accepted)) { plate.Parts.AddRange(nestParts); activeForm.PlateView.Invalidate(); @@ -880,7 +886,7 @@ namespace OpenNest.Forms } catch (Exception ex) { - activeForm.PlateView.ClearTemporaryParts(); + activeForm.PlateView.ClearPreviewParts(); MessageBox.Show($"Nesting error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } @@ -896,7 +902,7 @@ namespace OpenNest.Forms private void SequenceAllPlates_Click(object sender, EventArgs e) { - if (activeForm == null) + if (activeForm == null) return; activeForm.AutoSequenceAllPlates(); @@ -963,7 +969,12 @@ namespace OpenNest.Forms var progress = new Progress(p => { progressForm.UpdateProgress(p); - activeForm.PlateView.SetTemporaryParts(p.BestParts); + + if (p.IsOverallBest) + activeForm.PlateView.SetStationaryParts(p.BestParts); + else + activeForm.PlateView.SetActiveParts(p.BestParts); + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; }); @@ -980,15 +991,15 @@ namespace OpenNest.Forms plate.WorkArea(), progress, token)); if (parts.Count > 0) - activeForm.PlateView.AcceptTemporaryParts(); + activeForm.PlateView.AcceptPreviewParts(parts); else - activeForm.PlateView.ClearTemporaryParts(); + activeForm.PlateView.ClearPreviewParts(); progressForm.ShowCompleted(); } catch (Exception ex) { - activeForm.PlateView.ClearTemporaryParts(); + activeForm.PlateView.ClearPreviewParts(); MessageBox.Show($"Nesting error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } @@ -1025,16 +1036,21 @@ namespace OpenNest.Forms var progress = new Progress(p => { progressForm.UpdateProgress(p); - activeForm.PlateView.SetTemporaryParts(p.BestParts); + + if (p.IsOverallBest) + activeForm.PlateView.SetStationaryParts(p.BestParts); + else + activeForm.PlateView.SetActiveParts(p.BestParts); + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; }); Action> onComplete = parts => { if (parts != null && parts.Count > 0) - activeForm.PlateView.AcceptTemporaryParts(); + activeForm.PlateView.AcceptPreviewParts(parts); else - activeForm.PlateView.ClearTemporaryParts(); + activeForm.PlateView.ClearPreviewParts(); activeForm.PlateView.ActiveWorkArea = null; progressForm.Close(); diff --git a/OpenNest/Forms/NestProgressForm.Designer.cs b/OpenNest/Forms/NestProgressForm.Designer.cs index 9bed575..9bd7b12 100644 --- a/OpenNest/Forms/NestProgressForm.Designer.cs +++ b/OpenNest/Forms/NestProgressForm.Designer.cs @@ -2,15 +2,8 @@ namespace OpenNest.Forms { partial class NestProgressForm { - /// - /// Required designer variable. - /// private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) @@ -22,247 +15,323 @@ namespace OpenNest.Forms #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// private void InitializeComponent() { - table = new System.Windows.Forms.TableLayoutPanel(); - phaseLabel = new System.Windows.Forms.Label(); - phaseValue = new System.Windows.Forms.Label(); - plateLabel = new System.Windows.Forms.Label(); - plateValue = new System.Windows.Forms.Label(); + phaseStepper = new OpenNest.Controls.PhaseStepperControl(); + resultsPanel = new System.Windows.Forms.Panel(); + resultsTable = new System.Windows.Forms.TableLayoutPanel(); partsLabel = new System.Windows.Forms.Label(); partsValue = new System.Windows.Forms.Label(); densityLabel = new System.Windows.Forms.Label(); + densityPanel = new System.Windows.Forms.FlowLayoutPanel(); densityValue = new System.Windows.Forms.Label(); + densityBar = new OpenNest.Controls.DensityBar(); nestedAreaLabel = new System.Windows.Forms.Label(); nestedAreaValue = new System.Windows.Forms.Label(); - remnantLabel = new System.Windows.Forms.Label(); - remnantValue = new System.Windows.Forms.Label(); + resultsHeader = new System.Windows.Forms.Label(); + statusPanel = new System.Windows.Forms.Panel(); + statusTable = new System.Windows.Forms.TableLayoutPanel(); + plateLabel = new System.Windows.Forms.Label(); + plateValue = new System.Windows.Forms.Label(); elapsedLabel = new System.Windows.Forms.Label(); elapsedValue = new System.Windows.Forms.Label(); descriptionLabel = new System.Windows.Forms.Label(); descriptionValue = new System.Windows.Forms.Label(); - stopButton = new System.Windows.Forms.Button(); + statusHeader = new System.Windows.Forms.Label(); buttonPanel = new System.Windows.Forms.FlowLayoutPanel(); - table.SuspendLayout(); + stopButton = new System.Windows.Forms.Button(); + acceptButton = new System.Windows.Forms.Button(); + resultsPanel.SuspendLayout(); + resultsTable.SuspendLayout(); + densityPanel.SuspendLayout(); + statusPanel.SuspendLayout(); + statusTable.SuspendLayout(); buttonPanel.SuspendLayout(); SuspendLayout(); // - // table + // phaseStepper // - table.AutoSize = true; - table.ColumnCount = 2; - table.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 93F)); - table.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - table.Controls.Add(phaseLabel, 0, 0); - table.Controls.Add(phaseValue, 1, 0); - table.Controls.Add(plateLabel, 0, 1); - table.Controls.Add(plateValue, 1, 1); - table.Controls.Add(partsLabel, 0, 2); - table.Controls.Add(partsValue, 1, 2); - table.Controls.Add(densityLabel, 0, 3); - table.Controls.Add(densityValue, 1, 3); - table.Controls.Add(nestedAreaLabel, 0, 4); - table.Controls.Add(nestedAreaValue, 1, 4); - table.Controls.Add(remnantLabel, 0, 5); - table.Controls.Add(remnantValue, 1, 5); - table.Controls.Add(elapsedLabel, 0, 6); - table.Controls.Add(elapsedValue, 1, 6); - table.Controls.Add(descriptionLabel, 0, 7); - table.Controls.Add(descriptionValue, 1, 7); - table.Dock = System.Windows.Forms.DockStyle.Top; - table.Location = new System.Drawing.Point(0, 45); - table.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - table.Name = "table"; - table.Padding = new System.Windows.Forms.Padding(9, 9, 9, 9); - table.RowCount = 8; - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.RowStyles.Add(new System.Windows.Forms.RowStyle()); - table.Size = new System.Drawing.Size(425, 218); - table.TabIndex = 0; + phaseStepper.ActivePhase = null; + phaseStepper.Dock = System.Windows.Forms.DockStyle.Top; + phaseStepper.IsComplete = false; + phaseStepper.Location = new System.Drawing.Point(0, 0); + phaseStepper.Name = "phaseStepper"; + phaseStepper.Size = new System.Drawing.Size(450, 60); + phaseStepper.TabIndex = 0; // - // phaseLabel + // resultsPanel // - phaseLabel.AutoSize = true; - phaseLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - phaseLabel.Location = new System.Drawing.Point(14, 14); - phaseLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); - phaseLabel.Name = "phaseLabel"; - phaseLabel.Size = new System.Drawing.Size(46, 13); - phaseLabel.TabIndex = 0; - phaseLabel.Text = "Phase:"; + resultsPanel.BackColor = System.Drawing.Color.White; + resultsPanel.Controls.Add(resultsTable); + resultsPanel.Controls.Add(resultsHeader); + resultsPanel.Dock = System.Windows.Forms.DockStyle.Top; + resultsPanel.Location = new System.Drawing.Point(0, 60); + resultsPanel.Margin = new System.Windows.Forms.Padding(10, 4, 10, 4); + resultsPanel.Name = "resultsPanel"; + resultsPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); + resultsPanel.Size = new System.Drawing.Size(450, 120); + resultsPanel.TabIndex = 1; // - // phaseValue + // resultsTable // - phaseValue.AutoSize = true; - phaseValue.Location = new System.Drawing.Point(107, 14); - phaseValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); - phaseValue.Name = "phaseValue"; - phaseValue.Size = new System.Drawing.Size(19, 15); - phaseValue.TabIndex = 1; - phaseValue.Text = "—"; - // - // plateLabel - // - plateLabel.AutoSize = true; - plateLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - plateLabel.Location = new System.Drawing.Point(14, 39); - plateLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); - plateLabel.Name = "plateLabel"; - plateLabel.Size = new System.Drawing.Size(40, 13); - plateLabel.TabIndex = 2; - plateLabel.Text = "Plate:"; - // - // plateValue - // - plateValue.AutoSize = true; - plateValue.Location = new System.Drawing.Point(107, 39); - plateValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); - plateValue.Name = "plateValue"; - plateValue.Size = new System.Drawing.Size(19, 15); - plateValue.TabIndex = 3; - plateValue.Text = "—"; + resultsTable.AutoSize = true; + resultsTable.ColumnCount = 2; + resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 90F)); + resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + resultsTable.Controls.Add(partsLabel, 0, 0); + resultsTable.Controls.Add(partsValue, 1, 0); + resultsTable.Controls.Add(densityLabel, 0, 1); + resultsTable.Controls.Add(densityPanel, 1, 1); + resultsTable.Controls.Add(nestedAreaLabel, 0, 2); + resultsTable.Controls.Add(nestedAreaValue, 1, 2); + resultsTable.Dock = System.Windows.Forms.DockStyle.Top; + resultsTable.Location = new System.Drawing.Point(14, 29); + resultsTable.Name = "resultsTable"; + resultsTable.RowCount = 3; + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.Size = new System.Drawing.Size(422, 57); + resultsTable.TabIndex = 1; // // partsLabel // partsLabel.AutoSize = true; - partsLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - partsLabel.Location = new System.Drawing.Point(14, 64); - partsLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + partsLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); + partsLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + partsLabel.Location = new System.Drawing.Point(0, 3); + partsLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); partsLabel.Name = "partsLabel"; - partsLabel.Size = new System.Drawing.Size(40, 13); - partsLabel.TabIndex = 4; + partsLabel.Size = new System.Drawing.Size(36, 13); + partsLabel.TabIndex = 0; partsLabel.Text = "Parts:"; // // partsValue // partsValue.AutoSize = true; - partsValue.Location = new System.Drawing.Point(107, 64); - partsValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + partsValue.Font = new System.Drawing.Font("Consolas", 9.75F); + partsValue.Location = new System.Drawing.Point(80, 3); + partsValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); partsValue.Name = "partsValue"; - partsValue.Size = new System.Drawing.Size(19, 15); - partsValue.TabIndex = 5; - partsValue.Text = "—"; + partsValue.Size = new System.Drawing.Size(13, 13); + partsValue.TabIndex = 1; + partsValue.Text = "�"; // // densityLabel // densityLabel.AutoSize = true; - densityLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - densityLabel.Location = new System.Drawing.Point(14, 89); - densityLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + densityLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); + densityLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + densityLabel.Location = new System.Drawing.Point(0, 22); + densityLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); densityLabel.Name = "densityLabel"; - densityLabel.Size = new System.Drawing.Size(53, 13); - densityLabel.TabIndex = 6; + densityLabel.Size = new System.Drawing.Size(49, 13); + densityLabel.TabIndex = 2; densityLabel.Text = "Density:"; // + // densityPanel + // + densityPanel.AutoSize = true; + densityPanel.Controls.Add(densityValue); + densityPanel.Controls.Add(densityBar); + densityPanel.Location = new System.Drawing.Point(80, 19); + densityPanel.Margin = new System.Windows.Forms.Padding(0); + densityPanel.Name = "densityPanel"; + densityPanel.Size = new System.Drawing.Size(311, 19); + densityPanel.TabIndex = 3; + densityPanel.WrapContents = false; + // // densityValue // densityValue.AutoSize = true; - densityValue.Location = new System.Drawing.Point(107, 89); - densityValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + densityValue.Font = new System.Drawing.Font("Consolas", 9.75F); + densityValue.Location = new System.Drawing.Point(0, 3); + densityValue.Margin = new System.Windows.Forms.Padding(0, 3, 8, 3); densityValue.Name = "densityValue"; - densityValue.Size = new System.Drawing.Size(19, 15); - densityValue.TabIndex = 7; - densityValue.Text = "—"; + densityValue.Size = new System.Drawing.Size(13, 13); + densityValue.TabIndex = 0; + densityValue.Text = "�"; + // + // densityBar + // + densityBar.Location = new System.Drawing.Point(21, 5); + densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0); + densityBar.Name = "densityBar"; + densityBar.Size = new System.Drawing.Size(290, 8); + densityBar.TabIndex = 1; + densityBar.Value = 0D; // // nestedAreaLabel // nestedAreaLabel.AutoSize = true; - nestedAreaLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - nestedAreaLabel.Location = new System.Drawing.Point(14, 114); - nestedAreaLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); + nestedAreaLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + nestedAreaLabel.Location = new System.Drawing.Point(0, 41); + nestedAreaLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); nestedAreaLabel.Name = "nestedAreaLabel"; - nestedAreaLabel.Size = new System.Drawing.Size(51, 13); - nestedAreaLabel.TabIndex = 8; + nestedAreaLabel.Size = new System.Drawing.Size(47, 13); + nestedAreaLabel.TabIndex = 4; nestedAreaLabel.Text = "Nested:"; // // nestedAreaValue // nestedAreaValue.AutoSize = true; - nestedAreaValue.Location = new System.Drawing.Point(107, 114); - nestedAreaValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + nestedAreaValue.Font = new System.Drawing.Font("Consolas", 9.75F); + nestedAreaValue.Location = new System.Drawing.Point(80, 41); + nestedAreaValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); nestedAreaValue.Name = "nestedAreaValue"; - nestedAreaValue.Size = new System.Drawing.Size(19, 15); - nestedAreaValue.TabIndex = 9; - nestedAreaValue.Text = "—"; + nestedAreaValue.Size = new System.Drawing.Size(13, 13); + nestedAreaValue.TabIndex = 5; + nestedAreaValue.Text = "�"; // - // remnantLabel + // resultsHeader // - remnantLabel.AutoSize = true; - remnantLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - remnantLabel.Location = new System.Drawing.Point(14, 139); - remnantLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); - remnantLabel.Name = "remnantLabel"; - remnantLabel.Size = new System.Drawing.Size(54, 13); - remnantLabel.TabIndex = 10; - remnantLabel.Text = "Unused:"; + resultsHeader.AutoSize = true; + resultsHeader.Dock = System.Windows.Forms.DockStyle.Top; + resultsHeader.Font = new System.Drawing.Font("Segoe UI", 10.5F, System.Drawing.FontStyle.Bold); + resultsHeader.ForeColor = System.Drawing.Color.FromArgb(85, 85, 85); + resultsHeader.Location = new System.Drawing.Point(14, 10); + resultsHeader.Name = "resultsHeader"; + resultsHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); + resultsHeader.Size = new System.Drawing.Size(56, 19); + resultsHeader.TabIndex = 0; + resultsHeader.Text = "RESULTS"; // - // remnantValue + // statusPanel // - remnantValue.AutoSize = true; - remnantValue.Location = new System.Drawing.Point(107, 139); - remnantValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); - remnantValue.Name = "remnantValue"; - remnantValue.Size = new System.Drawing.Size(19, 15); - remnantValue.TabIndex = 11; - remnantValue.Text = "—"; + statusPanel.BackColor = System.Drawing.Color.White; + statusPanel.Controls.Add(statusTable); + statusPanel.Controls.Add(statusHeader); + statusPanel.Dock = System.Windows.Forms.DockStyle.Top; + statusPanel.Location = new System.Drawing.Point(0, 165); + statusPanel.Name = "statusPanel"; + statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); + statusPanel.Size = new System.Drawing.Size(450, 115); + statusPanel.TabIndex = 2; + // + // statusTable + // + statusTable.AutoSize = true; + statusTable.ColumnCount = 2; + statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 90F)); + statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + statusTable.Controls.Add(plateLabel, 0, 0); + statusTable.Controls.Add(plateValue, 1, 0); + statusTable.Controls.Add(elapsedLabel, 0, 1); + statusTable.Controls.Add(elapsedValue, 1, 1); + statusTable.Controls.Add(descriptionLabel, 0, 2); + statusTable.Controls.Add(descriptionValue, 1, 2); + statusTable.Dock = System.Windows.Forms.DockStyle.Top; + statusTable.Location = new System.Drawing.Point(14, 29); + statusTable.Name = "statusTable"; + statusTable.RowCount = 3; + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.Size = new System.Drawing.Size(422, 57); + statusTable.TabIndex = 1; + // + // plateLabel + // + plateLabel.AutoSize = true; + plateLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); + plateLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + plateLabel.Location = new System.Drawing.Point(0, 3); + plateLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + plateLabel.Name = "plateLabel"; + plateLabel.Size = new System.Drawing.Size(36, 13); + plateLabel.TabIndex = 0; + plateLabel.Text = "Plate:"; + // + // plateValue + // + plateValue.AutoSize = true; + plateValue.Font = new System.Drawing.Font("Consolas", 9.75F); + plateValue.Location = new System.Drawing.Point(80, 3); + plateValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + plateValue.Name = "plateValue"; + plateValue.Size = new System.Drawing.Size(13, 13); + plateValue.TabIndex = 1; + plateValue.Text = "�"; // // elapsedLabel // elapsedLabel.AutoSize = true; - elapsedLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - elapsedLabel.Location = new System.Drawing.Point(14, 164); - elapsedLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); + elapsedLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + elapsedLabel.Location = new System.Drawing.Point(0, 22); + elapsedLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); elapsedLabel.Name = "elapsedLabel"; - elapsedLabel.Size = new System.Drawing.Size(56, 13); - elapsedLabel.TabIndex = 12; + elapsedLabel.Size = new System.Drawing.Size(50, 13); + elapsedLabel.TabIndex = 2; elapsedLabel.Text = "Elapsed:"; // // elapsedValue // elapsedValue.AutoSize = true; - elapsedValue.Location = new System.Drawing.Point(107, 164); - elapsedValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + elapsedValue.Font = new System.Drawing.Font("Consolas", 9.75F); + elapsedValue.Location = new System.Drawing.Point(80, 22); + elapsedValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); elapsedValue.Name = "elapsedValue"; - elapsedValue.Size = new System.Drawing.Size(28, 15); - elapsedValue.TabIndex = 13; + elapsedValue.Size = new System.Drawing.Size(31, 13); + elapsedValue.TabIndex = 3; elapsedValue.Text = "0:00"; // // descriptionLabel // descriptionLabel.AutoSize = true; - descriptionLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold); - descriptionLabel.Location = new System.Drawing.Point(14, 189); - descriptionLabel.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold); + descriptionLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + descriptionLabel.Location = new System.Drawing.Point(0, 41); + descriptionLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); descriptionLabel.Name = "descriptionLabel"; - descriptionLabel.Size = new System.Drawing.Size(44, 13); - descriptionLabel.TabIndex = 14; + descriptionLabel.Size = new System.Drawing.Size(40, 13); + descriptionLabel.TabIndex = 4; descriptionLabel.Text = "Detail:"; // // descriptionValue // descriptionValue.AutoSize = true; - descriptionValue.Location = new System.Drawing.Point(107, 189); - descriptionValue.Margin = new System.Windows.Forms.Padding(5, 5, 5, 5); + descriptionValue.Font = new System.Drawing.Font("Segoe UI", 9.75F); + descriptionValue.Location = new System.Drawing.Point(80, 41); + descriptionValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); descriptionValue.Name = "descriptionValue"; - descriptionValue.Size = new System.Drawing.Size(19, 15); - descriptionValue.TabIndex = 15; - descriptionValue.Text = "—"; + descriptionValue.Size = new System.Drawing.Size(18, 13); + descriptionValue.TabIndex = 5; + descriptionValue.Text = "�"; + // + // statusHeader + // + statusHeader.AutoSize = true; + statusHeader.Dock = System.Windows.Forms.DockStyle.Top; + statusHeader.Font = new System.Drawing.Font("Segoe UI", 10.5F, System.Drawing.FontStyle.Bold); + statusHeader.ForeColor = System.Drawing.Color.FromArgb(85, 85, 85); + statusHeader.Location = new System.Drawing.Point(14, 10); + statusHeader.Name = "statusHeader"; + statusHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); + statusHeader.Size = new System.Drawing.Size(50, 19); + statusHeader.TabIndex = 0; + statusHeader.Text = "STATUS"; + // + // buttonPanel + // + buttonPanel.AutoSize = true; + buttonPanel.Controls.Add(stopButton); + buttonPanel.Controls.Add(acceptButton); + buttonPanel.Dock = System.Windows.Forms.DockStyle.Top; + buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + buttonPanel.Location = new System.Drawing.Point(0, 265); + buttonPanel.Name = "buttonPanel"; + buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6); + buttonPanel.Size = new System.Drawing.Size(450, 45); + buttonPanel.TabIndex = 3; // // stopButton // - stopButton.Anchor = System.Windows.Forms.AnchorStyles.None; - stopButton.Location = new System.Drawing.Point(314, 9); - stopButton.Margin = new System.Windows.Forms.Padding(0, 9, 0, 9); + stopButton.Enabled = false; + stopButton.Font = new System.Drawing.Font("Segoe UI", 9.75F); + stopButton.Location = new System.Drawing.Point(339, 9); + stopButton.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); stopButton.Name = "stopButton"; stopButton.Size = new System.Drawing.Size(93, 27); stopButton.TabIndex = 0; @@ -270,36 +339,45 @@ namespace OpenNest.Forms stopButton.UseVisualStyleBackColor = true; stopButton.Click += StopButton_Click; // - // buttonPanel + // acceptButton // - buttonPanel.AutoSize = true; - buttonPanel.Controls.Add(stopButton); - buttonPanel.Dock = System.Windows.Forms.DockStyle.Top; - buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; - buttonPanel.Location = new System.Drawing.Point(0, 0); - buttonPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - buttonPanel.Name = "buttonPanel"; - buttonPanel.Padding = new System.Windows.Forms.Padding(9, 0, 9, 0); - buttonPanel.Size = new System.Drawing.Size(425, 45); - buttonPanel.TabIndex = 1; + acceptButton.Enabled = false; + acceptButton.Font = new System.Drawing.Font("Segoe UI", 9.75F); + acceptButton.Location = new System.Drawing.Point(246, 9); + acceptButton.Margin = new System.Windows.Forms.Padding(6, 3, 0, 3); + acceptButton.Name = "acceptButton"; + acceptButton.Size = new System.Drawing.Size(93, 27); + acceptButton.TabIndex = 1; + acceptButton.Text = "Accept"; + acceptButton.UseVisualStyleBackColor = true; + acceptButton.Click += AcceptButton_Click; // // NestProgressForm // AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - ClientSize = new System.Drawing.Size(425, 266); - Controls.Add(table); + ClientSize = new System.Drawing.Size(450, 345); Controls.Add(buttonPanel); + Controls.Add(statusPanel); + Controls.Add(resultsPanel); + Controls.Add(phaseStepper); FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; - Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); MaximizeBox = false; MinimizeBox = false; Name = "NestProgressForm"; ShowInTaskbar = false; StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; Text = "Nesting Progress"; - table.ResumeLayout(false); - table.PerformLayout(); + resultsPanel.ResumeLayout(false); + resultsPanel.PerformLayout(); + resultsTable.ResumeLayout(false); + resultsTable.PerformLayout(); + densityPanel.ResumeLayout(false); + densityPanel.PerformLayout(); + statusPanel.ResumeLayout(false); + statusPanel.PerformLayout(); + statusTable.ResumeLayout(false); + statusTable.PerformLayout(); buttonPanel.ResumeLayout(false); ResumeLayout(false); PerformLayout(); @@ -307,24 +385,29 @@ namespace OpenNest.Forms #endregion - private System.Windows.Forms.TableLayoutPanel table; - private System.Windows.Forms.Label phaseLabel; - private System.Windows.Forms.Label phaseValue; - private System.Windows.Forms.Label plateLabel; - private System.Windows.Forms.Label plateValue; + private Controls.PhaseStepperControl phaseStepper; + private System.Windows.Forms.Panel resultsPanel; + private System.Windows.Forms.Label resultsHeader; + private System.Windows.Forms.TableLayoutPanel resultsTable; private System.Windows.Forms.Label partsLabel; private System.Windows.Forms.Label partsValue; private System.Windows.Forms.Label densityLabel; + private System.Windows.Forms.FlowLayoutPanel densityPanel; private System.Windows.Forms.Label densityValue; + private Controls.DensityBar densityBar; private System.Windows.Forms.Label nestedAreaLabel; private System.Windows.Forms.Label nestedAreaValue; - private System.Windows.Forms.Label remnantLabel; - private System.Windows.Forms.Label remnantValue; + private System.Windows.Forms.Panel statusPanel; + private System.Windows.Forms.Label statusHeader; + private System.Windows.Forms.TableLayoutPanel statusTable; + private System.Windows.Forms.Label plateLabel; + private System.Windows.Forms.Label plateValue; private System.Windows.Forms.Label elapsedLabel; private System.Windows.Forms.Label elapsedValue; private System.Windows.Forms.Label descriptionLabel; private System.Windows.Forms.Label descriptionValue; - private System.Windows.Forms.Button stopButton; private System.Windows.Forms.FlowLayoutPanel buttonPanel; + private System.Windows.Forms.Button acceptButton; + private System.Windows.Forms.Button stopButton; } } diff --git a/OpenNest/Forms/NestProgressForm.cs b/OpenNest/Forms/NestProgressForm.cs index 88788ee..d95e856 100644 --- a/OpenNest/Forms/NestProgressForm.cs +++ b/OpenNest/Forms/NestProgressForm.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Drawing; +using System.Linq; using System.Threading; using System.Windows.Forms; @@ -7,9 +10,22 @@ namespace OpenNest.Forms { public partial class NestProgressForm : Form { + private static readonly Color DefaultFlashColor = Color.FromArgb(0, 160, 0); + private static readonly Color DensityLowColor = Color.FromArgb(200, 40, 40); + private static readonly Color DensityMidColor = Color.FromArgb(200, 160, 0); + private static readonly Color DensityHighColor = Color.FromArgb(0, 160, 0); + + private const int FadeSteps = 20; + private const int FadeIntervalMs = 50; + private readonly CancellationTokenSource cts; private readonly Stopwatch stopwatch = Stopwatch.StartNew(); private readonly System.Windows.Forms.Timer elapsedTimer; + private readonly System.Windows.Forms.Timer fadeTimer; + private readonly Dictionary fadeCounters = new(); + private bool hasReceivedProgress; + + public bool Accepted { get; private set; } public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true) { @@ -25,6 +41,9 @@ namespace OpenNest.Forms elapsedTimer = new System.Windows.Forms.Timer { Interval = 1000 }; elapsedTimer.Tick += (s, e) => UpdateElapsed(); elapsedTimer.Start(); + + fadeTimer = new System.Windows.Forms.Timer { Interval = FadeIntervalMs }; + fadeTimer.Tick += FadeTimer_Tick; } public void UpdateProgress(NestProgress progress) @@ -32,14 +51,29 @@ namespace OpenNest.Forms if (IsDisposed || !IsHandleCreated) return; - phaseValue.Text = FormatPhase(progress.Phase); - plateValue.Text = progress.PlateNumber.ToString(); - partsValue.Text = progress.BestPartCount.ToString(); - densityValue.Text = progress.BestDensity.ToString("P1"); - nestedAreaValue.Text = $"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)"; + if (!hasReceivedProgress) + { + hasReceivedProgress = true; + acceptButton.Enabled = true; + stopButton.Enabled = true; + } - if (!string.IsNullOrEmpty(progress.Description)) - descriptionValue.Text = progress.Description; + phaseStepper.ActivePhase = progress.Phase; + + SetValueWithFlash(plateValue, progress.PlateNumber.ToString()); + SetValueWithFlash(partsValue, progress.BestPartCount.ToString()); + + var densityText = progress.BestDensity.ToString("P1"); + var densityFlashColor = GetDensityColor(progress.BestDensity); + SetValueWithFlash(densityValue, densityText, densityFlashColor); + densityBar.Value = progress.BestDensity; + + SetValueWithFlash(nestedAreaValue, + $"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)"); + + descriptionValue.Text = !string.IsNullOrEmpty(progress.Description) + ? progress.Description + : FormatPhase(progress.Phase); } public void ShowCompleted() @@ -51,8 +85,10 @@ namespace OpenNest.Forms elapsedTimer.Stop(); UpdateElapsed(); - phaseValue.Text = "Done"; + phaseStepper.IsComplete = true; descriptionValue.Text = "\u2014"; + + acceptButton.Visible = false; stopButton.Text = "Close"; stopButton.Enabled = true; stopButton.Click -= StopButton_Click; @@ -70,15 +106,28 @@ namespace OpenNest.Forms : elapsed.ToString(@"m\:ss"); } + private void AcceptButton_Click(object sender, EventArgs e) + { + Accepted = true; + cts.Cancel(); + acceptButton.Enabled = false; + stopButton.Enabled = false; + acceptButton.Text = "Accepted"; + stopButton.Text = "Stopping..."; + } + private void StopButton_Click(object sender, EventArgs e) { cts.Cancel(); - stopButton.Text = "Stopping..."; + acceptButton.Enabled = false; stopButton.Enabled = false; + stopButton.Text = "Stopping..."; } protected override void OnFormClosing(FormClosingEventArgs e) { + fadeTimer.Stop(); + fadeTimer.Dispose(); elapsedTimer.Stop(); elapsedTimer.Dispose(); stopwatch.Stop(); @@ -89,6 +138,65 @@ namespace OpenNest.Forms base.OnFormClosing(e); } + private void SetValueWithFlash(Label label, string text, Color? flashColor = null) + { + if (label.Text == text) + return; + + var color = flashColor ?? DefaultFlashColor; + label.Text = text; + label.ForeColor = color; + fadeCounters[label] = (FadeSteps, color); + + if (!fadeTimer.Enabled) + fadeTimer.Start(); + } + + private void FadeTimer_Tick(object sender, EventArgs e) + { + if (IsDisposed || !IsHandleCreated) + { + fadeTimer.Stop(); + return; + } + + var defaultColor = SystemColors.ControlText; + var labels = fadeCounters.Keys.ToList(); + + foreach (var label in labels) + { + var (remaining, flashColor) = fadeCounters[label]; + remaining--; + + if (remaining <= 0) + { + label.ForeColor = defaultColor; + fadeCounters.Remove(label); + } + else + { + var ratio = (float)remaining / FadeSteps; + var r = (int)(defaultColor.R + (flashColor.R - defaultColor.R) * ratio); + var g = (int)(defaultColor.G + (flashColor.G - defaultColor.G) * ratio); + var b = (int)(defaultColor.B + (flashColor.B - defaultColor.B) * ratio); + label.ForeColor = Color.FromArgb(r, g, b); + fadeCounters[label] = (remaining, flashColor); + } + } + + if (fadeCounters.Count == 0) + fadeTimer.Stop(); + } + + private static Color GetDensityColor(double density) + { + if (density < 0.5) + return DensityLowColor; + if (density < 0.7) + return DensityMidColor; + return DensityHighColor; + } + private static string FormatPhase(NestPhase phase) { switch (phase) @@ -96,6 +204,8 @@ namespace OpenNest.Forms case NestPhase.Linear: return "Trying rotations..."; case NestPhase.RectBestFit: return "Trying best fit..."; case NestPhase.Pairs: return "Trying pairs..."; + case NestPhase.Extents: return "Trying extents..."; + case NestPhase.Nfp: return "Trying NFP..."; default: return phase.ToString(); } } diff --git a/OpenNest/Forms/OptionsForm.cs b/OpenNest/Forms/OptionsForm.cs index 48896f7..7bb6d15 100644 --- a/OpenNest/Forms/OptionsForm.cs +++ b/OpenNest/Forms/OptionsForm.cs @@ -1,5 +1,5 @@ -using System.Windows.Forms; -using OpenNest.Properties; +using OpenNest.Properties; +using System.Windows.Forms; namespace OpenNest.Forms { diff --git a/OpenNest/Forms/PatternTileForm.Designer.cs b/OpenNest/Forms/PatternTileForm.Designer.cs index a5236c2..57d8a2b 100644 --- a/OpenNest/Forms/PatternTileForm.Designer.cs +++ b/OpenNest/Forms/PatternTileForm.Designer.cs @@ -13,140 +13,368 @@ namespace OpenNest.Forms private void InitializeComponent() { - this.topPanel = new System.Windows.Forms.FlowLayoutPanel(); - this.lblDrawingA = new System.Windows.Forms.Label(); - this.cboDrawingA = new System.Windows.Forms.ComboBox(); - this.lblDrawingB = new System.Windows.Forms.Label(); - this.cboDrawingB = new System.Windows.Forms.ComboBox(); - this.lblPlateSize = new System.Windows.Forms.Label(); - this.txtPlateSize = new System.Windows.Forms.TextBox(); - this.lblPartSpacing = new System.Windows.Forms.Label(); - this.nudPartSpacing = new System.Windows.Forms.NumericUpDown(); - this.btnAutoArrange = new System.Windows.Forms.Button(); - this.btnApply = new System.Windows.Forms.Button(); - this.splitContainer = new System.Windows.Forms.SplitContainer(); - this.topPanel.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.nudPartSpacing)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); - this.splitContainer.SuspendLayout(); - this.SuspendLayout(); - // + ColorScheme colorScheme1 = new ColorScheme(); + Plate plate1 = new Plate(); + Material material1 = new Material(); + Collections.ObservableList observableList_11 = new Collections.ObservableList(); + Plate plate2 = new Plate(); + Material material2 = new Material(); + Collections.ObservableList observableList_12 = new Collections.ObservableList(); + Plate plate3 = new Plate(); + Material material3 = new Material(); + Collections.ObservableList observableList_13 = new Collections.ObservableList(); + topPanel = new System.Windows.Forms.FlowLayoutPanel(); + lblDrawingA = new System.Windows.Forms.Label(); + cboDrawingA = new System.Windows.Forms.ComboBox(); + lblDrawingB = new System.Windows.Forms.Label(); + cboDrawingB = new System.Windows.Forms.ComboBox(); + lblPlateSize = new System.Windows.Forms.Label(); + txtPlateSize = new System.Windows.Forms.TextBox(); + lblPartSpacing = new System.Windows.Forms.Label(); + nudPartSpacing = new System.Windows.Forms.NumericUpDown(); + btnAutoArrange = new System.Windows.Forms.Button(); + btnApply = new System.Windows.Forms.Button(); + splitContainer = new System.Windows.Forms.SplitContainer(); + cellView = new OpenNest.Controls.PlateView(); + splitContainer1 = new System.Windows.Forms.SplitContainer(); + hPreview = new OpenNest.Controls.PlateView(); + hLabel = new System.Windows.Forms.Label(); + vPreview = new OpenNest.Controls.PlateView(); + vLabel = new System.Windows.Forms.Label(); + topPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)nudPartSpacing).BeginInit(); + ((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit(); + splitContainer.Panel1.SuspendLayout(); + splitContainer.Panel2.SuspendLayout(); + splitContainer.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); + splitContainer1.Panel1.SuspendLayout(); + splitContainer1.Panel2.SuspendLayout(); + splitContainer1.SuspendLayout(); + SuspendLayout(); + // // topPanel - // - this.topPanel.Controls.Add(this.lblDrawingA); - this.topPanel.Controls.Add(this.cboDrawingA); - this.topPanel.Controls.Add(this.lblDrawingB); - this.topPanel.Controls.Add(this.cboDrawingB); - this.topPanel.Controls.Add(this.lblPlateSize); - this.topPanel.Controls.Add(this.txtPlateSize); - this.topPanel.Controls.Add(this.lblPartSpacing); - this.topPanel.Controls.Add(this.nudPartSpacing); - this.topPanel.Controls.Add(this.btnAutoArrange); - this.topPanel.Controls.Add(this.btnApply); - this.topPanel.Dock = System.Windows.Forms.DockStyle.Top; - this.topPanel.Height = 36; - this.topPanel.Name = "topPanel"; - this.topPanel.WrapContents = false; - this.topPanel.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - // + // + topPanel.Controls.Add(lblDrawingA); + topPanel.Controls.Add(cboDrawingA); + topPanel.Controls.Add(lblDrawingB); + topPanel.Controls.Add(cboDrawingB); + topPanel.Controls.Add(lblPlateSize); + topPanel.Controls.Add(txtPlateSize); + topPanel.Controls.Add(lblPartSpacing); + topPanel.Controls.Add(nudPartSpacing); + topPanel.Controls.Add(btnAutoArrange); + topPanel.Controls.Add(btnApply); + topPanel.Dock = System.Windows.Forms.DockStyle.Top; + topPanel.Location = new System.Drawing.Point(0, 0); + topPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + topPanel.Name = "topPanel"; + topPanel.Padding = new System.Windows.Forms.Padding(5, 2, 5, 2); + topPanel.Size = new System.Drawing.Size(1220, 42); + topPanel.TabIndex = 2; + topPanel.WrapContents = false; + // // lblDrawingA - // - this.lblDrawingA.AutoSize = true; - this.lblDrawingA.Margin = new System.Windows.Forms.Padding(3, 5, 0, 0); - this.lblDrawingA.Name = "lblDrawingA"; - this.lblDrawingA.Text = "Drawing A:"; - // + // + lblDrawingA.AutoSize = true; + lblDrawingA.Location = new System.Drawing.Point(9, 8); + lblDrawingA.Margin = new System.Windows.Forms.Padding(4, 6, 0, 0); + lblDrawingA.Name = "lblDrawingA"; + lblDrawingA.Size = new System.Drawing.Size(65, 15); + lblDrawingA.TabIndex = 0; + lblDrawingA.Text = "Drawing A:"; + // // cboDrawingA - // - this.cboDrawingA.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.cboDrawingA.Margin = new System.Windows.Forms.Padding(3, 3, 0, 0); - this.cboDrawingA.Name = "cboDrawingA"; - this.cboDrawingA.Width = 130; - // + // + cboDrawingA.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + cboDrawingA.Location = new System.Drawing.Point(78, 5); + cboDrawingA.Margin = new System.Windows.Forms.Padding(4, 3, 0, 0); + cboDrawingA.Name = "cboDrawingA"; + cboDrawingA.Size = new System.Drawing.Size(151, 23); + cboDrawingA.TabIndex = 1; + // // lblDrawingB - // - this.lblDrawingB.AutoSize = true; - this.lblDrawingB.Margin = new System.Windows.Forms.Padding(10, 5, 0, 0); - this.lblDrawingB.Name = "lblDrawingB"; - this.lblDrawingB.Text = "Drawing B:"; - // + // + lblDrawingB.AutoSize = true; + lblDrawingB.Location = new System.Drawing.Point(241, 8); + lblDrawingB.Margin = new System.Windows.Forms.Padding(12, 6, 0, 0); + lblDrawingB.Name = "lblDrawingB"; + lblDrawingB.Size = new System.Drawing.Size(64, 15); + lblDrawingB.TabIndex = 2; + lblDrawingB.Text = "Drawing B:"; + // // cboDrawingB - // - this.cboDrawingB.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.cboDrawingB.Margin = new System.Windows.Forms.Padding(3, 3, 0, 0); - this.cboDrawingB.Name = "cboDrawingB"; - this.cboDrawingB.Width = 130; - // + // + cboDrawingB.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + cboDrawingB.Location = new System.Drawing.Point(309, 5); + cboDrawingB.Margin = new System.Windows.Forms.Padding(4, 3, 0, 0); + cboDrawingB.Name = "cboDrawingB"; + cboDrawingB.Size = new System.Drawing.Size(151, 23); + cboDrawingB.TabIndex = 3; + // // lblPlateSize - // - this.lblPlateSize.AutoSize = true; - this.lblPlateSize.Margin = new System.Windows.Forms.Padding(10, 5, 0, 0); - this.lblPlateSize.Name = "lblPlateSize"; - this.lblPlateSize.Text = "Plate:"; - // + // + lblPlateSize.AutoSize = true; + lblPlateSize.Location = new System.Drawing.Point(472, 8); + lblPlateSize.Margin = new System.Windows.Forms.Padding(12, 6, 0, 0); + lblPlateSize.Name = "lblPlateSize"; + lblPlateSize.Size = new System.Drawing.Size(36, 15); + lblPlateSize.TabIndex = 4; + lblPlateSize.Text = "Plate:"; + // // txtPlateSize - // - this.txtPlateSize.Margin = new System.Windows.Forms.Padding(3, 3, 0, 0); - this.txtPlateSize.Name = "txtPlateSize"; - this.txtPlateSize.Width = 90; - // + // + txtPlateSize.Location = new System.Drawing.Point(512, 5); + txtPlateSize.Margin = new System.Windows.Forms.Padding(4, 3, 0, 0); + txtPlateSize.Name = "txtPlateSize"; + txtPlateSize.Size = new System.Drawing.Size(104, 23); + txtPlateSize.TabIndex = 5; + // // lblPartSpacing - // - this.lblPartSpacing.AutoSize = true; - this.lblPartSpacing.Margin = new System.Windows.Forms.Padding(10, 5, 0, 0); - this.lblPartSpacing.Name = "lblPartSpacing"; - this.lblPartSpacing.Text = "Spacing:"; - // + // + lblPartSpacing.AutoSize = true; + lblPartSpacing.Location = new System.Drawing.Point(628, 8); + lblPartSpacing.Margin = new System.Windows.Forms.Padding(12, 6, 0, 0); + lblPartSpacing.Name = "lblPartSpacing"; + lblPartSpacing.Size = new System.Drawing.Size(52, 15); + lblPartSpacing.TabIndex = 6; + lblPartSpacing.Text = "Spacing:"; + // // nudPartSpacing - // - this.nudPartSpacing.DecimalPlaces = 2; - this.nudPartSpacing.Increment = new decimal(new int[] { 25, 0, 0, 131072 }); - this.nudPartSpacing.Maximum = new decimal(new int[] { 100, 0, 0, 0 }); - this.nudPartSpacing.Minimum = new decimal(new int[] { 0, 0, 0, 0 }); - this.nudPartSpacing.Margin = new System.Windows.Forms.Padding(3, 3, 0, 0); - this.nudPartSpacing.Name = "nudPartSpacing"; - this.nudPartSpacing.Width = 70; - // + // + nudPartSpacing.DecimalPlaces = 2; + nudPartSpacing.Increment = new decimal(new int[] { 25, 0, 0, 131072 }); + nudPartSpacing.Location = new System.Drawing.Point(684, 5); + nudPartSpacing.Margin = new System.Windows.Forms.Padding(4, 3, 0, 0); + nudPartSpacing.Name = "nudPartSpacing"; + nudPartSpacing.Size = new System.Drawing.Size(82, 23); + nudPartSpacing.TabIndex = 7; + // // btnAutoArrange - // - this.btnAutoArrange.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.btnAutoArrange.Margin = new System.Windows.Forms.Padding(10, 3, 0, 0); - this.btnAutoArrange.Name = "btnAutoArrange"; - this.btnAutoArrange.Size = new System.Drawing.Size(100, 26); - this.btnAutoArrange.Text = "Auto Arrange"; - // + // + btnAutoArrange.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + btnAutoArrange.Location = new System.Drawing.Point(778, 5); + btnAutoArrange.Margin = new System.Windows.Forms.Padding(12, 3, 0, 0); + btnAutoArrange.Name = "btnAutoArrange"; + btnAutoArrange.Size = new System.Drawing.Size(117, 30); + btnAutoArrange.TabIndex = 8; + btnAutoArrange.Text = "Auto Arrange"; + // // btnApply - // - this.btnApply.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.btnApply.Margin = new System.Windows.Forms.Padding(6, 3, 0, 0); - this.btnApply.Name = "btnApply"; - this.btnApply.Size = new System.Drawing.Size(80, 26); - this.btnApply.Text = "Apply"; - // + // + btnApply.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + btnApply.Location = new System.Drawing.Point(902, 5); + btnApply.Margin = new System.Windows.Forms.Padding(7, 3, 0, 0); + btnApply.Name = "btnApply"; + btnApply.Size = new System.Drawing.Size(93, 30); + btnApply.TabIndex = 9; + btnApply.Text = "Apply"; + // // splitContainer - // - this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; - this.splitContainer.Name = "splitContainer"; - this.splitContainer.SplitterDistance = 350; - this.splitContainer.TabIndex = 1; - // + // + splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; + splitContainer.Location = new System.Drawing.Point(0, 42); + splitContainer.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + splitContainer.Name = "splitContainer"; + // + // splitContainer.Panel1 + // + splitContainer.Panel1.Controls.Add(cellView); + // + // splitContainer.Panel2 + // + splitContainer.Panel2.Controls.Add(splitContainer1); + splitContainer.Size = new System.Drawing.Size(1220, 677); + splitContainer.SplitterDistance = 610; + splitContainer.SplitterWidth = 5; + splitContainer.TabIndex = 1; + // + // cellView + // + cellView.ActiveWorkArea = null; + cellView.AllowDrop = true; + cellView.AllowPan = true; + cellView.AllowSelect = true; + cellView.AllowZoom = true; + cellView.BackColor = System.Drawing.Color.DarkGray; + colorScheme1.BackgroundColor = System.Drawing.Color.DarkGray; + colorScheme1.BoundingBoxColor = System.Drawing.Color.FromArgb(128, 128, 255); + colorScheme1.EdgeSpacingColor = System.Drawing.Color.FromArgb(180, 180, 180); + colorScheme1.LayoutFillColor = System.Drawing.Color.WhiteSmoke; + colorScheme1.LayoutOutlineColor = System.Drawing.Color.Gray; + colorScheme1.OriginColor = System.Drawing.Color.Gray; + colorScheme1.PreviewPartColor = System.Drawing.Color.FromArgb(255, 140, 0); + colorScheme1.RapidColor = System.Drawing.Color.DodgerBlue; + cellView.ColorScheme = colorScheme1; + cellView.DebugRemnantPriorities = null; + cellView.DebugRemnants = null; + cellView.Dock = System.Windows.Forms.DockStyle.Fill; + cellView.DrawBounds = false; + cellView.DrawOffset = false; + cellView.DrawOrigin = false; + cellView.DrawRapid = false; + cellView.FillParts = true; + cellView.Location = new System.Drawing.Point(0, 0); + cellView.Name = "cellView"; + cellView.OffsetIncrementDistance = 10D; + cellView.OffsetTolerance = 0.001D; + material1.Density = 0D; + material1.Grade = null; + material1.Name = null; + plate1.Material = material1; + plate1.Parts = observableList_11; + plate1.PartSpacing = 0D; + plate1.Quadrant = 1; + plate1.Quantity = 0; + plate1.Thickness = 0D; + cellView.Plate = plate1; + cellView.RotateIncrementAngle = 10D; + cellView.Size = new System.Drawing.Size(610, 677); + cellView.Status = "Select"; + cellView.TabIndex = 0; + // + // splitContainer1 + // + splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + splitContainer1.IsSplitterFixed = true; + splitContainer1.Location = new System.Drawing.Point(0, 0); + splitContainer1.Name = "splitContainer1"; + splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainer1.Panel1 + // + splitContainer1.Panel1.Controls.Add(hPreview); + splitContainer1.Panel1.Controls.Add(hLabel); + // + // splitContainer1.Panel2 + // + splitContainer1.Panel2.Controls.Add(vPreview); + splitContainer1.Panel2.Controls.Add(vLabel); + splitContainer1.Size = new System.Drawing.Size(605, 677); + splitContainer1.SplitterDistance = 333; + splitContainer1.TabIndex = 0; + // + // hPreview + // + hPreview.ActiveWorkArea = null; + hPreview.AllowPan = true; + hPreview.AllowSelect = false; + hPreview.AllowZoom = true; + hPreview.BackColor = System.Drawing.Color.DarkGray; + hPreview.ColorScheme = colorScheme1; + hPreview.DebugRemnantPriorities = null; + hPreview.DebugRemnants = null; + hPreview.Dock = System.Windows.Forms.DockStyle.Fill; + hPreview.DrawBounds = false; + hPreview.DrawOffset = false; + hPreview.DrawOrigin = true; + hPreview.DrawRapid = false; + hPreview.FillParts = true; + hPreview.Location = new System.Drawing.Point(0, 20); + hPreview.Name = "hPreview"; + hPreview.OffsetIncrementDistance = 10D; + hPreview.OffsetTolerance = 0.001D; + material2.Density = 0D; + material2.Grade = null; + material2.Name = null; + plate2.Material = material2; + plate2.Parts = observableList_12; + plate2.PartSpacing = 0D; + plate2.Quadrant = 1; + plate2.Quantity = 0; + plate2.Thickness = 0D; + hPreview.Plate = plate2; + hPreview.RotateIncrementAngle = 10D; + hPreview.Size = new System.Drawing.Size(605, 313); + hPreview.Status = "Select"; + hPreview.TabIndex = 0; + // + // hLabel + // + hLabel.Dock = System.Windows.Forms.DockStyle.Top; + hLabel.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + hLabel.ForeColor = System.Drawing.Color.FromArgb(80, 80, 80); + hLabel.Location = new System.Drawing.Point(0, 0); + hLabel.Name = "hLabel"; + hLabel.Padding = new System.Windows.Forms.Padding(4, 0, 0, 0); + hLabel.Size = new System.Drawing.Size(605, 20); + hLabel.TabIndex = 1; + hLabel.Text = "Horizontal — 0 parts"; + hLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // vPreview + // + vPreview.ActiveWorkArea = null; + vPreview.AllowPan = true; + vPreview.AllowSelect = false; + vPreview.AllowZoom = true; + vPreview.BackColor = System.Drawing.Color.DarkGray; + vPreview.ColorScheme = colorScheme1; + vPreview.DebugRemnantPriorities = null; + vPreview.DebugRemnants = null; + vPreview.Dock = System.Windows.Forms.DockStyle.Fill; + vPreview.DrawBounds = false; + vPreview.DrawOffset = false; + vPreview.DrawOrigin = true; + vPreview.DrawRapid = false; + vPreview.FillParts = true; + vPreview.Location = new System.Drawing.Point(0, 20); + vPreview.Name = "vPreview"; + vPreview.OffsetIncrementDistance = 10D; + vPreview.OffsetTolerance = 0.001D; + material3.Density = 0D; + material3.Grade = null; + material3.Name = null; + plate3.Material = material3; + plate3.Parts = observableList_13; + plate3.PartSpacing = 0D; + plate3.Quadrant = 1; + plate3.Quantity = 0; + plate3.Thickness = 0D; + vPreview.Plate = plate3; + vPreview.RotateIncrementAngle = 10D; + vPreview.Size = new System.Drawing.Size(605, 320); + vPreview.Status = "Select"; + vPreview.TabIndex = 0; + // + // vLabel + // + vLabel.Dock = System.Windows.Forms.DockStyle.Top; + vLabel.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + vLabel.ForeColor = System.Drawing.Color.FromArgb(80, 80, 80); + vLabel.Location = new System.Drawing.Point(0, 0); + vLabel.Name = "vLabel"; + vLabel.Padding = new System.Windows.Forms.Padding(4, 0, 0, 0); + vLabel.Size = new System.Drawing.Size(605, 20); + vLabel.TabIndex = 1; + vLabel.Text = "Vertical — 0 parts"; + vLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // // PatternTileForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(900, 550); - this.Controls.Add(this.splitContainer); - this.Controls.Add(this.topPanel); - this.MinimumSize = new System.Drawing.Size(700, 400); - this.Name = "PatternTileForm"; - this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; - this.Text = "Pattern Tile"; - this.topPanel.ResumeLayout(false); - this.topPanel.PerformLayout(); - ((System.ComponentModel.ISupportInitialize)(this.nudPartSpacing)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit(); - this.splitContainer.ResumeLayout(false); - this.ResumeLayout(false); + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(1220, 719); + Controls.Add(splitContainer); + Controls.Add(topPanel); + Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + MinimumSize = new System.Drawing.Size(814, 456); + Name = "PatternTileForm"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Pattern Tile"; + WindowState = System.Windows.Forms.FormWindowState.Maximized; + topPanel.ResumeLayout(false); + topPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)nudPartSpacing).EndInit(); + splitContainer.Panel1.ResumeLayout(false); + splitContainer.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)splitContainer).EndInit(); + splitContainer.ResumeLayout(false); + splitContainer1.Panel1.ResumeLayout(false); + splitContainer1.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit(); + splitContainer1.ResumeLayout(false); + ResumeLayout(false); } private System.Windows.Forms.FlowLayoutPanel topPanel; @@ -161,5 +389,11 @@ namespace OpenNest.Forms private System.Windows.Forms.Button btnAutoArrange; private System.Windows.Forms.Button btnApply; private System.Windows.Forms.SplitContainer splitContainer; + private OpenNest.Controls.PlateView cellView; + private System.Windows.Forms.SplitContainer splitContainer1; + private OpenNest.Controls.PlateView hPreview; + private System.Windows.Forms.Label hLabel; + private OpenNest.Controls.PlateView vPreview; + private System.Windows.Forms.Label vLabel; } } diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index 5056da5..5e1089c 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -1,34 +1,16 @@ +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Geometry; using GeoSize = OpenNest.Geometry.Size; namespace OpenNest.Forms { - public enum PatternTileTarget - { - CurrentPlate, - NewPlate - } - - public class PatternTileResult - { - public List Parts { get; set; } - public PatternTileTarget Target { get; set; } - public GeoSize PlateSize { get; set; } - } - public partial class PatternTileForm : Form { private readonly Nest nest; - private readonly PlateView cellView; - private readonly PlateView hPreview; - private readonly PlateView vPreview; - private readonly Label hLabel; - private readonly Label vLabel; public PatternTileResult Result { get; private set; } @@ -37,53 +19,11 @@ namespace OpenNest.Forms this.nest = nest; InitializeComponent(); - // Unit cell editor — plate outline hidden via zero-size plate - cellView = new PlateView(); + // Hide plate outline via zero-size plate cellView.Plate.Size = new GeoSize(0, 0); - cellView.Plate.Quantity = 0; // prevent Drawing.Quantity.Nested side-effects - cellView.DrawOrigin = false; - cellView.DrawBounds = false; // hide selection bounding box overlay - cellView.Dock = DockStyle.Fill; - splitContainer.Panel1.Controls.Add(cellView); - - // Right side: vertical split with horizontal and vertical preview - var previewSplit = new SplitContainer - { - Dock = DockStyle.Fill, - Orientation = Orientation.Horizontal, - SplitterDistance = 250 - }; - splitContainer.Panel2.Controls.Add(previewSplit); - - hLabel = new Label - { - Dock = DockStyle.Top, - Height = 20, - Text = "Horizontal — 0 parts", - TextAlign = System.Drawing.ContentAlignment.MiddleLeft, - Font = new System.Drawing.Font("Segoe UI", 9f, System.Drawing.FontStyle.Bold), - ForeColor = System.Drawing.Color.FromArgb(80, 80, 80), - Padding = new Padding(4, 0, 0, 0) - }; - - hPreview = CreatePreviewView(); - previewSplit.Panel1.Controls.Add(hPreview); - previewSplit.Panel1.Controls.Add(hLabel); - - vLabel = new Label - { - Dock = DockStyle.Top, - Height = 20, - Text = "Vertical — 0 parts", - TextAlign = System.Drawing.ContentAlignment.MiddleLeft, - Font = new System.Drawing.Font("Segoe UI", 9f, System.Drawing.FontStyle.Bold), - ForeColor = System.Drawing.Color.FromArgb(80, 80, 80), - Padding = new Padding(4, 0, 0, 0) - }; - - vPreview = CreatePreviewView(); - previewSplit.Panel2.Controls.Add(vPreview); - previewSplit.Panel2.Controls.Add(vLabel); + cellView.Plate.Quantity = 0; + hPreview.Plate.Quantity = 0; + vPreview.Plate.Quantity = 0; // Populate drawing dropdowns var drawings = nest.Drawings.OrderBy(d => d.Name).ToList(); @@ -102,6 +42,12 @@ namespace OpenNest.Forms txtPlateSize.Text = defaults.Size.ToString(); nudPartSpacing.Value = (decimal)defaults.PartSpacing; + // Format drawing names in dropdowns + cboDrawingA.FormattingEnabled = true; + cboDrawingA.Format += ComboDrawing_Format; + cboDrawingB.FormattingEnabled = true; + cboDrawingB.Format += ComboDrawing_Format; + // Wire events cboDrawingA.SelectedIndexChanged += OnDrawingChanged; cboDrawingB.SelectedIndexChanged += OnDrawingChanged; @@ -112,6 +58,12 @@ namespace OpenNest.Forms cellView.MouseUp += OnCellMouseUp; } + private void ComboDrawing_Format(object sender, ListControlConvertEventArgs e) + { + if (e.Value is Drawing d) + e.Value = d.Name; + } + private Drawing SelectedDrawingA => cboDrawingA.SelectedItem as Drawing; @@ -144,6 +96,7 @@ namespace OpenNest.Forms if (e.Button == MouseButtons.Left && cellView.Plate.Parts.Count >= 2) { CompactCellParts(); + cellView.ZoomToFit(); } RebuildPreview(); @@ -212,12 +165,14 @@ namespace OpenNest.Forms if (System.Math.Sqrt(dx * dx + dy * dy) < 0.01) continue; - var angle = System.Math.Atan2(dy, dx); + var direction = new Vector(dx, dy); + var len = System.Math.Sqrt(dx * dx + dy * dy); + if (len > 0) direction = new Vector(dx / len, dy / len); var single = new List { part }; var obstacles = parts.Where(p => p != part).ToList(); totalMoved += Compactor.Push(single, obstacles, - syntheticWorkArea, spacing, angle); + syntheticWorkArea, spacing, direction); } if (totalMoved < 0.01) @@ -225,17 +180,6 @@ namespace OpenNest.Forms } } - private static PlateView CreatePreviewView() - { - var view = new PlateView(); - view.Plate.Quantity = 0; - view.AllowSelect = false; - view.AllowDrop = false; - view.DrawBounds = false; - view.Dock = DockStyle.Fill; - return view; - } - private void UpdatePreviewPlateSize() { if (!TryGetPlateSize(out var size)) @@ -275,7 +219,7 @@ namespace OpenNest.Forms if (pattern == null) return; - var workArea = new Box(0, 0, plateSize.Width, plateSize.Length); + var workArea = new Box(0, 0, plateSize.Length, plateSize.Width); var filler = new FillLinear(workArea, PartSpacing); var hParts = filler.Fill(pattern, NestDirection.Horizontal); @@ -384,7 +328,7 @@ namespace OpenNest.Forms if (pattern == null) return; - var filler = new FillLinear(new Box(0, 0, plateSize.Width, plateSize.Length), PartSpacing); + var filler = new FillLinear(new Box(0, 0, plateSize.Length, plateSize.Width), PartSpacing); var tiledParts = filler.Fill(pattern, applyDirection); Result = new PatternTileResult @@ -400,4 +344,17 @@ namespace OpenNest.Forms Close(); } } + + public enum PatternTileTarget + { + CurrentPlate, + NewPlate + } + + public class PatternTileResult + { + public List Parts { get; set; } + public PatternTileTarget Target { get; set; } + public GeoSize PlateSize { get; set; } + } } diff --git a/OpenNest/Forms/PatternTileForm.resx b/OpenNest/Forms/PatternTileForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/OpenNest/Forms/PatternTileForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/OpenNest/Forms/RemnantViewerForm.cs b/OpenNest/Forms/RemnantViewerForm.cs index 010850b..805b315 100644 --- a/OpenNest/Forms/RemnantViewerForm.cs +++ b/OpenNest/Forms/RemnantViewerForm.cs @@ -1,9 +1,10 @@ +using OpenNest.Controls; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Geometry; namespace OpenNest.Forms { diff --git a/OpenNest/GraphicsHelper.cs b/OpenNest/GraphicsHelper.cs index 5fa3ba1..f08130b 100644 --- a/OpenNest/GraphicsHelper.cs +++ b/OpenNest/GraphicsHelper.cs @@ -1,9 +1,8 @@ -using System; -using System.Drawing; -using System.Drawing.Drawing2D; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; +using System.Drawing; +using System.Drawing.Drawing2D; namespace OpenNest { @@ -53,7 +52,7 @@ namespace OpenNest var img = new Bitmap(size.Width, size.Height); var path = pgm.GetGraphicsPath(); var bounds = path.GetBounds(); - + var scalex = (size.Height - 10) / bounds.Height; var scaley = (size.Width - 10) / bounds.Width; var scale = scalex < scaley ? scalex : scaley; @@ -132,9 +131,9 @@ namespace OpenNest var sweepAngle = (endAngle - startAngle); path.AddArc( - pt.X, pt.Y, + pt.X, pt.Y, size, size, - (float)startAngle, + (float)startAngle, (float)sweepAngle); if (arc.Layer == LayerType.Leadin || arc.Layer == LayerType.Leadout) diff --git a/OpenNest/LayoutPart.cs b/OpenNest/LayoutPart.cs index d737bb3..05bfb76 100644 --- a/OpenNest/LayoutPart.cs +++ b/OpenNest/LayoutPart.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using OpenNest.Controls; +using OpenNest.Converters; +using OpenNest.Geometry; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; -using OpenNest.Controls; -using OpenNest.Converters; -using OpenNest.Geometry; namespace OpenNest { @@ -49,7 +49,7 @@ namespace OpenNest internal bool IsDirty { get; set; } public bool IsSelected { get; set; } - + public GraphicsPath Path { get; private set; } public Color Color diff --git a/OpenNest/MainApp.cs b/OpenNest/MainApp.cs index 67660c0..952cdfc 100644 --- a/OpenNest/MainApp.cs +++ b/OpenNest/MainApp.cs @@ -1,6 +1,6 @@ +using OpenNest.Forms; using System; using System.Windows.Forms; -using OpenNest.Forms; namespace OpenNest { diff --git a/OpenNest/ToolStripRenderer.cs b/OpenNest/ToolStripRenderer.cs index 3158aee..6b41021 100644 --- a/OpenNest/ToolStripRenderer.cs +++ b/OpenNest/ToolStripRenderer.cs @@ -151,7 +151,8 @@ namespace OpenNest return hot ? (int)MenuPopupItemStates.Hover : (int)MenuPopupItemStates.Normal; return hot ? (int)MenuPopupItemStates.DisabledHover : (int)MenuPopupItemStates.Disabled; } - else { + else + { if (item.Pressed) return item.Enabled ? (int)MenuBarItemStates.Pushed : (int)MenuBarItemStates.DisabledPushed; if (item.Enabled) @@ -265,7 +266,8 @@ namespace OpenNest e.Graphics.Clip = oldClip; } } - else { + else + { base.OnRenderToolStripBorder(e); } } @@ -299,7 +301,8 @@ namespace OpenNest Rectangle bgRect = GetBackgroundRectangle(e.Item); renderer.DrawBackground(e.Graphics, bgRect, bgRect); } - else { + else + { base.OnRenderMenuItemBackground(e); } } @@ -314,7 +317,8 @@ namespace OpenNest { renderer.SetParameters(RebarClass, RebarBackground, 0); } - else { + else + { renderer.SetParameters(RebarClass, 0, 0); } @@ -325,7 +329,8 @@ namespace OpenNest e.Handled = true; } - else { + else + { base.OnRenderToolStripPanelBackground(e); } } @@ -339,7 +344,8 @@ namespace OpenNest { renderer.SetParameters(MenuClass, (int)MenuParts.PopupBackground, 0); } - else { + else + { // It's a MenuStrip or a ToolStrip. If it's contained inside a larger panel, it should have a // transparent background, showing the panel's background. @@ -350,7 +356,8 @@ namespace OpenNest // if someone does that.) return; } - else { + else + { // A lone toolbar/menubar should act like it's inside a toolbox, I guess. // Maybe I should use the MenuClass in the case of a MenuStrip, although that would break // the other themes... @@ -366,7 +373,8 @@ namespace OpenNest renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds); } - else { + else + { base.OnRenderToolStripBackground(e); } } @@ -383,7 +391,8 @@ namespace OpenNest // It doesn't matter what colour of arrow we tell it to draw. OnRenderArrow will compute it from the item anyway. OnRenderArrow(new ToolStripArrowRenderEventArgs(e.Graphics, sb, sb.DropDownButtonBounds, Color.Red, ArrowDirection.Down)); } - else { + else + { base.OnRenderSplitButtonBackground(e); } } @@ -426,13 +435,15 @@ namespace OpenNest rect = new Rectangle(rect.X - extraWidth, rect.Y, sepWidth, rect.Height); rect.X += sepWidth; } - else { + else + { rect = new Rectangle(rect.Width + extraWidth - sepWidth, rect.Y, sepWidth, rect.Height); } renderer.DrawBackground(e.Graphics, rect); } } - else { + else + { base.OnRenderImageMargin(e); } } @@ -447,10 +458,10 @@ namespace OpenNest } else { - e.Graphics.DrawLine(Pens.LightGray, - e.Item.ContentRectangle.X, - e.Item.ContentRectangle.Y, - e.Item.ContentRectangle.X, + e.Graphics.DrawLine(Pens.LightGray, + e.Item.ContentRectangle.X, + e.Item.ContentRectangle.Y, + e.Item.ContentRectangle.X, e.Item.ContentRectangle.Y + e.Item.Height - 6); } } @@ -478,7 +489,8 @@ namespace OpenNest renderer.DrawBackground(e.Graphics, checkRect); } - else { + else + { base.OnRenderItemCheck(e); } } @@ -510,7 +522,8 @@ namespace OpenNest renderer.SetParameters(rebarClass, VisualStyleElement.Rebar.Chevron.Normal.Part, state); renderer.DrawBackground(e.Graphics, new Rectangle(Point.Empty, e.Item.Size)); } - else { + else + { base.OnRenderOverflowButtonBackground(e); } } diff --git a/README.md b/README.md index 0cc2b29..860ecb2 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ OpenNest takes your part drawings, lets you define your sheet (plate) sizes, and ## Features - **DXF Import/Export** — Load part drawings from DXF files and export completed nest layouts -- **Multiple Fill Strategies** — Grid-based linear fill, NFP (No Fit Polygon) pair fitting, and rectangle bin packing +- **Multiple Fill Strategies** — Grid-based linear fill and rectangle bin packing - **Part Rotation** — Automatically tries different rotation angles to find better fits - **Gravity Compaction** — After placing parts, pushes them together to close gaps - **Multi-Plate Support** — Work with multiple plates of different sizes and materials in a single nest - **G-code Output** — Post-process nested layouts to G-code for CNC cutting machines - **Built-in Shapes** — Create basic geometric parts (circles, rectangles, triangles, etc.) without needing a DXF file - **Interactive Editing** — Zoom, pan, select, clone, and manually arrange parts on the plate view -- **Lead-in/Lead-out & Tabs** — Configure cutting parameters like approach paths and holding tabs +- **Lead-in/Lead-out & Tabs** — Cutting parameters like approach paths and holding tabs (engine support, UI coming soon) ![OpenNest - 44 parts nested on a 60x120 plate](screenshots/screenshot-nest-2.png) @@ -67,7 +67,7 @@ dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- [ # Import a DXF and fill a 60x120 plate dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- part.dxf --size 60x120 -# Import multiple DXFs with NFP-based auto-nesting +# Import multiple DXFs with mixed-part auto-nesting (experimental) dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- part1.dxf part2.dxf --size 60x120 --autonest ``` @@ -86,7 +86,7 @@ dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- project.zip ext | Option | Description | |--------|-------------| | `--size ` | Plate size (e.g. `60x120`). Required for DXF-only mode. | -| `--autonest` | Use NFP-based mixed-part nesting instead of linear fill | +| `--autonest` | Use mixed-part nesting instead of linear fill (experimental) | | `--drawing ` | Select which drawing to fill with (default: first) | | `--quantity ` | Max parts to place (default: unlimited) | | `--spacing ` | Override part spacing | @@ -146,6 +146,14 @@ For most users, only these matter: | DWG (AutoCAD Drawing) | Yes | No | | G-code | No | Yes (via post-processors) | +## Roadmap + +- **NFP-based nesting** — No Fit Polygon algorithms and simulated annealing optimizer exist in the engine but aren't integrated into the UI or engine registry yet +- **Lead-in/Lead-out UI** — Engine support for lead-ins, lead-outs, and tabs is implemented; needs a UI for configuration +- **Sheet cut-offs** — Cut the sheet to size after nesting to reduce waste +- **Post-processors** — Plugin interface (`IPostProcessor`) is in place; need to ship built-in post-processors for common CNC controllers +- **Shape library UI** — Built-in shape generation code exists; needs a browsable library UI for quick access + ## Status OpenNest is under active development. The core nesting workflows function, but there's plenty of room for improvement in packing efficiency, UI polish, and format support. Contributions and feedback are welcome. diff --git a/docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md b/docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md new file mode 100644 index 0000000..71ab4ab --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md @@ -0,0 +1,1015 @@ +# NestProgressForm Redesign v2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Redesign the NestProgressForm with a phase stepper, grouped panels, density sparkline, color-coded flash & fade, and Accept/Stop buttons. + +**Architecture:** Four new/modified components: PhaseStepperControl (owner-drawn phase circles), DensityBar (owner-drawn sparkline), NestProgressForm (rewritten layout integrating both controls with grouped panels and dual buttons), and caller changes for Accept support. All controls use WinForms owner-draw with cached fonts/brushes. + +**Tech Stack:** C# .NET 8 WinForms, System.Drawing for GDI+ rendering + +**Spec:** `docs/superpowers/specs/2026-03-18-progress-form-redesign-v2-design.md` + +--- + +### Task 1: PhaseStepperControl + +**Files:** +- Create: `OpenNest/Controls/PhaseStepperControl.cs` + +- [ ] **Step 1: Create the PhaseStepperControl** + +```csharp +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace OpenNest.Controls +{ + public class PhaseStepperControl : UserControl + { + private static readonly Color AccentColor = Color.FromArgb(0, 120, 212); + private static readonly Color GlowColor = Color.FromArgb(60, 0, 120, 212); + private static readonly Color PendingBorder = Color.FromArgb(192, 192, 192); + private static readonly Color LineColor = Color.FromArgb(208, 208, 208); + private static readonly Color PendingTextColor = Color.FromArgb(153, 153, 153); + private static readonly Color ActiveTextColor = Color.FromArgb(51, 51, 51); + + private static readonly Font LabelFont = new Font("Segoe UI", 8f, FontStyle.Regular); + private static readonly Font BoldLabelFont = new Font("Segoe UI", 8f, FontStyle.Bold); + + private static readonly NestPhase[] Phases = (NestPhase[])Enum.GetValues(typeof(NestPhase)); + + private readonly HashSet visitedPhases = new(); + private NestPhase? activePhase; + private bool isComplete; + + public PhaseStepperControl() + { + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + Height = 60; + } + + public NestPhase? ActivePhase + { + get => activePhase; + set + { + activePhase = value; + if (value.HasValue) + visitedPhases.Add(value.Value); + Invalidate(); + } + } + + public bool IsComplete + { + get => isComplete; + set + { + isComplete = value; + if (value) + { + foreach (var phase in Phases) + visitedPhases.Add(phase); + activePhase = null; + } + Invalidate(); + } + } + + private static string GetDisplayName(NestPhase phase) + { + switch (phase) + { + case NestPhase.RectBestFit: return "BestFit"; + case NestPhase.Nfp: return "NFP"; + default: return phase.ToString(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + + var count = Phases.Length; + if (count == 0) return; + + var padding = 30; + var usableWidth = Width - padding * 2; + var spacing = usableWidth / (count - 1); + var circleY = 18; + var normalRadius = 9; + var activeRadius = 11; + + using var linePen = new Pen(LineColor, 2f); + using var accentBrush = new SolidBrush(AccentColor); + using var glowBrush = new SolidBrush(GlowColor); + using var pendingPen = new Pen(PendingBorder, 2f); + using var activeTextBrush = new SolidBrush(ActiveTextColor); + using var pendingTextBrush = new SolidBrush(PendingTextColor); + + // Draw connecting lines + for (var i = 0; i < count - 1; i++) + { + var x1 = padding + i * spacing; + var x2 = padding + (i + 1) * spacing; + g.DrawLine(linePen, x1, circleY, x2, circleY); + } + + // Draw circles and labels + for (var i = 0; i < count; i++) + { + var phase = Phases[i]; + var cx = padding + i * spacing; + var isActive = activePhase == phase && !isComplete; + var isVisited = visitedPhases.Contains(phase) || isComplete; + + if (isActive) + { + // Glow + g.FillEllipse(glowBrush, + cx - activeRadius - 3, circleY - activeRadius - 3, + (activeRadius + 3) * 2, (activeRadius + 3) * 2); + // Filled circle + g.FillEllipse(accentBrush, + cx - activeRadius, circleY - activeRadius, + activeRadius * 2, activeRadius * 2); + } + else if (isVisited) + { + g.FillEllipse(accentBrush, + cx - normalRadius, circleY - normalRadius, + normalRadius * 2, normalRadius * 2); + } + else + { + g.DrawEllipse(pendingPen, + cx - normalRadius, circleY - normalRadius, + normalRadius * 2, normalRadius * 2); + } + + // Label + var label = GetDisplayName(phase); + var font = isVisited || isActive ? BoldLabelFont : LabelFont; + var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush; + var labelSize = g.MeasureString(label, font); + g.DrawString(label, font, brush, + cx - labelSize.Width / 2, circleY + activeRadius + 5); + } + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest/Controls/PhaseStepperControl.cs +git commit -m "feat(ui): add PhaseStepperControl for nesting progress phases" +``` + +--- + +### Task 2: DensityBar Control + +**Files:** +- Create: `OpenNest/Controls/DensityBar.cs` + +- [ ] **Step 1: Create the DensityBar control** + +```csharp +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace OpenNest.Controls +{ + public class DensityBar : Control + { + private static readonly Color TrackColor = Color.FromArgb(224, 224, 224); + private static readonly Color LowColor = Color.FromArgb(245, 166, 35); + private static readonly Color HighColor = Color.FromArgb(76, 175, 80); + + private double value; + + public DensityBar() + { + DoubleBuffered = true; + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + Size = new Size(60, 8); + } + + public double Value + { + get => value; + set + { + this.value = System.Math.Clamp(value, 0.0, 1.0); + Invalidate(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + var rect = new Rectangle(0, 0, Width - 1, Height - 1); + + // Track background + using var trackPath = CreateRoundedRect(rect, 4); + using var trackBrush = new SolidBrush(TrackColor); + g.FillPath(trackBrush, trackPath); + + // Fill + var fillWidth = (int)(rect.Width * value); + if (fillWidth > 0) + { + var fillRect = new Rectangle(rect.X, rect.Y, fillWidth, rect.Height); + using var fillPath = CreateRoundedRect(fillRect, 4); + using var gradientBrush = new LinearGradientBrush( + new Point(rect.X, 0), new Point(rect.Right, 0), + LowColor, HighColor); + g.FillPath(gradientBrush, fillPath); + } + } + + private static GraphicsPath CreateRoundedRect(Rectangle rect, int radius) + { + var path = new GraphicsPath(); + var d = radius * 2; + path.AddArc(rect.X, rect.Y, d, d, 180, 90); + path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); + path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90); + path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); + path.CloseFigure(); + return path; + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest/Controls/DensityBar.cs +git commit -m "feat(ui): add DensityBar sparkline control for density visualization" +``` + +--- + +### Task 3: Rewrite NestProgressForm Designer + +**Files:** +- Modify: `OpenNest/Forms/NestProgressForm.Designer.cs` (full rewrite) + +The designer file creates the grouped layout: phase stepper at top, Results panel (Parts, Density + DensityBar, Nested), Status panel (Plate, Elapsed, Detail), and button bar (Accept + Stop). + +- [ ] **Step 1: Rewrite the designer** + +Replace the entire `InitializeComponent` method and field declarations. Key changes: +- Remove `phaseLabel`, `phaseValue`, `remnantLabel`, `remnantValue`, and the single flat `table` +- Add `phaseStepper` (PhaseStepperControl) +- Add `resultsPanel`, `statusPanel` (white Panels with header labels) +- Add `resultsTable`, `statusTable` (TableLayoutPanels inside panels) +- Add `densityBar` (DensityBar control, placed in density row) +- Add `acceptButton` alongside `stopButton` +- Fonts: Segoe UI 9pt bold for headers, 8.25pt bold for row labels, Consolas 8.25pt for values +- Form `ClientSize`: 450 x 315 + +Full designer code: + +```csharp +namespace OpenNest.Forms +{ + partial class NestProgressForm + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + phaseStepper = new Controls.PhaseStepperControl(); + resultsPanel = new System.Windows.Forms.Panel(); + resultsHeader = new System.Windows.Forms.Label(); + resultsTable = new System.Windows.Forms.TableLayoutPanel(); + partsLabel = new System.Windows.Forms.Label(); + partsValue = new System.Windows.Forms.Label(); + densityLabel = new System.Windows.Forms.Label(); + densityPanel = new System.Windows.Forms.FlowLayoutPanel(); + densityValue = new System.Windows.Forms.Label(); + densityBar = new Controls.DensityBar(); + nestedAreaLabel = new System.Windows.Forms.Label(); + nestedAreaValue = new System.Windows.Forms.Label(); + statusPanel = new System.Windows.Forms.Panel(); + statusHeader = new System.Windows.Forms.Label(); + statusTable = new System.Windows.Forms.TableLayoutPanel(); + plateLabel = new System.Windows.Forms.Label(); + plateValue = new System.Windows.Forms.Label(); + elapsedLabel = new System.Windows.Forms.Label(); + elapsedValue = new System.Windows.Forms.Label(); + descriptionLabel = new System.Windows.Forms.Label(); + descriptionValue = new System.Windows.Forms.Label(); + buttonPanel = new System.Windows.Forms.FlowLayoutPanel(); + acceptButton = new System.Windows.Forms.Button(); + stopButton = new System.Windows.Forms.Button(); + + resultsPanel.SuspendLayout(); + resultsTable.SuspendLayout(); + densityPanel.SuspendLayout(); + statusPanel.SuspendLayout(); + statusTable.SuspendLayout(); + buttonPanel.SuspendLayout(); + SuspendLayout(); + + // + // phaseStepper + // + phaseStepper.Dock = System.Windows.Forms.DockStyle.Top; + phaseStepper.Height = 60; + phaseStepper.Name = "phaseStepper"; + phaseStepper.TabIndex = 0; + + // + // resultsPanel + // + resultsPanel.BackColor = System.Drawing.Color.White; + resultsPanel.Controls.Add(resultsTable); + resultsPanel.Controls.Add(resultsHeader); + resultsPanel.Dock = System.Windows.Forms.DockStyle.Top; + resultsPanel.Location = new System.Drawing.Point(0, 60); + resultsPanel.Margin = new System.Windows.Forms.Padding(10, 4, 10, 4); + resultsPanel.Name = "resultsPanel"; + resultsPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); + resultsPanel.Size = new System.Drawing.Size(450, 105); + resultsPanel.TabIndex = 1; + + // + // resultsHeader + // + resultsHeader.AutoSize = true; + resultsHeader.Dock = System.Windows.Forms.DockStyle.Top; + resultsHeader.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + resultsHeader.ForeColor = System.Drawing.Color.FromArgb(85, 85, 85); + resultsHeader.Name = "resultsHeader"; + resultsHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); + resultsHeader.Size = new System.Drawing.Size(63, 19); + resultsHeader.TabIndex = 0; + resultsHeader.Text = "RESULTS"; + + // + // resultsTable + // + resultsTable.AutoSize = true; + resultsTable.ColumnCount = 2; + resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80F)); + resultsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + resultsTable.Controls.Add(partsLabel, 0, 0); + resultsTable.Controls.Add(partsValue, 1, 0); + resultsTable.Controls.Add(densityLabel, 0, 1); + resultsTable.Controls.Add(densityPanel, 1, 1); + resultsTable.Controls.Add(nestedAreaLabel, 0, 2); + resultsTable.Controls.Add(nestedAreaValue, 1, 2); + resultsTable.Dock = System.Windows.Forms.DockStyle.Top; + resultsTable.Name = "resultsTable"; + resultsTable.RowCount = 3; + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + resultsTable.TabIndex = 1; + + // + // partsLabel + // + partsLabel.AutoSize = true; + partsLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + partsLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + partsLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + partsLabel.Name = "partsLabel"; + partsLabel.Text = "Parts:"; + + // + // partsValue + // + partsValue.AutoSize = true; + partsValue.Font = new System.Drawing.Font("Consolas", 8.25F); + partsValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + partsValue.Name = "partsValue"; + partsValue.Text = "\u2014"; + + // + // densityLabel + // + densityLabel.AutoSize = true; + densityLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + densityLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + densityLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + densityLabel.Name = "densityLabel"; + densityLabel.Text = "Density:"; + + // + // densityPanel + // + densityPanel.AutoSize = true; + densityPanel.Controls.Add(densityValue); + densityPanel.Controls.Add(densityBar); + densityPanel.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight; + densityPanel.Margin = new System.Windows.Forms.Padding(0); + densityPanel.Name = "densityPanel"; + densityPanel.WrapContents = false; + + // + // densityValue + // + densityValue.AutoSize = true; + densityValue.Font = new System.Drawing.Font("Consolas", 8.25F); + densityValue.Margin = new System.Windows.Forms.Padding(0, 3, 8, 3); + densityValue.Name = "densityValue"; + densityValue.Text = "\u2014"; + + // + // densityBar + // + densityBar.Margin = new System.Windows.Forms.Padding(0, 5, 0, 0); + densityBar.Name = "densityBar"; + densityBar.Size = new System.Drawing.Size(60, 8); + + // + // nestedAreaLabel + // + nestedAreaLabel.AutoSize = true; + nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + nestedAreaLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + nestedAreaLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + nestedAreaLabel.Name = "nestedAreaLabel"; + nestedAreaLabel.Text = "Nested:"; + + // + // nestedAreaValue + // + nestedAreaValue.AutoSize = true; + nestedAreaValue.Font = new System.Drawing.Font("Consolas", 8.25F); + nestedAreaValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + nestedAreaValue.Name = "nestedAreaValue"; + nestedAreaValue.Text = "\u2014"; + + // + // statusPanel + // + statusPanel.BackColor = System.Drawing.Color.White; + statusPanel.Controls.Add(statusTable); + statusPanel.Controls.Add(statusHeader); + statusPanel.Dock = System.Windows.Forms.DockStyle.Top; + statusPanel.Location = new System.Drawing.Point(0, 169); + statusPanel.Name = "statusPanel"; + statusPanel.Padding = new System.Windows.Forms.Padding(14, 10, 14, 10); + statusPanel.Size = new System.Drawing.Size(450, 100); + statusPanel.TabIndex = 2; + + // + // statusHeader + // + statusHeader.AutoSize = true; + statusHeader.Dock = System.Windows.Forms.DockStyle.Top; + statusHeader.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + statusHeader.ForeColor = System.Drawing.Color.FromArgb(85, 85, 85); + statusHeader.Name = "statusHeader"; + statusHeader.Padding = new System.Windows.Forms.Padding(0, 0, 0, 4); + statusHeader.Size = new System.Drawing.Size(55, 19); + statusHeader.TabIndex = 0; + statusHeader.Text = "STATUS"; + + // + // statusTable + // + statusTable.AutoSize = true; + statusTable.ColumnCount = 2; + statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80F)); + statusTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + statusTable.Controls.Add(plateLabel, 0, 0); + statusTable.Controls.Add(plateValue, 1, 0); + statusTable.Controls.Add(elapsedLabel, 0, 1); + statusTable.Controls.Add(elapsedValue, 1, 1); + statusTable.Controls.Add(descriptionLabel, 0, 2); + statusTable.Controls.Add(descriptionValue, 1, 2); + statusTable.Dock = System.Windows.Forms.DockStyle.Top; + statusTable.Name = "statusTable"; + statusTable.RowCount = 3; + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.RowStyles.Add(new System.Windows.Forms.RowStyle()); + statusTable.TabIndex = 1; + + // + // plateLabel + // + plateLabel.AutoSize = true; + plateLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + plateLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + plateLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + plateLabel.Name = "plateLabel"; + plateLabel.Text = "Plate:"; + + // + // plateValue + // + plateValue.AutoSize = true; + plateValue.Font = new System.Drawing.Font("Consolas", 8.25F); + plateValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + plateValue.Name = "plateValue"; + plateValue.Text = "\u2014"; + + // + // elapsedLabel + // + elapsedLabel.AutoSize = true; + elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + elapsedLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + elapsedLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + elapsedLabel.Name = "elapsedLabel"; + elapsedLabel.Text = "Elapsed:"; + + // + // elapsedValue + // + elapsedValue.AutoSize = true; + elapsedValue.Font = new System.Drawing.Font("Consolas", 8.25F); + elapsedValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + elapsedValue.Name = "elapsedValue"; + elapsedValue.Text = "0:00"; + + // + // descriptionLabel + // + descriptionLabel.AutoSize = true; + descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + descriptionLabel.ForeColor = System.Drawing.Color.FromArgb(51, 51, 51); + descriptionLabel.Margin = new System.Windows.Forms.Padding(0, 3, 5, 3); + descriptionLabel.Name = "descriptionLabel"; + descriptionLabel.Text = "Detail:"; + + // + // descriptionValue + // + descriptionValue.AutoSize = true; + descriptionValue.Font = new System.Drawing.Font("Segoe UI", 8.25F); + descriptionValue.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + descriptionValue.Name = "descriptionValue"; + descriptionValue.Text = "\u2014"; + + // + // buttonPanel + // + buttonPanel.AutoSize = true; + buttonPanel.Controls.Add(stopButton); + buttonPanel.Controls.Add(acceptButton); + buttonPanel.Dock = System.Windows.Forms.DockStyle.Top; + buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + buttonPanel.Name = "buttonPanel"; + buttonPanel.Padding = new System.Windows.Forms.Padding(9, 6, 9, 6); + buttonPanel.Size = new System.Drawing.Size(450, 45); + buttonPanel.TabIndex = 3; + + // + // acceptButton + // + acceptButton.Enabled = false; + acceptButton.Font = new System.Drawing.Font("Segoe UI", 8.25F); + acceptButton.Margin = new System.Windows.Forms.Padding(6, 3, 0, 3); + acceptButton.Name = "acceptButton"; + acceptButton.Size = new System.Drawing.Size(93, 27); + acceptButton.TabIndex = 1; + acceptButton.Text = "Accept"; + acceptButton.UseVisualStyleBackColor = true; + acceptButton.Click += AcceptButton_Click; + + // + // stopButton + // + stopButton.Enabled = false; + stopButton.Font = new System.Drawing.Font("Segoe UI", 8.25F); + stopButton.Margin = new System.Windows.Forms.Padding(0, 3, 0, 3); + stopButton.Name = "stopButton"; + stopButton.Size = new System.Drawing.Size(93, 27); + stopButton.TabIndex = 0; + stopButton.Text = "Stop"; + stopButton.UseVisualStyleBackColor = true; + stopButton.Click += StopButton_Click; + + // + // NestProgressForm + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(450, 315); + Controls.Add(buttonPanel); + Controls.Add(statusPanel); + Controls.Add(resultsPanel); + Controls.Add(phaseStepper); + FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + MaximizeBox = false; + MinimizeBox = false; + Name = "NestProgressForm"; + ShowInTaskbar = false; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Nesting Progress"; + resultsPanel.ResumeLayout(false); + resultsPanel.PerformLayout(); + resultsTable.ResumeLayout(false); + resultsTable.PerformLayout(); + densityPanel.ResumeLayout(false); + densityPanel.PerformLayout(); + statusPanel.ResumeLayout(false); + statusPanel.PerformLayout(); + statusTable.ResumeLayout(false); + statusTable.PerformLayout(); + buttonPanel.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Controls.PhaseStepperControl phaseStepper; + private System.Windows.Forms.Panel resultsPanel; + private System.Windows.Forms.Label resultsHeader; + private System.Windows.Forms.TableLayoutPanel resultsTable; + private System.Windows.Forms.Label partsLabel; + private System.Windows.Forms.Label partsValue; + private System.Windows.Forms.Label densityLabel; + private System.Windows.Forms.FlowLayoutPanel densityPanel; + private System.Windows.Forms.Label densityValue; + private Controls.DensityBar densityBar; + private System.Windows.Forms.Label nestedAreaLabel; + private System.Windows.Forms.Label nestedAreaValue; + private System.Windows.Forms.Panel statusPanel; + private System.Windows.Forms.Label statusHeader; + private System.Windows.Forms.TableLayoutPanel statusTable; + private System.Windows.Forms.Label plateLabel; + private System.Windows.Forms.Label plateValue; + private System.Windows.Forms.Label elapsedLabel; + private System.Windows.Forms.Label elapsedValue; + private System.Windows.Forms.Label descriptionLabel; + private System.Windows.Forms.Label descriptionValue; + private System.Windows.Forms.FlowLayoutPanel buttonPanel; + private System.Windows.Forms.Button acceptButton; + private System.Windows.Forms.Button stopButton; + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: Build errors — `NestProgressForm.cs` still references removed fields (`phaseValue`, `remnantValue`, etc.). This is expected; Task 4 will fix it. + +- [ ] **Step 3: Commit** + +Do NOT commit yet — the code-behind (Task 4) must be updated first to compile. + +--- + +### Task 4: Rewrite NestProgressForm Code-Behind + +**Files:** +- Modify: `OpenNest/Forms/NestProgressForm.cs` (full rewrite) + +- [ ] **Step 1: Rewrite the form code-behind** + +Replace the entire file with the updated logic: phase stepper integration, color-coded flash & fade with per-label color tracking, Accept/Stop buttons, updated FormatPhase, and DensityBar updates. + +```csharp +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Threading; +using System.Windows.Forms; + +namespace OpenNest.Forms +{ + public partial class NestProgressForm : Form + { + private static readonly Color DefaultFlashColor = Color.FromArgb(0, 160, 0); + private static readonly Color DensityLowColor = Color.FromArgb(200, 40, 40); + private static readonly Color DensityMidColor = Color.FromArgb(200, 160, 0); + private static readonly Color DensityHighColor = Color.FromArgb(0, 160, 0); + + private const int FadeSteps = 20; + private const int FadeIntervalMs = 50; + + private readonly CancellationTokenSource cts; + private readonly Stopwatch stopwatch = Stopwatch.StartNew(); + private readonly System.Windows.Forms.Timer elapsedTimer; + private readonly System.Windows.Forms.Timer fadeTimer; + private readonly Dictionary fadeCounters = new(); + private bool hasReceivedProgress; + + public bool Accepted { get; private set; } + + public NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true) + { + this.cts = cts; + InitializeComponent(); + + if (!showPlateRow) + { + plateLabel.Visible = false; + plateValue.Visible = false; + } + + elapsedTimer = new System.Windows.Forms.Timer { Interval = 1000 }; + elapsedTimer.Tick += (s, e) => UpdateElapsed(); + elapsedTimer.Start(); + + fadeTimer = new System.Windows.Forms.Timer { Interval = FadeIntervalMs }; + fadeTimer.Tick += FadeTimer_Tick; + } + + public void UpdateProgress(NestProgress progress) + { + if (IsDisposed || !IsHandleCreated) + return; + + if (!hasReceivedProgress) + { + hasReceivedProgress = true; + acceptButton.Enabled = true; + stopButton.Enabled = true; + } + + phaseStepper.ActivePhase = progress.Phase; + + SetValueWithFlash(plateValue, progress.PlateNumber.ToString()); + SetValueWithFlash(partsValue, progress.BestPartCount.ToString()); + + var densityText = progress.BestDensity.ToString("P1"); + var densityFlashColor = GetDensityColor(progress.BestDensity); + SetValueWithFlash(densityValue, densityText, densityFlashColor); + densityBar.Value = progress.BestDensity; + + SetValueWithFlash(nestedAreaValue, + $"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)"); + + descriptionValue.Text = !string.IsNullOrEmpty(progress.Description) + ? progress.Description + : FormatPhase(progress.Phase); + } + + public void ShowCompleted() + { + if (IsDisposed || !IsHandleCreated) + return; + + stopwatch.Stop(); + elapsedTimer.Stop(); + UpdateElapsed(); + + phaseStepper.IsComplete = true; + descriptionValue.Text = "\u2014"; + + acceptButton.Visible = false; + stopButton.Text = "Close"; + stopButton.Enabled = true; + stopButton.Click -= StopButton_Click; + stopButton.Click += (s, e) => Close(); + } + + private void UpdateElapsed() + { + if (IsDisposed || !IsHandleCreated) + return; + + var elapsed = stopwatch.Elapsed; + elapsedValue.Text = elapsed.TotalHours >= 1 + ? elapsed.ToString(@"h\:mm\:ss") + : elapsed.ToString(@"m\:ss"); + } + + private void AcceptButton_Click(object sender, EventArgs e) + { + Accepted = true; + cts.Cancel(); + acceptButton.Enabled = false; + stopButton.Enabled = false; + acceptButton.Text = "Accepted"; + stopButton.Text = "Stopping..."; + } + + private void StopButton_Click(object sender, EventArgs e) + { + cts.Cancel(); + acceptButton.Enabled = false; + stopButton.Enabled = false; + stopButton.Text = "Stopping..."; + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + fadeTimer.Stop(); + fadeTimer.Dispose(); + elapsedTimer.Stop(); + elapsedTimer.Dispose(); + stopwatch.Stop(); + + if (!cts.IsCancellationRequested) + cts.Cancel(); + + base.OnFormClosing(e); + } + + private void SetValueWithFlash(Label label, string text, Color? flashColor = null) + { + if (label.Text == text) + return; + + var color = flashColor ?? DefaultFlashColor; + label.Text = text; + label.ForeColor = color; + fadeCounters[label] = (FadeSteps, color); + + if (!fadeTimer.Enabled) + fadeTimer.Start(); + } + + private void FadeTimer_Tick(object sender, EventArgs e) + { + if (IsDisposed || !IsHandleCreated) + { + fadeTimer.Stop(); + return; + } + + var defaultColor = SystemColors.ControlText; + var labels = fadeCounters.Keys.ToList(); + + foreach (var label in labels) + { + var (remaining, flashColor) = fadeCounters[label]; + remaining--; + + if (remaining <= 0) + { + label.ForeColor = defaultColor; + fadeCounters.Remove(label); + } + else + { + var ratio = (float)remaining / FadeSteps; + var r = (int)(defaultColor.R + (flashColor.R - defaultColor.R) * ratio); + var g = (int)(defaultColor.G + (flashColor.G - defaultColor.G) * ratio); + var b = (int)(defaultColor.B + (flashColor.B - defaultColor.B) * ratio); + label.ForeColor = Color.FromArgb(r, g, b); + fadeCounters[label] = (remaining, flashColor); + } + } + + if (fadeCounters.Count == 0) + fadeTimer.Stop(); + } + + private static Color GetDensityColor(double density) + { + if (density < 0.5) + return DensityLowColor; + if (density < 0.7) + return DensityMidColor; + return DensityHighColor; + } + + private static string FormatPhase(NestPhase phase) + { + switch (phase) + { + case NestPhase.Linear: return "Trying rotations..."; + case NestPhase.RectBestFit: return "Trying best fit..."; + case NestPhase.Pairs: return "Trying pairs..."; + case NestPhase.Extents: return "Trying extents..."; + case NestPhase.Nfp: return "Trying NFP..."; + default: return phase.ToString(); + } + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest/Forms/NestProgressForm.cs OpenNest/Forms/NestProgressForm.Designer.cs +git commit -m "feat(ui): rewrite NestProgressForm with grouped panels, stepper, density bar, and Accept button" +``` + +--- + +### Task 5: Update Callers for Accept Support + +**Files:** +- Modify: `OpenNest/Forms/MainForm.cs:868` +- Modify: `OpenNest/Controls/PlateView.cs:933` + +- [ ] **Step 1: Update `RunAutoNest_Click` in MainForm.cs** + +At line 868, change the cancellation check to also honor `Accepted`: + +```csharp +// Before: +if (nestParts.Count > 0 && !token.IsCancellationRequested) +// After: +if (nestParts.Count > 0 && (!token.IsCancellationRequested || progressForm.Accepted)) +``` + +- [ ] **Step 2: Update `FillWithProgress` in PlateView.cs** + +At line 933, same change: + +```csharp +// Before: +if (parts.Count > 0 && !cts.IsCancellationRequested) +// After: +if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted)) +``` + +- [ ] **Step 3: Build to verify** + +Run: `dotnet build OpenNest.sln --no-restore -v q` +Expected: 0 errors + +- [ ] **Step 4: Commit** + +```bash +git add OpenNest/Forms/MainForm.cs OpenNest/Controls/PlateView.cs +git commit -m "feat(ui): support Accept button in nesting callers" +``` + +--- + +### Task 6: Build Verification and Layout Tuning + +**Files:** None new — adjustments to existing files from Tasks 1-5 if needed. + +- [ ] **Step 1: Full solution build** + +Run: `dotnet build OpenNest.sln -v q` +Expected: 0 errors + +- [ ] **Step 2: Run existing tests** + +Run: `dotnet test OpenNest.Tests/OpenNest.Tests.csproj --no-build -v q` +Expected: All tests pass (no regressions — these tests don't touch the UI) + +- [ ] **Step 3: Visual review checklist** + +Launch the app and open or create a nest with drawings. Run a fill operation and verify: + +1. Phase stepper shows circles for all 6 phases — initially all hollow/pending +2. As the engine progresses, active phase circle is larger with glow, visited phases are filled blue +3. Results section has white background with "RESULTS" header, monospaced values +4. Status section has white background with "STATUS" header +5. Density bar shows next to percentage, gradient from orange to green +6. Values flash green (or red/yellow for low density) and fade back to black on change +7. Accept button stops engine and keeps result on plate +8. Stop button stops engine and discards result +9. Both buttons disabled until first progress update +10. After ShowCompleted(), both replaced by "Close" button + +- [ ] **Step 4: Final commit if any tuning was needed** + +```bash +git add -A +git commit -m "fix(ui): tune progress form layout spacing" +``` diff --git a/docs/superpowers/plans/2026-03-18-refactor-compactor.md b/docs/superpowers/plans/2026-03-18-refactor-compactor.md new file mode 100644 index 0000000..7dad20c --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-refactor-compactor.md @@ -0,0 +1,361 @@ +# Refactor Compactor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Prune dead code from Compactor and deduplicate the Push overloads into a single scanning core. + +**Architecture:** Delete 6 unused methods (Compact, CompactLoop, SavePositions, RestorePositions, CompactIndividual, CompactIndividualLoop). Unify the `Push(... PushDirection)` core overload to convert its PushDirection to a unit Vector and delegate to the `Push(... Vector)` overload, eliminating ~60 lines of duplicated obstacle scanning logic. PushBoundingBox stays separate since it's a fundamentally different algorithm (no geometry lines). + +**Tech Stack:** C# / .NET 8 + +--- + +### Task 1: Write Compactor Push tests as a safety net + +No Compactor tests exist. Before changing anything, add tests for the public Push methods that have live callers: `Push(parts, obstacles, workArea, spacing, PushDirection)` and `Push(parts, obstacles, workArea, spacing, angle)`. + +**Files:** +- Create: `OpenNest.Tests/CompactorTests.cs` + +- [ ] **Step 1: Write tests for Push with PushDirection** + +```csharp +using OpenNest; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using Xunit; +using System.Collections.Generic; + +namespace OpenNest.Tests +{ + public class CompactorTests + { + private static Drawing MakeRectDrawing(double w, double h) + { + var pgm = new OpenNest.CNC.Program(); + pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0))); + return new Drawing("rect", pgm); + } + + private static Part MakeRectPart(double x, double y, double w, double h) + { + var drawing = MakeRectDrawing(w, h); + var part = new Part(drawing) { Location = new Vector(x, y) }; + part.UpdateBounds(); + return part; + } + + [Fact] + public void Push_Left_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Left < 1); + } + + [Fact] + public void Push_Left_StopsAtObstacle() + { + var workArea = new Box(0, 0, 100, 100); + var obstacle = MakeRectPart(0, 0, 10, 10); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List { obstacle }; + + Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1); + } + + [Fact] + public void Push_Down_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(0, 50, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Down); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Bottom < 1); + } + + [Fact] + public void Push_ReturnsZero_WhenAlreadyAtEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(0, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.Equal(0, distance); + } + + [Fact] + public void Push_WithSpacing_MaintainsGap() + { + var workArea = new Box(0, 0, 100, 100); + var obstacle = MakeRectPart(0, 0, 10, 10); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List { obstacle }; + + Compactor.Push(moving, obstacles, workArea, 2, PushDirection.Left); + + Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right + 2 - 0.5); + } + } +} +``` + +- [ ] **Step 2: Write tests for Push with angle (Vector-based)** + +Add to the same file: + +```csharp + [Fact] + public void Push_AngleLeft_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + // angle = π = push left + var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Left < 1); + } + + [Fact] + public void Push_AngleDown_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(0, 50, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + // angle = 3π/2 = push down + var distance = Compactor.Push(moving, obstacles, workArea, 0, 3 * System.Math.PI / 2); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Bottom < 1); + } +``` + +- [ ] **Step 3: Write tests for PushBoundingBox** + +Add to the same file: + +```csharp + [Fact] + public void PushBoundingBox_Left_MovesPartTowardEdge() + { + var workArea = new Box(0, 0, 100, 100); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List(); + + var distance = Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(distance > 0); + Assert.True(part.BoundingBox.Left < 1); + } + + [Fact] + public void PushBoundingBox_StopsAtObstacle() + { + var workArea = new Box(0, 0, 100, 100); + var obstacle = MakeRectPart(0, 0, 10, 10); + var part = MakeRectPart(50, 0, 10, 10); + var moving = new List { part }; + var obstacles = new List { obstacle }; + + Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left); + + Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1); + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~CompactorTests" -v n` +Expected: All tests PASS (these test existing behavior before refactoring) + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Tests/CompactorTests.cs +git commit -m "test: add Compactor safety-net tests before refactor" +``` + +--- + +### Task 2: Delete dead code + +Remove the 6 methods that have zero live callers: `Compact`, `CompactLoop`, `SavePositions`, `RestorePositions`, `CompactIndividual`, `CompactIndividualLoop`. Also remove the unused `contactGap` variable (line 181) and the misplaced XML doc comment above `RepeatThreshold` (lines 16-20, describes the deleted `Compact` method). + +**Files:** +- Modify: `OpenNest.Engine/Fill/Compactor.cs` + +- [ ] **Step 1: Delete SavePositions and RestorePositions** + +Delete `SavePositions` (lines 64-70) and `RestorePositions` (lines 72-76). These are only used by `Compact` and `CompactIndividual`. + +- [ ] **Step 2: Delete Compact and CompactLoop** + +Delete the `Compact` method (lines 24-44) and `CompactLoop` method (lines 46-62). Zero callers. + +- [ ] **Step 3: Delete CompactIndividual and CompactIndividualLoop** + +Delete `CompactIndividual` (lines 312-332) and `CompactIndividualLoop` (lines 334-360). Only caller is a commented-out line in `StripNestEngine.cs:189`. + +- [ ] **Step 4: Remove the commented-out caller in StripNestEngine** + +In `OpenNest.Engine/StripNestEngine.cs`, delete the entire commented-out block (lines 186-194): +```csharp + // TODO: Compact strip parts individually to close geometry-based gaps. + // Disabled pending investigation — remnant finder picks up gaps created + // by compaction and scatters parts into them. + // Compactor.CompactIndividual(bestParts, workArea, Plate.PartSpacing); + // + // var compactedBox = bestParts.Cast().GetBoundingBox(); + // bestDim = direction == StripDirection.Bottom + // ? compactedBox.Top - workArea.Y + // : compactedBox.Right - workArea.X; +``` + +- [ ] **Step 5: Clean up stale doc comment and dead variable** + +Remove the orphaned XML doc comment above `RepeatThreshold` (lines 16-20 — it describes the deleted `Compact` method). Remove the `RepeatThreshold` and `MaxIterations` constants (only used by the deleted loop methods). Remove the unused `contactGap` variable from the `Push(... PushDirection)` method (line 181). + +- [ ] **Step 6: Run tests** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~CompactorTests" -v n` +Expected: All tests PASS (deleted code was unused) + +- [ ] **Step 7: Build full solution to verify no compilation errors** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeded, 0 errors + +- [ ] **Step 8: Commit** + +```bash +git add OpenNest.Engine/Fill/Compactor.cs OpenNest.Engine/StripNestEngine.cs +git commit -m "refactor(compactor): remove dead code — Compact, CompactIndividual, and helpers" +``` + +--- + +### Task 3: Deduplicate Push overloads + +The `Push(... PushDirection)` core overload (lines 166-238) duplicates the obstacle scanning loop from `Push(... Vector)` (lines 102-164). Convert `PushDirection` to a unit `Vector` and delegate. + +**Files:** +- Modify: `OpenNest.Engine/Fill/Compactor.cs` + +- [ ] **Step 1: Replace the Push(... PushDirection) core overload** + +Replace the full body of `Push(List movingParts, List obstacleParts, Box workArea, double partSpacing, PushDirection direction)` with a delegation to the Vector overload: + +```csharp +public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, PushDirection direction) +{ + var vector = SpatialQuery.DirectionToOffset(direction, 1.0); + return Push(movingParts, obstacleParts, workArea, partSpacing, vector); +} +``` + +This works because `DirectionToOffset(Left, 1.0)` returns `(-1, 0)`, which is the unit vector for "push left" — exactly what `new Vector(Math.Cos(π), Math.Sin(π))` produces. The Vector overload already handles edge distance, obstacle scanning, geometry lines, and offset application identically. + +- [ ] **Step 2: Update the angle-based Push to accept Vector directly** + +Rename the existing `Push(... double angle)` core overload to accept a `Vector` direction instead of computing it internally. This avoids a redundant cos/sin when the PushDirection overload already provides a unit vector. + +Change the signature from: +```csharp +public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, double angle) +``` +to: +```csharp +public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, Vector direction) +``` + +Remove the `var direction = new Vector(...)` line from the body since `direction` is now a parameter. + +- [ ] **Step 3: Update the angle convenience overload to convert** + +The convenience overload `Push(List movingParts, Plate plate, double angle)` must now convert the angle to a Vector before calling the core: + +```csharp +public static double Push(List movingParts, Plate plate, double angle) +{ + var obstacleParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); + return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); +} +``` + +- [ ] **Step 4: Run tests** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~CompactorTests" -v n` +Expected: All tests PASS + +- [ ] **Step 5: Build full solution** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeded, 0 errors. All callers in FillExtents, ActionClone, PlateView, PatternTileForm compile without changes — their call signatures are unchanged. + +- [ ] **Step 6: Commit** + +```bash +git add OpenNest.Engine/Fill/Compactor.cs +git commit -m "refactor(compactor): deduplicate Push — PushDirection delegates to Vector overload" +``` + +--- + +### Task 4: Final cleanup and verify + +**Files:** +- Modify: `OpenNest.Engine/Fill/Compactor.cs` (if needed) + +- [ ] **Step 1: Run full test suite** + +Run: `dotnet test OpenNest.Tests -v n` +Expected: All tests PASS + +- [ ] **Step 2: Verify Compactor is clean** + +The final Compactor should have 6 public methods: +1. `Push(parts, plate, PushDirection)` — convenience, extracts plate fields +2. `Push(parts, plate, angle)` — convenience, converts angle to Vector +3. `Push(parts, obstacles, workArea, spacing, PushDirection)` — converts to Vector, delegates +4. `Push(parts, obstacles, workArea, spacing, Vector)` — the single scanning core +5. `PushBoundingBox(parts, plate, direction)` — convenience +6. `PushBoundingBox(parts, obstacles, workArea, spacing, direction)` — BB-only core + +Plus one constant: `ChordTolerance`. + +File should be ~110-120 lines, down from 362. diff --git a/docs/superpowers/plans/2026-03-18-two-bucket-preview.md b/docs/superpowers/plans/2026-03-18-two-bucket-preview.md new file mode 100644 index 0000000..38062d6 --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-two-bucket-preview.md @@ -0,0 +1,660 @@ +# Two-Bucket Preview Parts Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Split PlateView nesting preview into stationary (overall best) and active (current strategy) layers so the preview never regresses and acceptance always uses the engine's result. + +**Architecture:** Add `IsOverallBest` flag to `NestProgress` so the engine can distinguish overall-best reports from strategy-local progress. PlateView maintains two `List` buckets drawn at different opacities. Acceptance uses the engine's returned parts directly, decoupling preview from acceptance. + +**Tech Stack:** C# / .NET 8 / WinForms / System.Drawing + +**Spec:** `docs/superpowers/specs/2026-03-18-two-bucket-preview-design.md` + +--- + +### Task 1: Add IsOverallBest to NestProgress and ReportProgress + +**Files:** +- Modify: `OpenNest.Engine/NestProgress.cs:37-49` +- Modify: `OpenNest.Engine/NestEngineBase.cs:188-236` + +- [ ] **Step 1: Add property to NestProgress** + +In `OpenNest.Engine/NestProgress.cs`, add after line 48 (`ActiveWorkArea`): + +```csharp +public bool IsOverallBest { get; set; } +``` + +- [ ] **Step 2: Add parameter to ReportProgress** + +In `OpenNest.Engine/NestEngineBase.cs`, change the `ReportProgress` signature (line 188) to: + +```csharp +internal static void ReportProgress( + IProgress progress, + NestPhase phase, + int plateNumber, + List best, + Box workArea, + string description, + bool isOverallBest = false) +``` + +In the same method, add `IsOverallBest = isOverallBest` to the `NestProgress` initializer (after line 235 `ActiveWorkArea = workArea`): + +```csharp +IsOverallBest = isOverallBest, +``` + +- [ ] **Step 3: Build to verify** + +Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj` +Expected: Build succeeded, 0 errors. Existing callers use the default `false`. + +- [ ] **Step 4: Commit** + +``` +feat(engine): add IsOverallBest flag to NestProgress +``` + +--- + +### Task 2: Flag overall-best reports in DefaultNestEngine + +**Files:** +- Modify: `OpenNest.Engine/DefaultNestEngine.cs:55-58` (final report in Fill(NestItem)) +- Modify: `OpenNest.Engine/DefaultNestEngine.cs:83-85` (final report in Fill(List)) +- Modify: `OpenNest.Engine/DefaultNestEngine.cs:132-139` (RunPipeline strategy loop) + +- [ ] **Step 1: Update RunPipeline — replace conditional report with unconditional overall-best report** + +In `RunPipeline` (line 132-139), change from: + +```csharp +if (IsBetterFill(result, context.CurrentBest, context.WorkArea)) +{ + context.CurrentBest = result; + context.CurrentBestScore = FillScore.Compute(result, context.WorkArea); + context.WinnerPhase = strategy.Phase; + ReportProgress(context.Progress, strategy.Phase, PlateNumber, + result, context.WorkArea, BuildProgressSummary()); +} +``` + +to: + +```csharp +if (IsBetterFill(result, context.CurrentBest, context.WorkArea)) +{ + context.CurrentBest = result; + context.CurrentBestScore = FillScore.Compute(result, context.WorkArea); + context.WinnerPhase = strategy.Phase; +} + +if (context.CurrentBest != null && context.CurrentBest.Count > 0) +{ + ReportProgress(context.Progress, context.WinnerPhase, PlateNumber, + context.CurrentBest, context.WorkArea, BuildProgressSummary(), + isOverallBest: true); +} +``` + +- [ ] **Step 2: Flag final report in Fill(NestItem, Box, ...)** + +In `Fill(NestItem item, Box workArea, ...)` (line 58), change: + +```csharp +ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary()); +``` + +to: + +```csharp +ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(), + isOverallBest: true); +``` + +- [ ] **Step 3: Flag final report in Fill(List, Box, ...)** + +In `Fill(List groupParts, Box workArea, ...)` (line 85), change: + +```csharp +ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary()); +``` + +to: + +```csharp +ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(), + isOverallBest: true); +``` + +- [ ] **Step 4: Build to verify** + +Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 5: Commit** + +``` +feat(engine): flag overall-best progress reports in DefaultNestEngine +``` + +--- + +### Task 3: Add active preview style to ColorScheme + +**Files:** +- Modify: `OpenNest/ColorScheme.cs:58-61` (pen/brush declarations) +- Modify: `OpenNest/ColorScheme.cs:160-176` (PreviewPartColor setter) + +- [ ] **Step 1: Add pen/brush declarations** + +In `ColorScheme.cs`, after line 60 (`PreviewPartBrush`), add: + +```csharp +public Pen ActivePreviewPartPen { get; private set; } + +public Brush ActivePreviewPartBrush { get; private set; } +``` + +- [ ] **Step 2: Create resources in PreviewPartColor setter** + +In the `PreviewPartColor` setter (lines 160-176), change from: + +```csharp +set +{ + previewPartColor = value; + + if (PreviewPartPen != null) + PreviewPartPen.Dispose(); + + if (PreviewPartBrush != null) + PreviewPartBrush.Dispose(); + + PreviewPartPen = new Pen(value, 1); + PreviewPartBrush = new SolidBrush(Color.FromArgb(60, value)); +} +``` + +to: + +```csharp +set +{ + previewPartColor = value; + + if (PreviewPartPen != null) + PreviewPartPen.Dispose(); + + if (PreviewPartBrush != null) + PreviewPartBrush.Dispose(); + + if (ActivePreviewPartPen != null) + ActivePreviewPartPen.Dispose(); + + if (ActivePreviewPartBrush != null) + ActivePreviewPartBrush.Dispose(); + + PreviewPartPen = new Pen(value, 1); + PreviewPartBrush = new SolidBrush(Color.FromArgb(60, value)); + ActivePreviewPartPen = new Pen(Color.FromArgb(128, value), 1); + ActivePreviewPartBrush = new SolidBrush(Color.FromArgb(30, value)); +} +``` + +- [ ] **Step 3: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 4: Commit** + +``` +feat(ui): add active preview brush/pen to ColorScheme +``` + +--- + +### Task 4: Two-bucket preview parts in PlateView + +**Files:** +- Modify: `OpenNest/Controls/PlateView.cs` + +This task replaces the single `temporaryParts` list with `stationaryParts` and `activeParts`, updates the public API, drawing, and all internal references. + +- [ ] **Step 1: Replace field and add new list** + +Change line 34: + +```csharp +private List temporaryParts = new List(); +``` + +to: + +```csharp +private List stationaryParts = new List(); +private List activeParts = new List(); +``` + +- [ ] **Step 2: Update SetPlate (line 152-153)** + +Change: + +```csharp +temporaryParts.Clear(); +``` + +to: + +```csharp +stationaryParts.Clear(); +activeParts.Clear(); +``` + +- [ ] **Step 3: Update Refresh (line 411)** + +Change: + +```csharp +temporaryParts.ForEach(p => p.Update(this)); +``` + +to: + +```csharp +stationaryParts.ForEach(p => p.Update(this)); +activeParts.ForEach(p => p.Update(this)); +``` + +- [ ] **Step 4: Update UpdateMatrix (line 1085)** + +Change: + +```csharp +temporaryParts.ForEach(p => p.Update(this)); +``` + +to: + +```csharp +stationaryParts.ForEach(p => p.Update(this)); +activeParts.ForEach(p => p.Update(this)); +``` + +- [ ] **Step 5: Replace the temporary parts drawing block in DrawParts (lines 506-522)** + +Change: + +```csharp +// Draw temporary (preview) parts +for (var i = 0; i < temporaryParts.Count; i++) +{ + var temp = temporaryParts[i]; + + if (temp.IsDirty) + temp.Update(this); + + var path = temp.Path; + var pathBounds = path.GetBounds(); + + if (!pathBounds.IntersectsWith(viewBounds)) + continue; + + g.FillPath(ColorScheme.PreviewPartBrush, path); + g.DrawPath(ColorScheme.PreviewPartPen, path); +} +``` + +to: + +```csharp +// Draw stationary preview parts (overall best — full opacity) +for (var i = 0; i < stationaryParts.Count; i++) +{ + var part = stationaryParts[i]; + + if (part.IsDirty) + part.Update(this); + + var path = part.Path; + if (!path.GetBounds().IntersectsWith(viewBounds)) + continue; + + g.FillPath(ColorScheme.PreviewPartBrush, path); + g.DrawPath(ColorScheme.PreviewPartPen, path); +} + +// Draw active preview parts (current strategy — reduced opacity) +for (var i = 0; i < activeParts.Count; i++) +{ + var part = activeParts[i]; + + if (part.IsDirty) + part.Update(this); + + var path = part.Path; + if (!path.GetBounds().IntersectsWith(viewBounds)) + continue; + + g.FillPath(ColorScheme.ActivePreviewPartBrush, path); + g.DrawPath(ColorScheme.ActivePreviewPartPen, path); +} +``` + +- [ ] **Step 6: Replace public API methods (lines 882-910)** + +Replace `SetTemporaryParts`, `ClearTemporaryParts`, and `AcceptTemporaryParts` with: + +```csharp +public void SetStationaryParts(List parts) +{ + stationaryParts.Clear(); + + if (parts != null) + { + foreach (var part in parts) + stationaryParts.Add(LayoutPart.Create(part, this)); + } + + Invalidate(); +} + +public void SetActiveParts(List parts) +{ + activeParts.Clear(); + + if (parts != null) + { + foreach (var part in parts) + activeParts.Add(LayoutPart.Create(part, this)); + } + + Invalidate(); +} + +public void ClearPreviewParts() +{ + stationaryParts.Clear(); + activeParts.Clear(); + Invalidate(); +} + +public void AcceptPreviewParts(List parts) +{ + if (parts != null) + { + foreach (var part in parts) + Plate.Parts.Add(part); + } + + stationaryParts.Clear(); + activeParts.Clear(); +} +``` + +- [ ] **Step 7: Update FillWithProgress (lines 912-957)** + +Change the progress callback (lines 918-923): + +```csharp +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + SetTemporaryParts(p.BestParts); + ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +to: + +```csharp +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + + if (p.IsOverallBest) + SetStationaryParts(p.BestParts); + else + SetActiveParts(p.BestParts); + + ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +Change the acceptance block (lines 933-943): + +```csharp +if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted)) +{ + SetTemporaryParts(parts); + AcceptTemporaryParts(); + sw.Stop(); + Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms"; +} +else +{ + ClearTemporaryParts(); +} +``` + +to: + +```csharp +if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted)) +{ + AcceptPreviewParts(parts); + sw.Stop(); + Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms"; +} +else +{ + ClearPreviewParts(); +} +``` + +Change the catch block (line 949): + +```csharp +ClearTemporaryParts(); +``` + +to: + +```csharp +ClearPreviewParts(); +``` + +- [ ] **Step 8: Build to verify** + +Run: `dotnet build OpenNest/OpenNest.csproj` +Expected: Build errors in `MainForm.cs` (still references old API). That is expected — Task 5 fixes it. + +- [ ] **Step 9: Commit** + +``` +feat(ui): two-bucket preview parts in PlateView +``` + +--- + +### Task 5: Update MainForm progress callbacks and acceptance + +**Files:** +- Modify: `OpenNest/Forms/MainForm.cs` + +Three progress callback sites and their acceptance points need updating. + +- [ ] **Step 1: Update auto-nest callback (RunAutoNest_Click, line 827)** + +Change: + +```csharp +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + activeForm.PlateView.SetTemporaryParts(p.BestParts); + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +to: + +```csharp +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + + if (p.IsOverallBest) + activeForm.PlateView.SetStationaryParts(p.BestParts); + else + activeForm.PlateView.SetActiveParts(p.BestParts); + + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +Change `ClearTemporaryParts()` on line 866 to `ClearPreviewParts()`. + +Change `ClearTemporaryParts()` in the catch block (line 884) to `ClearPreviewParts()`. + +- [ ] **Step 2: Update fill-plate callback (FillPlate_Click, line 962)** + +Replace the progress setup (lines 962-976): + +```csharp +var progressForm = new NestProgressForm(nestingCts, showPlateRow: false); +var highWaterMark = 0; + +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + + if (p.BestParts != null && p.BestPartCount >= highWaterMark) + { + highWaterMark = p.BestPartCount; + activeForm.PlateView.SetTemporaryParts(p.BestParts); + } + + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +with: + +```csharp +var progressForm = new NestProgressForm(nestingCts, showPlateRow: false); + +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + + if (p.IsOverallBest) + activeForm.PlateView.SetStationaryParts(p.BestParts); + else + activeForm.PlateView.SetActiveParts(p.BestParts); + + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +Change acceptance (line 990-993): + +```csharp +if (parts.Count > 0) + activeForm.PlateView.AcceptTemporaryParts(); +else + activeForm.PlateView.ClearTemporaryParts(); +``` + +to: + +```csharp +if (parts.Count > 0) + activeForm.PlateView.AcceptPreviewParts(parts); +else + activeForm.PlateView.ClearPreviewParts(); +``` + +Change `ClearTemporaryParts()` in the catch block to `ClearPreviewParts()`. + +- [ ] **Step 3: Update fill-area callback (FillArea_Click, line 1031)** + +Replace the progress setup (lines 1031-1045): + +```csharp +var progressForm = new NestProgressForm(nestingCts, showPlateRow: false); +var highWaterMark = 0; + +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + + if (p.BestParts != null && p.BestPartCount >= highWaterMark) + { + highWaterMark = p.BestPartCount; + activeForm.PlateView.SetTemporaryParts(p.BestParts); + } + + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +with: + +```csharp +var progressForm = new NestProgressForm(nestingCts, showPlateRow: false); + +var progress = new Progress(p => +{ + progressForm.UpdateProgress(p); + + if (p.IsOverallBest) + activeForm.PlateView.SetStationaryParts(p.BestParts); + else + activeForm.PlateView.SetActiveParts(p.BestParts); + + activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea; +}); +``` + +Change the `onComplete` callback (lines 1047-1052): + +```csharp +Action> onComplete = parts => +{ + if (parts != null && parts.Count > 0) + activeForm.PlateView.AcceptTemporaryParts(); + else + activeForm.PlateView.ClearTemporaryParts(); +``` + +to: + +```csharp +Action> onComplete = parts => +{ + if (parts != null && parts.Count > 0) + activeForm.PlateView.AcceptPreviewParts(parts); + else + activeForm.PlateView.ClearPreviewParts(); +``` + +- [ ] **Step 4: Build full solution** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeded, 0 errors (only pre-existing nullable warnings in OpenNest.Gpu). + +- [ ] **Step 5: Run tests** + +Run: `dotnet test OpenNest.Tests/OpenNest.Tests.csproj` +Expected: All tests pass. + +- [ ] **Step 6: Commit** + +``` +feat(ui): route progress to stationary/active buckets in MainForm +``` diff --git a/docs/superpowers/specs/2026-03-18-progress-form-redesign-v2-design.md b/docs/superpowers/specs/2026-03-18-progress-form-redesign-v2-design.md new file mode 100644 index 0000000..d243519 --- /dev/null +++ b/docs/superpowers/specs/2026-03-18-progress-form-redesign-v2-design.md @@ -0,0 +1,235 @@ +# NestProgressForm Redesign v2 + +## Problem + +The current `NestProgressForm` is a flat `TableLayoutPanel` of label/value pairs with default WinForms styling, MS Sans Serif font, and no visual hierarchy. It's functional but looks plain and gives no sense of where the engine is in its process or whether results are improving. + +## Solution + +Four combined improvements: + +1. A custom-drawn **phase stepper** control showing all 6 nesting phases with visited/active/pending states +2. **Grouped sections** separating Results from Status with white panels on a gray background +3. **Modern typography** — Segoe UI for labels, Consolas for values (monospaced so numbers don't shift width) +4. **Flash & fade with color-coded density** — values flash green on change and fade back; density flash color varies by quality (red < 50%, yellow 50-70%, green > 70%) + +## Phase Stepper Control + +**New file: `OpenNest/Controls/PhaseStepperControl.cs`** + +A custom `UserControl` that draws 6 circles with labels beneath, connected by lines: + +``` + ●━━━●━━━●━━━○━━━○━━━○ +Linear BestFit Pairs NFP Extents Custom +``` + +### All 6 phases + +The stepper displays all values from the `NestPhase` enum in enum order: `Linear`, `RectBestFit` (labeled "BestFit"), `Pairs`, `Nfp` (labeled "NFP"), `Extents`, `Custom`. This ensures the control stays accurate as new phases are added or existing ones start being used. + +### Non-sequential design + +The engine does not execute phases in a fixed order. `DefaultNestEngine` runs strategies in registration order (Linear → Pairs → RectBestFit → Extents by default), and custom engines may run any subset in any order. Some phases may never execute. + +The stepper tracks **which phases have been visited**, not a left-to-right progression. Each circle independently lights up when its phase reports progress. The connecting lines are purely decorative (always light gray). + +### Visual states + +- **Active:** Filled circle with accent color (`#0078D4`), slightly larger radius (11px vs 9px), subtle glow (`Color.FromArgb(60, 0, 120, 212)` drawn as a larger circle behind), bold label +- **Visited:** Filled circle with accent color, normal radius, bold label +- **Pending:** Hollow circle with gray border (`#C0C0C0`), dimmed label text (`#999999`) +- **All complete:** All 6 circles filled (set when `IsComplete = true`) +- **Initial state:** All 6 circles in Pending state + +### Implementation + +- Single `OnPaint` override. Circles evenly spaced across control width. Connecting lines drawn between circle centers in light gray (`#D0D0D0`). +- Colors and fonts defined as `static readonly` fields. Fonts cached (not created per paint call) to avoid GDI handle leaks during frequent progress updates. +- State: `HashSet VisitedPhases`, `NestPhase? ActivePhase` property. Setting `ActivePhase` adds to `VisitedPhases` and calls `Invalidate()`. `bool IsComplete` marks all phases done. +- `DoubleBuffered = true`. +- Fixed height: 60px. Docks to fill width. +- Namespace: `OpenNest.Controls`. +- Phase display order matches `NestPhase` enum order. Display names: `RectBestFit` → "BestFit", `Nfp` → "NFP", others use `ToString()`. + +## Form Layout + +Four vertical zones using `DockStyle.Top` stacking: + +``` +┌──────────────────────────────────────────────┐ +│ ●━━━●━━━●━━━○━━━○━━━○ │ Phase stepper +│ Linear BestFit Pairs Extents NFP Custom │ +├──────────────────────────────────────────────┤ +│ RESULTS │ Results group +│ Parts: 156 │ +│ Density: 68.3% ████████░░ │ +│ Nested: 24.1 x 36.0 (867.6 sq in) │ +├──────────────────────────────────────────────┤ +│ STATUS │ Status group +│ Plate: 2 │ +│ Elapsed: 1:24 │ +│ Detail: Trying best fit... │ +├──────────────────────────────────────────────┤ +│ [ Stop ] │ Button bar +└──────────────────────────────────────────────┘ +``` + +### Group panels + +Each group is a `Panel` containing: +- A header label ("RESULTS" / "STATUS") — Segoe UI 9pt bold, uppercase, color `#555555`, with `0.5px` letter spacing effect (achieved by drawing or just using uppercase text) +- A `TableLayoutPanel` with label/value rows beneath + +Group panels use `Color.White` `BackColor` against the form's `SystemColors.Control` (gray) background. Small padding (10px horizontal, 4px vertical gap between groups). + +### Typography + +- All fonts: Segoe UI +- Group headers: 9pt bold, uppercase, color `#555555` +- Row labels: 8.25pt bold, color `#333333` +- Row values: Consolas 8.25pt regular — monospaced so numeric values don't shift width as digits change +- Detail value: Segoe UI 8.25pt regular (not monospaced, since it's descriptive text) + +### Sizing + +- Width: ~450px +- Height: fixed `ClientSize` calculated to fit stepper (~60px) + results group (~115px) + status group (~95px) + button bar (~45px) + padding +- `FormBorderStyle.FixedToolWindow`, `StartPosition.CenterParent`, `ShowInTaskbar = false` + +### Plate row visibility + +The Plate row in the Status group is hidden when `showPlateRow: false` is passed to the constructor (same as current behavior). + +### Phase description text + +The phase stepper replaces the old Phase row. The descriptive text ("Trying rotations...") moves to the Detail row. `UpdateProgress` writes `FormatPhase(progress.Phase)` to the Detail value when `progress.Description` is empty, and writes `progress.Description` when set. + +### Unused row removed + +The current form has `remnantLabel`/`remnantValue` but `NestProgress` has no unused/remnant property — these labels are never updated and always show "—". The redesign drops this row entirely. + +### FormatPhase updates + +`FormatPhase` currently handles Linear, RectBestFit, and Pairs. Add entries for the three remaining phases: +- `Extents` → "Trying extents..." +- `Nfp` → "Trying NFP..." +- `Custom` → phase name via `ToString()` + +## Density Sparkline Bar + +A small inline visual next to the density percentage value: + +- Size: 60px wide, 8px tall +- Background: `#E0E0E0` (light gray track) +- Fill: gradient from orange (`#F5A623`) on the left to green (`#4CAF50`) on the right, clipped to the density percentage width +- Border radius: 4px +- Position: inline, 8px margin-left from the density text + +### Implementation + +Owner-drawn directly in a custom `Label` subclass or a small `Panel` placed next to the density value in the table. The simplest approach: a small `Panel` with `OnPaint` override that draws the track and fill. Updated whenever density changes. + +**New file: `OpenNest/Controls/DensityBar.cs`** — a lightweight `Control` subclass: +- `double Value` property (0.0 to 1.0), calls `Invalidate()` on set +- `OnPaint`: fills rounded rect background, then fills gradient portion proportional to `Value` +- Fixed size: 60 x 8px +- `DoubleBuffered = true` + +## Flash & Fade + +### Current implementation (keep, with modification) + +Values flash green (`Color.FromArgb(0, 160, 0)`) when they change and fade back to `SystemColors.ControlText` over ~1 second (20 steps at 50ms). A `SetValueWithFlash` helper checks if text actually changed before triggering. A single `System.Windows.Forms.Timer` drives all active fades. + +### Color-coded density flash + +Extend the flash color for the density value based on quality: +- Below 50%: red (`Color.FromArgb(200, 40, 40)`) +- 50% to 70%: yellow/orange (`Color.FromArgb(200, 160, 0)`) +- Above 70%: green (`Color.FromArgb(0, 160, 0)`) — same as current + +### Fade state changes + +The `SetValueWithFlash` method gains an optional `Color? flashColor` parameter. The fade dictionary changes from `Dictionary` to `Dictionary` so that each label fades from its own flash color. `FadeTimer_Tick` reads the per-label `flashColor` from the tuple when interpolating back to `SystemColors.ControlText`, rather than using the static `FlashColor` constant. `FlashColor` becomes the default when `flashColor` is null. + +`UpdateProgress` passes the density-appropriate color when updating `densityValue`. All other values continue using the default green. + +## Accept & Stop Buttons + +Currently the form has a single "Stop" button that cancels the `CancellationTokenSource`. Callers check `token.IsCancellationRequested` and discard results when true. This means there's no way to stop early and keep the current best result. + +### New button layout + +Two buttons in the button bar, right-aligned: + +``` + [ Accept ] [ Stop ] +``` + +- **Accept:** Stops the engine and keeps the current best result. Sets `Accepted = true`, then cancels the token. +- **Stop:** Stops the engine and discards results. Leaves `Accepted = false`, cancels the token. + +Both buttons are disabled until the first progress update arrives (so there's something to accept). After `ShowCompleted()`, both are replaced by a single "Close" button (same as current behavior). + +### Accepted property + +`bool Accepted { get; private set; }` — defaults to `false`. Set to `true` only by the Accept button click handler. + +### Caller changes + +Four callsites create a `NestProgressForm`. Each needs to honor the `Accepted` property: + +**`MainForm.cs` — `RunAutoNest_Click`** (line ~868): +```csharp +// Before: +if (nestParts.Count > 0 && !token.IsCancellationRequested) +// After: +if (nestParts.Count > 0 && (!token.IsCancellationRequested || progressForm.Accepted)) +``` + +**`MainForm.cs` — `FillPlate_Click`** (line ~983): No change needed — this path already accepts regardless of cancellation state (`if (parts.Count > 0)`). + +**`MainForm.cs` — `FillArea_Click`** (line ~1024): No change needed — this path delegates to `ActionFillArea` which handles its own completion via a callback. + +**`PlateView.cs` — `FillWithProgress`** (line ~933): +```csharp +// Before: +if (parts.Count > 0 && !cts.IsCancellationRequested) +// After: +if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted)) +``` + +## Public API + +### Constructor + +`NestProgressForm(CancellationTokenSource cts, bool showPlateRow = true)` — unchanged. + +### Properties + +- `bool Accepted { get; }` — **new**. True if user clicked Accept, false if user clicked Stop or form was closed. + +### UpdateProgress(NestProgress progress) + +Same as today, plus: +- Sets `phaseStepperControl.ActivePhase = progress.Phase` to update the stepper +- Updates `densityBar.Value = progress.BestDensity` +- Passes color-coded flash color for density value +- Writes `FormatPhase(progress.Phase)` to Detail row as fallback when `progress.Description` is empty +- Enables Accept/Stop buttons on first call (if not already enabled) + +### ShowCompleted() + +Same as today (stops timer, changes button to "Close"), plus sets `phaseStepperControl.IsComplete = true` to fill all circles. + +## Files Touched + +| File | Change | +|------|--------| +| `OpenNest/Controls/PhaseStepperControl.cs` | New — custom-drawn phase stepper control | +| `OpenNest/Controls/DensityBar.cs` | New — small density sparkline bar control | +| `OpenNest/Forms/NestProgressForm.cs` | Rewritten — grouped layout, stepper integration, color-coded flash, Accept/Stop buttons | +| `OpenNest/Forms/NestProgressForm.Designer.cs` | Rewritten — new control layout | +| `OpenNest/Forms/MainForm.cs` | Update `RunAutoNest_Click` to check `progressForm.Accepted` | +| `OpenNest/Controls/PlateView.cs` | Update `FillWithProgress` to check `progressForm.Accepted` | diff --git a/docs/superpowers/specs/2026-03-18-two-bucket-preview-design.md b/docs/superpowers/specs/2026-03-18-two-bucket-preview-design.md new file mode 100644 index 0000000..5e2e890 --- /dev/null +++ b/docs/superpowers/specs/2026-03-18-two-bucket-preview-design.md @@ -0,0 +1,138 @@ +# Two-Bucket Preview Parts + +## Problem + +During nesting, the PlateView preview shows whatever the latest progress report contains. When the engine runs multiple strategies sequentially (Pairs, Linear, RectBestFit, Extents), each strategy reports its own intermediate results. A later strategy starting fresh can report fewer parts than an earlier winner, causing the preview to visually regress. The user sees the part count drop and the layout change, even though the engine internally tracks the overall best. + +A simple high-water-mark filter at the UI level prevents regression but freezes the preview and can diverge from the engine's actual result, causing the wrong layout to be accepted. + +## Solution + +Split the preview into two visual layers: + +- **Stationary parts**: The overall best result found so far across all strategies. Never regresses. Drawn at full preview opacity. +- **Active parts**: The current strategy's work-in-progress. Updates freely as strategies iterate. Drawn at reduced opacity (~50% alpha). + +The engine flags progress reports as `IsOverallBest` so the UI knows which bucket to update. On acceptance, the engine's returned result is used directly, not the preview state. This also fixes an existing bug where `AcceptTemporaryParts()` could accept stale preview parts instead of the engine's actual output. + +## Changes + +### NestProgress + +Add one property: + +```csharp +public bool IsOverallBest { get; set; } +``` + +Default `false`. Set to `true` by `RunPipeline` when reporting the overall winner, and by the final `ReportProgress` calls in `DefaultNestEngine.Fill`. + +### NestEngineBase.ReportProgress + +Add an optional `isOverallBest` parameter: + +```csharp +internal static void ReportProgress( + IProgress progress, + NestPhase phase, + int plateNumber, + List best, + Box workArea, + string description, + bool isOverallBest = false) +``` + +Pass through to the `NestProgress` object. + +### DefaultNestEngine.RunPipeline + +Remove the existing `ReportProgress` call from inside the `if (IsBetterFill(...))` block. Replace it with an unconditional report of the overall best after each strategy completes: + +```csharp +if (IsBetterFill(result, context.CurrentBest, context.WorkArea)) +{ + context.CurrentBest = result; + context.CurrentBestScore = FillScore.Compute(result, context.WorkArea); + context.WinnerPhase = strategy.Phase; +} + +if (context.CurrentBest != null && context.CurrentBest.Count > 0) +{ + ReportProgress(context.Progress, context.WinnerPhase, PlateNumber, + context.CurrentBest, context.WorkArea, BuildProgressSummary(), + isOverallBest: true); +} +``` + +Strategy-internal progress reports (PairFiller, LinearFillStrategy, etc.) continue using the default `isOverallBest: false`. + +### DefaultNestEngine.Fill — final reports + +Both `Fill` overloads have a final `ReportProgress` call after the pipeline/fill completes. These must pass `isOverallBest: true` so the final preview goes to stationary parts at full opacity: + +- `Fill(NestItem, Box, ...)` line 58 — reports the pipeline winner after quantity trimming +- `Fill(List, Box, ...)` line 85 — reports the single-strategy linear result + +### ColorScheme + +Add two members for the active (transparent) preview style, created alongside the existing preview resources in the `PreviewPartColor` setter with the same disposal pattern: + +- `ActivePreviewPartBrush` — same color as `PreviewPartBrush` at ~50% alpha +- `ActivePreviewPartPen` — same color as `PreviewPartPen` at ~50% alpha + +### PlateView + +Rename `temporaryParts` to `activeParts`. Add `stationaryParts` (both `List`). + +**New public API:** + +- `SetStationaryParts(List)` — sets the overall-best preview, calls `Invalidate()` +- `SetActiveParts(List)` — sets the current-strategy preview, calls `Invalidate()` +- `ClearPreviewParts()` — clears both lists, calls `Invalidate()` (replaces `ClearTemporaryParts()`) +- `AcceptPreviewParts(List parts)` — adds the engine's returned `parts` directly to the plate, clears both lists. Decouples acceptance from preview state. + +**Internal references:** `Refresh()`, `UpdateMatrix()`, and `SetPlate()` currently reference `temporaryParts`. All must be updated to handle both `stationaryParts` and `activeParts` (clear both in `SetPlate`, update both in `Refresh`/`UpdateMatrix`). + +**Drawing order in `DrawParts`:** + +1. Stationary parts: `PreviewPartBrush` / `PreviewPartPen` (full opacity) +2. Active parts: `ActivePreviewPartBrush` / `ActivePreviewPartPen` (~50% alpha) + +**Remove:** `SetTemporaryParts`, `ClearTemporaryParts`, `AcceptTemporaryParts`. + +### Progress callbacks + +All four progress callback sites (PlateView.FillWithProgress, 3x MainForm) change from: + +```csharp +SetTemporaryParts(p.BestParts); +``` + +to: + +```csharp +if (p.IsOverallBest) + plateView.SetStationaryParts(p.BestParts); +else + plateView.SetActiveParts(p.BestParts); +``` + +### Acceptance points + +All acceptance points change from `AcceptTemporaryParts()` to `AcceptPreviewParts(engineResult)` where `engineResult` is the `List` returned by the engine. The multi-plate nest path in MainForm already uses `plate.Parts.AddRange(nestParts)` directly and needs no change beyond clearing preview parts. + +## Threading + +All `SetStationaryParts`/`SetActiveParts` calls arrive on the UI thread via `Progress` (which captures `SynchronizationContext` at construction). `DrawParts` also runs on the UI thread. No concurrent access to either list. + +## Files Modified + +| File | Change | +|------|--------| +| `OpenNest.Engine/NestProgress.cs` | Add `IsOverallBest` property | +| `OpenNest.Engine/NestEngineBase.cs` | Add `isOverallBest` parameter to `ReportProgress` | +| `OpenNest.Engine/DefaultNestEngine.cs` | Report overall best after each strategy; flag final reports | +| `OpenNest/Controls/PlateView.cs` | Two-bucket temp parts, new API, update internal references | +| `OpenNest/Controls/ColorScheme.cs` | Add active preview brush/pen with disposal | +| `OpenNest/Forms/MainForm.cs` | Update 3 progress callbacks and acceptance points | +| `OpenNest/Actions/ActionFillArea.cs` | No change needed (uses PlateView API) |