diff --git a/CLAUDE.md b/CLAUDE.md index 7e5e869..ce66236 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ Domain model, geometry, and CNC primitives organized into namespaces: - **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`. - **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output. -- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction). +- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction). - **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental). - **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed. - **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration. @@ -44,7 +44,7 @@ Domain model, geometry, and CNC primitives organized into namespaces: Nesting algorithms provide both a legacy single-plate API and a whole-job API. The legacy path centers on `NestEngineBase`, `DefaultNestEngine` (formerly `NestEngine`), and the global `NestEngineRegistry`. New job callers use immutable, ID-based contracts in `Jobs/`: `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named strategy without reading or changing the process-global registry. - **Whole-job API (`Jobs/`)**: `NestJob` owns part requirements, physical stock, and options for one material/thickness/unit system. `PartGeometrySnapshot` contains owned flat rapid/line/arc geometry; results contain stock IDs and placement poses (radians), not mutable desktop models. `NestJobPlacementValidator` validates contours, rotation, usable work area, overlap, and spacing before accounting commits. The runner selects valid trial candidates greedily by priority vector, sheet area, envelope, and input order; an incomplete result reports why but does not prove geometric impossibility. `DrawingJobMapper` and `NestResultMaterializer` are the domain-boundary adapters. -- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester` and `StripPlateNester` are migrated built-ins with run-scoped private geometry; `LegacyPlateNesterAdapter` remains for remnant strategies and legacy plugins/callers during rollout. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` and NFP/`AutoNester` retain legacy name-based helpers and are deliberately outside the runner path. +- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester` and `StripPlateNester` are migrated built-ins with run-scoped private geometry; `LegacyPlateNesterAdapter` remains for remnant strategies and legacy plugins/callers during rollout. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains legacy name-based helpers and is deliberately outside the runner path. - **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. Existing desktop, CLI, and MCP callers remain on this compatibility path until separate migrations preserve their existing-plate, preview, and accept/cancel semantics. - **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`. - **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. @@ -53,7 +53,6 @@ Nesting algorithms provide both a legacy single-plate API and a whole-job API. T - **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`): Internal NFP-based single-part placement utilities — `AutoNester` (NFP placement with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`OptimizationResult`. Not exposed as a nest engine; used internally for individual part placement. - **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. @@ -70,7 +69,7 @@ File I/O and format conversion. Uses ACadSharp for DXF/DWG support. - `Bending/BendRepair` — conservative opt-in repair configured by `CadImportOptions.BendRepair`. Requires explicit inches/mm source units and an endpoint movement limit above 0.001 and at most 3.175 physical mm. Only unambiguous paired ETCH/SCRIBE ticks may move along the existing bend axis; cut geometry and unrelated marks must remain unchanged. Opt-in imports preserve source marks without blanket etch regeneration and expose per-bend outcomes in `CadImportResult.BendRepairReports`. ### OpenNest.Console (console app, depends on Core + Engine + IO) -Command-line interface for batch nesting (`net8.0`). Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`). `--repair-bends-mm --cad-units inches|mm` opts newly imported DXFs into conservative bend repair and prints per-bend reports; it does not rescale coordinates or repair saved nests. +Command-line interface for batch nesting (`net8.0`). Supports DXF import, plate configuration, linear fill, and multi-drawing auto-nesting through the active engine's `Nest()` (`--autonest`). `--repair-bends-mm --cad-units inches|mm` opts newly imported DXFs into conservative bend repair and prints per-bend reports; it does not rescale coordinates or repair saved nests. ### OpenNest.Gpu (class library, depends on Core + Engine) GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` implements `IPairEvaluator`, `GpuSlideComputer` implements `ISlideComputer`, and `PartBitmap` handles rasterization. `GpuEvaluatorFactory` provides factory methods. @@ -130,7 +129,7 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe ## 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`. +- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`. - `ObservableList` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings. - Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion). - `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations. diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index 7adfe0a..ed123c8 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -597,7 +597,7 @@ static class NestConsole " --template Nest template for plate defaults (thickness, quadrant, material, spacing)" ); Console.Error.WriteLine( - " --autonest Use NFP-based mixed-part autonesting instead of linear fill" + " --autonest Use mixed-part autonesting (engine Nest) instead of linear fill" ); Console.Error.WriteLine( " --keep-parts Don't clear existing parts before filling" diff --git a/OpenNest.Core/Geometry/InnerFitPolygon.cs b/OpenNest.Core/Geometry/InnerFitPolygon.cs deleted file mode 100644 index b51e8b4..0000000 --- a/OpenNest.Core/Geometry/InnerFitPolygon.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Clipper2Lib; - -namespace OpenNest.Geometry -{ - /// - /// Computes the Inner-Fit Polygon (IFP) — the feasible region where a part's - /// reference point can be placed so the part stays entirely within the plate boundary. - /// For a rectangular plate, the IFP is the plate shrunk by the part's bounding dimensions. - /// - public static class InnerFitPolygon - { - /// - /// Computes the IFP for placing a part polygon inside a rectangular work area. - /// The result is a polygon representing all valid reference point positions. - /// - public static Polygon Compute(Box workArea, Polygon partPolygon) - { - // Get the part's bounding box relative to its reference point (origin). - var verts = partPolygon.Vertices; - - if (verts.Count < 3) - return new Polygon(); - - var minX = verts[0].X; - var maxX = verts[0].X; - var minY = verts[0].Y; - var maxY = verts[0].Y; - - for (var i = 1; i < verts.Count; i++) - { - if (verts[i].X < minX) - minX = verts[i].X; - if (verts[i].X > maxX) - maxX = verts[i].X; - if (verts[i].Y < minY) - minY = verts[i].Y; - if (verts[i].Y > maxY) - maxY = verts[i].Y; - } - - // The IFP is the work area shrunk inward by the part's extent in each direction. - // The reference point can range from (workArea.Left - minX) to (workArea.Right - maxX) - // and (workArea.Bottom - minY) to (workArea.Top - maxY). - var ifpLeft = workArea.X - minX; - var ifpRight = workArea.Right - maxX; - var ifpBottom = workArea.Y - minY; - var ifpTop = workArea.Top - maxY; - - // If the part doesn't fit, return an empty polygon. - if (ifpRight < ifpLeft || ifpTop < ifpBottom) - return new Polygon(); - - var result = new Polygon(); - result.Vertices.Add(new Vector(ifpLeft, ifpBottom)); - result.Vertices.Add(new Vector(ifpRight, ifpBottom)); - result.Vertices.Add(new Vector(ifpRight, ifpTop)); - result.Vertices.Add(new Vector(ifpLeft, ifpTop)); - result.Close(); - result.UpdateBounds(); - - return result; - } - - /// - /// Computes the feasible region for placing a part given already-placed parts. - /// FeasibleRegion = IFP(plate, part) - union(NFP(placed_i, part)) - /// Returns the polygon representing valid placement positions, or an empty - /// polygon if no valid position exists. - /// - public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths) - { - if (ifp.Vertices.Count < 3) - return new Polygon(); - - if (nfpPaths == null || nfpPaths.Count == 0) - return ifp; - - var ifpPath = NoFitPolygon.ToClipperPath(ifp); - var ifpPaths = new PathsD { ifpPath }; - - // Subtract the NFPs from the IFP. - // Clipper2 handles the implicit union of the clip paths. - var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero); - - if (feasible.Count == 0) - return new Polygon(); - - // Find the polygon with the bottom-left-most point. - // This ensures we pick the correct region for placement. - PathD bestPath = null; - var bestY = double.MaxValue; - var bestX = double.MaxValue; - - foreach (var path in feasible) - { - foreach (var pt in path) - { - if (pt.y < bestY || (pt.y == bestY && pt.x < bestX)) - { - bestY = pt.y; - bestX = pt.x; - bestPath = path; - } - } - } - - return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon(); - } - - /// - /// Computes the feasible region for placing a part given already-placed parts. - /// (Legacy overload for backward compatibility). - /// - public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps) - { - if (nfps == null || nfps.Length == 0) - return ifp; - - var nfpPaths = new PathsD(nfps.Length); - foreach (var nfp in nfps) - { - if (nfp.Vertices.Count >= 3) - nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp)); - } - - return ComputeFeasibleRegion(ifp, nfpPaths); - } - - /// - /// Finds the bottom-left-most point on a polygon boundary. - /// "Bottom-left" means: minimize Y first, then minimize X. - /// Returns Vector.Invalid if the polygon has no vertices. - /// - public static Vector FindBottomLeftPoint(Polygon polygon) - { - if (polygon.Vertices.Count == 0) - return Vector.Invalid; - - var best = polygon.Vertices[0]; - - for (var i = 1; i < polygon.Vertices.Count; i++) - { - var v = polygon.Vertices[i]; - - if (v.Y < best.Y || (v.Y == best.Y && v.X < best.X)) - best = v; - } - - return best; - } - } -} diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index 64eced2..82163ac 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -24,9 +24,6 @@ namespace OpenNest [Description("Trying pairs..."), ShortName("Pairs")] Pairs, - [Description("Trying NFP..."), ShortName("NFP")] - Nfp, - [Description("Trying extents..."), ShortName("Extents")] Extents, diff --git a/OpenNest.Engine/Nfp/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs deleted file mode 100644 index 554403b..0000000 --- a/OpenNest.Engine/Nfp/AutoNester.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using OpenNest.Geometry; -using OpenNest.Math; - -namespace OpenNest.Engine.Nfp -{ - /// - /// Mixed-part geometry-aware nesting using NFP-based collision avoidance - /// and simulated annealing optimization. - /// - public static class AutoNester - { - public static List Nest( - List items, - Plate plate, - IProgress progress = null, - CancellationToken cancellation = default - ) - { - var workArea = plate.WorkArea(); - var halfSpacing = plate.PartSpacing / 2.0; - var nfpCache = new NfpCache(); - var candidateRotations = new Dictionary>(); - - // Extract perimeter polygons for each unique drawing. - foreach (var item in items) - { - var drawing = item.Drawing; - - if (candidateRotations.ContainsKey(drawing.Id)) - continue; - - var perimeterPolygon = ExtractPerimeterPolygon(drawing, halfSpacing); - - if (perimeterPolygon == null) - { - Debug.WriteLine( - $"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter" - ); - continue; - } - - // Compute candidate rotations for this drawing. - var rotations = ComputeCandidateRotations(item, perimeterPolygon, workArea); - candidateRotations[drawing.Id] = rotations; - - // Register polygons at each candidate rotation. - foreach (var rotation in rotations) - { - var rotatedPolygon = RotatePolygon(perimeterPolygon, rotation); - nfpCache.RegisterPolygon(drawing.Id, rotation, rotatedPolygon); - } - } - - if (candidateRotations.Count == 0) - return new List(); - - // Pre-compute all NFPs. - nfpCache.PreComputeAll(); - - Debug.WriteLine( - $"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings" - ); - - // Run simulated annealing optimizer. - var optimizer = new SimulatedAnnealing(); - var result = optimizer.Optimize( - items, - workArea, - nfpCache, - candidateRotations, - progress, - cancellation - ); - - if (result.Sequence == null || result.Sequence.Count == 0) - return new List(); - - // Final BLF placement with the best solution. - var blf = new BottomLeftFill(workArea, nfpCache); - var placedParts = blf.Fill(result.Sequence); - var parts = BottomLeftFill.ToNestParts(placedParts); - - Debug.WriteLine( - $"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations" - ); - - NestEngineBase.ReportProgress( - progress, - new ProgressReport - { - Phase = NestPhase.Nfp, - PlateNumber = 0, - Parts = parts, - WorkArea = workArea, - Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations", - IsOverallBest = true, - } - ); - - return parts; - } - - /// - /// Re-places already-positioned parts using NFP-based BLF. - /// Returns the tighter layout if BLF improves density without losing parts. - /// - public static List Optimize(List parts, Plate plate) - { - return Optimize(parts, plate.WorkArea(), plate.PartSpacing); - } - - /// - /// Re-places already-positioned parts using NFP-based BLF within the given work area. - /// Returns the tighter layout if BLF improves density without losing parts. - /// - public static List Optimize(List parts, Box workArea, double partSpacing) - { - if (parts == null || parts.Count < 2) - return parts; - - var halfSpacing = partSpacing / 2.0; - var nfpCache = new NfpCache(); - var registeredRotations = new HashSet<(int id, double rotation)>(); - - // Extract polygons for each unique drawing+rotation used by the placed parts. - foreach (var part in parts) - { - var drawing = part.BaseDrawing; - var rotation = part.Rotation; - var key = (drawing.Id, rotation); - - if (registeredRotations.Contains(key)) - continue; - - var perimeterPolygon = ExtractPerimeterPolygon(drawing, halfSpacing); - - if (perimeterPolygon == null) - continue; - - var rotatedPolygon = RotatePolygon(perimeterPolygon, rotation); - nfpCache.RegisterPolygon(drawing.Id, rotation, rotatedPolygon); - registeredRotations.Add(key); - } - - if (registeredRotations.Count == 0) - return parts; - - nfpCache.PreComputeAll(); - - // Build BLF sequence sorted by area descending (largest first packs best). - var sequence = parts - .OrderByDescending(p => p.BaseDrawing.Area) - .Select(p => new SequenceEntry(p.BaseDrawing.Id, p.Rotation, p.BaseDrawing)) - .ToList(); - - var blf = new BottomLeftFill(workArea, nfpCache); - var placed = blf.Fill(sequence); - var optimized = BottomLeftFill.ToNestParts(placed); - - // Only use the NFP result if it kept all parts and improved density. - if (optimized.Count < parts.Count) - { - Debug.WriteLine( - $"[AutoNest.Optimize] Rejected: placed {optimized.Count}/{parts.Count} parts" - ); - return parts; - } - - // Reject if any part landed outside the work area. - if (!AllPartsInBounds(optimized, workArea)) - { - Debug.WriteLine("[AutoNest.Optimize] Rejected: parts outside work area"); - return parts; - } - - var originalScore = Fill.FillScore.Compute(parts, workArea); - var optimizedScore = Fill.FillScore.Compute(optimized, workArea); - - if (optimizedScore > originalScore) - { - Debug.WriteLine( - $"[AutoNest.Optimize] Improved: density {originalScore.Density:P1} -> {optimizedScore.Density:P1}" - ); - return optimized; - } - - Debug.WriteLine( - $"[AutoNest.Optimize] No improvement: {originalScore.Density:P1} >= {optimizedScore.Density:P1}" - ); - return parts; - } - - private static bool AllPartsInBounds(List parts, Box workArea) - { - var logPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - "nest-debug.log" - ); - - var allInBounds = true; - - // Append to the log that BLF already started - using var log = new StreamWriter(logPath, true); - log.WriteLine( - $"\n[Bounds] workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}" - ); - - foreach (var part in parts) - { - var bb = part.BoundingBox; - var outLeft = bb.Left < workArea.X - Tolerance.Epsilon; - var outBottom = bb.Bottom < workArea.Y - Tolerance.Epsilon; - var outRight = bb.Right > workArea.Right + Tolerance.Epsilon; - var outTop = bb.Top > workArea.Top + Tolerance.Epsilon; - var oob = outLeft || outBottom || outRight || outTop; - - if (oob) - { - log.WriteLine( - $"[Bounds] OOB DrawingId={part.BaseDrawing.Id} \"{part.BaseDrawing.Name}\" loc=({part.Location.X:F4},{part.Location.Y:F4}) rot={part.Rotation:F3} bb=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4}) violations: {(outLeft ? "LEFT " : "")}{(outBottom ? "BOTTOM " : "")}{(outRight ? "RIGHT " : "")}{(outTop ? "TOP " : "")}" - ); - allInBounds = false; - } - } - - if (allInBounds) - log.WriteLine($"[Bounds] All {parts.Count} parts in bounds."); - - return allInBounds; - } - - /// - /// Extracts the perimeter polygon from a drawing, inflated by half-spacing. - /// - private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing) - { - return BestFit.PolygonHelper.ExtractPerimeterPolygon(drawing, halfSpacing).Polygon; - } - - /// - /// Computes candidate rotation angles for a drawing. - /// - private static List ComputeCandidateRotations( - NestItem item, - Polygon perimeterPolygon, - Box workArea - ) - { - var rotations = new List { 0 }; - - // Add hull-edge angles from the polygon itself. - var hullAngles = ComputeHullEdgeAngles(perimeterPolygon); - - foreach (var angle in hullAngles) - { - if (!rotations.Any(r => r.IsEqualTo(angle))) - rotations.Add(angle); - } - - // Add 90-degree rotation. - if (!rotations.Any(r => r.IsEqualTo(Angle.HalfPI))) - rotations.Add(Angle.HalfPI); - - // For narrow work areas, add sweep angles. - var partBounds = perimeterPolygon.BoundingBox; - var partLongest = System.Math.Max(partBounds.Width, partBounds.Length); - var workShort = System.Math.Min(workArea.Width, workArea.Length); - - if (workShort < partLongest) - { - var step = Angle.ToRadians(5); - - for (var a = 0.0; a < System.Math.PI; a += step) - { - if (!rotations.Any(r => r.IsEqualTo(a))) - rotations.Add(a); - } - } - - return rotations; - } - - /// - /// Computes convex hull edge angles from a polygon for candidate rotations. - /// - private static List ComputeHullEdgeAngles(Polygon polygon) - { - var angles = new List(); - - if (polygon.Vertices.Count < 3) - return angles; - - var hull = ConvexHull.Compute(polygon.Vertices); - var verts = hull.Vertices; - var n = hull.IsClosed() ? verts.Count - 1 : verts.Count; - - for (var i = 0; i < n; i++) - { - var next = (i + 1) % n; - var dx = verts[next].X - verts[i].X; - var dy = verts[next].Y - verts[i].Y; - - if (dx * dx + dy * dy < Tolerance.Epsilon) - continue; - - var angle = -System.Math.Atan2(dy, dx); - - if (!angles.Any(a => a.IsEqualTo(angle))) - angles.Add(angle); - } - - return angles; - } - - /// - /// Creates a rotated copy of a polygon around the origin. - /// - private static Polygon RotatePolygon(Polygon polygon, double angle) - { - return BestFit.PolygonHelper.RotatePolygon(polygon, angle); - } - } -} diff --git a/OpenNest.Engine/Nfp/BottomLeftFill.cs b/OpenNest.Engine/Nfp/BottomLeftFill.cs deleted file mode 100644 index 3f55fd5..0000000 --- a/OpenNest.Engine/Nfp/BottomLeftFill.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Clipper2Lib; -using OpenNest.Geometry; - -namespace OpenNest.Engine.Nfp -{ - /// - /// NFP-based Bottom-Left Fill (BLF) placement engine. - /// Places parts one at a time using feasible regions computed from - /// the Inner-Fit Polygon minus the union of No-Fit Polygons. - /// - public class BottomLeftFill - { - private static readonly string DebugLogPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), - "nest-debug.log" - ); - - private readonly Box workArea; - private readonly NfpCache nfpCache; - - public BottomLeftFill(Box workArea, NfpCache nfpCache) - { - this.workArea = workArea; - this.nfpCache = nfpCache; - } - - /// - /// Places parts according to the given sequence using NFP-based BLF. - /// Returns the list of successfully placed parts with their positions. - /// - public List Fill(List sequence) - { - var placedParts = new List(); - - using var log = new StreamWriter(DebugLogPath, false); - log.WriteLine( - $"[BLF] {DateTime.Now:HH:mm:ss.fff} workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}" - ); - log.WriteLine($"[BLF] Sequence count: {sequence.Count}"); - - foreach (var entry in sequence) - { - var ifp = nfpCache.GetIfp(entry.DrawingId, entry.Rotation, workArea); - - if (ifp.Vertices.Count < 3) - { - log.WriteLine( - $"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)" - ); - continue; - } - - log.WriteLine( - $"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} IFP verts={ifp.Vertices.Count} bounds=({ifp.BoundingBox.X:F2},{ifp.BoundingBox.Y:F2},{ifp.BoundingBox.Width:F2},{ifp.BoundingBox.Length:F2})" - ); - - var nfpPaths = ComputeNfpPaths( - placedParts, - entry.DrawingId, - entry.Rotation, - ifp.BoundingBox - ); - var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfpPaths); - var point = InnerFitPolygon.FindBottomLeftPoint(feasible); - - if (double.IsNaN(point.X)) - { - log.WriteLine($"[BLF] -> NO feasible point (NaN)"); - continue; - } - - // Clamp to IFP bounds to correct Clipper2 floating-point drift. - var ifpBb = ifp.BoundingBox; - point = new Vector( - System.Math.Max(ifpBb.X, System.Math.Min(ifpBb.Right, point.X)), - System.Math.Max(ifpBb.Y, System.Math.Min(ifpBb.Top, point.Y)) - ); - - log.WriteLine( - $"[BLF] -> placed at ({point.X:F4}, {point.Y:F4}) nfpPaths={nfpPaths.Count} feasibleVerts={feasible.Vertices.Count}" - ); - - placedParts.Add( - new PlacedPart - { - DrawingId = entry.DrawingId, - Rotation = entry.Rotation, - Position = point, - Drawing = entry.Drawing, - } - ); - } - - log.WriteLine($"[BLF] Total placed: {placedParts.Count}/{sequence.Count}"); - return placedParts; - } - - /// - /// Converts placed parts to OpenNest Part instances positioned on the plate. - /// - public static List ToNestParts(List placedParts) - { - var parts = new List(placedParts.Count); - - foreach (var placed in placedParts) - { - var part = Part.CreateAtOrigin(placed.Drawing, placed.Rotation); - // CreateAtOrigin sets Location to compensate for the rotated program's - // bounding box offset. The BLF position is a displacement for the - // origin-normalized polygon, so we ADD it to the existing Location - // rather than replacing it. - part.Location = part.Location + placed.Position; - parts.Add(part); - } - - return parts; - } - - /// - /// Computes NFPs for a candidate part against all already-placed parts, - /// returned as Clipper paths with translations applied. - /// Filters NFPs that don't intersect the target IFP. - /// - private PathsD ComputeNfpPaths( - List placedParts, - int drawingId, - double rotation, - Box ifpBounds - ) - { - var nfpPaths = new PathsD(placedParts.Count); - - for (var i = 0; i < placedParts.Count; i++) - { - var placed = placedParts[i]; - var nfp = nfpCache.Get(placed.DrawingId, placed.Rotation, drawingId, rotation); - - if (nfp != null && nfp.Vertices.Count >= 3) - { - // Spatial pruning: only include NFPs that could actually subtract from the IFP. - var nfpBounds = nfp.BoundingBox.Translate(placed.Position); - if (nfpBounds.Intersects(ifpBounds)) - { - nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp, placed.Position)); - } - } - } - - return nfpPaths; - } - } -} diff --git a/OpenNest.Engine/Nfp/INestOptimizer.cs b/OpenNest.Engine/Nfp/INestOptimizer.cs deleted file mode 100644 index ec9c75d..0000000 --- a/OpenNest.Engine/Nfp/INestOptimizer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; - -namespace OpenNest.Engine.Nfp -{ - /// - /// Result of a nest optimization run. - /// - public class OptimizationResult - { - /// - /// The best placement sequence found. - /// - public List Sequence { get; set; } - - /// - /// The score achieved by the best sequence. - /// - public FillScore Score { get; set; } - - /// - /// Number of iterations performed. - /// - public int Iterations { get; set; } - } - - /// - /// Interface for nest optimization algorithms that search for the best - /// part ordering and rotation to maximize plate utilization. - /// - public interface INestOptimizer - { - OptimizationResult Optimize( - List items, - Box workArea, - NfpCache cache, - Dictionary> candidateRotations, - IProgress progress = null, - CancellationToken cancellation = default - ); - } -} diff --git a/OpenNest.Engine/Nfp/NfpCache.cs b/OpenNest.Engine/Nfp/NfpCache.cs deleted file mode 100644 index 8038473..0000000 --- a/OpenNest.Engine/Nfp/NfpCache.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using OpenNest.Geometry; - -namespace OpenNest.Engine.Nfp -{ - /// - /// Caches computed No-Fit Polygons keyed by (DrawingA.Id, RotationA, DrawingB.Id, RotationB). - /// NFPs are computed on first access and stored for reuse during optimization. - /// Thread-safe for concurrent reads after pre-computation. - /// - public class NfpCache - { - private readonly Dictionary cache = new Dictionary(); - private readonly Dictionary> polygonCache = - new Dictionary>(); - private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache = - new Dictionary<(int drawingId, double rotation), Polygon>(); - - /// - /// Registers a pre-computed polygon for a drawing at a specific rotation. - /// Call this during initialization before computing NFPs. - /// - public void RegisterPolygon(int drawingId, double rotation, Polygon polygon) - { - if (!polygonCache.TryGetValue(drawingId, out var rotations)) - { - rotations = new Dictionary(); - polygonCache[drawingId] = rotations; - } - - rotations[rotation] = polygon; - - // Clear IFP cache if a polygon is updated (though usually they aren't). - ifpCache.Remove((drawingId, rotation)); - } - - /// - /// Gets or computes the IFP for a drawing at a specific rotation within a work area. - /// - public Polygon GetIfp(int drawingId, double rotation, Box workArea) - { - if (ifpCache.TryGetValue((drawingId, rotation), out var ifp)) - return ifp; - - var polygon = GetPolygon(drawingId, rotation); - if (polygon == null) - return new Polygon(); - - ifp = InnerFitPolygon.Compute(workArea, polygon); - ifpCache[(drawingId, rotation)] = ifp; - return ifp; - } - - /// - /// Gets the polygon for a drawing at a specific rotation. - /// - public Polygon GetPolygon(int drawingId, double rotation) - { - if (polygonCache.TryGetValue(drawingId, out var rotations)) - { - if (rotations.TryGetValue(rotation, out var polygon)) - return polygon; - } - - return null; - } - - /// - /// Gets or computes the NFP between two drawings at their respective rotations. - /// The NFP is computed from the stationary polygon (drawingA at rotationA) and - /// the orbiting polygon (drawingB at rotationB). - /// - public Polygon Get(int drawingIdA, double rotationA, int drawingIdB, double rotationB) - { - var key = new NfpKey(drawingIdA, rotationA, drawingIdB, rotationB); - - if (cache.TryGetValue(key, out var nfp)) - return nfp; - - var polyA = GetPolygon(drawingIdA, rotationA); - var polyB = GetPolygon(drawingIdB, rotationB); - - if (polyA == null || polyB == null) - return new Polygon(); - - nfp = NoFitPolygon.Compute(polyA, polyB); - cache[key] = nfp; - return nfp; - } - - /// - /// Pre-computes all NFPs for every combination of registered polygons. - /// Call after all polygons are registered to front-load computation. - /// - public void PreComputeAll() - { - var entries = new List<(int drawingId, double rotation)>(); - - foreach (var kvp in polygonCache) - { - foreach (var rot in kvp.Value) - entries.Add((kvp.Key, rot.Key)); - } - - for (var i = 0; i < entries.Count; i++) - { - for (var j = 0; j < entries.Count; j++) - { - Get( - entries[i].drawingId, - entries[i].rotation, - entries[j].drawingId, - entries[j].rotation - ); - } - } - } - - /// - /// Number of cached NFP entries. - /// - public int Count => cache.Count; - - private readonly struct NfpKey : IEquatable - { - public readonly int DrawingIdA; - public readonly double RotationA; - public readonly int DrawingIdB; - public readonly double RotationB; - - public NfpKey(int drawingIdA, double rotationA, int drawingIdB, double rotationB) - { - DrawingIdA = drawingIdA; - RotationA = rotationA; - DrawingIdB = drawingIdB; - RotationB = rotationB; - } - - public bool Equals(NfpKey other) - { - return DrawingIdA == other.DrawingIdA - && RotationA == other.RotationA - && DrawingIdB == other.DrawingIdB - && RotationB == other.RotationB; - } - - public override bool Equals(object obj) => obj is NfpKey key && Equals(key); - - public override int GetHashCode() - { - unchecked - { - var hash = 17; - hash = hash * 31 + DrawingIdA; - hash = hash * 31 + RotationA.GetHashCode(); - hash = hash * 31 + DrawingIdB; - hash = hash * 31 + RotationB.GetHashCode(); - return hash; - } - } - } - } -} diff --git a/OpenNest.Engine/Nfp/PlacedPart.cs b/OpenNest.Engine/Nfp/PlacedPart.cs deleted file mode 100644 index a32fc49..0000000 --- a/OpenNest.Engine/Nfp/PlacedPart.cs +++ /dev/null @@ -1,15 +0,0 @@ -using OpenNest.Geometry; - -namespace OpenNest.Engine.Nfp -{ - /// - /// Represents a part that has been placed by the BLF algorithm. - /// - public class PlacedPart - { - public int DrawingId { get; set; } - public double Rotation { get; set; } - public Vector Position { get; set; } - public Drawing Drawing { get; set; } - } -} diff --git a/OpenNest.Engine/Nfp/SequenceEntry.cs b/OpenNest.Engine/Nfp/SequenceEntry.cs deleted file mode 100644 index 1cc6091..0000000 --- a/OpenNest.Engine/Nfp/SequenceEntry.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace OpenNest.Engine.Nfp -{ - /// - /// An entry in a placement sequence — identifies which drawing to place and at what rotation. - /// - public readonly struct SequenceEntry - { - public int DrawingId { get; } - public double Rotation { get; } - public Drawing Drawing { get; } - - public SequenceEntry(int drawingId, double rotation, Drawing drawing) - { - DrawingId = drawingId; - Rotation = rotation; - Drawing = drawing; - } - - public SequenceEntry WithRotation(double rotation) - { - return new SequenceEntry(DrawingId, rotation, Drawing); - } - } -} diff --git a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs deleted file mode 100644 index f28576c..0000000 --- a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs +++ /dev/null @@ -1,348 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; - -namespace OpenNest.Engine.Nfp -{ - /// - /// Simulated annealing optimizer for NFP-based nesting. - /// Searches for the best part ordering and rotation to maximize plate utilization. - /// - public class SimulatedAnnealing : INestOptimizer - { - private const double DefaultCoolingRate = 0.995; - private const double DefaultMinTemperature = 0.1; - private const int DefaultMaxNoImprovement = 500; - - public OptimizationResult Optimize( - List items, - Box workArea, - NfpCache cache, - Dictionary> candidateRotations, - IProgress progress = null, - CancellationToken cancellation = default - ) - { - var random = new Random(); - - // Build initial sequence: expand NestItems into individual entries, - // sorted by area descending. - var sequence = BuildInitialSequence(items, candidateRotations); - - if (sequence.Count == 0) - return new OptimizationResult - { - Sequence = sequence, - Score = default, - Iterations = 0, - }; - - // Evaluate initial solution. - var blf = new BottomLeftFill(workArea, cache); - var bestPlaced = blf.Fill(sequence); - var bestScore = FillScore.Compute(BottomLeftFill.ToNestParts(bestPlaced), workArea); - var bestSequence = new List(sequence); - - var currentSequence = new List(sequence); - var currentScore = bestScore; - - // Calibrate initial temperature so ~80% of worse moves are accepted. - var initialTemp = CalibrateTemperature( - currentSequence, - workArea, - cache, - candidateRotations, - random - ); - var temperature = initialTemp; - var noImprovement = 0; - var iteration = 0; - - Debug.WriteLine( - $"[SA] Initial: {bestScore.Count} parts, density={bestScore.Density:P1}, temp={initialTemp:F2}" - ); - - ReportBest( - progress, - BottomLeftFill.ToNestParts(bestPlaced), - workArea, - $"NFP: initial {bestScore.Count} parts, density={bestScore.Density:P1}" - ); - - while ( - temperature > DefaultMinTemperature - && noImprovement < DefaultMaxNoImprovement - && !cancellation.IsCancellationRequested - ) - { - iteration++; - - var candidate = new List(currentSequence); - Mutate(candidate, candidateRotations, random); - - var candidatePlaced = blf.Fill(candidate); - var candidateScore = FillScore.Compute( - BottomLeftFill.ToNestParts(candidatePlaced), - workArea - ); - - var delta = candidateScore.CompareTo(currentScore); - - if (delta > 0) - { - // Better solution — always accept. - currentSequence = candidate; - currentScore = candidateScore; - - if (currentScore > bestScore) - { - bestScore = currentScore; - bestSequence = new List(currentSequence); - noImprovement = 0; - - Debug.WriteLine( - $"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}" - ); - - ReportBest( - progress, - BottomLeftFill.ToNestParts(candidatePlaced), - workArea, - $"NFP: iter {iteration}, {bestScore.Count} parts, density={bestScore.Density:P1}" - ); - } - else - { - noImprovement++; - } - } - else if (delta < 0) - { - // Worse solution — accept with probability based on temperature. - var scoreDiff = ScoreDifference(currentScore, candidateScore); - var acceptProb = System.Math.Exp(-scoreDiff / temperature); - - if (random.NextDouble() < acceptProb) - { - currentSequence = candidate; - currentScore = candidateScore; - } - - noImprovement++; - } - else - { - noImprovement++; - } - - temperature *= DefaultCoolingRate; - } - - Debug.WriteLine( - $"[SA] Done: {iteration} iters, best={bestScore.Count} parts, density={bestScore.Density:P1}" - ); - - return new OptimizationResult - { - Sequence = bestSequence, - Score = bestScore, - Iterations = iteration, - }; - } - - /// - /// Builds the initial placement sequence sorted by drawing area descending. - /// Each NestItem is expanded by its quantity. - /// - private static List BuildInitialSequence( - List items, - Dictionary> candidateRotations - ) - { - var sequence = new List(); - - // Sort items by area descending. - var sorted = items.OrderByDescending(i => i.Drawing.Area).ToList(); - - foreach (var item in sorted) - { - var qty = item.Quantity > 0 ? item.Quantity : 1; - var rotation = 0.0; - - if ( - candidateRotations.TryGetValue(item.Drawing.Id, out var rotations) - && rotations.Count > 0 - ) - rotation = rotations[0]; - - for (var i = 0; i < qty; i++) - sequence.Add(new SequenceEntry(item.Drawing.Id, rotation, item.Drawing)); - } - - return sequence; - } - - /// - /// Applies a random mutation to the sequence. - /// - private static void Mutate( - List sequence, - Dictionary> candidateRotations, - Random random - ) - { - if (sequence.Count < 2) - return; - - var op = random.Next(3); - - switch (op) - { - case 0: // Swap - MutateSwap(sequence, random); - break; - case 1: // Rotate - MutateRotate(sequence, candidateRotations, random); - break; - case 2: // Segment reverse - MutateReverse(sequence, random); - break; - } - } - - /// - /// Swaps two random parts in the sequence. - /// - private static void MutateSwap(List sequence, Random random) - { - var i = random.Next(sequence.Count); - var j = random.Next(sequence.Count); - - while (j == i && sequence.Count > 1) - j = random.Next(sequence.Count); - - (sequence[i], sequence[j]) = (sequence[j], sequence[i]); - } - - /// - /// Changes a random part's rotation to another candidate angle. - /// - private static void MutateRotate( - List sequence, - Dictionary> candidateRotations, - Random random - ) - { - var idx = random.Next(sequence.Count); - var entry = sequence[idx]; - - if ( - !candidateRotations.TryGetValue(entry.DrawingId, out var rotations) - || rotations.Count <= 1 - ) - return; - - var newRotation = rotations[random.Next(rotations.Count)]; - sequence[idx] = entry.WithRotation(newRotation); - } - - /// - /// Reverses a random contiguous subsequence. - /// - private static void MutateReverse(List sequence, Random random) - { - var i = random.Next(sequence.Count); - var j = random.Next(sequence.Count); - - if (i > j) - (i, j) = (j, i); - - while (i < j) - { - (sequence[i], sequence[j]) = (sequence[j], sequence[i]); - i++; - j--; - } - } - - /// - /// Calibrates the initial temperature by sampling random mutations and - /// measuring score differences. Sets temperature so ~80% of worse moves - /// are accepted initially. - /// - private static double CalibrateTemperature( - List sequence, - Box workArea, - NfpCache cache, - Dictionary> candidateRotations, - Random random - ) - { - const int samples = 20; - var deltas = new List(); - var blf = new BottomLeftFill(workArea, cache); - - var basePlaced = blf.Fill(sequence); - var baseScore = FillScore.Compute(BottomLeftFill.ToNestParts(basePlaced), workArea); - - for (var i = 0; i < samples; i++) - { - var candidate = new List(sequence); - Mutate(candidate, candidateRotations, random); - - var placed = blf.Fill(candidate); - var score = FillScore.Compute(BottomLeftFill.ToNestParts(placed), workArea); - - var diff = ScoreDifference(baseScore, score); - - if (diff > 0) - deltas.Add(diff); - } - - if (deltas.Count == 0) - return 1.0; - - // T = -avgDelta / ln(0.8) ≈ avgDelta * 4.48 - var avgDelta = deltas.Average(); - return -avgDelta / System.Math.Log(0.8); - } - - /// - /// Computes a numeric difference between two scores for SA acceptance probability. - /// Uses a weighted combination of count and density. - /// - private static double ScoreDifference(FillScore better, FillScore worse) - { - // Weight count heavily (each part is worth 10 density points). - var countDiff = better.Count - worse.Count; - var densityDiff = better.Density - worse.Density; - - return countDiff * 10.0 + densityDiff; - } - - private static void ReportBest( - IProgress progress, - List parts, - Box workArea, - string description - ) - { - NestEngineBase.ReportProgress( - progress, - new ProgressReport - { - Phase = NestPhase.Nfp, - PlateNumber = 0, - Parts = parts, - WorkArea = workArea, - Description = description, - IsOverallBest = true, - } - ); - } - } -} diff --git a/OpenNest.Tests/Engine/NestPhaseExtensionsTests.cs b/OpenNest.Tests/Engine/NestPhaseExtensionsTests.cs index a5cac38..729afab 100644 --- a/OpenNest.Tests/Engine/NestPhaseExtensionsTests.cs +++ b/OpenNest.Tests/Engine/NestPhaseExtensionsTests.cs @@ -6,7 +6,6 @@ public class NestPhaseExtensionsTests [InlineData(NestPhase.Linear, "Trying rotations...")] [InlineData(NestPhase.RectBestFit, "Trying best fit...")] [InlineData(NestPhase.Pairs, "Trying pairs...")] - [InlineData(NestPhase.Nfp, "Trying NFP...")] [InlineData(NestPhase.Extents, "Trying extents...")] [InlineData(NestPhase.Custom, "Custom")] public void DisplayName_ReturnsDescription(NestPhase phase, string expected) @@ -18,7 +17,6 @@ public class NestPhaseExtensionsTests [InlineData(NestPhase.Linear, "Linear")] [InlineData(NestPhase.RectBestFit, "BestFit")] [InlineData(NestPhase.Pairs, "Pairs")] - [InlineData(NestPhase.Nfp, "NFP")] [InlineData(NestPhase.Extents, "Extents")] [InlineData(NestPhase.Custom, "Custom")] public void ShortName_ReturnsShortLabel(NestPhase phase, string expected)