From 0cba5285916cf3f8ee89ff61d094cbed877974b8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 16:45:50 -0400 Subject: [PATCH 01/34] docs: update README with accurate features and add roadmap Remove NFP pair fitting claim from features (not yet integrated). Qualify lead-in/lead-out as engine-only (UI coming soon). Mark --autonest CLI option as experimental. Add Roadmap section with planned work: NFP nesting, lead-in UI, sheet cut-offs, post-processors, and shape library UI. Add documentation maintenance instruction to CLAUDE.md requiring README.md and CLAUDE.md updates when project structure changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 20 +++++++++++-------- .../{ => Fill}/AccumulatingProgress.cs | 0 .../{ => Fill}/AngleCandidateBuilder.cs | 0 OpenNest.Engine/{ => Fill}/BestCombination.cs | 0 OpenNest.Engine/{ => Fill}/Compactor.cs | 0 OpenNest.Engine/{ => Fill}/FillExtents.cs | 0 OpenNest.Engine/{ => Fill}/FillLinear.cs | 0 OpenNest.Engine/{ => Fill}/FillScore.cs | 0 OpenNest.Engine/{ => Fill}/PairFiller.cs | 0 OpenNest.Engine/{ => Fill}/PartBoundary.cs | 0 OpenNest.Engine/{ => Fill}/Pattern.cs | 0 OpenNest.Engine/{ => Fill}/PatternTiler.cs | 0 OpenNest.Engine/{ => Fill}/RemnantFiller.cs | 0 OpenNest.Engine/{ => Fill}/RemnantFinder.cs | 0 .../{ => Fill}/RotationAnalysis.cs | 0 OpenNest.Engine/{ => Fill}/ShrinkFiller.cs | 0 OpenNest.Engine/{ => Nfp}/AutoNester.cs | 0 OpenNest.Engine/{ => Nfp}/BottomLeftFill.cs | 0 OpenNest.Engine/{ => Nfp}/INestOptimizer.cs | 0 OpenNest.Engine/{ => Nfp}/NfpCache.cs | 0 .../{ => Nfp}/SimulatedAnnealing.cs | 0 README.md | 16 +++++++++++---- 22 files changed, 24 insertions(+), 12 deletions(-) rename OpenNest.Engine/{ => Fill}/AccumulatingProgress.cs (100%) rename OpenNest.Engine/{ => Fill}/AngleCandidateBuilder.cs (100%) rename OpenNest.Engine/{ => Fill}/BestCombination.cs (100%) rename OpenNest.Engine/{ => Fill}/Compactor.cs (100%) rename OpenNest.Engine/{ => Fill}/FillExtents.cs (100%) rename OpenNest.Engine/{ => Fill}/FillLinear.cs (100%) rename OpenNest.Engine/{ => Fill}/FillScore.cs (100%) rename OpenNest.Engine/{ => Fill}/PairFiller.cs (100%) rename OpenNest.Engine/{ => Fill}/PartBoundary.cs (100%) rename OpenNest.Engine/{ => Fill}/Pattern.cs (100%) rename OpenNest.Engine/{ => Fill}/PatternTiler.cs (100%) rename OpenNest.Engine/{ => Fill}/RemnantFiller.cs (100%) rename OpenNest.Engine/{ => Fill}/RemnantFinder.cs (100%) rename OpenNest.Engine/{ => Fill}/RotationAnalysis.cs (100%) rename OpenNest.Engine/{ => Fill}/ShrinkFiller.cs (100%) rename OpenNest.Engine/{ => Nfp}/AutoNester.cs (100%) rename OpenNest.Engine/{ => Nfp}/BottomLeftFill.cs (100%) rename OpenNest.Engine/{ => Nfp}/INestOptimizer.cs (100%) rename OpenNest.Engine/{ => Nfp}/NfpCache.cs (100%) rename OpenNest.Engine/{ => Nfp}/SimulatedAnnealing.cs (100%) 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.Engine/AccumulatingProgress.cs b/OpenNest.Engine/Fill/AccumulatingProgress.cs similarity index 100% rename from OpenNest.Engine/AccumulatingProgress.cs rename to OpenNest.Engine/Fill/AccumulatingProgress.cs diff --git a/OpenNest.Engine/AngleCandidateBuilder.cs b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs similarity index 100% rename from OpenNest.Engine/AngleCandidateBuilder.cs rename to OpenNest.Engine/Fill/AngleCandidateBuilder.cs diff --git a/OpenNest.Engine/BestCombination.cs b/OpenNest.Engine/Fill/BestCombination.cs similarity index 100% rename from OpenNest.Engine/BestCombination.cs rename to OpenNest.Engine/Fill/BestCombination.cs diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs similarity index 100% rename from OpenNest.Engine/Compactor.cs rename to OpenNest.Engine/Fill/Compactor.cs diff --git a/OpenNest.Engine/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs similarity index 100% rename from OpenNest.Engine/FillExtents.cs rename to OpenNest.Engine/Fill/FillExtents.cs diff --git a/OpenNest.Engine/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs similarity index 100% rename from OpenNest.Engine/FillLinear.cs rename to OpenNest.Engine/Fill/FillLinear.cs diff --git a/OpenNest.Engine/FillScore.cs b/OpenNest.Engine/Fill/FillScore.cs similarity index 100% rename from OpenNest.Engine/FillScore.cs rename to OpenNest.Engine/Fill/FillScore.cs diff --git a/OpenNest.Engine/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs similarity index 100% rename from OpenNest.Engine/PairFiller.cs rename to OpenNest.Engine/Fill/PairFiller.cs diff --git a/OpenNest.Engine/PartBoundary.cs b/OpenNest.Engine/Fill/PartBoundary.cs similarity index 100% rename from OpenNest.Engine/PartBoundary.cs rename to OpenNest.Engine/Fill/PartBoundary.cs diff --git a/OpenNest.Engine/Pattern.cs b/OpenNest.Engine/Fill/Pattern.cs similarity index 100% rename from OpenNest.Engine/Pattern.cs rename to OpenNest.Engine/Fill/Pattern.cs diff --git a/OpenNest.Engine/PatternTiler.cs b/OpenNest.Engine/Fill/PatternTiler.cs similarity index 100% rename from OpenNest.Engine/PatternTiler.cs rename to OpenNest.Engine/Fill/PatternTiler.cs diff --git a/OpenNest.Engine/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs similarity index 100% rename from OpenNest.Engine/RemnantFiller.cs rename to OpenNest.Engine/Fill/RemnantFiller.cs diff --git a/OpenNest.Engine/RemnantFinder.cs b/OpenNest.Engine/Fill/RemnantFinder.cs similarity index 100% rename from OpenNest.Engine/RemnantFinder.cs rename to OpenNest.Engine/Fill/RemnantFinder.cs diff --git a/OpenNest.Engine/RotationAnalysis.cs b/OpenNest.Engine/Fill/RotationAnalysis.cs similarity index 100% rename from OpenNest.Engine/RotationAnalysis.cs rename to OpenNest.Engine/Fill/RotationAnalysis.cs diff --git a/OpenNest.Engine/ShrinkFiller.cs b/OpenNest.Engine/Fill/ShrinkFiller.cs similarity index 100% rename from OpenNest.Engine/ShrinkFiller.cs rename to OpenNest.Engine/Fill/ShrinkFiller.cs diff --git a/OpenNest.Engine/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs similarity index 100% rename from OpenNest.Engine/AutoNester.cs rename to OpenNest.Engine/Nfp/AutoNester.cs diff --git a/OpenNest.Engine/BottomLeftFill.cs b/OpenNest.Engine/Nfp/BottomLeftFill.cs similarity index 100% rename from OpenNest.Engine/BottomLeftFill.cs rename to OpenNest.Engine/Nfp/BottomLeftFill.cs diff --git a/OpenNest.Engine/INestOptimizer.cs b/OpenNest.Engine/Nfp/INestOptimizer.cs similarity index 100% rename from OpenNest.Engine/INestOptimizer.cs rename to OpenNest.Engine/Nfp/INestOptimizer.cs diff --git a/OpenNest.Engine/NfpCache.cs b/OpenNest.Engine/Nfp/NfpCache.cs similarity index 100% rename from OpenNest.Engine/NfpCache.cs rename to OpenNest.Engine/Nfp/NfpCache.cs diff --git a/OpenNest.Engine/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs similarity index 100% rename from OpenNest.Engine/SimulatedAnnealing.cs rename to OpenNest.Engine/Nfp/SimulatedAnnealing.cs 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. From 0e1e619f0a0ab76c41aaa8ea4f3113b1ec50ef4e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 16:46:11 -0400 Subject: [PATCH 02/34] refactor(engine): move fill and strategy code to dedicated namespaces Move fill algorithms to OpenNest.Engine.Fill namespace: FillLinear, FillExtents, PairFiller, ShrinkFiller, Compactor, RemnantFiller, RemnantFinder, FillScore, Pattern, PatternTiler, PartBoundary, RotationAnalysis, AngleCandidateBuilder, and AccumulatingProgress. Move strategy layer to OpenNest.Engine.Strategies namespace: IFillStrategy, FillContext, FillStrategyRegistry, FillHelpers, and all built-in strategy implementations. Add using directives to all consuming files across Engine, UI, MCP, and Tests projects. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/BestFit/PairEvaluator.cs | 5 +++-- OpenNest.Engine/Fill/AccumulatingProgress.cs | 2 +- OpenNest.Engine/Fill/AngleCandidateBuilder.cs | 8 +++---- OpenNest.Engine/Fill/Compactor.cs | 5 ++--- OpenNest.Engine/Fill/FillExtents.cs | 6 +++--- OpenNest.Engine/Fill/FillLinear.cs | 8 +++---- OpenNest.Engine/Fill/FillScore.cs | 4 ++-- OpenNest.Engine/Fill/PairFiller.cs | 15 ++++++------- OpenNest.Engine/Fill/PartBoundary.cs | 18 ++++++++-------- OpenNest.Engine/Fill/Pattern.cs | 4 ++-- OpenNest.Engine/Fill/PatternTiler.cs | 4 ++-- OpenNest.Engine/Fill/RemnantFiller.cs | 4 ++-- OpenNest.Engine/Fill/RemnantFinder.cs | 5 ++--- OpenNest.Engine/Fill/RotationAnalysis.cs | 6 +++--- OpenNest.Engine/Fill/ShrinkFiller.cs | 4 ++-- OpenNest.Engine/NestEngineBase.cs | 11 +++++++--- .../RectanglePacking/FillBestFit.cs | 5 ++--- .../Strategies/ExtentsFillStrategy.cs | 12 ++++------- OpenNest.Engine/Strategies/FillContext.cs | 5 +++-- OpenNest.Engine/Strategies/FillHelpers.cs | 7 ++++--- .../Strategies/FillStrategyRegistry.cs | 2 +- OpenNest.Engine/Strategies/IFillStrategy.cs | 2 +- .../Strategies/LinearFillStrategy.cs | 5 +++-- .../Strategies/PairsFillStrategy.cs | 5 +++-- .../Strategies/RectBestFitStrategy.cs | 4 ++-- OpenNest.Engine/StripNestEngine.cs | 3 ++- OpenNest.Engine/StripNestResult.cs | 3 ++- OpenNest.Mcp/Tools/InspectionTools.cs | 7 +++---- OpenNest.Mcp/Tools/NestingTools.cs | 5 +++-- OpenNest.Tests/AccumulatingProgressTests.cs | 2 ++ OpenNest.Tests/AngleCandidateBuilderTests.cs | 1 + OpenNest.Tests/FillExtentsTests.cs | 1 + OpenNest.Tests/FillScoreTests.cs | 2 ++ OpenNest.Tests/PairFillerTests.cs | 1 + OpenNest.Tests/PatternTilerTests.cs | 3 +-- OpenNest.Tests/RemnantFillerTests2.cs | 1 + OpenNest.Tests/RemnantFinderTests.cs | 1 + OpenNest.Tests/ShrinkFillerTests.cs | 1 + .../Strategies/FillStrategyRegistryTests.cs | 2 ++ OpenNest/Actions/ActionClone.cs | 19 +++++++++-------- OpenNest/Controls/PlateView.cs | 21 ++++++++++--------- OpenNest/Forms/MainForm.cs | 21 ++++++++++--------- OpenNest/Forms/PatternTileForm.cs | 5 +++-- OpenNest/Forms/RemnantViewerForm.cs | 5 +++-- 44 files changed, 141 insertions(+), 119 deletions(-) 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/Fill/AccumulatingProgress.cs b/OpenNest.Engine/Fill/AccumulatingProgress.cs index 7b84803..051ec22 100644 --- a/OpenNest.Engine/Fill/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 index 8c09ce0..1b42ce1 100644 --- a/OpenNest.Engine/Fill/AngleCandidateBuilder.cs +++ b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using OpenNest.Engine.ML; using OpenNest.Geometry; using OpenNest.Math; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; -namespace OpenNest +namespace OpenNest.Engine.Fill { /// /// Builds candidate rotation angles for single-item fill. Encapsulates the diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 90dd994..b438784 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.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 { /// /// Pushes a group of parts left and down to close gaps after placement. diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index bcb0689..ff06073 100644 --- a/OpenNest.Engine/Fill/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 { diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 8fe1ef0..674bff3 100644 --- a/OpenNest.Engine/Fill/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; /// diff --git a/OpenNest.Engine/Fill/FillScore.cs b/OpenNest.Engine/Fill/FillScore.cs index 7c31e4d..957768c 100644 --- a/OpenNest.Engine/Fill/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/Fill/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs index f1a30aa..0d34f68 100644 --- a/OpenNest.Engine/Fill/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -1,13 +1,14 @@ +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. @@ -52,11 +53,11 @@ namespace OpenNest 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); + var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0); + var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI); engine.RemainderPatterns = new List { p0, p90 }; - var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea); + var filled = FillHelpers.FillPattern(engine, pairParts, angles, workArea); if (filled != null && filled.Count > 0) { diff --git a/OpenNest.Engine/Fill/PartBoundary.cs b/OpenNest.Engine/Fill/PartBoundary.cs index 44c14bc..e39dbdd 100644 --- a/OpenNest.Engine/Fill/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/Fill/Pattern.cs b/OpenNest.Engine/Fill/Pattern.cs index 67f40e4..1432fd4 100644 --- a/OpenNest.Engine/Fill/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/Fill/PatternTiler.cs b/OpenNest.Engine/Fill/PatternTiler.cs index 39363fd..b9260d3 100644 --- a/OpenNest.Engine/Fill/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/Fill/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs index 44036d8..94d8b98 100644 --- a/OpenNest.Engine/Fill/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/Fill/RemnantFinder.cs b/OpenNest.Engine/Fill/RemnantFinder.cs index a6ad082..fabd193 100644 --- a/OpenNest.Engine/Fill/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/Fill/RotationAnalysis.cs b/OpenNest.Engine/Fill/RotationAnalysis.cs index e79af6f..5ce61ce 100644 --- a/OpenNest.Engine/Fill/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/Fill/ShrinkFiller.cs b/OpenNest.Engine/Fill/ShrinkFiller.cs index 3b62085..dcff9e7 100644 --- a/OpenNest.Engine/Fill/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/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index 20c3e84..36e0412 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 { @@ -212,9 +213,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 { 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/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..1a49604 100644 --- a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs +++ b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public static class FillStrategyRegistry { 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..79c0428 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; using OpenNest.Engine.BestFit; +using OpenNest.Engine.Fill; +using System.Collections.Generic; -namespace OpenNest +namespace OpenNest.Engine.Strategies { public class PairsFillStrategy : IFillStrategy { 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..6f4c6ef 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 { 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.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.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/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/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/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/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/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/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index c61d6b4..7bbfea8 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; @@ -59,7 +60,7 @@ namespace OpenNest.Controls public List SelectedParts; public ReadOnlyCollection Parts; - + public event EventHandler> PartAdded; public event EventHandler> PartRemoved; public event EventHandler StatusChanged; @@ -381,7 +382,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); @@ -1001,7 +1002,7 @@ namespace OpenNest.Controls { base.ZoomToPoint(pt, zoomFactor, false); - if (redraw) + if (redraw) Invalidate(); } diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 1086763..aa9cd2d 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)) @@ -896,7 +897,7 @@ namespace OpenNest.Forms private void SequenceAllPlates_Click(object sender, EventArgs e) { - if (activeForm == null) + if (activeForm == null) return; activeForm.AutoSequenceAllPlates(); diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index 5056da5..acbde4d 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -1,9 +1,10 @@ +using OpenNest.Controls; +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 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 { From 495ee6f0c39e81b0b5d5e654088ad902b9cf987b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 16:46:17 -0400 Subject: [PATCH 03/34] refactor(engine): move NFP code to OpenNest.Engine.Nfp namespace Move AutoNester, BottomLeftFill, NfpCache, SimulatedAnnealing, and INestOptimizer/NestResult to OpenNest.Engine.Nfp. These are not yet integrated into the engine registry. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Nfp/AutoNester.cs | 9 ++++----- OpenNest.Engine/Nfp/BottomLeftFill.cs | 4 ++-- OpenNest.Engine/Nfp/INestOptimizer.cs | 5 +++-- OpenNest.Engine/Nfp/NfpCache.cs | 4 ++-- OpenNest.Engine/Nfp/SimulatedAnnealing.cs | 5 +++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/OpenNest.Engine/Nfp/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs index 0140544..55d1d38 100644 --- a/OpenNest.Engine/Nfp/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/Nfp/BottomLeftFill.cs b/OpenNest.Engine/Nfp/BottomLeftFill.cs index 192560b..48ecd1a 100644 --- a/OpenNest.Engine/Nfp/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/Nfp/INestOptimizer.cs b/OpenNest.Engine/Nfp/INestOptimizer.cs index 42dce81..05562d5 100644 --- a/OpenNest.Engine/Nfp/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/Nfp/NfpCache.cs b/OpenNest.Engine/Nfp/NfpCache.cs index bfcabeb..90076b2 100644 --- a/OpenNest.Engine/Nfp/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/Nfp/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs index d2dc4b9..c6d43a0 100644 --- a/OpenNest.Engine/Nfp/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. From 6102dd5b85b15b70e627e864927d8deeaa787e8e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 16:46:24 -0400 Subject: [PATCH 04/34] refactor(engine): migrate Fill(List) to strategy pipeline Single-part group fills now delegate to Fill(NestItem) which runs the full strategy pipeline, eliminating ~70 lines of duplicated manual phase logic. Multi-part group fills retain the linear pattern fill (unique to multi-part groups). PairFiller now references FillHelpers directly instead of bouncing through DefaultNestEngine helper methods. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/DefaultNestEngine.cs | 103 ++++----------------------- 1 file changed, 14 insertions(+), 89 deletions(-) diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index 69ac4f1..b186890 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 { @@ -67,90 +66,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()); - return best ?? new List(); } @@ -214,13 +147,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); - } } From 1d9bcc63d212c917d899db05a54b054871c70a3f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 16:47:42 -0400 Subject: [PATCH 05/34] chore: sort using directives Auto-formatter reordering of using statements across the solution. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Console/Program.cs | 8 +- OpenNest.Core/Align.cs | 4 +- .../CuttingStrategy/ContourCuttingStrategy.cs | 2 +- .../CNC/CuttingStrategy/CuttingResult.cs | 1 - .../CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs | 2 +- .../LeadIns/CleanHoleLeadIn.cs | 2 +- .../CNC/CuttingStrategy/LeadIns/LeadIn.cs | 2 +- .../CuttingStrategy/LeadIns/LineArcLeadIn.cs | 2 +- .../CNC/CuttingStrategy/LeadIns/LineLeadIn.cs | 2 +- .../CuttingStrategy/LeadIns/LineLineLeadIn.cs | 2 +- .../CNC/CuttingStrategy/LeadIns/NoLeadIn.cs | 2 +- .../CuttingStrategy/LeadOuts/ArcLeadOut.cs | 2 +- .../CNC/CuttingStrategy/LeadOuts/LeadOut.cs | 2 +- .../CuttingStrategy/LeadOuts/LineLeadOut.cs | 2 +- .../LeadOuts/MicrotabLeadOut.cs | 2 +- .../CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs | 2 +- .../CNC/CuttingStrategy/Tabs/BreakerTab.cs | 2 +- .../CNC/CuttingStrategy/Tabs/MachineTab.cs | 2 +- .../CNC/CuttingStrategy/Tabs/NormalTab.cs | 2 +- OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs | 2 +- OpenNest.Core/CNC/Program.cs | 3 +- OpenNest.Core/Converters/ConvertGeometry.cs | 5 +- OpenNest.Core/Converters/ConvertProgram.cs | 6 +- OpenNest.Core/Drawing.cs | 8 +- OpenNest.Core/DwgQty.cs | 4 +- OpenNest.Core/Geometry/Arc.cs | 4 +- OpenNest.Core/Geometry/Circle.cs | 3 +- OpenNest.Core/Geometry/Entity.cs | 4 +- OpenNest.Core/Geometry/GeometryOptimizer.cs | 2 +- OpenNest.Core/Geometry/Intersect.cs | 2 +- OpenNest.Core/Geometry/Line.cs | 4 +- OpenNest.Core/Geometry/NoFitPolygon.cs | 3 +- OpenNest.Core/Geometry/PolyLabel.cs | 1 - OpenNest.Core/Geometry/Polygon.cs | 4 +- OpenNest.Core/Geometry/RotatingCalipers.cs | 3 +- OpenNest.Core/Geometry/ShapeBuilder.cs | 2 +- OpenNest.Core/Geometry/Size.cs | 2 +- OpenNest.Core/Geometry/SpatialQuery.cs | 81 +++++++++---------- OpenNest.Core/Geometry/Vector.cs | 4 +- OpenNest.Core/Math/Angle.cs | 4 +- OpenNest.Core/Math/Tolerance.cs | 4 +- OpenNest.Core/Math/Trigonometry.cs | 4 +- OpenNest.Core/Nest.cs | 4 +- OpenNest.Core/NestConstraints.cs | 3 +- OpenNest.Core/Part.cs | 6 +- OpenNest.Core/PartGeometry.cs | 12 +-- OpenNest.Core/Plate.cs | 8 +- OpenNest.Core/Sequence.cs | 4 +- OpenNest.Core/Shapes/CircleShape.cs | 2 +- OpenNest.Core/Shapes/FlangeShape.cs | 3 +- .../Shapes/IsoscelesTriangleShape.cs | 2 +- OpenNest.Core/Shapes/LShape.cs | 2 +- OpenNest.Core/Shapes/OctagonShape.cs | 2 +- OpenNest.Core/Shapes/RectangleShape.cs | 2 +- OpenNest.Core/Shapes/RightTriangleShape.cs | 2 +- OpenNest.Core/Shapes/RingShape.cs | 2 +- OpenNest.Core/Shapes/RoundedRectangleShape.cs | 2 +- OpenNest.Core/Shapes/ShapeDefinition.cs | 4 +- OpenNest.Core/Shapes/TShape.cs | 2 +- OpenNest.Core/Shapes/TrapezoidShape.cs | 2 +- OpenNest.Core/Timing.cs | 8 +- OpenNest.Core/Units.cs | 6 +- OpenNest.Engine/BestFit/BestFitFinder.cs | 8 +- OpenNest.Engine/BestFit/BestFitResult.cs | 2 +- .../BestFit/RotationSlideStrategy.cs | 2 +- .../BestFit/Tiling/TileEvaluator.cs | 2 +- OpenNest.Engine/BestFit/Tiling/TileResult.cs | 2 +- OpenNest.Engine/CirclePacking/Bin.cs | 4 +- OpenNest.Engine/CirclePacking/FillEndEven.cs | 4 +- OpenNest.Engine/CirclePacking/FillEndOdd.cs | 4 +- OpenNest.Engine/Fill/BestCombination.cs | 3 +- OpenNest.Engine/ML/AnglePredictor.cs | 6 +- OpenNest.Engine/ML/BruteForceRunner.cs | 1 - OpenNest.Engine/ML/FeatureExtractor.cs | 4 +- OpenNest.Engine/NestProgress.cs | 2 +- OpenNest.Engine/PlateProcessor.cs | 4 +- OpenNest.Engine/PlateResult.cs | 2 +- .../RapidPlanning/DirectRapidPlanner.cs | 2 +- .../RapidPlanning/IRapidPlanner.cs | 2 +- OpenNest.Engine/RapidPlanning/RapidPath.cs | 2 +- .../RapidPlanning/SafeHeightRapidPlanner.cs | 2 +- OpenNest.Engine/RectanglePacking/Bin.cs | 4 +- .../RectanglePacking/BinConverter.cs | 2 +- .../RectanglePacking/FillEngine.cs | 4 +- .../RectanglePacking/FillNoRotation.cs | 3 +- .../RectanglePacking/FillSameRotation.cs | 3 +- OpenNest.Engine/RectanglePacking/Item.cs | 4 +- .../RectanglePacking/PackBottomLeft.cs | 8 +- .../RectanglePacking/PackEngine.cs | 1 - .../PackFirstFitDecreasing.cs | 4 +- .../Sequencing/AdvancedSequencer.cs | 4 +- .../Sequencing/EdgeStartSequencer.cs | 6 +- .../Sequencing/LeastCodeSequencer.cs | 3 +- .../Sequencing/PartSequencerFactory.cs | 12 +-- OpenNest.Gpu/GpuEvaluatorFactory.cs | 4 +- OpenNest.Gpu/GpuPairEvaluator.cs | 9 +-- OpenNest.Gpu/GpuSlideComputer.cs | 24 +++--- OpenNest.Gpu/PartBitmap.cs | 6 +- OpenNest.IO/DxfExporter.cs | 7 +- OpenNest.IO/DxfImporter.cs | 6 +- OpenNest.IO/Extensions.cs | 57 ++++++++----- OpenNest.IO/NestReader.cs | 6 +- OpenNest.IO/NestWriter.cs | 6 +- OpenNest.IO/ProgramReader.cs | 6 +- OpenNest.Mcp/Program.cs | 1 - OpenNest.Mcp/Tools/InputTools.cs | 9 +-- OpenNest.Mcp/Tools/SetupTools.cs | 4 +- OpenNest.Mcp/Tools/TestTools.cs | 2 +- OpenNest.Tests/CuttingResultTests.cs | 1 - OpenNest.Tests/PartFlagTests.cs | 1 - OpenNest.Tests/PlateProcessorTests.cs | 5 -- .../RapidPlanning/DirectRapidPlannerTests.cs | 2 - .../SafeHeightRapidPlannerTests.cs | 2 - .../Sequencing/AdvancedSequencerTests.cs | 4 - .../Sequencing/DirectionalSequencerTests.cs | 4 - .../Sequencing/EdgeStartSequencerTests.cs | 4 - .../Sequencing/LeastCodeSequencerTests.cs | 4 - .../Sequencing/PartSequencerFactoryTests.cs | 2 - OpenNest.Tests/Shapes/FlangeShapeTests.cs | 1 - OpenNest.Training/Program.cs | 14 ++-- OpenNest.Training/TrainingDatabase.cs | 8 +- OpenNest/Actions/ActionFillArea.cs | 2 +- OpenNest/Actions/ActionSelect.cs | 6 +- OpenNest/Actions/ActionSelectArea.cs | 16 ++-- OpenNest/Actions/ActionSetSequence.cs | 10 +-- OpenNest/Actions/ActionZoomWindow.cs | 6 +- OpenNest/Controls/BestFitCell.cs | 4 +- OpenNest/Controls/DrawControl.cs | 4 +- OpenNest/Controls/DrawingListBox.cs | 3 +- OpenNest/Controls/EntityView.cs | 7 +- OpenNest/Controls/NumericUpDown.cs | 6 +- OpenNest/Document.cs | 7 +- OpenNest/Forms/BestFitViewerForm.cs | 4 +- OpenNest/Forms/CadConverterForm.cs | 19 +++-- OpenNest/Forms/CutParametersForm.cs | 2 +- OpenNest/Forms/EditNestForm.cs | 20 +++-- OpenNest/Forms/EditNestInfoForm.cs | 4 +- OpenNest/Forms/EditPlateForm.cs | 5 +- OpenNest/Forms/FillPlateForm.cs | 6 +- OpenNest/Forms/OptionsForm.cs | 4 +- OpenNest/GraphicsHelper.cs | 13 ++- OpenNest/LayoutPart.cs | 10 +-- OpenNest/MainApp.cs | 2 +- OpenNest/ToolStripRenderer.cs | 47 +++++++---- 144 files changed, 378 insertions(+), 426 deletions(-) diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index a4f750a..1150770 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 7e1b401..9b6ce47 100644 --- a/OpenNest.Core/Timing.cs +++ b/OpenNest.Core/Timing.cs @@ -1,8 +1,8 @@ -using System; -using System.Linq; -using OpenNest.CNC; +using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; +using System; +using System.Linq; namespace OpenNest { @@ -83,7 +83,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/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/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/Fill/BestCombination.cs b/OpenNest.Engine/Fill/BestCombination.cs index f0bc2ab..69bb186 100644 --- a/OpenNest.Engine/Fill/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/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/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index 49ae0ba..d7dccb8 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 { 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/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.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 d9583c5..1d7f978 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 95c1329..9e0a546 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/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 afea628..11ccf19 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/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/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/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/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.Training/Program.cs b/OpenNest.Training/Program.cs index eecc276..77ebedb 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/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/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/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/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 ae0561d..a44453b 100644 --- a/OpenNest/Forms/CutParametersForm.cs +++ b/OpenNest/Forms/CutParametersForm.cs @@ -47,7 +47,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 e85f678..ec1d6a1 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1,20 +1,18 @@ -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; using System.IO; using System.Linq; using System.Windows.Forms; -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 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/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/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); } } From 39d656ad2100417140fe85d17161b3b2b40a24d4 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:16:42 -0400 Subject: [PATCH 06/34] docs: add NestProgressForm redesign v2 spec Phase stepper, grouped panels, density sparkline bar, color-coded flash & fade, and Accept/Stop buttons. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...-03-18-progress-form-redesign-v2-design.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-18-progress-form-redesign-v2-design.md 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` | From a1810db96d953574bd018a6a8a913436eb578b33 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:22:49 -0400 Subject: [PATCH 07/34] docs: add NestProgressForm redesign v2 implementation plan 6-task plan covering PhaseStepperControl, DensityBar, form rewrite, color-coded flash & fade, Accept/Stop buttons, and caller changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-18-progress-form-redesign-v2.md | 1015 +++++++++++++++++ 1 file changed, 1015 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-progress-form-redesign-v2.md 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" +``` From 97ab33c8995db5a0ac61be40c0c29aca4444948c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:31:54 -0400 Subject: [PATCH 08/34] feat(ui): add PhaseStepperControl for nesting progress phases Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/Controls/PhaseStepperControl.cs | 146 +++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 OpenNest/Controls/PhaseStepperControl.cs diff --git a/OpenNest/Controls/PhaseStepperControl.cs b/OpenNest/Controls/PhaseStepperControl.cs new file mode 100644 index 0000000..c214306 --- /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 = 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); + } + } + } +} From 60a557bd379a35949fe7ba3991677c1e01bce44b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:31:59 -0400 Subject: [PATCH 09/34] feat(ui): add DensityBar sparkline control for density visualization Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/Controls/DensityBar.cs | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 OpenNest/Controls/DensityBar.cs diff --git a/OpenNest/Controls/DensityBar.cs b/OpenNest/Controls/DensityBar.cs new file mode 100644 index 0000000..e9a6c65 --- /dev/null +++ b/OpenNest/Controls/DensityBar.cs @@ -0,0 +1,70 @@ +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; + } + } +} From b5af5a118d20997381979f92f3176c90159ed43e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:34:50 -0400 Subject: [PATCH 10/34] feat(ui): rewrite NestProgressForm with grouped panels, stepper, density bar, and Accept button Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/NestProgressForm.Designer.cs | 504 +++++++++++--------- OpenNest/Forms/NestProgressForm.cs | 128 ++++- 2 files changed, 399 insertions(+), 233 deletions(-) diff --git a/OpenNest/Forms/NestProgressForm.Designer.cs b/OpenNest/Forms/NestProgressForm.Designer.cs index 9bed575..a6779d1 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,284 +15,342 @@ 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 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(); - remnantLabel = new System.Windows.Forms.Label(); - remnantValue = 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(); - stopButton = new System.Windows.Forms.Button(); buttonPanel = new System.Windows.Forms.FlowLayoutPanel(); - table.SuspendLayout(); + 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(); - // - // table - // - 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; - // - // phaseLabel - // - 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:"; - // - // phaseValue - // - 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 = "—"; - // + + // + // 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("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", 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.Size = new System.Drawing.Size(40, 13); - partsLabel.TabIndex = 4; 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", 8.25F); + 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.Text = "\u2014"; + + // // 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", 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.Size = new System.Drawing.Size(53, 13); - densityLabel.TabIndex = 6; 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.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", 8.25F); + 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.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("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", 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.Size = new System.Drawing.Size(51, 13); - nestedAreaLabel.TabIndex = 8; 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", 8.25F); + 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 = "—"; - // - // remnantLabel - // - 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:"; - // - // remnantValue - // - 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 = "—"; - // + 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("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", 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.Size = new System.Drawing.Size(56, 13); - elapsedLabel.TabIndex = 12; 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", 8.25F); + 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.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", 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.Size = new System.Drawing.Size(44, 13); - descriptionLabel.TabIndex = 14; 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", 8.25F); + 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.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.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", 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; - // - // buttonPanel - // - 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; - // + + // // 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, 315); 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 +358,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(); } } From 9a4f20ca009081e5a7e4390a0510eaa187346123 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:35:33 -0400 Subject: [PATCH 11/34] feat(ui): support Accept button in nesting callers --- OpenNest/Controls/PlateView.cs | 2 +- OpenNest/Forms/MainForm.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 7bbfea8..b32d506 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -930,7 +930,7 @@ 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(); sw.Stop(); diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index aa9cd2d..64aa35b 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -865,7 +865,7 @@ namespace OpenNest.Forms activeForm.PlateView.ClearTemporaryParts(); - if (nestParts.Count > 0 && !token.IsCancellationRequested) + if (nestParts.Count > 0 && (!token.IsCancellationRequested || progressForm.Accepted)) { plate.Parts.AddRange(nestParts); activeForm.PlateView.Invalidate(); From b6cde145e14fa8dc25fe2552eed27fd6d33666f5 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:39:13 -0400 Subject: [PATCH 12/34] fix(ui): handle edge cases in DensityBar and PhaseStepperControl DensityBar: clamp rounded rect radius for small fill widths to avoid GDI+ artifacts at very low density values. PhaseStepperControl: use float arithmetic for circle spacing to handle DPI-scaled widths evenly. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Controls/DensityBar.cs | 3 ++- OpenNest/Controls/PhaseStepperControl.cs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/OpenNest/Controls/DensityBar.cs b/OpenNest/Controls/DensityBar.cs index e9a6c65..88eb476 100644 --- a/OpenNest/Controls/DensityBar.cs +++ b/OpenNest/Controls/DensityBar.cs @@ -46,8 +46,9 @@ namespace OpenNest.Controls 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, 4); + using var fillPath = CreateRoundedRect(fillRect, fillRadius); using var gradientBrush = new LinearGradientBrush( new Point(rect.X, 0), new Point(rect.Right, 0), LowColor, HighColor); diff --git a/OpenNest/Controls/PhaseStepperControl.cs b/OpenNest/Controls/PhaseStepperControl.cs index c214306..a0f8e92 100644 --- a/OpenNest/Controls/PhaseStepperControl.cs +++ b/OpenNest/Controls/PhaseStepperControl.cs @@ -81,7 +81,7 @@ namespace OpenNest.Controls var padding = 30; var usableWidth = Width - padding * 2; - var spacing = usableWidth / (count - 1); + var spacing = (float)usableWidth / (count - 1); var circleY = 18; var normalRadius = 9; var activeRadius = 11; @@ -96,8 +96,8 @@ namespace OpenNest.Controls // Draw connecting lines for (var i = 0; i < count - 1; i++) { - var x1 = padding + i * spacing; - var x2 = padding + (i + 1) * spacing; + var x1 = (int)(padding + i * spacing); + var x2 = (int)(padding + (i + 1) * spacing); g.DrawLine(linePen, x1, circleY, x2, circleY); } @@ -105,7 +105,7 @@ namespace OpenNest.Controls for (var i = 0; i < count; i++) { var phase = Phases[i]; - var cx = padding + i * spacing; + var cx = (int)(padding + i * spacing); var isActive = activePhase == phase && !isComplete; var isVisited = visitedPhases.Contains(phase) || isComplete; From d6d7ba84809ac3eb6eef184982cd92c5bfb96f97 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 17:58:35 -0400 Subject: [PATCH 13/34] fix(ui): increase font sizes in progress form for readability Labels: 8.25pt -> 9.75pt, headers: 9pt -> 10.5pt, values: 8.25pt -> 9.75pt. Panel heights increased to accommodate larger text. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/NestProgressForm.Designer.cs | 303 +++++++++++--------- 1 file changed, 165 insertions(+), 138 deletions(-) diff --git a/OpenNest/Forms/NestProgressForm.Designer.cs b/OpenNest/Forms/NestProgressForm.Designer.cs index a6779d1..9bd7b12 100644 --- a/OpenNest/Forms/NestProgressForm.Designer.cs +++ b/OpenNest/Forms/NestProgressForm.Designer.cs @@ -17,20 +17,19 @@ namespace OpenNest.Forms private void InitializeComponent() { - phaseStepper = new Controls.PhaseStepperControl(); + phaseStepper = new OpenNest.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(); + densityBar = new OpenNest.Controls.DensityBar(); nestedAreaLabel = new System.Windows.Forms.Label(); nestedAreaValue = new System.Windows.Forms.Label(); + resultsHeader = 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(); @@ -38,10 +37,10 @@ namespace OpenNest.Forms elapsedValue = new System.Windows.Forms.Label(); descriptionLabel = new System.Windows.Forms.Label(); descriptionValue = new System.Windows.Forms.Label(); + statusHeader = new System.Windows.Forms.Label(); buttonPanel = new System.Windows.Forms.FlowLayoutPanel(); - acceptButton = new System.Windows.Forms.Button(); stopButton = new System.Windows.Forms.Button(); - + acceptButton = new System.Windows.Forms.Button(); resultsPanel.SuspendLayout(); resultsTable.SuspendLayout(); densityPanel.SuspendLayout(); @@ -49,18 +48,19 @@ namespace OpenNest.Forms statusTable.SuspendLayout(); buttonPanel.SuspendLayout(); SuspendLayout(); - - // + // // phaseStepper - // + // + phaseStepper.ActivePhase = null; phaseStepper.Dock = System.Windows.Forms.DockStyle.Top; - phaseStepper.Height = 60; + 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; - - // + // // resultsPanel - // + // resultsPanel.BackColor = System.Drawing.Color.White; resultsPanel.Controls.Add(resultsTable); resultsPanel.Controls.Add(resultsHeader); @@ -69,28 +69,14 @@ namespace OpenNest.Forms 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.Size = new System.Drawing.Size(450, 120); 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(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); @@ -99,120 +85,135 @@ namespace OpenNest.Forms 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("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + 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(36, 13); + partsLabel.TabIndex = 0; partsLabel.Text = "Parts:"; - - // + // // partsValue - // + // partsValue.AutoSize = true; - partsValue.Font = new System.Drawing.Font("Consolas", 8.25F); + 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.Text = "\u2014"; - - // + partsValue.Size = new System.Drawing.Size(13, 13); + partsValue.TabIndex = 1; + partsValue.Text = "�"; + // // densityLabel - // + // densityLabel.AutoSize = true; - densityLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + 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(49, 13); + densityLabel.TabIndex = 2; densityLabel.Text = "Density:"; - - // + // // densityPanel - // + // densityPanel.AutoSize = true; densityPanel.Controls.Add(densityValue); densityPanel.Controls.Add(densityBar); - densityPanel.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight; + 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.Font = new System.Drawing.Font("Consolas", 8.25F); + 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.Text = "\u2014"; - - // + 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(60, 8); - - // + densityBar.Size = new System.Drawing.Size(290, 8); + densityBar.TabIndex = 1; + densityBar.Value = 0D; + // // nestedAreaLabel - // + // nestedAreaLabel.AutoSize = true; - nestedAreaLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + 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(47, 13); + nestedAreaLabel.TabIndex = 4; nestedAreaLabel.Text = "Nested:"; - - // + // // nestedAreaValue - // + // nestedAreaValue.AutoSize = true; - nestedAreaValue.Font = new System.Drawing.Font("Consolas", 8.25F); + 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.Text = "\u2014"; - - // + nestedAreaValue.Size = new System.Drawing.Size(13, 13); + nestedAreaValue.TabIndex = 5; + nestedAreaValue.Text = "�"; + // + // resultsHeader + // + 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"; + // // 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.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, 100); + statusPanel.Size = new System.Drawing.Size(450, 115); 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(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); @@ -221,101 +222,115 @@ namespace OpenNest.Forms 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", 8.25F, System.Drawing.FontStyle.Bold); + 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", 8.25F); + 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.Text = "\u2014"; - - // + plateValue.Size = new System.Drawing.Size(13, 13); + plateValue.TabIndex = 1; + plateValue.Text = "�"; + // // elapsedLabel - // + // elapsedLabel.AutoSize = true; - elapsedLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + 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(50, 13); + elapsedLabel.TabIndex = 2; elapsedLabel.Text = "Elapsed:"; - - // + // // elapsedValue - // + // elapsedValue.AutoSize = true; - elapsedValue.Font = new System.Drawing.Font("Consolas", 8.25F); + 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(31, 13); + elapsedValue.TabIndex = 3; elapsedValue.Text = "0:00"; - - // + // // descriptionLabel - // + // descriptionLabel.AutoSize = true; - descriptionLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Bold); + 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(40, 13); + descriptionLabel.TabIndex = 4; descriptionLabel.Text = "Detail:"; - - // + // // descriptionValue - // + // descriptionValue.AutoSize = true; - descriptionValue.Font = new System.Drawing.Font("Segoe UI", 8.25F); + 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.Text = "\u2014"; - - // + 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; - - // - // 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.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); @@ -323,13 +338,25 @@ namespace OpenNest.Forms stopButton.Text = "Stop"; stopButton.UseVisualStyleBackColor = true; stopButton.Click += StopButton_Click; - - // + // + // acceptButton + // + 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(450, 315); + ClientSize = new System.Drawing.Size(450, 345); Controls.Add(buttonPanel); Controls.Add(statusPanel); Controls.Add(resultsPanel); From c98e024f9c3765910662ca93fa257a0694ceae53 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 18:07:20 -0400 Subject: [PATCH 14/34] feat(ui): disable remove plate button when only one plate exists Promotes btnRemovePlate to a field and toggles Enabled based on plate count in add/remove event handlers. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/EditNestForm.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index ec1d6a1..01a4764 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -29,6 +29,7 @@ namespace OpenNest.Forms private Panel plateHeaderPanel; private Label plateInfoLabel; private Button btnFirstPlate; + private Button btnRemovePlate; private Button btnPreviousPlate; private Button btnNextPlate; @@ -119,7 +120,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(); @@ -175,6 +176,7 @@ namespace OpenNest.Forms UpdatePlateList(); UpdateDrawingList(); + UpdateRemovePlateButton(); LoadFirstPlate(); @@ -728,6 +730,7 @@ namespace OpenNest.Forms PlateView.Plate = Nest.Plates[CurrentPlateIndex]; UpdatePlateList(); + UpdateRemovePlateButton(); PlateView.ZoomToFit(); } @@ -735,10 +738,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) From 0a33047ad6a3f1781e05348d48fd75e56c6688bf Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 19:53:08 -0400 Subject: [PATCH 15/34] fix(engine): prevent FillExtents overlap and add strategy filter API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FillExtents vertical copy distance was not clamped, allowing rows to be placed overlapping each other when slide calculations returned large values. Clamp to pairHeight + partSpacing minimum, matching FillLinear. Also add FillStrategyRegistry.SetEnabled() to restrict which strategies run — useful for isolating individual strategies during troubleshooting. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/FillExtents.cs | 14 ++++---------- .../Strategies/FillStrategyRegistry.cs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index ff06073..40f7dee 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -172,18 +172,12 @@ namespace OpenNest.Engine.Fill 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/Strategies/FillStrategyRegistry.cs b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs index 1a49604..581b4bf 100644 --- a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs +++ b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs @@ -11,6 +11,7 @@ namespace OpenNest.Engine.Strategies { private static readonly List strategies = new(); private static List sorted; + private static HashSet enabledFilter; static FillStrategyRegistry() { @@ -18,7 +19,21 @@ namespace OpenNest.Engine.Strategies } 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) { From c2b84009868d8952ed3e1268886ff990d11bb56a Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:02:30 -0400 Subject: [PATCH 16/34] refactor(engine): extract AngleCandidateBuilder.Build into focused helpers Move known-good pruning check before sweep/ML to avoid wasted work, extract ContainsAngle, NeedsSweep, AddSweepAngles, ApplyMlPrediction, and BuildPrunedList so Build reads as a clear pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/AngleCandidateBuilder.cs | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/OpenNest.Engine/Fill/AngleCandidateBuilder.cs b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs index 1b42ce1..670c459 100644 --- a/OpenNest.Engine/Fill/AngleCandidateBuilder.cs +++ b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs @@ -20,8 +20,24 @@ namespace OpenNest.Engine.Fill public List Build(NestItem item, double bestRotation, Box workArea) { - var angles = new List { bestRotation, bestRotation + Angle.HalfPI }; + 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); @@ -29,56 +45,57 @@ namespace OpenNest.Engine.Fill 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; + return workAreaShortSide < partLongestSide || ForceFullSweep; + } - if (needsSweep) + private static void AddSweepAngles(List angles) + { + var step = Angle.ToRadians(5); + for (var a = 0.0; a < System.Math.PI; a += step) { - 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 (!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); } - if (!ForceFullSweep && angles.Count > 2) + 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) { - 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 (!ContainsAngle(pruned, a)) + pruned.Add(a); } - if (knownGoodAngles.Count > 0 && !ForceFullSweep) - { - var pruned = new List { bestRotation, bestRotation + Angle.HalfPI }; + Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)"); + return pruned; + } - 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; + private static bool ContainsAngle(List angles, double angle) + { + return angles.Any(existing => existing.IsEqualTo(angle)); } /// From 24ed878d8eac3f3aa2c0f0687c8e3ca2b12b25e0 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:12:23 -0400 Subject: [PATCH 17/34] docs: add Compactor refactor implementation plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-18-refactor-compactor.md | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-refactor-compactor.md 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. From d1d47b52233103154fd057e043db891773be002c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:13:55 -0400 Subject: [PATCH 18/34] refactor(engine): simplify FillExtents logic using Compactor.Push Simplify geometry-aware positioning by replacing manual slide calculations with higher-level Compactor.Push utility. Extract pair creation into CreatePair helper, remove redundant UpdateBounds calls, and clean up column/horizontal repetition logic. --- OpenNest.Engine/Fill/FillExtents.cs | 262 ++++++++-------------------- 1 file changed, 69 insertions(+), 193 deletions(-) diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 40f7dee..b331b87 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -3,6 +3,7 @@ using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; namespace OpenNest.Engine.Fill @@ -13,13 +14,11 @@ namespace OpenNest.Engine.Fill private readonly Box workArea; private readonly double partSpacing; - private readonly double halfSpacing; public FillExtents(Box workArea, double partSpacing) { this.workArea = workArea; this.partSpacing = partSpacing; - halfSpacing = partSpacing / 2; } public List Fill(Drawing drawing, double rotationAngle = 0, @@ -27,18 +26,18 @@ namespace OpenNest.Engine.Fill CancellationToken token = default, IProgress progress = null) { - var pair = BuildPair(drawing, rotationAngle); - if (pair == null) + var initialPair = CreatePair(drawing, rotationAngle, rotationAngle + System.Math.PI); + if (initialPair == null) return new List(); - var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox); + var column = BuildColumn(initialPair.Value.part1, initialPair.Value.part2, initialPair.Value.pairBbox); if (column.Count == 0) return new List(); NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, column, workArea, $"Extents: initial column {column.Count} parts"); - var adjusted = AdjustColumn(pair.Value, column, token); + var adjusted = AdjustColumn(initialPair.Value, column, token); NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts"); @@ -53,52 +52,33 @@ namespace OpenNest.Engine.Fill // --- Step 1: Pair Construction --- - private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle) + private (Part part1, Part part2, Box pairBbox)? CreatePair( + Drawing drawing, double rotation1, double rotation2, double verticalShift2 = 0) { - var part1 = Part.CreateAtOrigin(drawing, rotationAngle); - var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI); + var p1 = Part.CreateAtOrigin(drawing, rotation1); + var p2 = Part.CreateAtOrigin(drawing, rotation2); - // Check that each part fits in the work area individually. - if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon || - part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon) - return null; + // Initial positioning: p2 to the right of p1, with optional vertical shift. + p2.Offset(p1.BoundingBox.Width + partSpacing, verticalShift2); - // Slide part2 toward part1 from the right using geometry-aware distance. - var boundary1 = new PartBoundary(part1, halfSpacing); - var boundary2 = new PartBoundary(part2, halfSpacing); + // Compact p2 left toward p1 using geometry-aware distance. + Compactor.Push(new List { p2 }, new List { p1 }, workArea, partSpacing, PushDirection.Left); - // Position part2 to the right of part1 at bounding box width distance. - var startOffset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing; - part2.Offset(startOffset, 0); - part2.UpdateBounds(); + var pairBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); - // Slide part2 left toward part1. - var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left); - var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right); - var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left); - - if (dist < double.MaxValue && dist > 0) - { - part2.Offset(-dist, 0); - part2.UpdateBounds(); - } - - // Re-anchor pair to work area origin. - var pairBbox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + // Re-anchor pair to work area origin (bottom-left). var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom); - part1.Offset(anchor); - part2.Offset(anchor); - part1.UpdateBounds(); - part2.UpdateBounds(); + p1.Offset(anchor); + p2.Offset(anchor); - pairBbox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + pairBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); // Verify pair fits in work area. if (pairBbox.Width > workArea.Width + Tolerance.Epsilon || pairBbox.Length > workArea.Length + Tolerance.Epsilon) return null; - return (part1, part2, pairBbox); + return (p1, p2, pairBbox); } // --- Step 2: Build Column (tile vertically) --- @@ -107,193 +87,104 @@ namespace OpenNest.Engine.Fill { var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; - // Find geometry-aware copy distance for the pair vertically. - var boundary1 = new PartBoundary(part1, halfSpacing); - var boundary2 = new PartBoundary(part2, halfSpacing); - - // Compute vertical copy distance using bounding boxes as starting point, - // then slide down to find true geometry distance. - var pairHeight = pairBbox.Length; - var testOffset = new Vector(0, pairHeight); - - // Create test parts for slide distance measurement. - var testPart1 = part1.CloneAtOffset(testOffset); - var testPart2 = part2.CloneAtOffset(testOffset); - - // Find minimum distance from test pair sliding down toward original pair. - var copyDistance = FindVerticalCopyDistance( - part1, part2, testPart1, testPart2, - boundary1, boundary2, pairHeight); - + var copyDistance = ComputeVerticalCopyDistance(part1, part2, pairBbox); if (copyDistance <= 0) return column; - var count = 1; - while (true) - { - var nextBottom = pairBbox.Bottom + copyDistance * count; - if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) - break; + var pairHeight = pairBbox.Length; + var currentY = pairBbox.Bottom + copyDistance; - var offset = new Vector(0, copyDistance * count); + while (currentY + pairHeight <= workArea.Top + Tolerance.Epsilon) + { + var offset = new Vector(0, currentY - pairBbox.Bottom); column.Add(part1.CloneAtOffset(offset)); column.Add(part2.CloneAtOffset(offset)); - count++; + currentY += copyDistance; } return column; } - private double FindVerticalCopyDistance( - Part origPart1, Part origPart2, - Part testPart1, Part testPart2, - PartBoundary boundary1, PartBoundary boundary2, - double pairHeight) + private double ComputeVerticalCopyDistance(Part p1, Part p2, Box pairBbox) { - // Check all 4 combinations: test parts sliding down toward original parts. - var minSlide = double.MaxValue; + var pairHeight = pairBbox.Length; + // Start the test pair high enough so it doesn't overlap the original pair's bounding box initially. + var startOffset = pairHeight + partSpacing; + var testParts = new List { p1.CloneAtOffset(new Vector(0, startOffset)), p2.CloneAtOffset(new Vector(0, startOffset)) }; + var obstacles = new List { p1, p2 }; - // Test1 -> Orig1 - var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - // Test1 -> Orig2 - d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - // Test2 -> Orig1 - d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - // Test2 -> Orig2 - d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - if (minSlide >= double.MaxValue || minSlide < 0) - return pairHeight + partSpacing; - - // 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; + // Use a large work area to prevent edge-clamping during distance measurement. + var largeWorkArea = new Box(workArea.X, workArea.Y - pairHeight, workArea.Width, workArea.Length + pairHeight * 3); + var slide = Compactor.Push(testParts, obstacles, largeWorkArea, partSpacing, PushDirection.Down); + // True copy distance = start - slide. Clamp to BB height + spacing to prevent BB overlap. + var copyDist = startOffset - slide; return System.Math.Max(copyDist, pairHeight + partSpacing); } - private static double SlideDistance( - PartBoundary movingBoundary, Vector movingLocation, - PartBoundary stationaryBoundary, Vector stationaryLocation, - PushDirection direction) - { - var opposite = SpatialQuery.OppositeDirection(direction); - var movingEdges = movingBoundary.GetEdges(direction); - var stationaryEdges = stationaryBoundary.GetEdges(opposite); - - return SpatialQuery.DirectionalDistance( - movingEdges, movingLocation, - stationaryEdges, stationaryLocation, - direction); - } - // --- Step 3: Iterative Adjustment --- private List AdjustColumn( - (Part part1, Part part2, Box pairBbox) pair, - List column, + (Part part1, Part part2, Box pairBbox) initialPair, + List initialColumn, CancellationToken token) { - var originalPairWidth = pair.pairBbox.Width; + var currentPair = initialPair; + var currentColumn = initialColumn; + var originalWidth = initialPair.pairBbox.Width; for (var iteration = 0; iteration < MaxIterations; iteration++) { if (token.IsCancellationRequested) break; - // Measure current gap. - var topEdge = double.MinValue; - foreach (var p in column) - if (p.BoundingBox.Top > topEdge) - topEdge = p.BoundingBox.Top; - - var gap = workArea.Top - topEdge; + var columnBbox = ((IEnumerable)currentColumn).GetBoundingBox(); + var gap = workArea.Top - columnBbox.Top; if (gap <= Tolerance.Epsilon) break; - var pairCount = column.Count / 2; - if (pairCount <= 0) - break; - + var pairCount = currentColumn.Count / 2; var adjustment = gap / pairCount; if (adjustment <= Tolerance.Epsilon) break; - // Try adjusting the pair and rebuilding the column. - var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth); + // Try shifting p2 up or down relative to p1 to see if we can close the gap + // without making the pair wider than its initial horizontal footprint. + var adjusted = TryAdjustPair(currentPair, adjustment, originalWidth); if (adjusted == null) break; var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox); - if (newColumn.Count == 0) - break; + if (newColumn.Count <= currentColumn.Count) + break; // No improvement in part count. - column = newColumn; - pair = adjusted.Value; + currentColumn = newColumn; + currentPair = adjusted.Value; } - return column; + return currentColumn; } private (Part part1, Part part2, Box pairBbox)? TryAdjustPair( (Part part1, Part part2, Box pairBbox) pair, - double adjustment, double originalPairWidth) + double adjustment, double maxWidth) { // Try shifting part2 up first. - var result = TryShiftDirection(pair, adjustment, originalPairWidth); + var result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, + (pair.part2.Location.Y - pair.part1.Location.Y) + adjustment); - if (result != null) + if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) return result; - // Up made the pair wider — try down instead. - return TryShiftDirection(pair, -adjustment, originalPairWidth); - } + // Up made it wider or didn't fit — try down instead. + result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, + (pair.part2.Location.Y - pair.part1.Location.Y) - adjustment); - private (Part part1, Part part2, Box pairBbox)? TryShiftDirection( - (Part part1, Part part2, Box pairBbox) pair, - double verticalShift, double originalPairWidth) - { - // Clone parts so we don't mutate the originals. - var p1 = (Part)pair.part1.Clone(); - var p2 = (Part)pair.part2.Clone(); + if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) + return result; - // Separate: shift part2 right so bounding boxes don't touch. - p2.Offset(partSpacing, 0); - p2.UpdateBounds(); - - // Apply the vertical shift. - p2.Offset(0, verticalShift); - p2.UpdateBounds(); - - // Compact part2 left toward part1. - var moving = new List { p2 }; - var obstacles = new List { p1 }; - Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left); - - // Check if the pair got wider. - var newBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); - - if (newBbox.Width > originalPairWidth + Tolerance.Epsilon) - return null; - - // Re-anchor to work area origin. - var anchor = new Vector(workArea.X - newBbox.Left, workArea.Y - newBbox.Bottom); - p1.Offset(anchor); - p2.Offset(anchor); - p1.UpdateBounds(); - p2.UpdateBounds(); - - newBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); - return (p1, p2, newBbox); + return null; } // --- Step 4: Horizontal Repetition --- @@ -306,36 +197,21 @@ namespace OpenNest.Engine.Fill var columnBbox = ((IEnumerable)column).GetBoundingBox(); var columnWidth = columnBbox.Width; - // Create a test column shifted right by columnWidth + spacing. - var testOffset = columnWidth + partSpacing; - var testColumn = new List(column.Count); - foreach (var part in column) - testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0))); + // Create a test column shifted right and compact it left to find the true copy distance. + var startOffset = columnWidth + partSpacing; + var testColumn = column.Select(p => p.CloneAtOffset(new Vector(startOffset, 0))).ToList(); - // Compact the test column left against the original column. - var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); - - // Derive the true copy distance from where the test column ended up. - var testBbox = ((IEnumerable)testColumn).GetBoundingBox(); - var copyDistance = testBbox.Left - columnBbox.Left; + var slide = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); + var copyDistance = startOffset - slide; if (copyDistance <= Tolerance.Epsilon) copyDistance = columnWidth + partSpacing; Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); - // Build all columns. var result = new List(column); + var colIndex = 1; - // Add the test column we already computed as column 2. - foreach (var part in testColumn) - { - if (IsWithinWorkArea(part)) - result.Add(part); - } - - // Tile additional columns at the copy distance. - var colIndex = 2; while (!token.IsCancellationRequested) { var offset = new Vector(copyDistance * colIndex, 0); From 794ef166290d81c7a8ad6cfc85bc09c884ae6cc5 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:17:06 -0400 Subject: [PATCH 19/34] test: add Compactor safety-net tests before refactor Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Tests/CompactorTests.cs | 162 +++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 OpenNest.Tests/CompactorTests.cs diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs new file mode 100644 index 0000000..a205d24 --- /dev/null +++ b/OpenNest.Tests/CompactorTests.cs @@ -0,0 +1,162 @@ +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(); + + // 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); + } + + [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); + } + } +} From dddc890a96ec19187ea183ad6e7ef77f893f5c35 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:17:57 -0400 Subject: [PATCH 20/34] Revert "refactor(engine): simplify FillExtents logic using Compactor.Push" This reverts commit d1d47b52233103154fd057e043db891773be002c. --- OpenNest.Engine/Fill/FillExtents.cs | 262 ++++++++++++++++++++-------- 1 file changed, 193 insertions(+), 69 deletions(-) diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index b331b87..40f7dee 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -3,7 +3,6 @@ using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; namespace OpenNest.Engine.Fill @@ -14,11 +13,13 @@ namespace OpenNest.Engine.Fill private readonly Box workArea; private readonly double partSpacing; + private readonly double halfSpacing; public FillExtents(Box workArea, double partSpacing) { this.workArea = workArea; this.partSpacing = partSpacing; + halfSpacing = partSpacing / 2; } public List Fill(Drawing drawing, double rotationAngle = 0, @@ -26,18 +27,18 @@ namespace OpenNest.Engine.Fill CancellationToken token = default, IProgress progress = null) { - var initialPair = CreatePair(drawing, rotationAngle, rotationAngle + System.Math.PI); - if (initialPair == null) + var pair = BuildPair(drawing, rotationAngle); + if (pair == null) return new List(); - var column = BuildColumn(initialPair.Value.part1, initialPair.Value.part2, initialPair.Value.pairBbox); + var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox); if (column.Count == 0) return new List(); NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, column, workArea, $"Extents: initial column {column.Count} parts"); - var adjusted = AdjustColumn(initialPair.Value, column, token); + var adjusted = AdjustColumn(pair.Value, column, token); NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts"); @@ -52,33 +53,52 @@ namespace OpenNest.Engine.Fill // --- Step 1: Pair Construction --- - private (Part part1, Part part2, Box pairBbox)? CreatePair( - Drawing drawing, double rotation1, double rotation2, double verticalShift2 = 0) + private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle) { - var p1 = Part.CreateAtOrigin(drawing, rotation1); - var p2 = Part.CreateAtOrigin(drawing, rotation2); + var part1 = Part.CreateAtOrigin(drawing, rotationAngle); + var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI); - // Initial positioning: p2 to the right of p1, with optional vertical shift. - p2.Offset(p1.BoundingBox.Width + partSpacing, verticalShift2); + // Check that each part fits in the work area individually. + if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon || + part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon) + return null; - // Compact p2 left toward p1 using geometry-aware distance. - Compactor.Push(new List { p2 }, new List { p1 }, workArea, partSpacing, PushDirection.Left); + // Slide part2 toward part1 from the right using geometry-aware distance. + var boundary1 = new PartBoundary(part1, halfSpacing); + var boundary2 = new PartBoundary(part2, halfSpacing); - var pairBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); + // Position part2 to the right of part1 at bounding box width distance. + var startOffset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing; + part2.Offset(startOffset, 0); + part2.UpdateBounds(); - // Re-anchor pair to work area origin (bottom-left). + // Slide part2 left toward part1. + var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left); + var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right); + var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left); + + if (dist < double.MaxValue && dist > 0) + { + part2.Offset(-dist, 0); + part2.UpdateBounds(); + } + + // Re-anchor pair to work area origin. + var pairBbox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom); - p1.Offset(anchor); - p2.Offset(anchor); + part1.Offset(anchor); + part2.Offset(anchor); + part1.UpdateBounds(); + part2.UpdateBounds(); - pairBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); + pairBbox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); // Verify pair fits in work area. if (pairBbox.Width > workArea.Width + Tolerance.Epsilon || pairBbox.Length > workArea.Length + Tolerance.Epsilon) return null; - return (p1, p2, pairBbox); + return (part1, part2, pairBbox); } // --- Step 2: Build Column (tile vertically) --- @@ -87,104 +107,193 @@ namespace OpenNest.Engine.Fill { var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; - var copyDistance = ComputeVerticalCopyDistance(part1, part2, pairBbox); + // Find geometry-aware copy distance for the pair vertically. + var boundary1 = new PartBoundary(part1, halfSpacing); + var boundary2 = new PartBoundary(part2, halfSpacing); + + // Compute vertical copy distance using bounding boxes as starting point, + // then slide down to find true geometry distance. + var pairHeight = pairBbox.Length; + var testOffset = new Vector(0, pairHeight); + + // Create test parts for slide distance measurement. + var testPart1 = part1.CloneAtOffset(testOffset); + var testPart2 = part2.CloneAtOffset(testOffset); + + // Find minimum distance from test pair sliding down toward original pair. + var copyDistance = FindVerticalCopyDistance( + part1, part2, testPart1, testPart2, + boundary1, boundary2, pairHeight); + if (copyDistance <= 0) return column; - var pairHeight = pairBbox.Length; - var currentY = pairBbox.Bottom + copyDistance; - - while (currentY + pairHeight <= workArea.Top + Tolerance.Epsilon) + var count = 1; + while (true) { - var offset = new Vector(0, currentY - pairBbox.Bottom); + var nextBottom = pairBbox.Bottom + copyDistance * count; + if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) + break; + + var offset = new Vector(0, copyDistance * count); column.Add(part1.CloneAtOffset(offset)); column.Add(part2.CloneAtOffset(offset)); - currentY += copyDistance; + count++; } return column; } - private double ComputeVerticalCopyDistance(Part p1, Part p2, Box pairBbox) + private double FindVerticalCopyDistance( + Part origPart1, Part origPart2, + Part testPart1, Part testPart2, + PartBoundary boundary1, PartBoundary boundary2, + double pairHeight) { - var pairHeight = pairBbox.Length; - // Start the test pair high enough so it doesn't overlap the original pair's bounding box initially. - var startOffset = pairHeight + partSpacing; - var testParts = new List { p1.CloneAtOffset(new Vector(0, startOffset)), p2.CloneAtOffset(new Vector(0, startOffset)) }; - var obstacles = new List { p1, p2 }; + // Check all 4 combinations: test parts sliding down toward original parts. + var minSlide = double.MaxValue; - // Use a large work area to prevent edge-clamping during distance measurement. - var largeWorkArea = new Box(workArea.X, workArea.Y - pairHeight, workArea.Width, workArea.Length + pairHeight * 3); - var slide = Compactor.Push(testParts, obstacles, largeWorkArea, partSpacing, PushDirection.Down); + // Test1 -> Orig1 + var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down); + if (d < minSlide) minSlide = d; + + // Test1 -> Orig2 + d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down); + if (d < minSlide) minSlide = d; + + // Test2 -> Orig1 + d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down); + if (d < minSlide) minSlide = d; + + // Test2 -> Orig2 + d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down); + if (d < minSlide) minSlide = d; + + if (minSlide >= double.MaxValue || minSlide < 0) + return pairHeight + partSpacing; + + // 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; - // True copy distance = start - slide. Clamp to BB height + spacing to prevent BB overlap. - var copyDist = startOffset - slide; return System.Math.Max(copyDist, pairHeight + partSpacing); } + private static double SlideDistance( + PartBoundary movingBoundary, Vector movingLocation, + PartBoundary stationaryBoundary, Vector stationaryLocation, + PushDirection direction) + { + var opposite = SpatialQuery.OppositeDirection(direction); + var movingEdges = movingBoundary.GetEdges(direction); + var stationaryEdges = stationaryBoundary.GetEdges(opposite); + + return SpatialQuery.DirectionalDistance( + movingEdges, movingLocation, + stationaryEdges, stationaryLocation, + direction); + } + // --- Step 3: Iterative Adjustment --- private List AdjustColumn( - (Part part1, Part part2, Box pairBbox) initialPair, - List initialColumn, + (Part part1, Part part2, Box pairBbox) pair, + List column, CancellationToken token) { - var currentPair = initialPair; - var currentColumn = initialColumn; - var originalWidth = initialPair.pairBbox.Width; + var originalPairWidth = pair.pairBbox.Width; for (var iteration = 0; iteration < MaxIterations; iteration++) { if (token.IsCancellationRequested) break; - var columnBbox = ((IEnumerable)currentColumn).GetBoundingBox(); - var gap = workArea.Top - columnBbox.Top; + // Measure current gap. + var topEdge = double.MinValue; + foreach (var p in column) + if (p.BoundingBox.Top > topEdge) + topEdge = p.BoundingBox.Top; + + var gap = workArea.Top - topEdge; if (gap <= Tolerance.Epsilon) break; - var pairCount = currentColumn.Count / 2; + var pairCount = column.Count / 2; + if (pairCount <= 0) + break; + var adjustment = gap / pairCount; if (adjustment <= Tolerance.Epsilon) break; - // Try shifting p2 up or down relative to p1 to see if we can close the gap - // without making the pair wider than its initial horizontal footprint. - var adjusted = TryAdjustPair(currentPair, adjustment, originalWidth); + // Try adjusting the pair and rebuilding the column. + var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth); if (adjusted == null) break; var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox); - if (newColumn.Count <= currentColumn.Count) - break; // No improvement in part count. + if (newColumn.Count == 0) + break; - currentColumn = newColumn; - currentPair = adjusted.Value; + column = newColumn; + pair = adjusted.Value; } - return currentColumn; + return column; } private (Part part1, Part part2, Box pairBbox)? TryAdjustPair( (Part part1, Part part2, Box pairBbox) pair, - double adjustment, double maxWidth) + double adjustment, double originalPairWidth) { // Try shifting part2 up first. - var result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, - (pair.part2.Location.Y - pair.part1.Location.Y) + adjustment); + var result = TryShiftDirection(pair, adjustment, originalPairWidth); - if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) + if (result != null) return result; - // Up made it wider or didn't fit — try down instead. - result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, - (pair.part2.Location.Y - pair.part1.Location.Y) - adjustment); + // Up made the pair wider — try down instead. + return TryShiftDirection(pair, -adjustment, originalPairWidth); + } - if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) - return result; + private (Part part1, Part part2, Box pairBbox)? TryShiftDirection( + (Part part1, Part part2, Box pairBbox) pair, + double verticalShift, double originalPairWidth) + { + // Clone parts so we don't mutate the originals. + var p1 = (Part)pair.part1.Clone(); + var p2 = (Part)pair.part2.Clone(); - return null; + // Separate: shift part2 right so bounding boxes don't touch. + p2.Offset(partSpacing, 0); + p2.UpdateBounds(); + + // Apply the vertical shift. + p2.Offset(0, verticalShift); + p2.UpdateBounds(); + + // Compact part2 left toward part1. + var moving = new List { p2 }; + var obstacles = new List { p1 }; + Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left); + + // Check if the pair got wider. + var newBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); + + if (newBbox.Width > originalPairWidth + Tolerance.Epsilon) + return null; + + // Re-anchor to work area origin. + var anchor = new Vector(workArea.X - newBbox.Left, workArea.Y - newBbox.Bottom); + p1.Offset(anchor); + p2.Offset(anchor); + p1.UpdateBounds(); + p2.UpdateBounds(); + + newBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); + return (p1, p2, newBbox); } // --- Step 4: Horizontal Repetition --- @@ -197,21 +306,36 @@ namespace OpenNest.Engine.Fill var columnBbox = ((IEnumerable)column).GetBoundingBox(); var columnWidth = columnBbox.Width; - // Create a test column shifted right and compact it left to find the true copy distance. - var startOffset = columnWidth + partSpacing; - var testColumn = column.Select(p => p.CloneAtOffset(new Vector(startOffset, 0))).ToList(); + // Create a test column shifted right by columnWidth + spacing. + var testOffset = columnWidth + partSpacing; + var testColumn = new List(column.Count); + foreach (var part in column) + testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0))); - var slide = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); - var copyDistance = startOffset - slide; + // Compact the test column left against the original column. + var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); + + // Derive the true copy distance from where the test column ended up. + var testBbox = ((IEnumerable)testColumn).GetBoundingBox(); + var copyDistance = testBbox.Left - columnBbox.Left; if (copyDistance <= Tolerance.Epsilon) copyDistance = columnWidth + partSpacing; Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); + // Build all columns. var result = new List(column); - var colIndex = 1; + // Add the test column we already computed as column 2. + foreach (var part in testColumn) + { + if (IsWithinWorkArea(part)) + result.Add(part); + } + + // Tile additional columns at the copy distance. + var colIndex = 2; while (!token.IsCancellationRequested) { var offset = new Vector(copyDistance * colIndex, 0); From b009f195be356b776656d7c72c14aad5d4c7ad4a Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:19:55 -0400 Subject: [PATCH 21/34] =?UTF-8?q?refactor(compactor):=20remove=20dead=20co?= =?UTF-8?q?de=20=E2=80=94=20Compact,=20CompactIndividual,=20and=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/Compactor.cs | 120 ----------------------------- OpenNest.Engine/StripNestEngine.cs | 10 --- 2 files changed, 130 deletions(-) diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index b438784..676f625 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -13,68 +13,6 @@ namespace OpenNest.Engine.Fill { 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 @@ -177,9 +115,6 @@ namespace OpenNest.Engine.Fill 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); @@ -303,60 +238,5 @@ namespace OpenNest.Engine.Fill 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/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 6f4c6ef..d114f11 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -183,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 From 9012a9fc1cc8345ee8b2578c1ed27fa220567326 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:23:50 -0400 Subject: [PATCH 22/34] =?UTF-8?q?refactor(compactor):=20deduplicate=20Push?= =?UTF-8?q?=20=E2=80=94=20PushDirection=20delegates=20to=20Vector=20overlo?= =?UTF-8?q?ad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fix missing using for FillHelpers in FillLinear and FillExtents, and update callers (CompactorTests, PatternTileForm) for the new Vector parameter. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/Compactor.cs | 74 ++--------------------------- OpenNest.Engine/Fill/FillExtents.cs | 63 ++++-------------------- OpenNest.Engine/Fill/FillLinear.cs | 53 +-------------------- OpenNest.Tests/CompactorTests.cs | 11 +++-- OpenNest/Forms/PatternTileForm.cs | 6 ++- 5 files changed, 26 insertions(+), 181 deletions(-) diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 676f625..66af511 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -31,16 +31,16 @@ namespace OpenNest.Engine.Fill .Where(p => !movingParts.Contains(p)) .ToList(); - return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle); + 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, double angle) + Box workArea, double partSpacing, Vector direction) { - var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); var opposite = -direction; var obstacleBoxes = new Box[obstacleParts.Count]; @@ -104,72 +104,8 @@ namespace OpenNest.Engine.Fill 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; - - 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; + var vector = SpatialQuery.DirectionToOffset(direction, 1.0); + return Push(movingParts, obstacleParts, workArea, partSpacing, vector); } /// diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 40f7dee..e5ccd6f 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,8 +1,10 @@ +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; namespace OpenNest.Engine.Fill @@ -105,7 +107,7 @@ namespace OpenNest.Engine.Fill private List BuildColumn(Part part1, Part part2, Box pairBbox) { - var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; + var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() }; // Find geometry-aware copy distance for the pair vertically. var boundary1 = new PartBoundary(part1, halfSpacing); @@ -126,22 +128,11 @@ namespace OpenNest.Engine.Fill boundary1, boundary2, pairHeight); if (copyDistance <= 0) - return column; + return pairParts; - var count = 1; - while (true) - { - var nextBottom = pairBbox.Bottom + copyDistance * count; - if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) - break; - - var offset = new Vector(0, copyDistance * count); - column.Add(part1.CloneAtOffset(offset)); - column.Add(part2.CloneAtOffset(offset)); - count++; - } - - return column; + var result = new List(pairParts); + result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false)); + return result; } private double FindVerticalCopyDistance( @@ -324,48 +315,10 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); - // Build all columns. var result = new List(column); - - // Add the test column we already computed as column 2. - foreach (var part in testColumn) - { - if (IsWithinWorkArea(part)) - result.Add(part); - } - - // Tile additional columns at the copy distance. - var colIndex = 2; - while (!token.IsCancellationRequested) - { - var offset = new Vector(copyDistance * colIndex, 0); - var anyFit = false; - - foreach (var part in column) - { - var clone = part.CloneAtOffset(offset); - if (IsWithinWorkArea(clone)) - { - result.Add(clone); - anyFit = true; - } - } - - if (!anyFit) - break; - - colIndex++; - } + result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true)); return result; } - - private bool IsWithinWorkArea(Part part) - { - return part.BoundingBox.Right <= workArea.Right + Tolerance.Epsilon && - part.BoundingBox.Top <= workArea.Top + Tolerance.Epsilon && - part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon && - part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon; - } } } diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 674bff3..98e88ed 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Generic; @@ -249,57 +250,7 @@ namespace OpenNest.Engine.Fill private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); - - if (copyDistance <= 0) - return new List(); - - var dim = GetDimension(basePattern.BoundingBox, direction); - var start = GetStart(basePattern.BoundingBox, direction); - var limit = GetLimit(direction); - - var estimatedCopies = (int)((limit - start - dim) / copyDistance); - var result = new List(estimatedCopies * basePattern.Parts.Count); - - var count = 1; - - while (true) - { - var nextPos = start + copyDistance * count; - - if (nextPos + dim > limit + Tolerance.Epsilon) - break; - - var offset = MakeOffset(direction, copyDistance * count); - - foreach (var part in basePattern.Parts) - result.Add(part.CloneAtOffset(offset)); - - count++; - } - - // For multi-part patterns, try to place individual parts from the - // next copy that didn't fit as a whole. This handles cases where - // e.g. a 2-part pair only partially fits — one part may still be - // within the work area even though the full pattern exceeds it. - if (basePattern.Parts.Count > 1) - { - var offset = MakeOffset(direction, copyDistance * count); - - foreach (var basePart in basePattern.Parts) - { - var part = basePart.CloneAtOffset(offset); - - if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && - part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && - part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && - part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) - { - result.Add(part); - } - } - } - - return result; + return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true); } /// diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs index a205d24..8d3e2c5 100644 --- a/OpenNest.Tests/CompactorTests.cs +++ b/OpenNest.Tests/CompactorTests.cs @@ -109,8 +109,9 @@ namespace OpenNest.Tests var moving = new List { part }; var obstacles = new List(); - // angle = π = push left - var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI); + // 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); @@ -124,8 +125,10 @@ namespace OpenNest.Tests 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); + // 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); diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index acbde4d..5d2a52d 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -213,12 +213,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) From e695e29355ee0dcbcf937f6bb0fe64f8e8f003d8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:24:33 -0400 Subject: [PATCH 23/34] =?UTF-8?q?Revert=20"refactor(compactor):=20deduplic?= =?UTF-8?q?ate=20Push=20=E2=80=94=20PushDirection=20delegates=20to=20Vecto?= =?UTF-8?q?r=20overload"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9012a9fc1cc8345ee8b2578c1ed27fa220567326. --- OpenNest.Engine/Fill/Compactor.cs | 74 +++++++++++++++++++++++++++-- OpenNest.Engine/Fill/FillExtents.cs | 63 ++++++++++++++++++++---- OpenNest.Engine/Fill/FillLinear.cs | 53 ++++++++++++++++++++- OpenNest.Tests/CompactorTests.cs | 11 ++--- OpenNest/Forms/PatternTileForm.cs | 6 +-- 5 files changed, 181 insertions(+), 26 deletions(-) diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 66af511..676f625 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -31,16 +31,16 @@ namespace OpenNest.Engine.Fill .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); + 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, Vector direction) + 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]; @@ -104,8 +104,72 @@ namespace OpenNest.Engine.Fill 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); + 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; + + 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; } /// diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index e5ccd6f..40f7dee 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,10 +1,8 @@ -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; namespace OpenNest.Engine.Fill @@ -107,7 +105,7 @@ namespace OpenNest.Engine.Fill private List BuildColumn(Part part1, Part part2, Box pairBbox) { - var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() }; + var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; // Find geometry-aware copy distance for the pair vertically. var boundary1 = new PartBoundary(part1, halfSpacing); @@ -128,11 +126,22 @@ namespace OpenNest.Engine.Fill boundary1, boundary2, pairHeight); if (copyDistance <= 0) - return pairParts; + return column; - var result = new List(pairParts); - result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false)); - return result; + var count = 1; + while (true) + { + var nextBottom = pairBbox.Bottom + copyDistance * count; + if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) + break; + + var offset = new Vector(0, copyDistance * count); + column.Add(part1.CloneAtOffset(offset)); + column.Add(part2.CloneAtOffset(offset)); + count++; + } + + return column; } private double FindVerticalCopyDistance( @@ -315,10 +324,48 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); + // Build all columns. var result = new List(column); - result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true)); + + // Add the test column we already computed as column 2. + foreach (var part in testColumn) + { + if (IsWithinWorkArea(part)) + result.Add(part); + } + + // Tile additional columns at the copy distance. + var colIndex = 2; + while (!token.IsCancellationRequested) + { + var offset = new Vector(copyDistance * colIndex, 0); + var anyFit = false; + + foreach (var part in column) + { + var clone = part.CloneAtOffset(offset); + if (IsWithinWorkArea(clone)) + { + result.Add(clone); + anyFit = true; + } + } + + if (!anyFit) + break; + + colIndex++; + } return result; } + + private bool IsWithinWorkArea(Part part) + { + return part.BoundingBox.Right <= workArea.Right + Tolerance.Epsilon && + part.BoundingBox.Top <= workArea.Top + Tolerance.Epsilon && + part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon && + part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon; + } } } diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 98e88ed..674bff3 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,4 +1,3 @@ -using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Generic; @@ -250,7 +249,57 @@ namespace OpenNest.Engine.Fill private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); - return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true); + + if (copyDistance <= 0) + return new List(); + + var dim = GetDimension(basePattern.BoundingBox, direction); + var start = GetStart(basePattern.BoundingBox, direction); + var limit = GetLimit(direction); + + var estimatedCopies = (int)((limit - start - dim) / copyDistance); + var result = new List(estimatedCopies * basePattern.Parts.Count); + + var count = 1; + + while (true) + { + var nextPos = start + copyDistance * count; + + if (nextPos + dim > limit + Tolerance.Epsilon) + break; + + var offset = MakeOffset(direction, copyDistance * count); + + foreach (var part in basePattern.Parts) + result.Add(part.CloneAtOffset(offset)); + + count++; + } + + // For multi-part patterns, try to place individual parts from the + // next copy that didn't fit as a whole. This handles cases where + // e.g. a 2-part pair only partially fits — one part may still be + // within the work area even though the full pattern exceeds it. + if (basePattern.Parts.Count > 1) + { + var offset = MakeOffset(direction, copyDistance * count); + + foreach (var basePart in basePattern.Parts) + { + var part = basePart.CloneAtOffset(offset); + + if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && + part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && + part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && + part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) + { + result.Add(part); + } + } + } + + return result; } /// diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs index 8d3e2c5..a205d24 100644 --- a/OpenNest.Tests/CompactorTests.cs +++ b/OpenNest.Tests/CompactorTests.cs @@ -109,9 +109,8 @@ namespace OpenNest.Tests 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); + // angle = π = push left + var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI); Assert.True(distance > 0); Assert.True(part.BoundingBox.Left < 1); @@ -125,10 +124,8 @@ namespace OpenNest.Tests 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); + // 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); diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index 5d2a52d..acbde4d 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -213,14 +213,12 @@ namespace OpenNest.Forms if (System.Math.Sqrt(dx * dx + dy * dy) < 0.01) continue; - 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 angle = System.Math.Atan2(dy, dx); var single = new List { part }; var obstacles = parts.Where(p => p != part).ToList(); totalMoved += Compactor.Push(single, obstacles, - syntheticWorkArea, spacing, direction); + syntheticWorkArea, spacing, angle); } if (totalMoved < 0.01) From 62f00055b725db8b0622141eef8136a55d194891 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:26:14 -0400 Subject: [PATCH 24/34] =?UTF-8?q?Reapply=20"refactor(compactor):=20dedupli?= =?UTF-8?q?cate=20Push=20=E2=80=94=20PushDirection=20delegates=20to=20Vect?= =?UTF-8?q?or=20overload"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e695e29355ee0dcbcf937f6bb0fe64f8e8f003d8. --- OpenNest.Engine/Fill/Compactor.cs | 74 ++--------------------------- OpenNest.Engine/Fill/FillExtents.cs | 63 ++++-------------------- OpenNest.Engine/Fill/FillLinear.cs | 53 +-------------------- OpenNest.Tests/CompactorTests.cs | 11 +++-- OpenNest/Forms/PatternTileForm.cs | 6 ++- 5 files changed, 26 insertions(+), 181 deletions(-) diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 676f625..66af511 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -31,16 +31,16 @@ namespace OpenNest.Engine.Fill .Where(p => !movingParts.Contains(p)) .ToList(); - return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle); + 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, double angle) + Box workArea, double partSpacing, Vector direction) { - var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); var opposite = -direction; var obstacleBoxes = new Box[obstacleParts.Count]; @@ -104,72 +104,8 @@ namespace OpenNest.Engine.Fill 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; - - 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; + var vector = SpatialQuery.DirectionToOffset(direction, 1.0); + return Push(movingParts, obstacleParts, workArea, partSpacing, vector); } /// diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 40f7dee..e5ccd6f 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,8 +1,10 @@ +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; namespace OpenNest.Engine.Fill @@ -105,7 +107,7 @@ namespace OpenNest.Engine.Fill private List BuildColumn(Part part1, Part part2, Box pairBbox) { - var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; + var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() }; // Find geometry-aware copy distance for the pair vertically. var boundary1 = new PartBoundary(part1, halfSpacing); @@ -126,22 +128,11 @@ namespace OpenNest.Engine.Fill boundary1, boundary2, pairHeight); if (copyDistance <= 0) - return column; + return pairParts; - var count = 1; - while (true) - { - var nextBottom = pairBbox.Bottom + copyDistance * count; - if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) - break; - - var offset = new Vector(0, copyDistance * count); - column.Add(part1.CloneAtOffset(offset)); - column.Add(part2.CloneAtOffset(offset)); - count++; - } - - return column; + var result = new List(pairParts); + result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false)); + return result; } private double FindVerticalCopyDistance( @@ -324,48 +315,10 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); - // Build all columns. var result = new List(column); - - // Add the test column we already computed as column 2. - foreach (var part in testColumn) - { - if (IsWithinWorkArea(part)) - result.Add(part); - } - - // Tile additional columns at the copy distance. - var colIndex = 2; - while (!token.IsCancellationRequested) - { - var offset = new Vector(copyDistance * colIndex, 0); - var anyFit = false; - - foreach (var part in column) - { - var clone = part.CloneAtOffset(offset); - if (IsWithinWorkArea(clone)) - { - result.Add(clone); - anyFit = true; - } - } - - if (!anyFit) - break; - - colIndex++; - } + result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true)); return result; } - - private bool IsWithinWorkArea(Part part) - { - return part.BoundingBox.Right <= workArea.Right + Tolerance.Epsilon && - part.BoundingBox.Top <= workArea.Top + Tolerance.Epsilon && - part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon && - part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon; - } } } diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 674bff3..98e88ed 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,3 +1,4 @@ +using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Generic; @@ -249,57 +250,7 @@ namespace OpenNest.Engine.Fill private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); - - if (copyDistance <= 0) - return new List(); - - var dim = GetDimension(basePattern.BoundingBox, direction); - var start = GetStart(basePattern.BoundingBox, direction); - var limit = GetLimit(direction); - - var estimatedCopies = (int)((limit - start - dim) / copyDistance); - var result = new List(estimatedCopies * basePattern.Parts.Count); - - var count = 1; - - while (true) - { - var nextPos = start + copyDistance * count; - - if (nextPos + dim > limit + Tolerance.Epsilon) - break; - - var offset = MakeOffset(direction, copyDistance * count); - - foreach (var part in basePattern.Parts) - result.Add(part.CloneAtOffset(offset)); - - count++; - } - - // For multi-part patterns, try to place individual parts from the - // next copy that didn't fit as a whole. This handles cases where - // e.g. a 2-part pair only partially fits — one part may still be - // within the work area even though the full pattern exceeds it. - if (basePattern.Parts.Count > 1) - { - var offset = MakeOffset(direction, copyDistance * count); - - foreach (var basePart in basePattern.Parts) - { - var part = basePart.CloneAtOffset(offset); - - if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && - part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && - part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && - part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) - { - result.Add(part); - } - } - } - - return result; + return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true); } /// diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs index a205d24..8d3e2c5 100644 --- a/OpenNest.Tests/CompactorTests.cs +++ b/OpenNest.Tests/CompactorTests.cs @@ -109,8 +109,9 @@ namespace OpenNest.Tests var moving = new List { part }; var obstacles = new List(); - // angle = π = push left - var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI); + // 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); @@ -124,8 +125,10 @@ namespace OpenNest.Tests 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); + // 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); diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index acbde4d..5d2a52d 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -213,12 +213,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) From 0da970ec9a1d020c4b4aa4df8775d3f85a62cfc9 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:29:38 -0400 Subject: [PATCH 25/34] fix: revert FillExtents/FillLinear FillHelpers.Tile calls (not yet available) Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/FillExtents.cs | 63 +++++++++++++++++++++++++---- OpenNest.Engine/Fill/FillLinear.cs | 53 +++++++++++++++++++++++- 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index e5ccd6f..40f7dee 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,10 +1,8 @@ -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; namespace OpenNest.Engine.Fill @@ -107,7 +105,7 @@ namespace OpenNest.Engine.Fill private List BuildColumn(Part part1, Part part2, Box pairBbox) { - var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() }; + var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; // Find geometry-aware copy distance for the pair vertically. var boundary1 = new PartBoundary(part1, halfSpacing); @@ -128,11 +126,22 @@ namespace OpenNest.Engine.Fill boundary1, boundary2, pairHeight); if (copyDistance <= 0) - return pairParts; + return column; - var result = new List(pairParts); - result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false)); - return result; + var count = 1; + while (true) + { + var nextBottom = pairBbox.Bottom + copyDistance * count; + if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) + break; + + var offset = new Vector(0, copyDistance * count); + column.Add(part1.CloneAtOffset(offset)); + column.Add(part2.CloneAtOffset(offset)); + count++; + } + + return column; } private double FindVerticalCopyDistance( @@ -315,10 +324,48 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); + // Build all columns. var result = new List(column); - result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true)); + + // Add the test column we already computed as column 2. + foreach (var part in testColumn) + { + if (IsWithinWorkArea(part)) + result.Add(part); + } + + // Tile additional columns at the copy distance. + var colIndex = 2; + while (!token.IsCancellationRequested) + { + var offset = new Vector(copyDistance * colIndex, 0); + var anyFit = false; + + foreach (var part in column) + { + var clone = part.CloneAtOffset(offset); + if (IsWithinWorkArea(clone)) + { + result.Add(clone); + anyFit = true; + } + } + + if (!anyFit) + break; + + colIndex++; + } return result; } + + private bool IsWithinWorkArea(Part part) + { + return part.BoundingBox.Right <= workArea.Right + Tolerance.Epsilon && + part.BoundingBox.Top <= workArea.Top + Tolerance.Epsilon && + part.BoundingBox.Left >= workArea.Left - Tolerance.Epsilon && + part.BoundingBox.Bottom >= workArea.Bottom - Tolerance.Epsilon; + } } } diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 98e88ed..674bff3 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,4 +1,3 @@ -using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Generic; @@ -250,7 +249,57 @@ namespace OpenNest.Engine.Fill private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); - return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true); + + if (copyDistance <= 0) + return new List(); + + var dim = GetDimension(basePattern.BoundingBox, direction); + var start = GetStart(basePattern.BoundingBox, direction); + var limit = GetLimit(direction); + + var estimatedCopies = (int)((limit - start - dim) / copyDistance); + var result = new List(estimatedCopies * basePattern.Parts.Count); + + var count = 1; + + while (true) + { + var nextPos = start + copyDistance * count; + + if (nextPos + dim > limit + Tolerance.Epsilon) + break; + + var offset = MakeOffset(direction, copyDistance * count); + + foreach (var part in basePattern.Parts) + result.Add(part.CloneAtOffset(offset)); + + count++; + } + + // For multi-part patterns, try to place individual parts from the + // next copy that didn't fit as a whole. This handles cases where + // e.g. a 2-part pair only partially fits — one part may still be + // within the work area even though the full pattern exceeds it. + if (basePattern.Parts.Count > 1) + { + var offset = MakeOffset(direction, copyDistance * count); + + foreach (var basePart in basePattern.Parts) + { + var part = basePart.CloneAtOffset(offset); + + if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && + part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && + part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && + part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) + { + result.Add(part); + } + } + } + + return result; } /// From f73bb2bc2f617c739a7d3b5ae8825a838f14b53b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 21:04:35 -0400 Subject: [PATCH 26/34] =?UTF-8?q?refactor(fill):=20simplify=20FindPatternC?= =?UTF-8?q?opyDistance=20=E2=80=94=20extract=20pair=20loop,=20remove=20red?= =?UTF-8?q?undant=20span=20calculation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern bounding box already computes max(upper) - min(lower), so the manual loop was redundant. Extract the N×N pair distance loop into a static FindMaxPairDistance helper. Drop pre-cached edge arrays since GetEdges() returns stored references with zero allocation. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/FillLinear.cs | 58 +++++++++++++----------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 674bff3..4a09190 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -110,47 +110,40 @@ namespace OpenNest.Engine.Fill 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.Engine.Fill } } - if (maxCopyDistance < Tolerance.Epsilon) - return bboxDim + PartSpacing; - return maxCopyDistance; } From e789fe312d7f70d10f78cc0f92c708213d36178d Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:11:29 -0400 Subject: [PATCH 27/34] feat(engine): add IsOverallBest flag to NestProgress Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Engine/NestEngineBase.cs | 4 +++- OpenNest.Engine/NestProgress.cs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index 36e0412..d7b7362 100644 --- a/OpenNest.Engine/NestEngineBase.cs +++ b/OpenNest.Engine/NestEngineBase.cs @@ -191,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; @@ -233,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 d7dccb8..a8ff655 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -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; } } } From 76e30d91c0f63323c0693d77749696eda972da6f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:12:20 -0400 Subject: [PATCH 28/34] feat(engine): flag overall-best progress reports in DefaultNestEngine Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Engine/DefaultNestEngine.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index b186890..e630392 100644 --- a/OpenNest.Engine/DefaultNestEngine.cs +++ b/OpenNest.Engine/DefaultNestEngine.cs @@ -55,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; } @@ -82,7 +83,8 @@ namespace OpenNest 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()); + ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(), + isOverallBest: true); return best ?? new List(); } @@ -134,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); } } } From 231f97fafca3d8e6fb2ffeb633a1b789023842ea Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:13:10 -0400 Subject: [PATCH 29/34] feat(ui): add active preview brush/pen to ColorScheme Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/ColorScheme.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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)); } } From 4fc8f1f6cff1d7ed7e2995eff075476c2c89fa79 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:14:59 -0400 Subject: [PATCH 30/34] feat(ui): two-bucket preview parts in PlateView Replace single temporaryParts list with stationaryParts (overall best, full opacity) and activeParts (current strategy, reduced opacity). Update SetPlate, Refresh, UpdateMatrix, DrawParts, and FillWithProgress accordingly. Replace SetTemporaryParts/ClearTemporaryParts/AcceptTemporaryParts with SetStationaryParts/SetActiveParts/ClearPreviewParts/AcceptPreviewParts. Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/Controls/PlateView.cs | 94 ++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index b32d506..d4f545f 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -31,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; @@ -149,7 +150,8 @@ namespace OpenNest.Controls plate.PartAdded -= plate_PartAdded; plate.PartRemoved -= plate_PartRemoved; parts.Clear(); - temporaryParts.Clear(); + stationaryParts.Clear(); + activeParts.Clear(); SelectedParts.Clear(); } @@ -408,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(); } @@ -503,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); @@ -879,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) @@ -918,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; }); @@ -932,20 +969,20 @@ namespace OpenNest.Controls 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 { @@ -1082,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)); } } } From a9a9dc8a0a5335ea999603f0cbe28cdb3feafdd8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:16:55 -0400 Subject: [PATCH 31/34] feat(ui): route progress to stationary/active buckets in MainForm Replace SetTemporaryParts/ClearTemporaryParts/AcceptTemporaryParts in all three progress callbacks (RunAutoNest, FillPlate, FillArea) with the new two-bucket API: SetStationaryParts for IsOverallBest updates, SetActiveParts for transient updates, AcceptPreviewParts(parts) and ClearPreviewParts for completion. Also removes the now-redundant highWaterMark guards from FillPlate_Click and FillArea_Click. Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/Forms/MainForm.cs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 64aa35b..463dff2 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -827,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; }); @@ -863,7 +868,7 @@ 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 || progressForm.Accepted)) { @@ -881,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); } @@ -964,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; }); @@ -981,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); } @@ -1026,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(); From 0472c12113d2ad6673afd60dac34ee96c43fb1af Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:48:12 -0400 Subject: [PATCH 32/34] refactor(fill): extract constants and EvaluateCandidate in PairFiller Extract magic numbers into named constants (MaxTopCandidates, EarlyExitMinTried, etc.), extract candidate evaluation into EvaluateCandidate method, and expose BestFits property so PairsFillStrategy can reuse without redundant BestFitCache call. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/PairFiller.cs | 52 ++++++++++++------- .../Strategies/PairsFillStrategy.cs | 7 +-- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/OpenNest.Engine/Fill/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs index 0d34f68..1a2b2fe 100644 --- a/OpenNest.Engine/Fill/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -12,13 +12,24 @@ 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; @@ -30,11 +41,11 @@ namespace OpenNest.Engine.Fill 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; @@ -47,17 +58,7 @@ namespace OpenNest.Engine.Fill { 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 = FillHelpers.BuildRotatedPattern(pairParts, 0); - var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI); - engine.RemainderPatterns = new List { p0, p90 }; - - var filled = FillHelpers.FillPattern(engine, pairParts, angles, workArea); + var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea); if (filled != null && filled.Count > 0) { @@ -81,8 +82,7 @@ namespace OpenNest.Engine.Fill 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; @@ -98,10 +98,22 @@ namespace OpenNest.Engine.Fill 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); @@ -110,14 +122,14 @@ namespace OpenNest.Engine.Fill { 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/Strategies/PairsFillStrategy.cs b/OpenNest.Engine/Strategies/PairsFillStrategy.cs index 79c0428..118c732 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -1,4 +1,3 @@ -using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; using System.Collections.Generic; @@ -16,11 +15,7 @@ namespace OpenNest.Engine.Strategies 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; } From 62ec6484c845537b75e8ec4b24921aeb55f80b85 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:48:19 -0400 Subject: [PATCH 33/34] fix(ui): PatternTileForm layout, orientation, and dropdown display Move PlateViews and labels to designer file so they show in VS. Fix nest orientation by swapping Box(Width,Length) to Box(Length,Width) matching plate convention (Length=X, Width=Y). Add ComboBox Format handler to show Drawing.Name. Zoom to fit after moving parts. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/PatternTileForm.Designer.cs | 476 +++++++++++++++------ OpenNest/Forms/PatternTileForm.cs | 110 ++--- OpenNest/Forms/PatternTileForm.resx | 120 ++++++ 3 files changed, 507 insertions(+), 199 deletions(-) create mode 100644 OpenNest/Forms/PatternTileForm.resx 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 5d2a52d..5e1089c 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -1,4 +1,3 @@ -using OpenNest.Controls; using OpenNest.Engine.Fill; using OpenNest.Geometry; using System; @@ -9,27 +8,9 @@ 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; } @@ -38,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(); @@ -103,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; @@ -113,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; @@ -145,6 +96,7 @@ namespace OpenNest.Forms if (e.Button == MouseButtons.Left && cellView.Plate.Parts.Count >= 2) { CompactCellParts(); + cellView.ZoomToFit(); } RebuildPreview(); @@ -228,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)) @@ -278,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); @@ -387,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 @@ -403,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 From 0f953b870164695f8f18bf26ee7bc4a7cd7eb72b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:48:25 -0400 Subject: [PATCH 34/34] docs: add two-bucket preview spec and plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-18-two-bucket-preview.md | 660 ++++++++++++++++++ .../2026-03-18-two-bucket-preview-design.md | 138 ++++ 2 files changed, 798 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-two-bucket-preview.md create mode 100644 docs/superpowers/specs/2026-03-18-two-bucket-preview-design.md 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-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) |