Compare commits

...

34 Commits

Author SHA1 Message Date
aj 0f953b8701 docs: add two-bucket preview spec and plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:48:25 -04:00
aj 62ec6484c8 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) <noreply@anthropic.com>
2026-03-18 22:48:19 -04:00
aj 0472c12113 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) <noreply@anthropic.com>
2026-03-18 22:48:12 -04:00
aj a9a9dc8a0a 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 <noreply@anthropic.com>
2026-03-18 22:16:55 -04:00
aj 4fc8f1f6cf 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 <noreply@anthropic.com>
2026-03-18 22:14:59 -04:00
aj 231f97fafc feat(ui): add active preview brush/pen to ColorScheme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:13:10 -04:00
aj 76e30d91c0 feat(engine): flag overall-best progress reports in DefaultNestEngine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:12:20 -04:00
aj e789fe312d feat(engine): add IsOverallBest flag to NestProgress
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:11:29 -04:00
aj f73bb2bc2f refactor(fill): simplify FindPatternCopyDistance — extract pair loop, remove redundant span calculation
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) <noreply@anthropic.com>
2026-03-18 21:04:35 -04:00
aj 0da970ec9a fix: revert FillExtents/FillLinear FillHelpers.Tile calls (not yet available)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:29:38 -04:00
aj 62f00055b7 Reapply "refactor(compactor): deduplicate Push — PushDirection delegates to Vector overload"
This reverts commit e695e29355.
2026-03-18 20:26:14 -04:00
aj e695e29355 Revert "refactor(compactor): deduplicate Push — PushDirection delegates to Vector overload"
This reverts commit 9012a9fc1c.
2026-03-18 20:24:33 -04:00
aj 9012a9fc1c refactor(compactor): deduplicate Push — PushDirection delegates to Vector overload
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) <noreply@anthropic.com>
2026-03-18 20:23:50 -04:00
aj b009f195be refactor(compactor): remove dead code — Compact, CompactIndividual, and helpers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:19:55 -04:00
aj dddc890a96 Revert "refactor(engine): simplify FillExtents logic using Compactor.Push"
This reverts commit d1d47b5223.
2026-03-18 20:17:57 -04:00
aj 794ef16629 test: add Compactor safety-net tests before refactor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:17:06 -04:00
aj d1d47b5223 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.
2026-03-18 20:13:55 -04:00
aj 24ed878d8e docs: add Compactor refactor implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:12:23 -04:00
aj c2b8400986 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) <noreply@anthropic.com>
2026-03-18 20:02:30 -04:00
aj 0a33047ad6 fix(engine): prevent FillExtents overlap and add strategy filter API
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) <noreply@anthropic.com>
2026-03-18 19:53:08 -04:00
aj c98e024f9c 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) <noreply@anthropic.com>
2026-03-18 18:07:20 -04:00
aj d6d7ba8480 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) <noreply@anthropic.com>
2026-03-18 17:58:35 -04:00
aj b6cde145e1 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) <noreply@anthropic.com>
2026-03-18 17:39:13 -04:00
aj 9a4f20ca00 feat(ui): support Accept button in nesting callers 2026-03-18 17:35:33 -04:00
aj b5af5a118d feat(ui): rewrite NestProgressForm with grouped panels, stepper, density bar, and Accept button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:34:50 -04:00
aj 60a557bd37 feat(ui): add DensityBar sparkline control for density visualization
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 17:31:59 -04:00
aj 97ab33c899 feat(ui): add PhaseStepperControl for nesting progress phases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 17:31:54 -04:00
aj a1810db96d 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) <noreply@anthropic.com>
2026-03-18 17:22:49 -04:00
aj 39d656ad21 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) <noreply@anthropic.com>
2026-03-18 17:16:42 -04:00
aj 1d9bcc63d2 chore: sort using directives
Auto-formatter reordering of using statements across the solution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 16:47:42 -04:00
aj 6102dd5b85 refactor(engine): migrate Fill(List<Part>) 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) <noreply@anthropic.com>
2026-03-18 16:46:24 -04:00
aj 495ee6f0c3 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) <noreply@anthropic.com>
2026-03-18 16:46:17 -04:00
aj 0e1e619f0a 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) <noreply@anthropic.com>
2026-03-18 16:46:11 -04:00
aj 0cba528591 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) <noreply@anthropic.com>
2026-03-18 16:45:50 -04:00
211 changed files with 4751 additions and 1632 deletions
+12 -8
View File
@@ -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<Part>();` not `List<Part> parts = new List<Part>();`).
## 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<T>` 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.
+4 -4
View File
@@ -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);
+2 -2
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,4 +1,3 @@
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
+1 -2
View File
@@ -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
{
+2 -3
View File
@@ -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
{
+2 -4
View File
@@ -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
{
+4 -4
View File
@@ -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
{
+2 -2
View File
@@ -8,10 +8,10 @@
public int Remaining
{
get
get
{
var x = Required - Nested;
return x < 0 ? 0: x;
return x < 0 ? 0 : x;
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
using System;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+1 -2
View File
@@ -1,6 +1,5 @@
using System;
using OpenNest.Math;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+2 -2
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Math;
using System.Collections.Generic;
using System.Drawing;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+1 -1
View File
@@ -1,7 +1,7 @@
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+1 -1
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+2 -2
View File
@@ -1,6 +1,6 @@
using System;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+1 -2
View File
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Clipper2Lib;
using System.Collections.Generic;
namespace OpenNest.Geometry
{
-1
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
namespace OpenNest.Geometry
+2 -2
View File
@@ -1,7 +1,7 @@
using System;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+1 -2
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Geometry
{
+1 -1
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using OpenNest.Math;
namespace OpenNest.Geometry
{
+1 -1
View File
@@ -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)}";
}
}
+40 -41
View File
@@ -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;
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
using System;
using OpenNest.Math;
using OpenNest.Math;
using System;
namespace OpenNest.Geometry
{
+1 -3
View File
@@ -1,6 +1,4 @@
using System;
namespace OpenNest.Math
namespace OpenNest.Math
{
public static class Angle
{
+1 -3
View File
@@ -1,6 +1,4 @@
using System;
namespace OpenNest.Math
namespace OpenNest.Math
{
public static class Tolerance
{
+1 -3
View File
@@ -1,6 +1,4 @@
using System;
namespace OpenNest.Math
namespace OpenNest.Math
{
public static class Trigonometry
{
+2 -2
View File
@@ -1,6 +1,6 @@
using System;
using OpenNest.Collections;
using OpenNest.Collections;
using OpenNest.Geometry;
using System;
namespace OpenNest
{
+1 -2
View File
@@ -1,5 +1,4 @@
using System;
using OpenNest.Math;
using OpenNest.Math;
namespace OpenNest
{
+3 -3
View File
@@ -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
{
+6 -6
View File
@@ -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;
}
+4 -4
View File
@@ -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
{
+2 -2
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -2
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+2 -2
View File
@@ -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
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Shapes
{
+4 -4
View File
@@ -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;
+3 -3
View File
@@ -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;
}
}
-97
View File
@@ -1,97 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
/// <summary>
/// Builds candidate rotation angles for single-item fill. Encapsulates the
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
/// known-good pruning across fills.
/// </summary>
public class AngleCandidateBuilder
{
private readonly HashSet<double> knownGoodAngles = new();
public bool ForceFullSweep { get; set; }
public List<double> Build(NestItem item, double bestRotation, Box workArea)
{
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
var testPart = new Part(item.Drawing);
if (!bestRotation.IsEqualTo(0))
testPart.Rotate(bestRotation);
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep;
if (needsSweep)
{
var step = Angle.ToRadians(5);
for (var a = 0.0; a < System.Math.PI; a += step)
{
if (!angles.Any(existing => existing.IsEqualTo(a)))
angles.Add(a);
}
}
if (!ForceFullSweep && angles.Count > 2)
{
var features = FeatureExtractor.Extract(item.Drawing);
if (features != null)
{
var predicted = AnglePredictor.PredictAngles(
features, workArea.Width, workArea.Length);
if (predicted != null)
{
var mlAngles = new List<double>(predicted);
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
mlAngles.Add(bestRotation);
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
mlAngles.Add(bestRotation + Angle.HalfPI);
Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
angles = mlAngles;
}
}
}
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
{
var pruned = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
foreach (var a in knownGoodAngles)
{
if (!pruned.Any(existing => existing.IsEqualTo(a)))
pruned.Add(a);
}
Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
return pruned;
}
return angles;
}
/// <summary>
/// Records angles that produced results. These are used to prune
/// subsequent Build() calls.
/// </summary>
public void RecordProductive(List<AngleResult> angleResults)
{
foreach (var ar in angleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
}
}
}
+4 -4
View File
@@ -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
{
+1 -1
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{
+3 -2
View File
@@ -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
{
@@ -1,6 +1,6 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit
{
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit.Tiling
{
+1 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit.Tiling
{
+2 -2
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.CirclePacking
{
+2 -2
View File
@@ -1,6 +1,6 @@
using System;
using OpenNest.Geometry;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
namespace OpenNest.CirclePacking
{
+2 -2
View File
@@ -1,6 +1,6 @@
using System;
using OpenNest.Geometry;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
namespace OpenNest.CirclePacking
{
-363
View File
@@ -1,363 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest
{
/// <summary>
/// Pushes a group of parts left and down to close gaps after placement.
/// Uses the same directional-distance logic as PlateView.PushSelected
/// but operates on Part objects directly.
/// </summary>
public static class Compactor
{
private const double ChordTolerance = 0.001;
/// <summary>
/// Compacts movingParts toward the bottom-left of the plate work area.
/// Everything already on the plate (excluding movingParts) is treated
/// as stationary obstacles.
/// </summary>
private const double RepeatThreshold = 0.01;
private const int MaxIterations = 20;
public static void Compact(List<Part> 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<Part> 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<Part> 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<Part> parts, Vector[] positions)
{
for (var i = 0; i < parts.Count; i++)
parts[i].Location = positions[i];
}
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, Plate plate, double angle)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, double angle)
{
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var halfSpacing = partSpacing / 2;
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List<Line> movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = direction * distance;
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var halfSpacing = partSpacing / 2;
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
// BB gap at which offset geometries are expected to be touching.
var contactGap = (halfSpacing + ChordTolerance) * 2;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List<Line> 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;
}
/// <summary>
/// Pushes movingParts using bounding-box distances only (no geometry lines).
/// Much faster but less precise — use as a coarse positioning pass before
/// a full geometry Push.
/// </summary>
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
var d = gap - partSpacing;
if (d < 0) d = 0;
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
/// <summary>
/// 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.
/// </summary>
public static void CompactIndividual(List<Part> 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<Part> 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<Part>(1) { part };
var obstacles = new List<Part>(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;
}
}
}
+25 -93
View File
@@ -1,13 +1,12 @@
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.RectanglePacking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
using OpenNest.RectanglePacking;
namespace OpenNest
{
@@ -56,7 +55,8 @@ namespace OpenNest
if (item.Quantity > 0 && best.Count > item.Quantity)
best = best.Take(item.Quantity).ToList();
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary());
ReportProgress(progress, WinnerPhase, PlateNumber, best, workArea, BuildProgressSummary(),
isOverallBest: true);
return best;
}
@@ -67,89 +67,24 @@ namespace OpenNest
if (groupParts == null || groupParts.Count == 0)
return new List<Part>();
// 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> { 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<Part> bestExtents2 = null;
foreach (var angle in extentsAngles2)
{
token.ThrowIfCancellationRequested();
var result = extentsFiller.Fill(groupParts[0].BaseDrawing, angle, PlateNumber, token, progress, bestFits2);
if (result != null && result.Count > (bestExtents2?.Count ?? 0))
bestExtents2 = result;
}
PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents2?.Count ?? 0, 0));
Debug.WriteLine($"[Fill(groupParts,Box)] Extents: {bestExtents2?.Count ?? 0} parts");
if (IsBetterFill(bestExtents2, best, workArea))
{
best = bestExtents2;
ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary());
}
}
catch (OperationCanceledException)
{
Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
}
}
// Always report the final winner so the UI's temporary parts
// match the returned result.
var winPhase = PhaseResults.Count > 0
? PhaseResults.OrderByDescending(r => r.PartCount).First().Phase
: NestPhase.Linear;
ReportProgress(progress, winPhase, PlateNumber, best, workArea, BuildProgressSummary());
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary(),
isOverallBest: true);
return best ?? new List<Part>();
}
@@ -201,8 +136,13 @@ namespace OpenNest
context.CurrentBest = result;
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
context.WinnerPhase = strategy.Phase;
ReportProgress(context.Progress, strategy.Phase, PlateNumber,
result, context.WorkArea, BuildProgressSummary());
}
if (context.CurrentBest != null && context.CurrentBest.Count > 0)
{
ReportProgress(context.Progress, context.WinnerPhase, PlateNumber,
context.CurrentBest, context.WorkArea, BuildProgressSummary(),
isOverallBest: true);
}
}
}
@@ -214,13 +154,5 @@ namespace OpenNest
angleBuilder.RecordProductive(context.AngleResults);
}
// --- Pattern helpers ---
internal static Pattern BuildRotatedPattern(List<Part> groupParts, double angle)
=> FillHelpers.BuildRotatedPattern(groupParts, angle);
internal static List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
=> FillHelpers.FillPattern(engine, groupParts, angles, workArea);
}
}
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Wraps an IProgress to prepend previously placed parts to each report,
@@ -0,0 +1,114 @@
using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Builds candidate rotation angles for single-item fill. Encapsulates the
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
/// known-good pruning across fills.
/// </summary>
public class AngleCandidateBuilder
{
private readonly HashSet<double> knownGoodAngles = new();
public bool ForceFullSweep { get; set; }
public List<double> Build(NestItem item, double bestRotation, Box workArea)
{
var baseAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
return BuildPrunedList(baseAngles);
var angles = new List<double>(baseAngles);
if (NeedsSweep(item, bestRotation, workArea))
AddSweepAngles(angles);
if (!ForceFullSweep && angles.Count > 2)
angles = ApplyMlPrediction(item, workArea, baseAngles, angles);
return angles;
}
private bool NeedsSweep(NestItem item, double bestRotation, Box workArea)
{
var testPart = new Part(item.Drawing);
if (!bestRotation.IsEqualTo(0))
testPart.Rotate(bestRotation);
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
return workAreaShortSide < partLongestSide || ForceFullSweep;
}
private static void AddSweepAngles(List<double> angles)
{
var step = Angle.ToRadians(5);
for (var a = 0.0; a < System.Math.PI; a += step)
{
if (!ContainsAngle(angles, a))
angles.Add(a);
}
}
private static List<double> ApplyMlPrediction(
NestItem item, Box workArea, double[] baseAngles, List<double> 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<double>(predicted);
foreach (var b in baseAngles)
{
if (!ContainsAngle(mlAngles, b))
mlAngles.Add(b);
}
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} angles -> {mlAngles.Count} predicted");
return mlAngles;
}
private List<double> BuildPrunedList(double[] baseAngles)
{
var pruned = new List<double>(baseAngles);
foreach (var a in knownGoodAngles)
{
if (!ContainsAngle(pruned, a))
pruned.Add(a);
}
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
return pruned;
}
private static bool ContainsAngle(List<double> angles, double angle)
{
return angles.Any(existing => existing.IsEqualTo(angle));
}
/// <summary>
/// Records angles that produced results. These are used to prune
/// subsequent Build() calls.
/// </summary>
public void RecordProductive(List<AngleResult> angleResults)
{
foreach (var ar in angleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
}
}
}
@@ -1,5 +1,4 @@
using System;
using OpenNest.Math;
using OpenNest.Math;
namespace OpenNest
{
+178
View File
@@ -0,0 +1,178 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Pushes a group of parts left and down to close gaps after placement.
/// Uses the same directional-distance logic as PlateView.PushSelected
/// but operates on Part objects directly.
/// </summary>
public static class Compactor
{
private const double ChordTolerance = 0.001;
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> 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);
}
/// <summary>
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
/// </summary>
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, Vector direction)
{
var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var halfSpacing = partSpacing / 2;
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
List<Line> movingLines = null;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
if (gap >= distance)
continue;
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue;
movingLines ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
obstacleLines[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = direction * distance;
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
}
/// <summary>
/// Pushes movingParts using bounding-box distances only (no geometry lines).
/// Much faster but less precise — use as a coarse positioning pass before
/// a full geometry Push.
/// </summary>
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
{
var obstacleParts = plate.Parts
.Where(p => !movingParts.Contains(p))
.ToList();
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
}
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
Box workArea, double partSpacing, PushDirection direction)
{
var obstacleBoxes = new Box[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
var opposite = SpatialQuery.OppositeDirection(direction);
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
var distance = double.MaxValue;
foreach (var moving in movingParts)
{
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
if (edgeDist <= 0)
distance = 0;
else if (edgeDist < distance)
distance = edgeDist;
var movingBox = moving.BoundingBox;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
if (reverseGap > 0)
continue;
var perpOverlap = isHorizontal
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
if (!perpOverlap)
continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
var d = gap - partSpacing;
if (d < 0) d = 0;
if (d < distance)
distance = d;
}
}
if (distance < double.MaxValue && distance > 0)
{
var offset = SpatialQuery.DirectionToOffset(direction, distance);
foreach (var moving in movingParts)
moving.Offset(offset);
return distance;
}
return 0;
}
}
}
@@ -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
{
@@ -172,18 +172,12 @@ namespace OpenNest
if (minSlide >= double.MaxValue || minSlide < 0)
return pairHeight + partSpacing;
// Boundaries are inflated by halfSpacing, so when inflated edges touch
// the actual parts have partSpacing gap. Match FillLinear's pattern:
// startOffset = pairHeight (no extra spacing), copyDist = height - slide.
// Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide,
// clamped so it never goes below pairHeight + partSpacing to prevent
// bounding-box overlap from spurious slide values.
var copyDist = pairHeight - minSlide;
// Boundaries are inflated by halfSpacing, so the geometry-aware
// distance already guarantees partSpacing gap. Only fall back to
// bounding-box distance if the calculation produced a non-positive value.
if (copyDist <= Tolerance.Epsilon)
return pairHeight + partSpacing;
return copyDist;
return System.Math.Max(copyDist, pairHeight + partSpacing);
}
private static double SlideDistance(
@@ -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;
/// <summary>
@@ -110,47 +110,40 @@ namespace OpenNest
var pushDir = GetPushDirection(direction);
var opposite = SpatialQuery.OppositeDirection(pushDir);
// Compute a starting offset large enough that every part-pair in
// patternB has its offset geometry beyond patternA's offset geometry.
var maxUpper = double.MinValue;
var minLower = double.MaxValue;
for (var i = 0; i < patternA.Parts.Count; i++)
{
var bb = patternA.Parts[i].BoundingBox;
var upper = direction == NestDirection.Horizontal ? bb.Right : bb.Top;
var lower = direction == NestDirection.Horizontal ? bb.Left : bb.Bottom;
if (upper > maxUpper) maxUpper = upper;
if (lower < minLower) minLower = lower;
}
var startOffset = System.Math.Max(bboxDim,
maxUpper - minLower + PartSpacing + Tolerance.Epsilon);
// bboxDim already spans max(upper) - min(lower) across all parts,
// so the start offset just needs to push beyond that plus spacing.
var startOffset = bboxDim + PartSpacing + Tolerance.Epsilon;
var offset = MakeOffset(direction, startOffset);
// Pre-cache edge arrays.
var movingEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
var stationaryEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
var maxCopyDistance = FindMaxPairDistance(
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
for (var i = 0; i < patternA.Parts.Count; i++)
{
movingEdges[i] = boundaries[i].GetEdges(pushDir);
stationaryEdges[i] = boundaries[i].GetEdges(opposite);
}
if (maxCopyDistance < Tolerance.Epsilon)
return bboxDim + PartSpacing;
return maxCopyDistance;
}
/// <summary>
/// Tests every pair of parts across adjacent pattern copies and returns the
/// maximum copy distance found. Returns 0 if no valid slide was found.
/// </summary>
private static double FindMaxPairDistance(
List<Part> parts, PartBoundary[] boundaries, Vector offset,
PushDirection pushDir, PushDirection opposite, double startOffset)
{
var maxCopyDistance = 0.0;
for (var j = 0; j < patternA.Parts.Count; j++)
for (var j = 0; j < parts.Count; j++)
{
var locationB = patternA.Parts[j].Location + offset;
var movingEdges = boundaries[j].GetEdges(pushDir);
var locationB = parts[j].Location + offset;
for (var i = 0; i < patternA.Parts.Count; i++)
for (var i = 0; i < parts.Count; i++)
{
var slideDistance = SpatialQuery.DirectionalDistance(
movingEdges[j], locationB,
stationaryEdges[i], patternA.Parts[i].Location,
movingEdges, locationB,
boundaries[i].GetEdges(opposite), parts[i].Location,
pushDir);
if (slideDistance >= double.MaxValue || slideDistance < 0)
@@ -163,9 +156,6 @@ namespace OpenNest
}
}
if (maxCopyDistance < Tolerance.Epsilon)
return bboxDim + PartSpacing;
return maxCopyDistance;
}
@@ -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<FillScore>
{
@@ -1,23 +1,35 @@
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Fills a work area using interlocking part pairs from BestFitCache.
/// Extracted from DefaultNestEngine.FillWithPairs.
/// </summary>
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;
/// <summary>
/// The best-fit results computed during the last Fill call.
/// Available after Fill returns so callers can reuse without recomputing.
/// </summary>
public List<BestFitResult> BestFits { get; private set; }
public PairFiller(Size plateSize, double partSpacing)
{
this.plateSize = plateSize;
@@ -29,11 +41,11 @@ namespace OpenNest
CancellationToken token = default,
IProgress<NestProgress> 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<Part> best = null;
@@ -46,17 +58,7 @@ namespace OpenNest
{
token.ThrowIfCancellationRequested();
var result = candidates[i];
var pairParts = result.BuildParts(item.Drawing);
var angles = result.HullAngles;
var engine = new FillLinear(workArea, partSpacing);
// Let the remainder strip try pair-based filling too.
var p0 = DefaultNestEngine.BuildRotatedPattern(pairParts, 0);
var p90 = DefaultNestEngine.BuildRotatedPattern(pairParts, Angle.HalfPI);
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea);
var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea);
if (filled != null && filled.Count > 0)
{
@@ -80,8 +82,7 @@ namespace OpenNest
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts");
// Early exit: stop if we've tried enough candidates without improvement.
if (i >= 9 && sinceImproved >= 10)
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
{
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
break;
@@ -97,10 +98,22 @@ namespace OpenNest
return best ?? new List<Part>();
}
private List<Part> 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<Pattern> { p0, p90 };
return FillHelpers.FillPattern(engine, pairParts, candidate.HullAngles, workArea);
}
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
{
var kept = bestFits.Where(r => r.Keep).ToList();
var top = kept.Take(50).ToList();
var top = kept.Take(MaxTopCandidates).ToList();
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
@@ -109,14 +122,14 @@ namespace OpenNest
{
var stripCandidates = bestFits
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
&& r.Utilization >= 0.3)
&& r.Utilization >= MinStripUtilization)
.OrderByDescending(r => r.Utilization);
var existing = new HashSet<BestFitResult>(top);
foreach (var r in stripCandidates)
{
if (top.Count >= 100)
if (top.Count >= MaxStripCandidates)
break;
if (existing.Add(r))
@@ -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
{
/// <summary>
/// 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;
}
}
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Fill
{
public class Pattern
{
@@ -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
{
@@ -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
{
/// <summary>
/// Iteratively fills remnant boxes with items using a RemnantFinder.
@@ -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
{
/// <summary>
/// A remnant box with a priority tier.
@@ -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
{
@@ -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 }
+3 -3
View File
@@ -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
{
-1
View File
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Engine.ML
{
+1 -3
View File
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;
using System.Linq;
namespace OpenNest.Engine.ML
{
+11 -4
View File
@@ -1,9 +1,10 @@
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
{
@@ -190,7 +191,8 @@ namespace OpenNest
int plateNumber,
List<Part> best,
Box workArea,
string description)
string description,
bool isOverallBest = false)
{
if (progress == null || best == null || best.Count == 0)
return;
@@ -212,9 +214,13 @@ namespace OpenNest
$"PartArea={totalPartArea:F0}, Remnant={workArea.Area() - totalPartArea:F0}, " +
$"WorkArea={workArea.Width:F1}x{workArea.Length:F1} | {description}";
Debug.WriteLine(msg);
try { System.IO.File.AppendAllText(
try
{
System.IO.File.AppendAllText(
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"),
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); } catch { }
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n");
}
catch { }
progress.Report(new NestProgress
{
@@ -228,6 +234,7 @@ namespace OpenNest
BestParts = clonedParts,
Description = description,
ActiveWorkArea = workArea,
IsOverallBest = isOverallBest,
});
}
+2 -1
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
{
@@ -46,5 +46,6 @@ namespace OpenNest
public List<Part> BestParts { get; set; }
public string Description { get; set; }
public Box ActiveWorkArea { get; set; }
public bool IsOverallBest { get; set; }
}
}
@@ -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
{
/// <summary>
/// Mixed-part geometry-aware nesting using NFP-based collision avoidance
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// NFP-based Bottom-Left Fill (BLF) placement engine.
@@ -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
{
/// <summary>
/// Result of a nest optimization run.
@@ -1,8 +1,8 @@
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest
namespace OpenNest.Engine.Nfp
{
/// <summary>
/// Caches computed No-Fit Polygons keyed by (DrawingA.Id, RotationA, DrawingB.Id, RotationB).
@@ -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
{
/// <summary>
/// Simulated annealing optimizer for NFP-based nesting.

Some files were not shown because too many files have changed in this diff Show More