docs: revise lead-in UI spec with external/internal split and LayerType tagging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -202,5 +202,9 @@ FakesAssemblies/
|
|||||||
# Git worktrees
|
# Git worktrees
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# SQLite databases
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
# Claude Code
|
# Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
53
CLAUDE.md
53
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts on material plates to minimize waste. It imports DXF drawings, places parts onto plates using rectangle-packing algorithms, and can export nest layouts as DXF or post-process them to G-code for CNC cutting machines.
|
OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts on material plates to minimize waste. It imports DXF drawings, places parts onto plates using NFP-based (No Fit Polygon) and rectangle-packing algorithms, and can export nest layouts as DXF or post-process them to G-code for CNC cutting machines.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -14,41 +14,57 @@ This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-wind
|
|||||||
dotnet build OpenNest.sln
|
dotnet build OpenNest.sln
|
||||||
```
|
```
|
||||||
|
|
||||||
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp).
|
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp), `Microsoft.ML.OnnxRuntime` (in OpenNest.Engine for ML angle prediction), `Microsoft.EntityFrameworkCore.Sqlite` (in OpenNest.Training).
|
||||||
|
|
||||||
No test projects exist in this solution.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Five projects form a layered architecture:
|
Eight projects form a layered architecture:
|
||||||
|
|
||||||
### OpenNest.Core (class library)
|
### OpenNest.Core (class library)
|
||||||
Domain model, geometry, and CNC primitives organized into namespaces:
|
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: `Helper`, `Align`, `Sequence`, `Timing`.
|
- **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`). Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning.
|
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`). Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning.
|
||||||
- **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.
|
- **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`, and `RotatingCalipers`.
|
||||||
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
- **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`. Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding). 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.
|
||||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||||
|
|
||||||
### OpenNest.Engine (class library, depends on Core)
|
### OpenNest.Engine (class library, depends on Core)
|
||||||
Nesting algorithms. `NestEngine` orchestrates filling plates with parts.
|
Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine. `AutoNester` handles mixed-part NFP-based nesting with simulated annealing (not yet integrated into the registry).
|
||||||
|
|
||||||
|
- **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.
|
- **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.
|
- **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).
|
||||||
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
||||||
- `BestCombination`: Finds optimal mix of normal/rotated columns for grid fills.
|
- `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)
|
### OpenNest.IO (class library, depends on Core)
|
||||||
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
||||||
|
|
||||||
- `DxfImporter`/`DxfExporter` — DXF file import/export via ACadSharp.
|
- `DxfImporter`/`DxfExporter` — DXF file import/export via ACadSharp.
|
||||||
- `NestReader`/`NestWriter` — custom ZIP-based nest format (XML metadata + G-code programs).
|
- `NestReader`/`NestWriter` — custom ZIP-based nest format (JSON metadata + G-code programs, v2 format).
|
||||||
- `ProgramReader` — G-code text parser.
|
- `ProgramReader` — G-code text parser.
|
||||||
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
||||||
|
|
||||||
|
### OpenNest.Console (console app, depends on Core + Engine + IO)
|
||||||
|
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`).
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### OpenNest.Training (console app, depends on Core + Engine)
|
||||||
|
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
|
||||||
|
|
||||||
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
||||||
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
||||||
|
|
||||||
@@ -62,16 +78,16 @@ MCP server for Claude Code integration. Exposes nesting operations as MCP tools
|
|||||||
The UI application with MDI interface.
|
The UI application with MDI interface.
|
||||||
|
|
||||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionAddPart`, `ActionClone`, `ActionFillArea`, `ActionZoomWindow`, `ActionSetSequence`.
|
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`.
|
||||||
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
||||||
|
|
||||||
## File Format
|
## File Format
|
||||||
|
|
||||||
Nest files (`.zip`) contain:
|
Nest files (`.nest`, ZIP-based) use v2 JSON format:
|
||||||
- `info` — XML with nest metadata and plate defaults
|
- `info.json` — nest metadata and plate defaults
|
||||||
- `drawing-info` — XML with drawing metadata (name, material, quantities, colors)
|
- `drawing-info.json` — drawing metadata (name, material, quantities, colors)
|
||||||
- `plate-info` — XML with plate metadata (size, material, spacing)
|
- `plate-info.json` — plate metadata (size, material, spacing)
|
||||||
- `program-NNN` — G-code text for each drawing's cut program
|
- `program-NNN` — G-code text for each drawing's cut program
|
||||||
- `plate-NNN` — G-code text encoding part placements (G00 for position, G65 for sub-program call with rotation)
|
- `plate-NNN` — G-code text encoding part placements (G00 for position, G65 for sub-program call with rotation)
|
||||||
|
|
||||||
@@ -89,3 +105,6 @@ Always use Roslyn Bridge MCP tools (`mcp__RoslynBridge__*`) as the primary metho
|
|||||||
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
|
- `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).
|
- 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.
|
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
|
||||||
|
- Nesting uses async progress/cancellation: `IProgress<NestProgress>` and `CancellationToken` flow through the engine to the UI's `NestProgressForm`.
|
||||||
|
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||||
|
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
||||||
|
|||||||
@@ -3,246 +3,426 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using OpenNest;
|
using OpenNest;
|
||||||
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
|
|
||||||
// Parse arguments.
|
return NestConsole.Run(args);
|
||||||
var nestFile = (string)null;
|
|
||||||
var drawingName = (string)null;
|
|
||||||
var plateIndex = 0;
|
|
||||||
var outputFile = (string)null;
|
|
||||||
var quantity = 0;
|
|
||||||
var spacing = (double?)null;
|
|
||||||
var plateWidth = (double?)null;
|
|
||||||
var plateHeight = (double?)null;
|
|
||||||
var checkOverlaps = false;
|
|
||||||
var noSave = false;
|
|
||||||
var noLog = false;
|
|
||||||
var keepParts = false;
|
|
||||||
var autoNest = false;
|
|
||||||
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
static class NestConsole
|
||||||
{
|
{
|
||||||
switch (args[i])
|
public static int Run(string[] args)
|
||||||
{
|
{
|
||||||
case "--drawing" when i + 1 < args.Length:
|
var options = ParseArgs(args);
|
||||||
drawingName = args[++i];
|
|
||||||
break;
|
if (options == null)
|
||||||
case "--plate" when i + 1 < args.Length:
|
return 0; // --help was requested
|
||||||
plateIndex = int.Parse(args[++i]);
|
|
||||||
break;
|
if (options.InputFiles.Count == 0)
|
||||||
case "--output" when i + 1 < args.Length:
|
{
|
||||||
outputFile = args[++i];
|
|
||||||
break;
|
|
||||||
case "--quantity" when i + 1 < args.Length:
|
|
||||||
quantity = int.Parse(args[++i]);
|
|
||||||
break;
|
|
||||||
case "--spacing" when i + 1 < args.Length:
|
|
||||||
spacing = double.Parse(args[++i]);
|
|
||||||
break;
|
|
||||||
case "--size" when i + 1 < args.Length:
|
|
||||||
var parts = args[++i].Split('x');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
plateWidth = double.Parse(parts[0]);
|
|
||||||
plateHeight = double.Parse(parts[1]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "--check-overlaps":
|
|
||||||
checkOverlaps = true;
|
|
||||||
break;
|
|
||||||
case "--no-save":
|
|
||||||
noSave = true;
|
|
||||||
break;
|
|
||||||
case "--no-log":
|
|
||||||
noLog = true;
|
|
||||||
break;
|
|
||||||
case "--keep-parts":
|
|
||||||
keepParts = true;
|
|
||||||
break;
|
|
||||||
case "--autonest":
|
|
||||||
autoNest = true;
|
|
||||||
break;
|
|
||||||
case "--help":
|
|
||||||
case "-h":
|
|
||||||
PrintUsage();
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var f in options.InputFiles)
|
||||||
|
{
|
||||||
|
if (!File.Exists(f))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: file not found: {f}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var log = SetUpLog(options);
|
||||||
|
var nest = LoadOrCreateNest(options);
|
||||||
|
|
||||||
|
if (nest == null)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
var plate = nest.Plates[options.PlateIndex];
|
||||||
|
|
||||||
|
ApplyTemplate(plate, options);
|
||||||
|
ApplyOverrides(plate, options);
|
||||||
|
|
||||||
|
var drawing = ResolveDrawing(nest, options);
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
var existingCount = plate.Parts.Count;
|
||||||
|
|
||||||
|
if (!options.KeepParts)
|
||||||
|
plate.Parts.Clear();
|
||||||
|
|
||||||
|
PrintHeader(nest, plate, drawing, existingCount, options);
|
||||||
|
|
||||||
|
var (success, elapsed) = Fill(nest, plate, drawing, options);
|
||||||
|
|
||||||
|
var overlapCount = CheckOverlaps(plate, options);
|
||||||
|
|
||||||
|
// Flush and close the log before printing results.
|
||||||
|
Trace.Flush();
|
||||||
|
log?.Dispose();
|
||||||
|
|
||||||
|
PrintResults(success, plate, elapsed);
|
||||||
|
Save(nest, options);
|
||||||
|
|
||||||
|
return options.CheckOverlaps && overlapCount > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Options ParseArgs(string[] args)
|
||||||
|
{
|
||||||
|
var o = new Options();
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--drawing" when i + 1 < args.Length:
|
||||||
|
o.DrawingName = args[++i];
|
||||||
|
break;
|
||||||
|
case "--plate" when i + 1 < args.Length:
|
||||||
|
o.PlateIndex = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--output" when i + 1 < args.Length:
|
||||||
|
o.OutputFile = args[++i];
|
||||||
|
break;
|
||||||
|
case "--quantity" when i + 1 < args.Length:
|
||||||
|
o.Quantity = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--spacing" when i + 1 < args.Length:
|
||||||
|
o.Spacing = double.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--size" when i + 1 < args.Length:
|
||||||
|
var parts = args[++i].Split('x');
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
o.PlateHeight = double.Parse(parts[0]);
|
||||||
|
o.PlateWidth = double.Parse(parts[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "--check-overlaps":
|
||||||
|
o.CheckOverlaps = true;
|
||||||
|
break;
|
||||||
|
case "--no-save":
|
||||||
|
o.NoSave = true;
|
||||||
|
break;
|
||||||
|
case "--no-log":
|
||||||
|
o.NoLog = true;
|
||||||
|
break;
|
||||||
|
case "--keep-parts":
|
||||||
|
o.KeepParts = true;
|
||||||
|
break;
|
||||||
|
case "--template" when i + 1 < args.Length:
|
||||||
|
o.TemplateFile = args[++i];
|
||||||
|
break;
|
||||||
|
case "--autonest":
|
||||||
|
o.AutoNest = true;
|
||||||
|
break;
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
PrintUsage();
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
if (!args[i].StartsWith("--"))
|
||||||
|
o.InputFiles.Add(args[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
static StreamWriter SetUpLog(Options options)
|
||||||
|
{
|
||||||
|
if (options.NoLog)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var baseDir = Path.GetDirectoryName(options.InputFiles[0]);
|
||||||
|
var logDir = Path.Combine(baseDir, "test-harness-logs");
|
||||||
|
Directory.CreateDirectory(logDir);
|
||||||
|
var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
||||||
|
var writer = new StreamWriter(logFile) { AutoFlush = true };
|
||||||
|
Trace.Listeners.Add(new TextWriterTraceListener(writer));
|
||||||
|
Console.WriteLine($"Debug log: {logFile}");
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Nest LoadOrCreateNest(Options options)
|
||||||
|
{
|
||||||
|
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
||||||
|
f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var dxfFiles = options.InputFiles.Where(f =>
|
||||||
|
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
// If we have a nest file, load it and optionally add DXFs.
|
||||||
|
if (nestFile != null)
|
||||||
|
{
|
||||||
|
var nest = new NestReader(nestFile).Read();
|
||||||
|
|
||||||
|
if (nest.Plates.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: nest file contains no plates");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.PlateIndex >= nest.Plates.Count)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dxf in dxfFiles)
|
||||||
|
{
|
||||||
|
var drawing = ImportDxf(dxf);
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
Console.WriteLine($"Imported: {drawing.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DXF-only mode: create a fresh nest.
|
||||||
|
if (dxfFiles.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: no nest (.zip) or DXF (.dxf) files specified");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.PlateWidth.HasValue || !options.PlateHeight.HasValue)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: --size WxH is required when importing DXF files without a nest");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newNest = new Nest { Name = "DXF Import" };
|
||||||
|
var plate = new Plate { Size = new Size(options.PlateWidth.Value, options.PlateHeight.Value) };
|
||||||
|
newNest.Plates.Add(plate);
|
||||||
|
|
||||||
|
foreach (var dxf in dxfFiles)
|
||||||
|
{
|
||||||
|
var drawing = ImportDxf(dxf);
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
newNest.Drawings.Add(drawing);
|
||||||
|
Console.WriteLine($"Imported: {drawing.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Drawing ImportDxf(string path)
|
||||||
|
{
|
||||||
|
var importer = new DxfImporter();
|
||||||
|
|
||||||
|
if (!importer.GetGeometry(path, out var geometry))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: failed to read DXF file: {path}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geometry.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: no geometry found in DXF file: {path}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||||
|
|
||||||
|
if (pgm == null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: failed to convert geometry: {path}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
return new Drawing(name, pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyTemplate(Plate plate, Options options)
|
||||||
|
{
|
||||||
|
if (options.TemplateFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!File.Exists(options.TemplateFile))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: Template not found: {options.TemplateFile}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var templatePlate = new NestReader(options.TemplateFile).Read().PlateDefaults.CreateNew();
|
||||||
|
plate.Thickness = templatePlate.Thickness;
|
||||||
|
plate.Quadrant = templatePlate.Quadrant;
|
||||||
|
plate.Material = templatePlate.Material;
|
||||||
|
plate.EdgeSpacing = templatePlate.EdgeSpacing;
|
||||||
|
plate.PartSpacing = templatePlate.PartSpacing;
|
||||||
|
Console.WriteLine($"Template: {options.TemplateFile}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ApplyOverrides(Plate plate, Options options)
|
||||||
|
{
|
||||||
|
if (options.Spacing.HasValue)
|
||||||
|
plate.PartSpacing = options.Spacing.Value;
|
||||||
|
|
||||||
|
// Only apply size override when it wasn't already used to create the plate.
|
||||||
|
var hasDxfOnly = !options.InputFiles.Any(f => f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (options.PlateWidth.HasValue && options.PlateHeight.HasValue && !hasDxfOnly)
|
||||||
|
plate.Size = new Size(options.PlateWidth.Value, options.PlateHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Drawing ResolveDrawing(Nest nest, Options options)
|
||||||
|
{
|
||||||
|
var drawing = options.DrawingName != null
|
||||||
|
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
|
||||||
|
: nest.Drawings.FirstOrDefault();
|
||||||
|
|
||||||
|
if (drawing != null)
|
||||||
|
return drawing;
|
||||||
|
|
||||||
|
Console.Error.WriteLine(options.DrawingName != null
|
||||||
|
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
||||||
|
: "Error: nest file contains no drawings");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Nest: {nest.Name}");
|
||||||
|
var wa = plate.WorkArea();
|
||||||
|
Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Length:F1} x {plate.Size.Width:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Length:F1}x{wa.Width:F1}");
|
||||||
|
Console.WriteLine($"Drawing: {drawing.Name}");
|
||||||
|
Console.WriteLine(options.KeepParts
|
||||||
|
? $"Keeping {existingCount} existing parts"
|
||||||
|
: $"Cleared {existingCount} existing parts");
|
||||||
|
Console.WriteLine("---");
|
||||||
|
}
|
||||||
|
|
||||||
|
static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
bool success;
|
||||||
|
|
||||||
|
if (options.AutoNest)
|
||||||
|
{
|
||||||
|
var nestItems = new List<NestItem>();
|
||||||
|
var qty = options.Quantity > 0 ? options.Quantity : 1;
|
||||||
|
|
||||||
|
if (options.DrawingName != null)
|
||||||
|
{
|
||||||
|
nestItems.Add(new NestItem { Drawing = drawing, Quantity = qty });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var d in nest.Drawings)
|
||||||
|
nestItems.Add(new NestItem { Drawing = d, Quantity = qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
||||||
|
|
||||||
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
var nestParts = engine.Nest(nestItems, null, CancellationToken.None);
|
||||||
|
plate.Parts.AddRange(nestParts);
|
||||||
|
success = nestParts.Count > 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
var item = new NestItem { Drawing = drawing, Quantity = options.Quantity };
|
||||||
|
success = engine.Fill(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
return (success, sw.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int CheckOverlaps(Plate plate, Options options)
|
||||||
|
{
|
||||||
|
if (!options.CheckOverlaps || plate.Parts.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
default:
|
|
||||||
if (!args[i].StartsWith("--") && nestFile == null)
|
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
||||||
nestFile = args[i];
|
Console.WriteLine(hasOverlaps
|
||||||
break;
|
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
||||||
|
: "Overlap check: PASS");
|
||||||
|
|
||||||
|
return overlapPts.Count;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(nestFile) || !File.Exists(nestFile))
|
static void PrintResults(bool success, Plate plate, long elapsedMs)
|
||||||
{
|
|
||||||
PrintUsage();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up debug log file.
|
|
||||||
StreamWriter logWriter = null;
|
|
||||||
|
|
||||||
if (!noLog)
|
|
||||||
{
|
|
||||||
var logDir = Path.Combine(Path.GetDirectoryName(nestFile), "test-harness-logs");
|
|
||||||
Directory.CreateDirectory(logDir);
|
|
||||||
var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
|
||||||
logWriter = new StreamWriter(logFile) { AutoFlush = true };
|
|
||||||
Trace.Listeners.Add(new TextWriterTraceListener(logWriter));
|
|
||||||
Console.WriteLine($"Debug log: {logFile}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load nest.
|
|
||||||
var reader = new NestReader(nestFile);
|
|
||||||
var nest = reader.Read();
|
|
||||||
|
|
||||||
if (nest.Plates.Count == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("Error: nest file contains no plates");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plateIndex >= nest.Plates.Count)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"Error: plate index {plateIndex} out of range (0-{nest.Plates.Count - 1})");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var plate = nest.Plates[plateIndex];
|
|
||||||
|
|
||||||
// Apply overrides.
|
|
||||||
if (spacing.HasValue)
|
|
||||||
plate.PartSpacing = spacing.Value;
|
|
||||||
|
|
||||||
if (plateWidth.HasValue && plateHeight.HasValue)
|
|
||||||
plate.Size = new Size(plateWidth.Value, plateHeight.Value);
|
|
||||||
|
|
||||||
// Find drawing.
|
|
||||||
var drawing = drawingName != null
|
|
||||||
? nest.Drawings.FirstOrDefault(d => d.Name == drawingName)
|
|
||||||
: nest.Drawings.FirstOrDefault();
|
|
||||||
|
|
||||||
if (drawing == null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(drawingName != null
|
|
||||||
? $"Error: drawing '{drawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
|
||||||
: "Error: nest file contains no drawings");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing parts.
|
|
||||||
var existingCount = plate.Parts.Count;
|
|
||||||
|
|
||||||
if (!keepParts)
|
|
||||||
plate.Parts.Clear();
|
|
||||||
|
|
||||||
Console.WriteLine($"Nest: {nest.Name}");
|
|
||||||
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}");
|
|
||||||
Console.WriteLine($"Drawing: {drawing.Name}");
|
|
||||||
|
|
||||||
if (!keepParts)
|
|
||||||
Console.WriteLine($"Cleared {existingCount} existing parts");
|
|
||||||
else
|
|
||||||
Console.WriteLine($"Keeping {existingCount} existing parts");
|
|
||||||
|
|
||||||
Console.WriteLine("---");
|
|
||||||
|
|
||||||
// Run fill or autonest.
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
bool success;
|
|
||||||
|
|
||||||
if (autoNest)
|
|
||||||
{
|
|
||||||
// AutoNest: use all drawings (or specific drawing if --drawing given).
|
|
||||||
var nestItems = new List<NestItem>();
|
|
||||||
|
|
||||||
if (drawingName != null)
|
|
||||||
{
|
{
|
||||||
nestItems.Add(new NestItem { Drawing = drawing, Quantity = quantity > 0 ? quantity : 1 });
|
Console.WriteLine($"Result: {(success ? "success" : "failed")}");
|
||||||
|
Console.WriteLine($"Parts placed: {plate.Parts.Count}");
|
||||||
|
Console.WriteLine($"Utilization: {plate.Utilization():P1}");
|
||||||
|
Console.WriteLine($"Time: {elapsedMs}ms");
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
static void Save(Nest nest, Options options)
|
||||||
{
|
{
|
||||||
foreach (var d in nest.Drawings)
|
if (options.NoSave)
|
||||||
nestItems.Add(new NestItem { Drawing = d, Quantity = quantity > 0 ? quantity : 1 });
|
return;
|
||||||
|
|
||||||
|
var firstInput = options.InputFiles[0];
|
||||||
|
var outputFile = options.OutputFile ?? Path.Combine(
|
||||||
|
Path.GetDirectoryName(firstInput),
|
||||||
|
$"{Path.GetFileNameWithoutExtension(firstInput)}-result.zip");
|
||||||
|
|
||||||
|
new NestWriter(nest).Write(outputFile);
|
||||||
|
Console.WriteLine($"Saved: {outputFile}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
static void PrintUsage()
|
||||||
|
|
||||||
var parts = NestEngine.AutoNest(nestItems, plate);
|
|
||||||
plate.Parts.AddRange(parts);
|
|
||||||
success = parts.Count > 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var engine = new NestEngine(plate);
|
|
||||||
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
|
||||||
success = engine.Fill(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
sw.Stop();
|
|
||||||
|
|
||||||
// Check overlaps.
|
|
||||||
var overlapCount = 0;
|
|
||||||
|
|
||||||
if (checkOverlaps && plate.Parts.Count > 0)
|
|
||||||
{
|
|
||||||
List<Vector> overlapPts;
|
|
||||||
var hasOverlaps = plate.HasOverlappingParts(out overlapPts);
|
|
||||||
overlapCount = overlapPts.Count;
|
|
||||||
|
|
||||||
if (hasOverlaps)
|
|
||||||
Console.WriteLine($"OVERLAPS DETECTED: {overlapCount} intersection points");
|
|
||||||
else
|
|
||||||
Console.WriteLine("Overlap check: PASS");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush and close the log.
|
|
||||||
Trace.Flush();
|
|
||||||
logWriter?.Dispose();
|
|
||||||
|
|
||||||
// Print results.
|
|
||||||
Console.WriteLine($"Result: {(success ? "success" : "failed")}");
|
|
||||||
Console.WriteLine($"Parts placed: {plate.Parts.Count}");
|
|
||||||
Console.WriteLine($"Utilization: {plate.Utilization():P1}");
|
|
||||||
Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");
|
|
||||||
|
|
||||||
// Save output.
|
|
||||||
if (!noSave)
|
|
||||||
{
|
|
||||||
if (outputFile == null)
|
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(nestFile);
|
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
||||||
var name = Path.GetFileNameWithoutExtension(nestFile);
|
Console.Error.WriteLine();
|
||||||
outputFile = Path.Combine(dir, $"{name}-result.zip");
|
Console.Error.WriteLine("Arguments:");
|
||||||
|
Console.Error.WriteLine(" input-files One or more .zip nest files or .dxf drawing files");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Modes:");
|
||||||
|
Console.Error.WriteLine(" <nest.zip> Load nest and fill (existing behavior)");
|
||||||
|
Console.Error.WriteLine(" <part.dxf> --size LxW Import DXF, create plate, and fill");
|
||||||
|
Console.Error.WriteLine(" <nest.zip> <part.dxf> Load nest and add imported DXF drawings");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Options:");
|
||||||
|
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
||||||
|
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
||||||
|
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
||||||
|
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
||||||
|
Console.Error.WriteLine(" --size <LxW> Override plate size (e.g. 120x60); required for DXF-only mode");
|
||||||
|
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.zip)");
|
||||||
|
Console.Error.WriteLine(" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)");
|
||||||
|
Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
|
||||||
|
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
||||||
|
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
||||||
|
Console.Error.WriteLine(" --no-save Skip saving output file");
|
||||||
|
Console.Error.WriteLine(" --no-log Skip writing debug log file");
|
||||||
|
Console.Error.WriteLine(" -h, --help Show this help");
|
||||||
}
|
}
|
||||||
|
|
||||||
var writer = new NestWriter(nest);
|
class Options
|
||||||
writer.Write(outputFile);
|
{
|
||||||
Console.WriteLine($"Saved: {outputFile}");
|
public List<string> InputFiles = new();
|
||||||
}
|
public string DrawingName;
|
||||||
|
public int PlateIndex;
|
||||||
return checkOverlaps && overlapCount > 0 ? 1 : 0;
|
public string OutputFile;
|
||||||
|
public int Quantity;
|
||||||
void PrintUsage()
|
public double? Spacing;
|
||||||
{
|
public double? PlateWidth;
|
||||||
Console.Error.WriteLine("Usage: OpenNest.Console <nest-file> [options]");
|
public double? PlateHeight;
|
||||||
Console.Error.WriteLine();
|
public bool CheckOverlaps;
|
||||||
Console.Error.WriteLine("Arguments:");
|
public bool NoSave;
|
||||||
Console.Error.WriteLine(" nest-file Path to a .zip nest file");
|
public bool NoLog;
|
||||||
Console.Error.WriteLine();
|
public bool KeepParts;
|
||||||
Console.Error.WriteLine("Options:");
|
public bool AutoNest;
|
||||||
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
public string TemplateFile;
|
||||||
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
}
|
||||||
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
|
||||||
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
|
||||||
Console.Error.WriteLine(" --size <WxH> Override plate size (e.g. 120x60)");
|
|
||||||
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.zip)");
|
|
||||||
Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
|
|
||||||
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
|
||||||
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
|
||||||
Console.Error.WriteLine(" --no-save Skip saving output file");
|
|
||||||
Console.Error.WriteLine(" --no-log Skip writing debug log file");
|
|
||||||
Console.Error.WriteLine(" -h, --help Show this help");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public CuttingParameters Parameters { get; set; }
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
public Program Apply(Program partProgram, Plate plate)
|
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||||
{
|
{
|
||||||
var exitPoint = GetExitPoint(plate);
|
var exitPoint = approachPoint;
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
var profile = new ShapeProfile(entities);
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
@@ -44,9 +44,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
currentPoint = closestPt;
|
currentPoint = closestPt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastCutPoint = exitPoint;
|
||||||
|
|
||||||
// Perimeter last
|
// Perimeter last
|
||||||
{
|
{
|
||||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
||||||
|
lastCutPoint = perimeterPt;
|
||||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
||||||
var winding = DetermineWinding(profile.Perimeter);
|
var winding = DetermineWinding(profile.Perimeter);
|
||||||
|
|
||||||
@@ -60,21 +63,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return new CuttingResult
|
||||||
}
|
|
||||||
|
|
||||||
private Vector GetExitPoint(Plate plate)
|
|
||||||
{
|
|
||||||
var w = plate.Size.Width;
|
|
||||||
var l = plate.Size.Length;
|
|
||||||
|
|
||||||
return plate.Quadrant switch
|
|
||||||
{
|
{
|
||||||
1 => new Vector(w, l), // Q1 origin BottomLeft -> exit TopRight
|
Program = result,
|
||||||
2 => new Vector(0, l), // Q2 origin BottomRight -> exit TopLeft
|
LastCutPoint = lastCutPoint
|
||||||
3 => new Vector(0, 0), // Q3 origin TopRight -> exit BottomLeft
|
|
||||||
4 => new Vector(w, 0), // Q4 origin TopLeft -> exit BottomRight
|
|
||||||
_ => new Vector(w, l)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs
Normal file
11
OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public readonly struct CuttingResult
|
||||||
|
{
|
||||||
|
public Program Program { get; init; }
|
||||||
|
public Vector LastCutPoint { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
@@ -84,6 +84,23 @@ namespace OpenNest.CNC
|
|||||||
Rotation = Angle.NormalizeRad(Rotation + angle);
|
Rotation = Angle.NormalizeRad(Rotation + angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine(mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
foreach (var code in Codes)
|
||||||
|
{
|
||||||
|
if (code is Motion m)
|
||||||
|
{
|
||||||
|
var cmd = m is RapidMove ? "G00" : (m is ArcMove am ? (am.Rotation == RotationType.CW ? "G02" : "G03") : "G01");
|
||||||
|
sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}");
|
||||||
|
if (m is ArcMove arc) sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void Rotate(double angle, Vector origin)
|
public virtual void Rotate(double angle, Vector origin)
|
||||||
{
|
{
|
||||||
var mode = Mode;
|
var mode = Mode;
|
||||||
@@ -99,7 +116,7 @@ namespace OpenNest.CNC
|
|||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
|
|
||||||
if (subpgm.Program != null)
|
if (subpgm.Program != null)
|
||||||
subpgm.Program.Rotate(angle);
|
subpgm.Program.Rotate(angle, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace OpenNest.Converters
|
|||||||
{
|
{
|
||||||
public static Program ToProgram(IList<Entity> geometry)
|
public static Program ToProgram(IList<Entity> geometry)
|
||||||
{
|
{
|
||||||
var shapes = Helper.GetShapes(geometry);
|
var shapes = ShapeBuilder.GetShapes(geometry);
|
||||||
|
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace OpenNest
|
|||||||
public void UpdateArea()
|
public void UpdateArea()
|
||||||
{
|
{
|
||||||
var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid);
|
var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(geometry);
|
var shapes = ShapeBuilder.GetShapes(geometry);
|
||||||
|
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, arc, out pts);
|
return Intersect.Intersects(this, arc, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -476,7 +476,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, arc, out pts); ;
|
return Intersect.Intersects(this, arc, out pts); ;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -487,7 +487,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, circle, out pts);
|
return Intersect.Intersects(this, circle, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -498,7 +498,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, circle, out pts);
|
return Intersect.Intersects(this, circle, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -509,7 +509,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -520,7 +520,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -531,7 +531,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -542,7 +542,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -553,7 +553,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -564,7 +564,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -331,7 +331,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -353,7 +353,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, circle, out pts);
|
return Intersect.Intersects(this, circle, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -364,7 +364,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -375,7 +375,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -386,7 +386,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -397,7 +397,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -408,7 +408,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -419,7 +419,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
202
OpenNest.Core/Geometry/GeometryOptimizer.cs
Normal file
202
OpenNest.Core/Geometry/GeometryOptimizer.cs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class GeometryOptimizer
|
||||||
|
{
|
||||||
|
public static void Optimize(IList<Arc> arcs)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < arcs.Count; ++i)
|
||||||
|
{
|
||||||
|
var arc = arcs[i];
|
||||||
|
|
||||||
|
var coradialArcs = arcs.GetCoradialArs(arc, i);
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
while (index < coradialArcs.Count)
|
||||||
|
{
|
||||||
|
Arc arc2 = coradialArcs[index];
|
||||||
|
Arc joinArc;
|
||||||
|
|
||||||
|
if (!TryJoinArcs(arc, arc2, out joinArc))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
coradialArcs.Remove(arc2);
|
||||||
|
arcs.Remove(arc2);
|
||||||
|
|
||||||
|
arc = joinArc;
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
arcs[i] = arc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Optimize(IList<Line> lines)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < lines.Count; ++i)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
|
||||||
|
var collinearLines = lines.GetCollinearLines(line, i);
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
while (index < collinearLines.Count)
|
||||||
|
{
|
||||||
|
Line line2 = collinearLines[index];
|
||||||
|
Line joinLine;
|
||||||
|
|
||||||
|
if (!TryJoinLines(line, line2, out joinLine))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
collinearLines.Remove(line2);
|
||||||
|
lines.Remove(line2);
|
||||||
|
|
||||||
|
line = joinLine;
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines[i] = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryJoinLines(Line line1, Line line2, out Line lineOut)
|
||||||
|
{
|
||||||
|
lineOut = null;
|
||||||
|
|
||||||
|
if (line1 == line2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!line1.IsCollinearTo(line2))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool onPoint = false;
|
||||||
|
|
||||||
|
if (line1.StartPoint == line2.StartPoint)
|
||||||
|
onPoint = true;
|
||||||
|
else if (line1.StartPoint == line2.EndPoint)
|
||||||
|
onPoint = true;
|
||||||
|
else if (line1.EndPoint == line2.StartPoint)
|
||||||
|
onPoint = true;
|
||||||
|
else if (line1.EndPoint == line2.EndPoint)
|
||||||
|
onPoint = true;
|
||||||
|
|
||||||
|
var t1 = line1.StartPoint.Y > line1.EndPoint.Y ? line1.StartPoint.Y : line1.EndPoint.Y;
|
||||||
|
var t2 = line2.StartPoint.Y > line2.EndPoint.Y ? line2.StartPoint.Y : line2.EndPoint.Y;
|
||||||
|
var b1 = line1.StartPoint.Y < line1.EndPoint.Y ? line1.StartPoint.Y : line1.EndPoint.Y;
|
||||||
|
var b2 = line2.StartPoint.Y < line2.EndPoint.Y ? line2.StartPoint.Y : line2.EndPoint.Y;
|
||||||
|
var l1 = line1.StartPoint.X < line1.EndPoint.X ? line1.StartPoint.X : line1.EndPoint.X;
|
||||||
|
var l2 = line2.StartPoint.X < line2.EndPoint.X ? line2.StartPoint.X : line2.EndPoint.X;
|
||||||
|
var r1 = line1.StartPoint.X > line1.EndPoint.X ? line1.StartPoint.X : line1.EndPoint.X;
|
||||||
|
var r2 = line2.StartPoint.X > line2.EndPoint.X ? line2.StartPoint.X : line2.EndPoint.X;
|
||||||
|
|
||||||
|
if (!onPoint)
|
||||||
|
{
|
||||||
|
if (t1 < b2 - Tolerance.Epsilon) return false;
|
||||||
|
if (b1 > t2 + Tolerance.Epsilon) return false;
|
||||||
|
if (l1 > r2 + Tolerance.Epsilon) return false;
|
||||||
|
if (r1 < l2 - Tolerance.Epsilon) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var l = l1 < l2 ? l1 : l2;
|
||||||
|
var r = r1 > r2 ? r1 : r2;
|
||||||
|
var t = t1 > t2 ? t1 : t2;
|
||||||
|
var b = b1 < b2 ? b1 : b2;
|
||||||
|
|
||||||
|
if (!line1.IsVertical() && line1.Slope() < 0)
|
||||||
|
lineOut = new Line(new Vector(l, t), new Vector(r, b));
|
||||||
|
else
|
||||||
|
lineOut = new Line(new Vector(l, b), new Vector(r, t));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryJoinArcs(Arc arc1, Arc arc2, out Arc arcOut)
|
||||||
|
{
|
||||||
|
arcOut = null;
|
||||||
|
|
||||||
|
if (arc1 == arc2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (arc1.Center != arc2.Center)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!arc1.Radius.IsEqualTo(arc2.Radius))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (arc1.StartAngle > arc1.EndAngle)
|
||||||
|
arc1.StartAngle -= Angle.TwoPI;
|
||||||
|
|
||||||
|
if (arc2.StartAngle > arc2.EndAngle)
|
||||||
|
arc2.StartAngle -= Angle.TwoPI;
|
||||||
|
|
||||||
|
if (arc1.EndAngle < arc2.StartAngle || arc1.StartAngle > arc2.EndAngle)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var startAngle = arc1.StartAngle < arc2.StartAngle ? arc1.StartAngle : arc2.StartAngle;
|
||||||
|
var endAngle = arc1.EndAngle > arc2.EndAngle ? arc1.EndAngle : arc2.EndAngle;
|
||||||
|
|
||||||
|
if (startAngle < 0) startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0) endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
|
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex)
|
||||||
|
{
|
||||||
|
var collinearLines = new List<Line>();
|
||||||
|
|
||||||
|
Parallel.For(startIndex, lines.Count, index =>
|
||||||
|
{
|
||||||
|
var compareLine = lines[index];
|
||||||
|
|
||||||
|
if (Object.ReferenceEquals(line, compareLine))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!line.IsCollinearTo(compareLine))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (collinearLines)
|
||||||
|
{
|
||||||
|
collinearLines.Add(compareLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return collinearLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Arc> GetCoradialArs(this IList<Arc> arcs, Arc arc, int startIndex)
|
||||||
|
{
|
||||||
|
var coradialArcs = new List<Arc>();
|
||||||
|
|
||||||
|
Parallel.For(startIndex, arcs.Count, index =>
|
||||||
|
{
|
||||||
|
var compareArc = arcs[index];
|
||||||
|
|
||||||
|
if (Object.ReferenceEquals(arc, compareArc))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!arc.IsCoradialTo(compareArc))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (coradialArcs)
|
||||||
|
{
|
||||||
|
coradialArcs.Add(compareArc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return coradialArcs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
373
OpenNest.Core/Geometry/Intersect.cs
Normal file
373
OpenNest.Core/Geometry/Intersect.cs
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class Intersect
|
||||||
|
{
|
||||||
|
internal static bool Intersects(Arc arc1, Arc arc2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var c1 = new Circle(arc1.Center, arc1.Radius);
|
||||||
|
var c2 = new Circle(arc2.Center, arc2.Radius);
|
||||||
|
|
||||||
|
if (!Intersects(c1, c2, out pts))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts.Where(pt =>
|
||||||
|
Angle.IsBetweenRad(arc1.Center.AngleTo(pt), arc1.StartAngle, arc1.EndAngle, arc1.IsReversed) &&
|
||||||
|
Angle.IsBetweenRad(arc2.Center.AngleTo(pt), arc2.StartAngle, arc2.EndAngle, arc2.IsReversed))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Circle circle, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var c1 = new Circle(arc.Center, arc.Radius);
|
||||||
|
|
||||||
|
if (!Intersects(c1, circle, out pts))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Line line, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var c1 = new Circle(arc.Center, arc.Radius);
|
||||||
|
|
||||||
|
if (!Intersects(c1, line, out pts))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Shape shape, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var pts2 = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var geo in shape.Entities)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
geo.Intersects(arc, out pts3);
|
||||||
|
pts2.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts2.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var pts2 = new List<Vector>();
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
Intersects(arc, line, out pts3);
|
||||||
|
pts2.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts2.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle1, Circle circle2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var distance = circle1.Center.DistanceTo(circle2.Center);
|
||||||
|
|
||||||
|
// check if circles are too far apart
|
||||||
|
if (distance > circle1.Radius + circle2.Radius)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if one circle contains the other
|
||||||
|
if (distance < System.Math.Abs(circle1.Radius - circle2.Radius))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = circle2.Center - circle1.Center;
|
||||||
|
var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance);
|
||||||
|
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
|
||||||
|
|
||||||
|
var pt = new Vector(
|
||||||
|
circle1.Center.X + (a * d.X) / distance,
|
||||||
|
circle1.Center.Y + (a * d.Y) / distance);
|
||||||
|
|
||||||
|
var i1 = new Vector(
|
||||||
|
pt.X + (h * d.Y) / distance,
|
||||||
|
pt.Y - (h * d.X) / distance);
|
||||||
|
|
||||||
|
var i2 = new Vector(
|
||||||
|
pt.X - (h * d.Y) / distance,
|
||||||
|
pt.Y + (h * d.X) / distance);
|
||||||
|
|
||||||
|
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle, Line line, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var d1 = line.EndPoint - line.StartPoint;
|
||||||
|
var d2 = line.StartPoint - circle.Center;
|
||||||
|
|
||||||
|
var a = d1.X * d1.X + d1.Y * d1.Y;
|
||||||
|
var b = (d1.X * d2.X + d1.Y * d2.Y) * 2;
|
||||||
|
var c = (d2.X * d2.X + d2.Y * d2.Y) - circle.Radius * circle.Radius;
|
||||||
|
|
||||||
|
var det = b * b - 4 * a * c;
|
||||||
|
|
||||||
|
if ((a <= Tolerance.Epsilon) || (det < 0))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double t;
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
if (det.IsEqualTo(0))
|
||||||
|
{
|
||||||
|
t = -b / (2 * a);
|
||||||
|
var pt1 = new Vector(line.StartPoint.X + t * d1.X, line.StartPoint.Y + t * d1.Y);
|
||||||
|
|
||||||
|
if (line.BoundingBox.Contains(pt1))
|
||||||
|
pts.Add(pt1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
t = (-b + System.Math.Sqrt(det)) / (2 * a);
|
||||||
|
var pt2 = new Vector(line.StartPoint.X + t * d1.X, line.StartPoint.Y + t * d1.Y);
|
||||||
|
|
||||||
|
if (line.BoundingBox.Contains(pt2))
|
||||||
|
pts.Add(pt2);
|
||||||
|
|
||||||
|
t = (-b - System.Math.Sqrt(det)) / (2 * a);
|
||||||
|
var pt3 = new Vector(line.StartPoint.X + t * d1.X, line.StartPoint.Y + t * d1.Y);
|
||||||
|
|
||||||
|
if (line.BoundingBox.Contains(pt3))
|
||||||
|
pts.Add(pt3);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle, Shape shape, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var geo in shape.Entities)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
geo.Intersects(circle, out pts3);
|
||||||
|
pts.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
Intersects(circle, line, out pts3);
|
||||||
|
pts.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Line line1, Line line2, out Vector pt)
|
||||||
|
{
|
||||||
|
var a1 = line1.EndPoint.Y - line1.StartPoint.Y;
|
||||||
|
var b1 = line1.StartPoint.X - line1.EndPoint.X;
|
||||||
|
var c1 = a1 * line1.StartPoint.X + b1 * line1.StartPoint.Y;
|
||||||
|
|
||||||
|
var a2 = line2.EndPoint.Y - line2.StartPoint.Y;
|
||||||
|
var b2 = line2.StartPoint.X - line2.EndPoint.X;
|
||||||
|
var c2 = a2 * line2.StartPoint.X + b2 * line2.StartPoint.Y;
|
||||||
|
|
||||||
|
var d = a1 * b2 - a2 * b1;
|
||||||
|
|
||||||
|
if (d.IsEqualTo(0.0))
|
||||||
|
{
|
||||||
|
pt = Vector.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = (b2 * c1 - b1 * c2) / d;
|
||||||
|
var y = (a1 * c2 - a2 * c1) / d;
|
||||||
|
|
||||||
|
pt = new Vector(x, y);
|
||||||
|
return line1.BoundingBox.Contains(pt) && line2.BoundingBox.Contains(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Line line, Shape shape, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var geo in shape.Entities)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
geo.Intersects(line, out pts3);
|
||||||
|
pts.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Line line, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
foreach (var line2 in lines)
|
||||||
|
{
|
||||||
|
Vector pt;
|
||||||
|
|
||||||
|
if (Intersects(line, line2, out pt))
|
||||||
|
pts.Add(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Shape shape1, Shape shape2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
for (int i = 0; i < shape1.Entities.Count; i++)
|
||||||
|
{
|
||||||
|
var geo1 = shape1.Entities[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < shape2.Entities.Count; j++)
|
||||||
|
{
|
||||||
|
List<Vector> pts2;
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
var geo2 = shape2.Entities[j];
|
||||||
|
|
||||||
|
switch (geo2.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
success = geo1.Intersects((Arc)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Circle:
|
||||||
|
success = geo1.Intersects((Circle)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
success = geo1.Intersects((Line)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Shape:
|
||||||
|
success = geo1.Intersects((Shape)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Polygon:
|
||||||
|
success = geo1.Intersects((Polygon)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
pts.AddRange(pts2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Shape shape, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
for (int i = 0; i < shape.Entities.Count; i++)
|
||||||
|
{
|
||||||
|
var geo = shape.Entities[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < lines.Count; j++)
|
||||||
|
{
|
||||||
|
var line = lines[j];
|
||||||
|
|
||||||
|
List<Vector> pts2;
|
||||||
|
|
||||||
|
if (geo.Intersects(line, out pts2))
|
||||||
|
pts.AddRange(pts2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Polygon polygon1, Polygon polygon2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
var lines1 = polygon1.ToLines();
|
||||||
|
var lines2 = polygon2.ToLines();
|
||||||
|
|
||||||
|
for (int i = 0; i < lines1.Count; i++)
|
||||||
|
{
|
||||||
|
var line1 = lines1[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < lines2.Count; j++)
|
||||||
|
{
|
||||||
|
var line2 = lines2[j];
|
||||||
|
Vector pt;
|
||||||
|
|
||||||
|
if (Intersects(line1, line2, out pt))
|
||||||
|
pts.Add(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -456,7 +456,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -467,7 +467,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -478,7 +478,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -489,7 +489,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -512,7 +512,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
Vector pt;
|
Vector pt;
|
||||||
var success = Helper.Intersects(this, line, out pt);
|
var success = Intersect.Intersects(this, line, out pt);
|
||||||
pts = new List<Vector>(new[] { pt });
|
pts = new List<Vector>(new[] { pt });
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -525,7 +525,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -536,7 +536,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -547,7 +547,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -558,7 +558,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -375,7 +375,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -386,7 +386,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -397,7 +397,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -408,7 +408,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -419,7 +419,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -430,7 +430,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -441,7 +441,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -452,7 +452,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(shape, this, out pts);
|
return Intersect.Intersects(shape, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -463,7 +463,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(shape, this, out pts);
|
return Intersect.Intersects(shape, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -493,13 +493,37 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var n = Vertices.Count - 1;
|
var n = Vertices.Count - 1;
|
||||||
|
|
||||||
|
// Pre-calculate edge bounding boxes to speed up intersection checks.
|
||||||
|
var edgeBounds = new (double minX, double maxX, double minY, double maxY)[n];
|
||||||
for (var i = 0; i < n; i++)
|
for (var i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
|
var v1 = Vertices[i];
|
||||||
|
var v2 = Vertices[i + 1];
|
||||||
|
edgeBounds[i] = (
|
||||||
|
System.Math.Min(v1.X, v2.X) - Tolerance.Epsilon,
|
||||||
|
System.Math.Max(v1.X, v2.X) + Tolerance.Epsilon,
|
||||||
|
System.Math.Min(v1.Y, v2.Y) - Tolerance.Epsilon,
|
||||||
|
System.Math.Max(v1.Y, v2.Y) + Tolerance.Epsilon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
var bi = edgeBounds[i];
|
||||||
for (var j = i + 2; j < n; j++)
|
for (var j = i + 2; j < n; j++)
|
||||||
{
|
{
|
||||||
if (i == 0 && j == n - 1)
|
if (i == 0 && j == n - 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
var bj = edgeBounds[j];
|
||||||
|
|
||||||
|
// Prune with bounding box check.
|
||||||
|
if (bi.maxX < bj.minX || bj.maxX < bi.minX ||
|
||||||
|
bi.maxY < bj.minY || bj.maxY < bi.minY)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
|
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
|
||||||
{
|
{
|
||||||
edgeI = i;
|
edgeI = i;
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Helper.Optimize(lines);
|
GeometryOptimizer.Optimize(lines);
|
||||||
Helper.Optimize(arcs);
|
GeometryOptimizer.Optimize(arcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -534,7 +534,7 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
Vector intersection;
|
Vector intersection;
|
||||||
|
|
||||||
if (Helper.Intersects(offsetLine, lastOffsetLine, out intersection))
|
if (Intersect.Intersects(offsetLine, lastOffsetLine, out intersection))
|
||||||
{
|
{
|
||||||
offsetLine.StartPoint = intersection;
|
offsetLine.StartPoint = intersection;
|
||||||
lastOffsetLine.EndPoint = intersection;
|
lastOffsetLine.EndPoint = intersection;
|
||||||
@@ -577,7 +577,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -588,7 +588,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -599,7 +599,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -610,7 +610,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -621,7 +621,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -632,7 +632,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -643,7 +643,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -654,7 +654,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -665,7 +665,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -676,7 +676,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
150
OpenNest.Core/Geometry/ShapeBuilder.cs
Normal file
150
OpenNest.Core/Geometry/ShapeBuilder.cs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class ShapeBuilder
|
||||||
|
{
|
||||||
|
public static List<Shape> GetShapes(IEnumerable<Entity> entities)
|
||||||
|
{
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var arcs = new List<Arc>();
|
||||||
|
var circles = new List<Circle>();
|
||||||
|
var shapes = new List<Shape>();
|
||||||
|
|
||||||
|
var entities2 = new Queue<Entity>(entities);
|
||||||
|
|
||||||
|
while (entities2.Count > 0)
|
||||||
|
{
|
||||||
|
var entity = entities2.Dequeue();
|
||||||
|
|
||||||
|
switch (entity.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
arcs.Add((Arc)entity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Circle:
|
||||||
|
circles.Add((Circle)entity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
lines.Add((Line)entity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Shape:
|
||||||
|
var shape = (Shape)entity;
|
||||||
|
shape.Entities.ForEach(e => entities2.Enqueue(e));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Debug.Fail("Unhandled geometry type");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var circle in circles)
|
||||||
|
{
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(circle);
|
||||||
|
shape.UpdateBounds();
|
||||||
|
shapes.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityList = new List<Entity>();
|
||||||
|
|
||||||
|
entityList.AddRange(lines);
|
||||||
|
entityList.AddRange(arcs);
|
||||||
|
|
||||||
|
while (entityList.Count > 0)
|
||||||
|
{
|
||||||
|
var next = entityList[0];
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(next);
|
||||||
|
|
||||||
|
entityList.RemoveAt(0);
|
||||||
|
|
||||||
|
Vector startPoint = new Vector();
|
||||||
|
Entity connected;
|
||||||
|
|
||||||
|
switch (next.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)next;
|
||||||
|
startPoint = arc.EndPoint();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)next;
|
||||||
|
startPoint = line.EndPoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((connected = GetConnected(startPoint, entityList)) != null)
|
||||||
|
{
|
||||||
|
shape.Entities.Add(connected);
|
||||||
|
entityList.Remove(connected);
|
||||||
|
|
||||||
|
switch (connected.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)connected;
|
||||||
|
startPoint = arc.EndPoint();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)connected;
|
||||||
|
startPoint = line.EndPoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shape.UpdateBounds();
|
||||||
|
shapes.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shapes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Entity GetConnected(Vector pt, IEnumerable<Entity> geometry)
|
||||||
|
{
|
||||||
|
var tol = Tolerance.ChainTolerance;
|
||||||
|
|
||||||
|
foreach (var geo in geometry)
|
||||||
|
{
|
||||||
|
switch (geo.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)geo;
|
||||||
|
|
||||||
|
if (arc.StartPoint().DistanceTo(pt) <= tol)
|
||||||
|
return arc;
|
||||||
|
|
||||||
|
if (arc.EndPoint().DistanceTo(pt) <= tol)
|
||||||
|
{
|
||||||
|
arc.Reverse();
|
||||||
|
return arc;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)geo;
|
||||||
|
|
||||||
|
if (line.StartPoint.DistanceTo(pt) <= tol)
|
||||||
|
return line;
|
||||||
|
|
||||||
|
if (line.EndPoint.DistanceTo(pt) <= tol)
|
||||||
|
{
|
||||||
|
line.Reverse();
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
private void Update(List<Entity> entities)
|
private void Update(List<Entity> entities)
|
||||||
{
|
{
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
Perimeter = shapes[0];
|
Perimeter = shapes[0];
|
||||||
Cutouts = new List<Shape>();
|
Cutouts = new List<Shape>();
|
||||||
|
|||||||
614
OpenNest.Core/Geometry/SpatialQuery.cs
Normal file
614
OpenNest.Core/Geometry/SpatialQuery.cs
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class SpatialQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the distance from a vertex to a line segment along a push axis.
|
||||||
|
/// Returns double.MaxValue if the ray does not hit the segment.
|
||||||
|
/// </summary>
|
||||||
|
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
|
||||||
|
{
|
||||||
|
return RayEdgeDistance(
|
||||||
|
vertex.X, vertex.Y,
|
||||||
|
edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y,
|
||||||
|
direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static double RayEdgeDistance(
|
||||||
|
double vx, double vy,
|
||||||
|
double p1x, double p1y, double p2x, double p2y,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left:
|
||||||
|
case PushDirection.Right:
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PushDirection.Down:
|
||||||
|
case PushDirection.Up:
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along a push direction before
|
||||||
|
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||||
|
/// Returns double.MaxValue if no collision path exists.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
var movingVertices = new HashSet<Vector>();
|
||||||
|
for (int i = 0; i < movingLines.Count; i++)
|
||||||
|
{
|
||||||
|
movingVertices.Add(movingLines[i].pt1);
|
||||||
|
movingVertices.Add(movingLines[i].pt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
||||||
|
for (int i = 0; i < stationaryLines.Count; i++)
|
||||||
|
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
||||||
|
|
||||||
|
// Sort edges for pruning if not already sorted (usually they aren't here)
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||||
|
else
|
||||||
|
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||||
|
|
||||||
|
foreach (var mv in movingVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
|
var opposite = OppositeDirection(direction);
|
||||||
|
var stationaryVertices = new HashSet<Vector>();
|
||||||
|
for (int i = 0; i < stationaryLines.Count; i++)
|
||||||
|
{
|
||||||
|
stationaryVertices.Add(stationaryLines[i].pt1);
|
||||||
|
stationaryVertices.Add(stationaryLines[i].pt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
||||||
|
for (int i = 0; i < movingLines.Count; i++)
|
||||||
|
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
||||||
|
|
||||||
|
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
||||||
|
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||||
|
else
|
||||||
|
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||||
|
|
||||||
|
foreach (var sv in stationaryVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum directional distance with the moving lines translated
|
||||||
|
/// by (movingDx, movingDy) without creating new Line objects.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
List<Line> movingLines, double movingDx, double movingDy,
|
||||||
|
List<Line> stationaryLines, PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var movingOffset = new Vector(movingDx, movingDy);
|
||||||
|
|
||||||
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
var movingVertices = new HashSet<Vector>();
|
||||||
|
for (int i = 0; i < movingLines.Count; i++)
|
||||||
|
{
|
||||||
|
movingVertices.Add(movingLines[i].pt1 + movingOffset);
|
||||||
|
movingVertices.Add(movingLines[i].pt2 + movingOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
||||||
|
for (int i = 0; i < stationaryLines.Count; i++)
|
||||||
|
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
||||||
|
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||||
|
else
|
||||||
|
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||||
|
|
||||||
|
foreach (var mv in movingVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
|
var opposite = OppositeDirection(direction);
|
||||||
|
var stationaryVertices = new HashSet<Vector>();
|
||||||
|
for (int i = 0; i < stationaryLines.Count; i++)
|
||||||
|
{
|
||||||
|
stationaryVertices.Add(stationaryLines[i].pt1);
|
||||||
|
stationaryVertices.Add(stationaryLines[i].pt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
||||||
|
for (int i = 0; i < movingLines.Count; i++)
|
||||||
|
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
||||||
|
|
||||||
|
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
||||||
|
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||||
|
else
|
||||||
|
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||||
|
|
||||||
|
foreach (var sv in stationaryVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Packs line segments into a flat double array [x1,y1,x2,y2, ...] for GPU transfer.
|
||||||
|
/// </summary>
|
||||||
|
public static double[] FlattenLines(List<Line> lines)
|
||||||
|
{
|
||||||
|
var result = new double[lines.Count * 4];
|
||||||
|
for (int i = 0; i < lines.Count; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
result[i * 4] = line.pt1.X;
|
||||||
|
result[i * 4 + 1] = line.pt1.Y;
|
||||||
|
result[i * 4 + 2] = line.pt2.X;
|
||||||
|
result[i * 4 + 3] = line.pt2.Y;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum directional distance using raw edge arrays and location offsets
|
||||||
|
/// to avoid all intermediate object allocations.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
(Vector start, Vector end)[] movingEdges, Vector movingOffset,
|
||||||
|
(Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
|
// Extract unique vertices from moving edges.
|
||||||
|
var movingVertices = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < movingEdges.Length; i++)
|
||||||
|
{
|
||||||
|
movingVertices.Add(movingEdges[i].start + movingOffset);
|
||||||
|
movingVertices.Add(movingEdges[i].end + movingOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
foreach (var mv in movingVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
|
var opposite = OppositeDirection(direction);
|
||||||
|
var stationaryVertices = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < stationaryEdges.Length; i++)
|
||||||
|
{
|
||||||
|
stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset);
|
||||||
|
stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var sv in stationaryVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double OneWayDistance(
|
||||||
|
Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var vx = vertex.X;
|
||||||
|
var vy = vertex.Y;
|
||||||
|
|
||||||
|
// Pruning: edges are sorted by their perpendicular min-coordinate in PartBoundary.
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
var e1 = edges[i].start + edgeOffset;
|
||||||
|
var e2 = edges[i].end + edgeOffset;
|
||||||
|
|
||||||
|
var minY = e1.Y < e2.Y ? e1.Y : e2.Y;
|
||||||
|
var maxY = e1.Y > e2.Y ? e1.Y : e2.Y;
|
||||||
|
|
||||||
|
// Since edges are sorted by minY, if vy < minY, then vy < all subsequent minY.
|
||||||
|
if (vy < minY - Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (vy > maxY + Tolerance.Epsilon)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // Up/Down
|
||||||
|
{
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
var e1 = edges[i].start + edgeOffset;
|
||||||
|
var e2 = edges[i].end + edgeOffset;
|
||||||
|
|
||||||
|
var minX = e1.X < e2.X ? e1.X : e2.X;
|
||||||
|
var maxX = e1.X > e2.X ? e1.X : e2.X;
|
||||||
|
|
||||||
|
// Since edges are sorted by minX, if vx < minX, then vx < all subsequent minX.
|
||||||
|
if (vx < minX - Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (vx > maxX + Tolerance.Epsilon)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PushDirection OppositeDirection(PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left: return PushDirection.Right;
|
||||||
|
case PushDirection.Right: return PushDirection.Left;
|
||||||
|
case PushDirection.Up: return PushDirection.Down;
|
||||||
|
case PushDirection.Down: return PushDirection.Up;
|
||||||
|
default: return direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsHorizontalDirection(PushDirection direction)
|
||||||
|
{
|
||||||
|
return direction is PushDirection.Left or PushDirection.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double EdgeDistance(Box box, Box boundary, PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
default: return double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector DirectionToOffset(PushDirection direction, double distance)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
default: return new Vector();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double DirectionalGap(Box from, Box to, PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
default: return double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ClosestDistanceLeft(Box box, List<Box> boxes)
|
||||||
|
{
|
||||||
|
var closestDistance = double.MaxValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < boxes.Count; i++)
|
||||||
|
{
|
||||||
|
var compareBox = boxes[i];
|
||||||
|
|
||||||
|
RelativePosition pos;
|
||||||
|
|
||||||
|
if (!box.IsHorizontalTo(compareBox, out pos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pos != RelativePosition.Right)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distance = box.Left - compareBox.Right;
|
||||||
|
|
||||||
|
if (distance < closestDistance)
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ClosestDistanceRight(Box box, List<Box> boxes)
|
||||||
|
{
|
||||||
|
var closestDistance = double.MaxValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < boxes.Count; i++)
|
||||||
|
{
|
||||||
|
var compareBox = boxes[i];
|
||||||
|
|
||||||
|
RelativePosition pos;
|
||||||
|
|
||||||
|
if (!box.IsHorizontalTo(compareBox, out pos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pos != RelativePosition.Left)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distance = compareBox.Left - box.Right;
|
||||||
|
|
||||||
|
if (distance < closestDistance)
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ClosestDistanceUp(Box box, List<Box> boxes)
|
||||||
|
{
|
||||||
|
var closestDistance = double.MaxValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < boxes.Count; i++)
|
||||||
|
{
|
||||||
|
var compareBox = boxes[i];
|
||||||
|
|
||||||
|
RelativePosition pos;
|
||||||
|
|
||||||
|
if (!box.IsVerticalTo(compareBox, out pos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pos != RelativePosition.Bottom)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distance = compareBox.Bottom - box.Top;
|
||||||
|
|
||||||
|
if (distance < closestDistance)
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ClosestDistanceDown(Box box, List<Box> boxes)
|
||||||
|
{
|
||||||
|
var closestDistance = double.MaxValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < boxes.Count; i++)
|
||||||
|
{
|
||||||
|
var compareBox = boxes[i];
|
||||||
|
|
||||||
|
RelativePosition pos;
|
||||||
|
|
||||||
|
if (!box.IsVerticalTo(compareBox, out pos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pos != RelativePosition.Top)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distance = box.Bottom - compareBox.Top;
|
||||||
|
|
||||||
|
if (distance < closestDistance)
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestDistance == double.MaxValue ? double.NaN : closestDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Box GetLargestBoxVertically(Vector pt, Box bounds, IEnumerable<Box> boxes)
|
||||||
|
{
|
||||||
|
var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).ToList();
|
||||||
|
|
||||||
|
#region Find Top/Bottom Limits
|
||||||
|
|
||||||
|
var top = double.MaxValue;
|
||||||
|
var btm = double.MinValue;
|
||||||
|
|
||||||
|
foreach (var box in verticalBoxes)
|
||||||
|
{
|
||||||
|
var boxBtm = box.Bottom;
|
||||||
|
var boxTop = box.Top;
|
||||||
|
|
||||||
|
if (boxBtm > pt.Y && boxBtm < top)
|
||||||
|
top = boxBtm;
|
||||||
|
|
||||||
|
else if (box.Top < pt.Y && boxTop > btm)
|
||||||
|
btm = boxTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top == double.MaxValue)
|
||||||
|
{
|
||||||
|
if (bounds.Top > pt.Y)
|
||||||
|
top = bounds.Top;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btm == double.MinValue)
|
||||||
|
{
|
||||||
|
if (bounds.Bottom < pt.Y)
|
||||||
|
btm = bounds.Bottom;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList();
|
||||||
|
|
||||||
|
#region Find Left/Right Limits
|
||||||
|
|
||||||
|
var lft = double.MinValue;
|
||||||
|
var rgt = double.MaxValue;
|
||||||
|
|
||||||
|
foreach (var box in horizontalBoxes)
|
||||||
|
{
|
||||||
|
var boxLft = box.Left;
|
||||||
|
var boxRgt = box.Right;
|
||||||
|
|
||||||
|
if (boxLft > pt.X && boxLft < rgt)
|
||||||
|
rgt = boxLft;
|
||||||
|
|
||||||
|
else if (boxRgt < pt.X && boxRgt > lft)
|
||||||
|
lft = boxRgt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rgt == double.MaxValue)
|
||||||
|
{
|
||||||
|
if (bounds.Right > pt.X)
|
||||||
|
rgt = bounds.Right;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lft == double.MinValue)
|
||||||
|
{
|
||||||
|
if (bounds.Left < pt.X)
|
||||||
|
lft = bounds.Left;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return new Box(lft, btm, rgt - lft, top - btm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Box GetLargestBoxHorizontally(Vector pt, Box bounds, IEnumerable<Box> boxes)
|
||||||
|
{
|
||||||
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList();
|
||||||
|
|
||||||
|
#region Find Left/Right Limits
|
||||||
|
|
||||||
|
var lft = double.MinValue;
|
||||||
|
var rgt = double.MaxValue;
|
||||||
|
|
||||||
|
foreach (var box in horizontalBoxes)
|
||||||
|
{
|
||||||
|
var boxLft = box.Left;
|
||||||
|
var boxRgt = box.Right;
|
||||||
|
|
||||||
|
if (boxLft > pt.X && boxLft < rgt)
|
||||||
|
rgt = boxLft;
|
||||||
|
|
||||||
|
else if (boxRgt < pt.X && boxRgt > lft)
|
||||||
|
lft = boxRgt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rgt == double.MaxValue)
|
||||||
|
{
|
||||||
|
if (bounds.Right > pt.X)
|
||||||
|
rgt = bounds.Right;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lft == double.MinValue)
|
||||||
|
{
|
||||||
|
if (bounds.Left < pt.X)
|
||||||
|
lft = bounds.Left;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList();
|
||||||
|
|
||||||
|
#region Find Top/Bottom Limits
|
||||||
|
|
||||||
|
var top = double.MaxValue;
|
||||||
|
var btm = double.MinValue;
|
||||||
|
|
||||||
|
foreach (var box in verticalBoxes)
|
||||||
|
{
|
||||||
|
var boxBtm = box.Bottom;
|
||||||
|
var boxTop = box.Top;
|
||||||
|
|
||||||
|
if (boxBtm > pt.Y && boxBtm < top)
|
||||||
|
top = boxBtm;
|
||||||
|
|
||||||
|
else if (box.Top < pt.Y && boxTop > btm)
|
||||||
|
btm = boxTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top == double.MaxValue)
|
||||||
|
{
|
||||||
|
if (bounds.Top > pt.Y)
|
||||||
|
top = bounds.Top;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btm == double.MinValue)
|
||||||
|
{
|
||||||
|
if (bounds.Bottom < pt.Y)
|
||||||
|
btm = bounds.Bottom;
|
||||||
|
else return Box.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return new Box(lft, btm, rgt - lft, top - btm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using OpenNest.Math;
|
|||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public struct Vector
|
public struct Vector : IEquatable<Vector>
|
||||||
{
|
{
|
||||||
public static readonly Vector Invalid = new Vector(double.NaN, double.NaN);
|
public static readonly Vector Invalid = new Vector(double.NaN, double.NaN);
|
||||||
public static readonly Vector Zero = new Vector(0, 0);
|
public static readonly Vector Zero = new Vector(0, 0);
|
||||||
@@ -17,6 +17,29 @@ namespace OpenNest.Geometry
|
|||||||
Y = y;
|
Y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Equals(Vector other)
|
||||||
|
{
|
||||||
|
return X.IsEqualTo(other.X) && Y.IsEqualTo(other.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is Vector other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
// Use a simple but effective hash combine.
|
||||||
|
// We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet
|
||||||
|
// during a single operation, raw bits or slightly rounded is usually fine.
|
||||||
|
// However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision.
|
||||||
|
// But typically for these geometric algorithms, exact matches (or very close) are what we want to prune.
|
||||||
|
return (X.GetHashCode() * 397) ^ Y.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public double DistanceTo(Vector pt)
|
public double DistanceTo(Vector pt)
|
||||||
{
|
{
|
||||||
var vx = pt.X - X;
|
var vx = pt.X - X;
|
||||||
@@ -186,21 +209,6 @@ namespace OpenNest.Geometry
|
|||||||
return new Vector(X, Y);
|
return new Vector(X, Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (!(obj is Vector))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var pt = (Vector)obj;
|
|
||||||
|
|
||||||
return (X.IsEqualTo(pt.X)) && (Y.IsEqualTo(pt.Y));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return base.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("[Vector: X:{0}, Y:{1}]", X, Y);
|
return string.Format("[Vector: X:{0}, Y:{1}]", X, Y);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
38
OpenNest.Core/Math/Rounding.cs
Normal file
38
OpenNest.Core/Math/Rounding.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
namespace OpenNest.Math
|
||||||
|
{
|
||||||
|
public static class Rounding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a number down to the nearest factor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num"></param>
|
||||||
|
/// <param name="factor"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static double RoundDownToNearest(double num, double factor)
|
||||||
|
{
|
||||||
|
return factor.IsEqualTo(0) ? num : System.Math.Floor(num / factor) * factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a number up to the nearest factor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num"></param>
|
||||||
|
/// <param name="factor"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static double RoundUpToNearest(double num, double factor)
|
||||||
|
{
|
||||||
|
return factor.IsEqualTo(0) ? num : System.Math.Ceiling(num / factor) * factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a number to the nearest factor using midpoint rounding convention.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num"></param>
|
||||||
|
/// <param name="factor"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static double RoundToNearest(double num, double factor)
|
||||||
|
{
|
||||||
|
return factor.IsEqualTo(0) ? num : System.Math.Round(num / factor) * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ namespace OpenNest
|
|||||||
|
|
||||||
public Program Program { get; private set; }
|
public Program Program { get; private set; }
|
||||||
|
|
||||||
|
public bool HasManualLeadIns { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the rotation of the part in radians.
|
/// Gets the rotation of the part in radians.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -149,31 +151,25 @@ namespace OpenNest
|
|||||||
pts = new List<Vector>();
|
pts = new List<Vector>();
|
||||||
|
|
||||||
var entities1 = ConvertProgram.ToGeometry(Program)
|
var entities1 = ConvertProgram.ToGeometry(Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
var entities2 = ConvertProgram.ToGeometry(part.Program)
|
var entities2 = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var shapes1 = Helper.GetShapes(entities1);
|
if (entities1.Count == 0 || entities2.Count == 0)
|
||||||
var shapes2 = Helper.GetShapes(entities2);
|
return false;
|
||||||
|
|
||||||
shapes1.ForEach(shape => shape.Offset(Location));
|
var perimeter1 = new ShapeProfile(entities1).Perimeter;
|
||||||
shapes2.ForEach(shape => shape.Offset(part.Location));
|
var perimeter2 = new ShapeProfile(entities2).Perimeter;
|
||||||
|
|
||||||
for (int i = 0; i < shapes1.Count; i++)
|
if (perimeter1 == null || perimeter2 == null)
|
||||||
{
|
return false;
|
||||||
var shape1 = shapes1[i];
|
|
||||||
|
|
||||||
for (int j = 0; j < shapes2.Count; j++)
|
perimeter1.Offset(Location);
|
||||||
{
|
perimeter2.Offset(part.Location);
|
||||||
var shape2 = shapes2[j];
|
|
||||||
List<Vector> pts2;
|
|
||||||
|
|
||||||
if (shape1.Intersects(shape2, out pts2))
|
return perimeter1.Intersects(perimeter2, out pts);
|
||||||
pts.AddRange(pts2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pts.Count > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Left
|
public double Left
|
||||||
@@ -216,8 +212,9 @@ namespace OpenNest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Part CloneAtOffset(Vector offset)
|
public Part CloneAtOffset(Vector offset)
|
||||||
{
|
{
|
||||||
var clonedProgram = Program.Clone() as Program;
|
// Share the Program instance — offset-only copies don't modify the program codes.
|
||||||
var part = new Part(BaseDrawing, clonedProgram,
|
// This is a major performance win for tiling large patterns.
|
||||||
|
var part = new Part(BaseDrawing, Program,
|
||||||
location + offset,
|
location + offset,
|
||||||
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
|
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
|
||||||
BoundingBox.Width, BoundingBox.Length));
|
BoundingBox.Width, BoundingBox.Length));
|
||||||
|
|||||||
126
OpenNest.Core/PartGeometry.cs
Normal file
126
OpenNest.Core/PartGeometry.cs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public static class PartGeometry
|
||||||
|
{
|
||||||
|
public static List<Line> GetPartLines(Part part, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(polygon.ToLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetPartLines(Part part, PushDirection facingDirection, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
// Add chord tolerance to compensate for inscribed polygon chords
|
||||||
|
// being inside the actual offset arcs.
|
||||||
|
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
|
if (offsetEntity == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(polygon.ToLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
|
if (offsetEntity == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns only polygon edges whose outward normal faces the specified direction.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Line> GetDirectionalLines(Polygon polygon, PushDirection facingDirection)
|
||||||
|
{
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return polygon.ToLines();
|
||||||
|
|
||||||
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var last = polygon.Vertices[0];
|
||||||
|
|
||||||
|
for (int i = 1; i < polygon.Vertices.Count; i++)
|
||||||
|
{
|
||||||
|
var current = polygon.Vertices[i];
|
||||||
|
var dx = current.X - last.X;
|
||||||
|
var dy = current.Y - last.Y;
|
||||||
|
|
||||||
|
bool keep;
|
||||||
|
|
||||||
|
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;
|
||||||
|
default: keep = true; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keep)
|
||||||
|
lines.Add(new Line(last, current));
|
||||||
|
|
||||||
|
last = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -412,8 +412,8 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
Size = new Size(
|
Size = new Size(
|
||||||
Helper.RoundUpToNearest(width, roundingFactor),
|
Rounding.RoundUpToNearest(width, roundingFactor),
|
||||||
Helper.RoundUpToNearest(length, roundingFactor));
|
Rounding.RoundUpToNearest(length, roundingFactor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace OpenNest
|
|||||||
public static TimingInfo GetTimingInfo(Program pgm)
|
public static TimingInfo GetTimingInfo(Program pgm)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(pgm);
|
var entities = ConvertProgram.ToGeometry(pgm);
|
||||||
var shapes = Helper.GetShapes(entities.Where(entity => entity.Layer != SpecialLayers.Rapid));
|
var shapes = ShapeBuilder.GetShapes(entities.Where(entity => entity.Layer != SpecialLayers.Rapid));
|
||||||
var info = new TimingInfo { PierceCount = shapes.Count };
|
var info = new TimingInfo { PierceCount = shapes.Count };
|
||||||
|
|
||||||
var last = entities[0];
|
var last = entities[0];
|
||||||
|
|||||||
223
OpenNest.Engine/AutoNester.cs
Normal file
223
OpenNest.Engine/AutoNester.cs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mixed-part geometry-aware nesting using NFP-based collision avoidance
|
||||||
|
/// and simulated annealing optimization.
|
||||||
|
/// </summary>
|
||||||
|
public static class AutoNester
|
||||||
|
{
|
||||||
|
public static List<Part> Nest(List<NestItem> items, Plate plate,
|
||||||
|
CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
var workArea = plate.WorkArea();
|
||||||
|
var halfSpacing = plate.PartSpacing / 2.0;
|
||||||
|
var nfpCache = new NfpCache();
|
||||||
|
var candidateRotations = new Dictionary<int, List<double>>();
|
||||||
|
|
||||||
|
// 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<Part>();
|
||||||
|
|
||||||
|
// 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, cancellation);
|
||||||
|
|
||||||
|
if (result.Sequence == null || result.Sequence.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the perimeter polygon from a drawing, inflated by half-spacing.
|
||||||
|
/// </summary>
|
||||||
|
private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (entities.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var definedShape = new ShapeProfile(entities);
|
||||||
|
var perimeter = definedShape.Perimeter;
|
||||||
|
|
||||||
|
if (perimeter == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Inflate by half-spacing if spacing is non-zero.
|
||||||
|
Shape inflated;
|
||||||
|
|
||||||
|
if (halfSpacing > 0)
|
||||||
|
{
|
||||||
|
var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Right);
|
||||||
|
inflated = offsetEntity as Shape ?? perimeter;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inflated = perimeter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||||
|
var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
|
||||||
|
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Normalize: move reference point to origin.
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
var bb = polygon.BoundingBox;
|
||||||
|
polygon.Offset(-bb.Left, -bb.Bottom);
|
||||||
|
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes candidate rotation angles for a drawing.
|
||||||
|
/// </summary>
|
||||||
|
private static List<double> ComputeCandidateRotations(NestItem item,
|
||||||
|
Polygon perimeterPolygon, Box workArea)
|
||||||
|
{
|
||||||
|
var rotations = new List<double> { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes convex hull edge angles from a polygon for candidate rotations.
|
||||||
|
/// </summary>
|
||||||
|
private static List<double> ComputeHullEdgeAngles(Polygon polygon)
|
||||||
|
{
|
||||||
|
var angles = new List<double>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a rotated copy of a polygon around the origin.
|
||||||
|
/// </summary>
|
||||||
|
private static Polygon RotatePolygon(Polygon polygon, double angle)
|
||||||
|
{
|
||||||
|
if (angle.IsEqualTo(0))
|
||||||
|
return polygon;
|
||||||
|
|
||||||
|
var result = new Polygon();
|
||||||
|
var cos = System.Math.Cos(angle);
|
||||||
|
var sin = System.Math.Sin(angle);
|
||||||
|
|
||||||
|
foreach (var v in polygon.Vertices)
|
||||||
|
{
|
||||||
|
result.Vertices.Add(new Vector(
|
||||||
|
v.X * cos - v.Y * sin,
|
||||||
|
v.X * sin + v.Y * cos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-normalize to origin.
|
||||||
|
result.UpdateBounds();
|
||||||
|
var bb = result.BoundingBox;
|
||||||
|
result.Offset(-bb.Left, -bb.Bottom);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
new ConcurrentDictionary<CacheKey, List<BestFitResult>>();
|
new ConcurrentDictionary<CacheKey, List<BestFitResult>>();
|
||||||
|
|
||||||
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
|
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
|
||||||
|
public static Func<ISlideComputer> CreateSlideComputer { get; set; }
|
||||||
|
|
||||||
public static List<BestFitResult> GetOrCompute(
|
public static List<BestFitResult> GetOrCompute(
|
||||||
Drawing drawing, double plateWidth, double plateHeight,
|
Drawing drawing, double plateWidth, double plateHeight,
|
||||||
@@ -24,6 +25,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
IPairEvaluator evaluator = null;
|
IPairEvaluator evaluator = null;
|
||||||
|
ISlideComputer slideComputer = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -33,13 +35,107 @@ namespace OpenNest.Engine.BestFit
|
|||||||
catch { /* fall back to default evaluator */ }
|
catch { /* fall back to default evaluator */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator);
|
if (CreateSlideComputer != null)
|
||||||
|
{
|
||||||
|
try { slideComputer = CreateSlideComputer(); }
|
||||||
|
catch { /* fall back to CPU slide computation */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
var finder = new BestFitFinder(plateWidth, plateHeight, evaluator, slideComputer);
|
||||||
var results = finder.FindBestFits(drawing, spacing, StepSize);
|
var results = finder.FindBestFits(drawing, spacing, StepSize);
|
||||||
|
|
||||||
_cache.TryAdd(key, results);
|
_cache.TryAdd(key, results);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
{
|
||||||
|
(evaluator as IDisposable)?.Dispose();
|
||||||
|
// Slide computer is managed by the factory as a singleton — don't dispose here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ComputeForSizes(
|
||||||
|
Drawing drawing, double spacing,
|
||||||
|
IEnumerable<(double Width, double Height)> plateSizes)
|
||||||
|
{
|
||||||
|
// Skip sizes that are already cached.
|
||||||
|
var needed = new List<(double Width, double Height)>();
|
||||||
|
foreach (var size in plateSizes)
|
||||||
|
{
|
||||||
|
var key = new CacheKey(drawing, size.Width, size.Height, spacing);
|
||||||
|
if (!_cache.ContainsKey(key))
|
||||||
|
needed.Add(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needed.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find the largest plate to use for the initial computation — this
|
||||||
|
// keeps the filter maximally permissive so we don't discard results
|
||||||
|
// that a smaller plate might still use after re-filtering.
|
||||||
|
var maxWidth = 0.0;
|
||||||
|
var maxHeight = 0.0;
|
||||||
|
foreach (var size in needed)
|
||||||
|
{
|
||||||
|
if (size.Width > maxWidth) maxWidth = size.Width;
|
||||||
|
if (size.Height > maxHeight) maxHeight = size.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPairEvaluator evaluator = null;
|
||||||
|
ISlideComputer slideComputer = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (CreateEvaluator != null)
|
||||||
|
{
|
||||||
|
try { evaluator = CreateEvaluator(drawing, spacing); }
|
||||||
|
catch { /* fall back to default evaluator */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CreateSlideComputer != null)
|
||||||
|
{
|
||||||
|
try { slideComputer = CreateSlideComputer(); }
|
||||||
|
catch { /* fall back to CPU slide computation */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute candidates and evaluate once with the largest plate.
|
||||||
|
var finder = new BestFitFinder(maxWidth, maxHeight, evaluator, slideComputer);
|
||||||
|
var baseResults = finder.FindBestFits(drawing, spacing, StepSize);
|
||||||
|
|
||||||
|
// Cache a filtered copy for each plate size.
|
||||||
|
foreach (var size in needed)
|
||||||
|
{
|
||||||
|
var filter = new BestFitFilter
|
||||||
|
{
|
||||||
|
MaxPlateWidth = size.Width,
|
||||||
|
MaxPlateHeight = size.Height
|
||||||
|
};
|
||||||
|
|
||||||
|
var copy = new List<BestFitResult>(baseResults.Count);
|
||||||
|
for (var i = 0; i < baseResults.Count; i++)
|
||||||
|
{
|
||||||
|
var r = baseResults[i];
|
||||||
|
copy.Add(new BestFitResult
|
||||||
|
{
|
||||||
|
Candidate = r.Candidate,
|
||||||
|
RotatedArea = r.RotatedArea,
|
||||||
|
BoundingWidth = r.BoundingWidth,
|
||||||
|
BoundingHeight = r.BoundingHeight,
|
||||||
|
OptimalRotation = r.OptimalRotation,
|
||||||
|
TrueArea = r.TrueArea,
|
||||||
|
HullAngles = r.HullAngles,
|
||||||
|
Keep = r.Keep,
|
||||||
|
Reason = r.Reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.Apply(copy);
|
||||||
|
|
||||||
|
var key = new CacheKey(drawing, size.Width, size.Height, spacing);
|
||||||
|
_cache.TryAdd(key, copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
(evaluator as IDisposable)?.Dispose();
|
(evaluator as IDisposable)?.Dispose();
|
||||||
}
|
}
|
||||||
@@ -54,6 +150,28 @@ namespace OpenNest.Engine.BestFit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Populate(Drawing drawing, double plateWidth, double plateHeight,
|
||||||
|
double spacing, List<BestFitResult> results)
|
||||||
|
{
|
||||||
|
if (results == null || results.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var key = new CacheKey(drawing, plateWidth, plateHeight, spacing);
|
||||||
|
_cache.TryAdd(key, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<(double PlateWidth, double PlateHeight, double Spacing), List<BestFitResult>>
|
||||||
|
GetAllForDrawing(Drawing drawing)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<(double, double, double), List<BestFitResult>>();
|
||||||
|
foreach (var kvp in _cache)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(kvp.Key.Drawing, drawing))
|
||||||
|
result[(kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing)] = kvp.Value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public static void Clear()
|
public static void Clear()
|
||||||
{
|
{
|
||||||
_cache.Clear();
|
_cache.Clear();
|
||||||
|
|||||||
@@ -12,15 +12,21 @@ namespace OpenNest.Engine.BestFit
|
|||||||
public class BestFitFinder
|
public class BestFitFinder
|
||||||
{
|
{
|
||||||
private readonly IPairEvaluator _evaluator;
|
private readonly IPairEvaluator _evaluator;
|
||||||
|
private readonly ISlideComputer _slideComputer;
|
||||||
private readonly BestFitFilter _filter;
|
private readonly BestFitFilter _filter;
|
||||||
|
|
||||||
public BestFitFinder(double maxPlateWidth, double maxPlateHeight, IPairEvaluator evaluator = null)
|
public BestFitFinder(double maxPlateWidth, double maxPlateHeight,
|
||||||
|
IPairEvaluator evaluator = null, ISlideComputer slideComputer = null)
|
||||||
{
|
{
|
||||||
_evaluator = evaluator ?? new PairEvaluator();
|
_evaluator = evaluator ?? new PairEvaluator();
|
||||||
|
_slideComputer = slideComputer;
|
||||||
|
var plateAspect = System.Math.Max(maxPlateWidth, maxPlateHeight) /
|
||||||
|
System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001);
|
||||||
_filter = new BestFitFilter
|
_filter = new BestFitFilter
|
||||||
{
|
{
|
||||||
MaxPlateWidth = maxPlateWidth,
|
MaxPlateWidth = maxPlateWidth,
|
||||||
MaxPlateHeight = maxPlateHeight
|
MaxPlateHeight = maxPlateHeight,
|
||||||
|
MaxAspectRatio = System.Math.Max(5.0, plateAspect)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +84,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
foreach (var angle in angles)
|
foreach (var angle in angles)
|
||||||
{
|
{
|
||||||
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
|
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
|
||||||
strategies.Add(new RotationSlideStrategy(angle, type++, desc));
|
strategies.Add(new RotationSlideStrategy(angle, type++, desc, _slideComputer));
|
||||||
}
|
}
|
||||||
|
|
||||||
return strategies;
|
return strategies;
|
||||||
@@ -102,6 +108,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
|
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
angles.Sort();
|
||||||
return angles;
|
return angles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,14 +116,30 @@ namespace OpenNest.Engine.BestFit
|
|||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
var polygon = shape.ToPolygonWithTolerance(0.01);
|
// Extract key points from original geometry — line endpoints
|
||||||
points.AddRange(polygon.Vertices);
|
// plus arc endpoints and cardinal extreme points. This avoids
|
||||||
|
// tessellating arcs into many chords that flood the hull with
|
||||||
|
// near-duplicate edge angles.
|
||||||
|
foreach (var entity in shape.Entities)
|
||||||
|
{
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
points.Add(line.StartPoint);
|
||||||
|
points.Add(line.EndPoint);
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
points.Add(arc.StartPoint());
|
||||||
|
points.Add(arc.EndPoint());
|
||||||
|
AddArcExtremes(points, arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (points.Count < 3)
|
if (points.Count < 3)
|
||||||
@@ -143,13 +166,49 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return hullAngles;
|
return hullAngles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°)
|
||||||
|
/// if they fall within the arc's angular span.
|
||||||
|
/// </summary>
|
||||||
|
private static void AddArcExtremes(List<Vector> points, Arc arc)
|
||||||
|
{
|
||||||
|
var a1 = arc.StartAngle;
|
||||||
|
var a2 = arc.EndAngle;
|
||||||
|
|
||||||
|
if (arc.IsReversed)
|
||||||
|
Generic.Swap(ref a1, ref a2);
|
||||||
|
|
||||||
|
// Right (0°)
|
||||||
|
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
|
||||||
|
|
||||||
|
// Top (90°)
|
||||||
|
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
|
||||||
|
|
||||||
|
// Left (180°)
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
|
||||||
|
|
||||||
|
// Bottom (270°)
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum angular separation (radians) between hull-derived rotation candidates.
|
||||||
|
/// Tessellated arcs produce many hull edges with nearly identical angles;
|
||||||
|
/// a 1° threshold collapses those into a single representative.
|
||||||
|
/// </summary>
|
||||||
|
private const double AngleTolerance = System.Math.PI / 36; // 5 degrees
|
||||||
|
|
||||||
private static void AddUniqueAngle(List<double> angles, double angle)
|
private static void AddUniqueAngle(List<double> angles, double angle)
|
||||||
{
|
{
|
||||||
angle = Angle.NormalizeRad(angle);
|
angle = Angle.NormalizeRad(angle);
|
||||||
|
|
||||||
foreach (var existing in angles)
|
foreach (var existing in angles)
|
||||||
{
|
{
|
||||||
if (existing.IsEqualTo(angle))
|
if (existing.IsEqualTo(angle, AngleTolerance))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
public bool Keep { get; set; }
|
public bool Keep { get; set; }
|
||||||
public string Reason { get; set; }
|
public string Reason { get; set; }
|
||||||
public double TrueArea { get; set; }
|
public double TrueArea { get; set; }
|
||||||
|
public List<double> HullAngles { get; set; }
|
||||||
|
|
||||||
public double Utilization
|
public double Utilization
|
||||||
{
|
{
|
||||||
|
|||||||
38
OpenNest.Engine/BestFit/ISlideComputer.cs
Normal file
38
OpenNest.Engine/BestFit/ISlideComputer.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.BestFit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Batches directional-distance computations for multiple offset positions.
|
||||||
|
/// GPU implementations can process all offsets in a single kernel launch.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISlideComputer : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum directional distance for each offset position.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stationarySegments">Flat array [x1,y1,x2,y2, ...] for stationary edges.</param>
|
||||||
|
/// <param name="stationaryCount">Number of line segments in stationarySegments.</param>
|
||||||
|
/// <param name="movingTemplateSegments">Flat array [x1,y1,x2,y2, ...] for moving edges at origin.</param>
|
||||||
|
/// <param name="movingCount">Number of line segments in movingTemplateSegments.</param>
|
||||||
|
/// <param name="offsets">Flat array [dx,dy, dx,dy, ...] of translation offsets.</param>
|
||||||
|
/// <param name="offsetCount">Number of offset positions.</param>
|
||||||
|
/// <param name="direction">Push direction.</param>
|
||||||
|
/// <returns>Array of minimum distances, one per offset position.</returns>
|
||||||
|
double[] ComputeBatch(
|
||||||
|
double[] stationarySegments, int stationaryCount,
|
||||||
|
double[] movingTemplateSegments, int movingCount,
|
||||||
|
double[] offsets, int offsetCount,
|
||||||
|
PushDirection direction);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes minimum directional distance for offsets with per-offset directions.
|
||||||
|
/// Uploads segment data once for all offsets, reducing GPU round-trips.
|
||||||
|
/// </summary>
|
||||||
|
double[] ComputeBatchMultiDir(
|
||||||
|
double[] stationarySegments, int stationaryCount,
|
||||||
|
double[] movingTemplateSegments, int movingCount,
|
||||||
|
double[] offsets, int offsetCount,
|
||||||
|
int[] directions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
// Find optimal bounding rectangle via rotating calipers
|
// Find optimal bounding rectangle via rotating calipers
|
||||||
double bestArea, bestWidth, bestHeight, bestRotation;
|
double bestArea, bestWidth, bestHeight, bestRotation;
|
||||||
|
List<double> hullAngles = null;
|
||||||
|
|
||||||
if (allPoints.Count >= 3)
|
if (allPoints.Count >= 3)
|
||||||
{
|
{
|
||||||
@@ -51,6 +52,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
bestWidth = result.Width;
|
bestWidth = result.Width;
|
||||||
bestHeight = result.Height;
|
bestHeight = result.Height;
|
||||||
bestRotation = result.Angle;
|
bestRotation = result.Angle;
|
||||||
|
hullAngles = RotationAnalysis.GetHullEdgeAngles(hull);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -59,6 +61,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
bestWidth = combinedBox.Width;
|
bestWidth = combinedBox.Width;
|
||||||
bestHeight = combinedBox.Length;
|
bestHeight = combinedBox.Length;
|
||||||
bestRotation = 0;
|
bestRotation = 0;
|
||||||
|
hullAngles = new List<double> { 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
var trueArea = drawing.Area * 2;
|
var trueArea = drawing.Area * 2;
|
||||||
@@ -71,6 +74,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
BoundingHeight = bestHeight,
|
BoundingHeight = bestHeight,
|
||||||
OptimalRotation = bestRotation,
|
OptimalRotation = bestRotation,
|
||||||
TrueArea = trueArea,
|
TrueArea = trueArea,
|
||||||
|
HullAngles = hullAngles,
|
||||||
Keep = !overlaps,
|
Keep = !overlaps,
|
||||||
Reason = overlaps ? "Overlap detected" : "Valid"
|
Reason = overlaps ? "Overlap detected" : "Valid"
|
||||||
};
|
};
|
||||||
@@ -99,7 +103,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
shapes.ForEach(s => s.Offset(part.Location));
|
shapes.ForEach(s => s.Offset(part.Location));
|
||||||
return shapes;
|
return shapes;
|
||||||
}
|
}
|
||||||
@@ -108,7 +112,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Engine.BestFit
|
namespace OpenNest.Engine.BestFit
|
||||||
{
|
{
|
||||||
public class RotationSlideStrategy : IBestFitStrategy
|
public class RotationSlideStrategy : IBestFitStrategy
|
||||||
{
|
{
|
||||||
public RotationSlideStrategy(double part2Rotation, int type, string description)
|
private readonly ISlideComputer _slideComputer;
|
||||||
|
|
||||||
|
private static readonly PushDirection[] AllDirections =
|
||||||
|
{
|
||||||
|
PushDirection.Left, PushDirection.Down, PushDirection.Right, PushDirection.Up
|
||||||
|
};
|
||||||
|
|
||||||
|
public RotationSlideStrategy(double part2Rotation, int type, string description,
|
||||||
|
ISlideComputer slideComputer = null)
|
||||||
{
|
{
|
||||||
Part2Rotation = part2Rotation;
|
Part2Rotation = part2Rotation;
|
||||||
Type = type;
|
Type = type;
|
||||||
Description = description;
|
Description = description;
|
||||||
|
_slideComputer = slideComputer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Part2Rotation { get; }
|
public double Part2Rotation { get; }
|
||||||
@@ -23,43 +33,64 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var part1 = Part.CreateAtOrigin(drawing);
|
var part1 = Part.CreateAtOrigin(drawing);
|
||||||
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
||||||
|
|
||||||
|
var halfSpacing = spacing / 2;
|
||||||
|
var part1Lines = PartGeometry.GetOffsetPartLines(part1, halfSpacing);
|
||||||
|
var part2TemplateLines = PartGeometry.GetOffsetPartLines(part2Template, halfSpacing);
|
||||||
|
|
||||||
|
var bbox1 = part1.BoundingBox;
|
||||||
|
var bbox2 = part2Template.BoundingBox;
|
||||||
|
|
||||||
|
// Collect offsets and directions across all 4 axes
|
||||||
|
var allDx = new List<double>();
|
||||||
|
var allDy = new List<double>();
|
||||||
|
var allDirs = new List<PushDirection>();
|
||||||
|
|
||||||
|
foreach (var pushDir in AllDirections)
|
||||||
|
BuildOffsets(bbox1, bbox2, spacing, stepSize, pushDir, allDx, allDy, allDirs);
|
||||||
|
|
||||||
|
if (allDx.Count == 0)
|
||||||
|
return candidates;
|
||||||
|
|
||||||
|
// Compute all distances — single GPU dispatch or CPU loop
|
||||||
|
var distances = ComputeAllDistances(
|
||||||
|
part1Lines, part2TemplateLines, allDx, allDy, allDirs);
|
||||||
|
|
||||||
|
// Create candidates from valid results
|
||||||
var testNumber = 0;
|
var testNumber = 0;
|
||||||
|
|
||||||
// Try pushing left (horizontal slide)
|
for (var i = 0; i < allDx.Count; i++)
|
||||||
GenerateCandidatesForAxis(
|
{
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
var slideDist = distances[i];
|
||||||
PushDirection.Left, candidates, ref testNumber);
|
if (slideDist >= double.MaxValue || slideDist < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Try pushing down (vertical slide)
|
var dx = allDx[i];
|
||||||
GenerateCandidatesForAxis(
|
var dy = allDy[i];
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
var pushVector = GetPushVector(allDirs[i], slideDist);
|
||||||
PushDirection.Down, candidates, ref testNumber);
|
var finalPosition = new Vector(
|
||||||
|
part2Template.Location.X + dx + pushVector.X,
|
||||||
|
part2Template.Location.Y + dy + pushVector.Y);
|
||||||
|
|
||||||
// Try pushing right (approach from left — finds concave interlocking)
|
candidates.Add(new PairCandidate
|
||||||
GenerateCandidatesForAxis(
|
{
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
Drawing = drawing,
|
||||||
PushDirection.Right, candidates, ref testNumber);
|
Part1Rotation = 0,
|
||||||
|
Part2Rotation = Part2Rotation,
|
||||||
// Try pushing up (approach from below — finds concave interlocking)
|
Part2Offset = finalPosition,
|
||||||
GenerateCandidatesForAxis(
|
StrategyType = Type,
|
||||||
part1, part2Template, drawing, spacing, stepSize,
|
TestNumber = testNumber++,
|
||||||
PushDirection.Up, candidates, ref testNumber);
|
Spacing = spacing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateCandidatesForAxis(
|
private static void BuildOffsets(
|
||||||
Part part1, Part part2Template, Drawing drawing,
|
Box bbox1, Box bbox2, double spacing, double stepSize,
|
||||||
double spacing, double stepSize, PushDirection pushDir,
|
PushDirection pushDir, List<double> allDx, List<double> allDy,
|
||||||
List<PairCandidate> candidates, ref int testNumber)
|
List<PushDirection> allDirs)
|
||||||
{
|
{
|
||||||
const int CoarseMultiplier = 16;
|
|
||||||
const int MaxRegions = 5;
|
|
||||||
|
|
||||||
var bbox1 = part1.BoundingBox;
|
|
||||||
var bbox2 = part2Template.BoundingBox;
|
|
||||||
var halfSpacing = spacing / 2;
|
|
||||||
|
|
||||||
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
||||||
|
|
||||||
double perpMin, perpMax, pushStartOffset;
|
double perpMin, perpMax, pushStartOffset;
|
||||||
@@ -77,103 +108,124 @@ namespace OpenNest.Engine.BestFit
|
|||||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
var part1Lines = Helper.GetOffsetPartLines(part1, halfSpacing);
|
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
|
||||||
|
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
||||||
|
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
||||||
|
|
||||||
// Start with the full range as a single region.
|
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
||||||
var regions = new List<(double min, double max)> { (perpMin, perpMax) };
|
|
||||||
var currentStep = stepSize * CoarseMultiplier;
|
|
||||||
|
|
||||||
// Iterative halving: coarse sweep, select top regions, narrow, repeat.
|
|
||||||
while (currentStep > stepSize)
|
|
||||||
{
|
{
|
||||||
var hits = new List<(double offset, double slideDist)>();
|
allDx.Add(isHorizontalPush ? startPos : offset);
|
||||||
|
allDy.Add(isHorizontalPush ? offset : startPos);
|
||||||
|
allDirs.Add(pushDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var (regionMin, regionMax) in regions)
|
private double[] ComputeAllDistances(
|
||||||
|
List<Line> part1Lines, List<Line> part2TemplateLines,
|
||||||
|
List<double> allDx, List<double> allDy, List<PushDirection> allDirs)
|
||||||
|
{
|
||||||
|
var count = allDx.Count;
|
||||||
|
|
||||||
|
if (_slideComputer != null)
|
||||||
|
{
|
||||||
|
var stationarySegments = SpatialQuery.FlattenLines(part1Lines);
|
||||||
|
var movingSegments = SpatialQuery.FlattenLines(part2TemplateLines);
|
||||||
|
var offsets = new double[count * 2];
|
||||||
|
var directions = new int[count];
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var alignedStart = System.Math.Ceiling(regionMin / currentStep) * currentStep;
|
offsets[i * 2] = allDx[i];
|
||||||
|
offsets[i * 2 + 1] = allDy[i];
|
||||||
for (var offset = alignedStart; offset <= regionMax; offset += currentStep)
|
directions[i] = (int)allDirs[i];
|
||||||
{
|
|
||||||
var slideDist = ComputeSlideDistance(
|
|
||||||
part2Template, part1Lines, halfSpacing,
|
|
||||||
offset, pushStartOffset, isHorizontalPush, pushDir);
|
|
||||||
|
|
||||||
if (slideDist >= double.MaxValue || slideDist < 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
hits.Add((offset, slideDist));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hits.Count == 0)
|
return _slideComputer.ComputeBatchMultiDir(
|
||||||
return;
|
stationarySegments, part1Lines.Count,
|
||||||
|
movingSegments, part2TemplateLines.Count,
|
||||||
// Select top regions by tightest fit, deduplicating nearby hits.
|
offsets, count, directions);
|
||||||
hits.Sort((a, b) => a.slideDist.CompareTo(b.slideDist));
|
|
||||||
|
|
||||||
var selectedOffsets = new List<double>();
|
|
||||||
|
|
||||||
foreach (var (offset, _) in hits)
|
|
||||||
{
|
|
||||||
var tooClose = false;
|
|
||||||
|
|
||||||
foreach (var selected in selectedOffsets)
|
|
||||||
{
|
|
||||||
if (System.Math.Abs(offset - selected) < currentStep)
|
|
||||||
{
|
|
||||||
tooClose = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tooClose)
|
|
||||||
{
|
|
||||||
selectedOffsets.Add(offset);
|
|
||||||
|
|
||||||
if (selectedOffsets.Count >= MaxRegions)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build narrowed regions around selected offsets.
|
|
||||||
regions = new List<(double min, double max)>();
|
|
||||||
|
|
||||||
foreach (var offset in selectedOffsets)
|
|
||||||
{
|
|
||||||
var regionMin = System.Math.Max(perpMin, offset - currentStep);
|
|
||||||
var regionMax = System.Math.Min(perpMax, offset + currentStep);
|
|
||||||
regions.Add((regionMin, regionMax));
|
|
||||||
}
|
|
||||||
|
|
||||||
currentStep /= 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final pass: sweep refined regions at stepSize, generating candidates.
|
var results = new double[count];
|
||||||
foreach (var (regionMin, regionMax) in regions)
|
|
||||||
|
// Pre-calculate moving vertices in local space.
|
||||||
|
var movingVerticesLocal = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < part2TemplateLines.Count; i++)
|
||||||
{
|
{
|
||||||
var alignedStart = System.Math.Ceiling(regionMin / stepSize) * stepSize;
|
movingVerticesLocal.Add(part2TemplateLines[i].StartPoint);
|
||||||
|
movingVerticesLocal.Add(part2TemplateLines[i].EndPoint);
|
||||||
for (var offset = alignedStart; offset <= regionMax; offset += stepSize)
|
|
||||||
{
|
|
||||||
var (slideDist, finalPosition) = ComputeSlideResult(
|
|
||||||
part2Template, part1Lines, halfSpacing,
|
|
||||||
offset, pushStartOffset, isHorizontalPush, pushDir);
|
|
||||||
|
|
||||||
if (slideDist >= double.MaxValue || slideDist < 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
candidates.Add(new PairCandidate
|
|
||||||
{
|
|
||||||
Drawing = drawing,
|
|
||||||
Part1Rotation = 0,
|
|
||||||
Part2Rotation = Part2Rotation,
|
|
||||||
Part2Offset = finalPosition,
|
|
||||||
StrategyType = Type,
|
|
||||||
TestNumber = testNumber++,
|
|
||||||
Spacing = spacing
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var movingVerticesArray = movingVerticesLocal.ToArray();
|
||||||
|
|
||||||
|
// Pre-calculate stationary vertices in local space.
|
||||||
|
var stationaryVerticesLocal = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < part1Lines.Count; i++)
|
||||||
|
{
|
||||||
|
stationaryVerticesLocal.Add(part1Lines[i].StartPoint);
|
||||||
|
stationaryVerticesLocal.Add(part1Lines[i].EndPoint);
|
||||||
|
}
|
||||||
|
var stationaryVerticesArray = stationaryVerticesLocal.ToArray();
|
||||||
|
|
||||||
|
// Pre-sort stationary and moving edges for all 4 directions.
|
||||||
|
var stationaryEdgesByDir = new Dictionary<PushDirection, (Vector start, Vector end)[]>();
|
||||||
|
var movingEdgesByDir = new Dictionary<PushDirection, (Vector start, Vector end)[]>();
|
||||||
|
|
||||||
|
foreach (var dir in AllDirections)
|
||||||
|
{
|
||||||
|
var sEdges = new (Vector start, Vector end)[part1Lines.Count];
|
||||||
|
for (var i = 0; i < part1Lines.Count; i++)
|
||||||
|
sEdges[i] = (part1Lines[i].StartPoint, part1Lines[i].EndPoint);
|
||||||
|
|
||||||
|
if (dir == PushDirection.Left || dir == PushDirection.Right)
|
||||||
|
sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||||
|
else
|
||||||
|
sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||||
|
stationaryEdgesByDir[dir] = sEdges;
|
||||||
|
|
||||||
|
var opposite = SpatialQuery.OppositeDirection(dir);
|
||||||
|
var mEdges = new (Vector start, Vector end)[part2TemplateLines.Count];
|
||||||
|
for (var i = 0; i < part2TemplateLines.Count; i++)
|
||||||
|
mEdges[i] = (part2TemplateLines[i].StartPoint, part2TemplateLines[i].EndPoint);
|
||||||
|
|
||||||
|
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
||||||
|
mEdges = mEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
||||||
|
else
|
||||||
|
mEdges = mEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
||||||
|
movingEdgesByDir[dir] = mEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Parallel.For for the heavy lifting.
|
||||||
|
System.Threading.Tasks.Parallel.For(0, count, i =>
|
||||||
|
{
|
||||||
|
var dx = allDx[i];
|
||||||
|
var dy = allDy[i];
|
||||||
|
var dir = allDirs[i];
|
||||||
|
var movingOffset = new Vector(dx, dy);
|
||||||
|
|
||||||
|
var sEdges = stationaryEdgesByDir[dir];
|
||||||
|
var mEdges = movingEdgesByDir[dir];
|
||||||
|
var opposite = SpatialQuery.OppositeDirection(dir);
|
||||||
|
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
|
// Case 1: Moving vertices -> Stationary edges
|
||||||
|
foreach (var mv in movingVerticesArray)
|
||||||
|
{
|
||||||
|
var d = SpatialQuery.OneWayDistance(mv + movingOffset, sEdges, Vector.Zero, dir);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Stationary vertices -> Moving edges (translated)
|
||||||
|
foreach (var sv in stationaryVerticesArray)
|
||||||
|
{
|
||||||
|
var d = SpatialQuery.OneWayDistance(sv, mEdges, movingOffset, opposite);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
results[i] = minDist;
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector GetPushVector(PushDirection direction, double distance)
|
private static Vector GetPushVector(PushDirection direction, double distance)
|
||||||
@@ -187,48 +239,5 @@ namespace OpenNest.Engine.BestFit
|
|||||||
default: return Vector.Zero;
|
default: return Vector.Zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static double ComputeSlideDistance(
|
|
||||||
Part part2Template, List<Line> part1Lines, double halfSpacing,
|
|
||||||
double offset, double pushStartOffset,
|
|
||||||
bool isHorizontalPush, PushDirection pushDir)
|
|
||||||
{
|
|
||||||
var part2 = (Part)part2Template.Clone();
|
|
||||||
|
|
||||||
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
|
||||||
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
|
||||||
|
|
||||||
if (isHorizontalPush)
|
|
||||||
part2.Offset(startPos, offset);
|
|
||||||
else
|
|
||||||
part2.Offset(offset, startPos);
|
|
||||||
|
|
||||||
var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing);
|
|
||||||
|
|
||||||
return Helper.DirectionalDistance(part2Lines, part1Lines, pushDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (double slideDist, Vector finalPosition) ComputeSlideResult(
|
|
||||||
Part part2Template, List<Line> part1Lines, double halfSpacing,
|
|
||||||
double offset, double pushStartOffset,
|
|
||||||
bool isHorizontalPush, PushDirection pushDir)
|
|
||||||
{
|
|
||||||
var part2 = (Part)part2Template.Clone();
|
|
||||||
|
|
||||||
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
|
||||||
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
|
||||||
|
|
||||||
if (isHorizontalPush)
|
|
||||||
part2.Offset(startPos, offset);
|
|
||||||
else
|
|
||||||
part2.Offset(offset, startPos);
|
|
||||||
|
|
||||||
var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing);
|
|
||||||
var slideDist = Helper.DirectionalDistance(part2Lines, part1Lines, pushDir);
|
|
||||||
|
|
||||||
var pushVector = GetPushVector(pushDir, slideDist);
|
|
||||||
var finalPosition = part2.Location + pushVector;
|
|
||||||
|
|
||||||
return (slideDist, finalPosition);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
156
OpenNest.Engine/Compactor.cs
Normal file
156
OpenNest.Engine/Compactor.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
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 = plate.PartSpacing / 2;
|
||||||
|
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||||
|
var workArea = plate.WorkArea();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
697
OpenNest.Engine/DefaultNestEngine.cs
Normal file
697
OpenNest.Engine/DefaultNestEngine.cs
Normal file
@@ -0,0 +1,697 @@
|
|||||||
|
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.Engine.ML;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using OpenNest.RectanglePacking;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class DefaultNestEngine : NestEngineBase
|
||||||
|
{
|
||||||
|
public DefaultNestEngine(Plate plate) : base(plate) { }
|
||||||
|
|
||||||
|
public override string Name => "Default";
|
||||||
|
|
||||||
|
public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)";
|
||||||
|
|
||||||
|
public bool ForceFullAngleSweep { get; set; }
|
||||||
|
|
||||||
|
// Angles that have produced results across multiple Fill calls.
|
||||||
|
// Populated after each Fill; used to prune subsequent fills.
|
||||||
|
private readonly HashSet<double> knownGoodAngles = new();
|
||||||
|
|
||||||
|
// --- Public Fill API ---
|
||||||
|
|
||||||
|
public override List<Part> Fill(NestItem item, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
PhaseResults.Clear();
|
||||||
|
AngleResults.Clear();
|
||||||
|
var best = FindBestFill(item, workArea, progress, token);
|
||||||
|
|
||||||
|
if (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Try improving by filling the remainder strip separately.
|
||||||
|
var remainderSw = Stopwatch.StartNew();
|
||||||
|
var improved = TryRemainderImprovement(item, workArea, best);
|
||||||
|
remainderSw.Stop();
|
||||||
|
|
||||||
|
if (IsBetterFill(improved, best, workArea))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[Fill] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})");
|
||||||
|
best = improved;
|
||||||
|
WinnerPhase = NestPhase.Remainder;
|
||||||
|
PhaseResults.Add(new PhaseResult(NestPhase.Remainder, improved.Count, remainderSw.ElapsedMilliseconds));
|
||||||
|
ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best == null || best.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
if (item.Quantity > 0 && best.Count > item.Quantity)
|
||||||
|
best = best.Take(item.Quantity).ToList();
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fast fill count using linear fill with two angles plus the top cached
|
||||||
|
/// pair candidates. Used by binary search to estimate capacity at a given
|
||||||
|
/// box size without running the full Fill pipeline.
|
||||||
|
/// </summary>
|
||||||
|
private int QuickFillCount(Drawing drawing, Box testBox, double bestRotation)
|
||||||
|
{
|
||||||
|
var engine = new FillLinear(testBox, Plate.PartSpacing);
|
||||||
|
var bestCount = 0;
|
||||||
|
|
||||||
|
// Single-part linear fills.
|
||||||
|
var angles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||||
|
|
||||||
|
foreach (var angle in angles)
|
||||||
|
{
|
||||||
|
var h = engine.Fill(drawing, angle, NestDirection.Horizontal);
|
||||||
|
if (h != null && h.Count > bestCount)
|
||||||
|
bestCount = h.Count;
|
||||||
|
|
||||||
|
var v = engine.Fill(drawing, angle, NestDirection.Vertical);
|
||||||
|
if (v != null && v.Count > bestCount)
|
||||||
|
bestCount = v.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top pair candidates — check if pairs tile better in this box.
|
||||||
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
|
drawing, Plate.Size.Width, Plate.Size.Length, Plate.PartSpacing);
|
||||||
|
var topPairs = bestFits.Where(r => r.Keep).Take(3);
|
||||||
|
|
||||||
|
foreach (var pair in topPairs)
|
||||||
|
{
|
||||||
|
var pairParts = pair.BuildParts(drawing);
|
||||||
|
var pairAngles = pair.HullAngles ?? new List<double> { 0 };
|
||||||
|
var pairEngine = new FillLinear(testBox, Plate.PartSpacing);
|
||||||
|
|
||||||
|
foreach (var angle in pairAngles)
|
||||||
|
{
|
||||||
|
var pattern = BuildRotatedPattern(pairParts, angle);
|
||||||
|
if (pattern.Parts.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var h = pairEngine.Fill(pattern, NestDirection.Horizontal);
|
||||||
|
if (h != null && h.Count > bestCount)
|
||||||
|
bestCount = h.Count;
|
||||||
|
|
||||||
|
var v = pairEngine.Fill(pattern, NestDirection.Vertical);
|
||||||
|
if (v != null && v.Count > bestCount)
|
||||||
|
bestCount = v.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (groupParts == null || groupParts.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
PhaseResults.Clear();
|
||||||
|
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
||||||
|
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
||||||
|
var best = 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}");
|
||||||
|
|
||||||
|
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
|
||||||
|
if (groupParts.Count == 1)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
|
||||||
|
var rectResult = FillRectangleBestFit(nestItem, workArea);
|
||||||
|
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 pairResult = FillWithPairs(nestItem, workArea, 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try improving by filling the remainder strip separately.
|
||||||
|
var improved = TryRemainderImprovement(nestItem, workArea, best);
|
||||||
|
|
||||||
|
if (IsBetterFill(improved, best, workArea))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[Fill(groupParts,Box)] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})");
|
||||||
|
best = improved;
|
||||||
|
PhaseResults.Add(new PhaseResult(NestPhase.Remainder, improved.Count, 0));
|
||||||
|
ReportProgress(progress, NestPhase.Remainder, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best ?? new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pack API ---
|
||||||
|
|
||||||
|
public override List<Part> PackArea(Box box, List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area());
|
||||||
|
var bin = BinConverter.CreateBin(box, Plate.PartSpacing);
|
||||||
|
|
||||||
|
var engine = new PackBottomLeft(bin);
|
||||||
|
engine.Pack(binItems);
|
||||||
|
|
||||||
|
return BinConverter.ToParts(bin, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FindBestFill: core orchestration ---
|
||||||
|
|
||||||
|
private List<Part> FindBestFill(NestItem item, Box workArea,
|
||||||
|
IProgress<NestProgress> progress = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
List<Part> best = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bestRotation = RotationAnalysis.FindBestRotation(item);
|
||||||
|
var angles = BuildCandidateAngles(item, bestRotation, workArea);
|
||||||
|
|
||||||
|
// Pairs phase
|
||||||
|
var pairSw = Stopwatch.StartNew();
|
||||||
|
var pairResult = FillWithPairs(item, workArea, token, progress);
|
||||||
|
pairSw.Stop();
|
||||||
|
best = pairResult;
|
||||||
|
var bestScore = FillScore.Compute(best, workArea);
|
||||||
|
WinnerPhase = NestPhase.Pairs;
|
||||||
|
PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, pairSw.ElapsedMilliseconds));
|
||||||
|
|
||||||
|
Debug.WriteLine($"[FindBestFill] Pair: {bestScore.Count} parts");
|
||||||
|
ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Linear phase
|
||||||
|
var linearSw = Stopwatch.StartNew();
|
||||||
|
var bestLinearCount = 0;
|
||||||
|
|
||||||
|
for (var ai = 0; ai < angles.Count; ai++)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var angle = angles[ai];
|
||||||
|
var localEngine = new FillLinear(workArea, Plate.PartSpacing);
|
||||||
|
var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal);
|
||||||
|
var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical);
|
||||||
|
|
||||||
|
var angleDeg = Angle.ToDegrees(angle);
|
||||||
|
if (h != null && h.Count > 0)
|
||||||
|
{
|
||||||
|
var scoreH = FillScore.Compute(h, workArea);
|
||||||
|
AngleResults.Add(new AngleResult { AngleDeg = angleDeg, Direction = NestDirection.Horizontal, PartCount = h.Count });
|
||||||
|
if (h.Count > bestLinearCount) bestLinearCount = h.Count;
|
||||||
|
if (scoreH > bestScore)
|
||||||
|
{
|
||||||
|
best = h;
|
||||||
|
bestScore = scoreH;
|
||||||
|
WinnerPhase = NestPhase.Linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v != null && v.Count > 0)
|
||||||
|
{
|
||||||
|
var scoreV = FillScore.Compute(v, workArea);
|
||||||
|
AngleResults.Add(new AngleResult { AngleDeg = angleDeg, Direction = NestDirection.Vertical, PartCount = v.Count });
|
||||||
|
if (v.Count > bestLinearCount) bestLinearCount = v.Count;
|
||||||
|
if (scoreV > bestScore)
|
||||||
|
{
|
||||||
|
best = v;
|
||||||
|
bestScore = scoreV;
|
||||||
|
WinnerPhase = NestPhase.Linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea,
|
||||||
|
$"Linear: {ai + 1}/{angles.Count} angles, {angleDeg:F0}° best = {bestScore.Count} parts");
|
||||||
|
}
|
||||||
|
|
||||||
|
linearSw.Stop();
|
||||||
|
PhaseResults.Add(new PhaseResult(NestPhase.Linear, bestLinearCount, linearSw.ElapsedMilliseconds));
|
||||||
|
|
||||||
|
// Record productive angles for future fills.
|
||||||
|
foreach (var ar in AngleResults)
|
||||||
|
{
|
||||||
|
if (ar.PartCount > 0)
|
||||||
|
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[FindBestFill] Linear: {bestScore.Count} parts, density={bestScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}");
|
||||||
|
|
||||||
|
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// RectBestFit phase
|
||||||
|
var rectSw = Stopwatch.StartNew();
|
||||||
|
var rectResult = FillRectangleBestFit(item, workArea);
|
||||||
|
rectSw.Stop();
|
||||||
|
var rectScore = rectResult != null ? FillScore.Compute(rectResult, workArea) : default;
|
||||||
|
Debug.WriteLine($"[FindBestFill] RectBestFit: {rectScore.Count} parts");
|
||||||
|
PhaseResults.Add(new PhaseResult(NestPhase.RectBestFit, rectResult?.Count ?? 0, rectSw.ElapsedMilliseconds));
|
||||||
|
|
||||||
|
if (rectScore > bestScore)
|
||||||
|
{
|
||||||
|
best = rectResult;
|
||||||
|
WinnerPhase = NestPhase.RectBestFit;
|
||||||
|
ReportProgress(progress, NestPhase.RectBestFit, PlateNumber, best, workArea, BuildProgressSummary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[FindBestFill] Cancelled, returning current best");
|
||||||
|
}
|
||||||
|
|
||||||
|
return best ?? new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Angle building ---
|
||||||
|
|
||||||
|
private List<double> BuildCandidateAngles(NestItem item, double bestRotation, Box workArea)
|
||||||
|
{
|
||||||
|
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||||
|
|
||||||
|
// When the work area is narrow relative to the part, sweep rotation
|
||||||
|
// angles so we can find one that fits the part into the tight strip.
|
||||||
|
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 || ForceFullAngleSweep;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the work area triggers a full sweep (and we're not forcing it for training),
|
||||||
|
// try ML angle prediction to reduce the sweep.
|
||||||
|
if (!ForceFullAngleSweep && 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($"[BuildCandidateAngles] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
|
||||||
|
angles = mlAngles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have known-good angles from previous fills, use only those
|
||||||
|
// plus the defaults (bestRotation + 90°). This prunes the expensive
|
||||||
|
// angle sweep after the first fill.
|
||||||
|
if (knownGoodAngles.Count > 0 && !ForceFullAngleSweep)
|
||||||
|
{
|
||||||
|
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($"[BuildCandidateAngles] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
|
||||||
|
return pruned;
|
||||||
|
}
|
||||||
|
|
||||||
|
return angles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fill strategies ---
|
||||||
|
|
||||||
|
private List<Part> FillRectangleBestFit(NestItem item, Box workArea)
|
||||||
|
{
|
||||||
|
var binItem = BinConverter.ToItem(item, Plate.PartSpacing);
|
||||||
|
var bin = BinConverter.CreateBin(workArea, Plate.PartSpacing);
|
||||||
|
|
||||||
|
var engine = new FillBestFit(bin);
|
||||||
|
engine.Fill(binItem);
|
||||||
|
|
||||||
|
return BinConverter.ToParts(bin, new List<NestItem> { item });
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Part> FillWithPairs(NestItem item, Box workArea,
|
||||||
|
CancellationToken token = default, IProgress<NestProgress> progress = null)
|
||||||
|
{
|
||||||
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
|
item.Drawing, Plate.Size.Width, Plate.Size.Length,
|
||||||
|
Plate.PartSpacing);
|
||||||
|
|
||||||
|
var candidates = SelectPairCandidates(bestFits, workArea);
|
||||||
|
var diagMsg = $"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}\n" +
|
||||||
|
$"[FillWithPairs] Plate: {Plate.Size.Width:F2}x{Plate.Size.Length:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}";
|
||||||
|
Debug.WriteLine(diagMsg);
|
||||||
|
try { System.IO.File.AppendAllText(
|
||||||
|
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"),
|
||||||
|
$"{DateTime.Now:HH:mm:ss} {diagMsg}\n"); } catch { }
|
||||||
|
|
||||||
|
List<Part> best = null;
|
||||||
|
var bestScore = default(FillScore);
|
||||||
|
var sinceImproved = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (var i = 0; i < candidates.Count; i++)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var result = candidates[i];
|
||||||
|
var pairParts = result.BuildParts(item.Drawing);
|
||||||
|
var angles = result.HullAngles;
|
||||||
|
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
||||||
|
var filled = FillPattern(engine, pairParts, angles, workArea);
|
||||||
|
|
||||||
|
if (filled != null && filled.Count > 0)
|
||||||
|
{
|
||||||
|
var score = FillScore.Compute(filled, workArea);
|
||||||
|
if (best == null || score > bestScore)
|
||||||
|
{
|
||||||
|
best = filled;
|
||||||
|
bestScore = score;
|
||||||
|
sinceImproved = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sinceImproved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sinceImproved++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[FillWithPairs] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[FillWithPairs] Cancelled mid-phase, using results so far");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}");
|
||||||
|
try { System.IO.File.AppendAllText(
|
||||||
|
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"),
|
||||||
|
$"{DateTime.Now:HH:mm:ss} [FillWithPairs] Best: {bestScore.Count} parts, density={bestScore.Density:P1}\n"); } catch { }
|
||||||
|
return best ?? new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects pair candidates to try for the given work area. Always includes
|
||||||
|
/// the top 50 by area. For narrow work areas, also includes all pairs whose
|
||||||
|
/// shortest side fits the strip width.
|
||||||
|
/// </summary>
|
||||||
|
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
||||||
|
{
|
||||||
|
var kept = bestFits.Where(r => r.Keep).ToList();
|
||||||
|
var top = kept.Take(50).ToList();
|
||||||
|
|
||||||
|
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||||
|
var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Length);
|
||||||
|
|
||||||
|
// When the work area is significantly narrower than the plate,
|
||||||
|
// search ALL candidates (not just kept) for pairs that fit the
|
||||||
|
// narrow dimension. Pairs rejected by aspect ratio for the full
|
||||||
|
// plate may be exactly what's needed for a narrow remainder strip.
|
||||||
|
if (workShortSide < plateShortSide * 0.5)
|
||||||
|
{
|
||||||
|
var stripCandidates = bestFits
|
||||||
|
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
||||||
|
&& r.Utilization >= 0.3)
|
||||||
|
.OrderByDescending(r => r.Utilization);
|
||||||
|
|
||||||
|
var existing = new HashSet<BestFitResult>(top);
|
||||||
|
|
||||||
|
foreach (var r in stripCandidates)
|
||||||
|
{
|
||||||
|
if (top.Count >= 100)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (existing.Add(r))
|
||||||
|
top.Add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[SelectPairCandidates] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pattern helpers ---
|
||||||
|
|
||||||
|
private Pattern BuildRotatedPattern(List<Part> groupParts, double angle)
|
||||||
|
{
|
||||||
|
var pattern = new Pattern();
|
||||||
|
var center = ((IEnumerable<IBoundable>)groupParts).GetBoundingBox().Center;
|
||||||
|
|
||||||
|
foreach (var part in groupParts)
|
||||||
|
{
|
||||||
|
var clone = (Part)part.Clone();
|
||||||
|
clone.UpdateBounds();
|
||||||
|
|
||||||
|
if (!angle.IsEqualTo(0))
|
||||||
|
clone.Rotate(angle, center);
|
||||||
|
|
||||||
|
pattern.Parts.Add(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern.UpdateBounds();
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
|
||||||
|
{
|
||||||
|
var results = new System.Collections.Concurrent.ConcurrentBag<(List<Part> Parts, FillScore Score)>();
|
||||||
|
|
||||||
|
Parallel.ForEach(angles, angle =>
|
||||||
|
{
|
||||||
|
var pattern = BuildRotatedPattern(groupParts, angle);
|
||||||
|
|
||||||
|
if (pattern.Parts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var h = engine.Fill(pattern, NestDirection.Horizontal);
|
||||||
|
if (h != null && h.Count > 0)
|
||||||
|
results.Add((h, FillScore.Compute(h, workArea)));
|
||||||
|
|
||||||
|
var v = engine.Fill(pattern, NestDirection.Vertical);
|
||||||
|
if (v != null && v.Count > 0)
|
||||||
|
results.Add((v, FillScore.Compute(v, workArea)));
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Part> best = null;
|
||||||
|
var bestScore = default(FillScore);
|
||||||
|
|
||||||
|
foreach (var res in results)
|
||||||
|
{
|
||||||
|
if (best == null || res.Score > bestScore)
|
||||||
|
{
|
||||||
|
best = res.Parts;
|
||||||
|
bestScore = res.Score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Remainder improvement ---
|
||||||
|
|
||||||
|
private List<Part> TryRemainderImprovement(NestItem item, Box workArea, List<Part> currentBest)
|
||||||
|
{
|
||||||
|
if (currentBest == null || currentBest.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
List<Part> best = null;
|
||||||
|
|
||||||
|
var hResult = TryStripRefill(item, workArea, currentBest, horizontal: true);
|
||||||
|
|
||||||
|
if (IsBetterFill(hResult, best, workArea))
|
||||||
|
best = hResult;
|
||||||
|
|
||||||
|
var vResult = TryStripRefill(item, workArea, currentBest, horizontal: false);
|
||||||
|
|
||||||
|
if (IsBetterFill(vResult, best, workArea))
|
||||||
|
best = vResult;
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Part> TryStripRefill(NestItem item, Box workArea, List<Part> parts, bool horizontal)
|
||||||
|
{
|
||||||
|
if (parts == null || parts.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var clusters = ClusterParts(parts, horizontal);
|
||||||
|
|
||||||
|
if (clusters.Count < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Determine the mode (most common) cluster count, excluding the last cluster.
|
||||||
|
var mainClusters = clusters.Take(clusters.Count - 1).ToList();
|
||||||
|
var modeCount = mainClusters
|
||||||
|
.GroupBy(c => c.Count)
|
||||||
|
.OrderByDescending(g => g.Count())
|
||||||
|
.First()
|
||||||
|
.Key;
|
||||||
|
|
||||||
|
var lastCluster = clusters[clusters.Count - 1];
|
||||||
|
|
||||||
|
// Only attempt refill if the last cluster is smaller than the mode.
|
||||||
|
if (lastCluster.Count >= modeCount)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[TryStripRefill] {(horizontal ? "H" : "V")} clusters: {clusters.Count}, mode: {modeCount}, last: {lastCluster.Count}");
|
||||||
|
|
||||||
|
// Build the main parts list (everything except the last cluster).
|
||||||
|
var mainParts = clusters.Take(clusters.Count - 1).SelectMany(c => c).ToList();
|
||||||
|
var mainBox = ((IEnumerable<IBoundable>)mainParts).GetBoundingBox();
|
||||||
|
|
||||||
|
// Compute the strip box from the main grid edge to the work area edge.
|
||||||
|
Box stripBox;
|
||||||
|
|
||||||
|
if (horizontal)
|
||||||
|
{
|
||||||
|
var stripLeft = mainBox.Right + Plate.PartSpacing;
|
||||||
|
var stripWidth = workArea.Right - stripLeft;
|
||||||
|
|
||||||
|
if (stripWidth <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
stripBox = new Box(stripLeft, workArea.Y, stripWidth, workArea.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var stripBottom = mainBox.Top + Plate.PartSpacing;
|
||||||
|
var stripHeight = workArea.Top - stripBottom;
|
||||||
|
|
||||||
|
if (stripHeight <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
stripBox = new Box(workArea.X, stripBottom, workArea.Width, stripHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[TryStripRefill] Strip: {stripBox.Width:F1}x{stripBox.Length:F1} at ({stripBox.X:F1},{stripBox.Y:F1})");
|
||||||
|
|
||||||
|
var stripParts = FindBestFill(item, stripBox);
|
||||||
|
|
||||||
|
if (stripParts == null || stripParts.Count <= lastCluster.Count)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[TryStripRefill] No improvement: strip={stripParts?.Count ?? 0} vs oddball={lastCluster.Count}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"[TryStripRefill] Improvement: strip={stripParts.Count} vs oddball={lastCluster.Count}");
|
||||||
|
|
||||||
|
var combined = new List<Part>(mainParts.Count + stripParts.Count);
|
||||||
|
combined.AddRange(mainParts);
|
||||||
|
combined.AddRange(stripParts);
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Groups parts into positional clusters along the given axis.
|
||||||
|
/// Parts whose center positions are separated by more than half
|
||||||
|
/// the part dimension start a new cluster.
|
||||||
|
/// </summary>
|
||||||
|
private static List<List<Part>> ClusterParts(List<Part> parts, bool horizontal)
|
||||||
|
{
|
||||||
|
var sorted = horizontal
|
||||||
|
? parts.OrderBy(p => p.BoundingBox.Center.X).ToList()
|
||||||
|
: parts.OrderBy(p => p.BoundingBox.Center.Y).ToList();
|
||||||
|
|
||||||
|
var refDim = horizontal
|
||||||
|
? sorted.Max(p => p.BoundingBox.Width)
|
||||||
|
: sorted.Max(p => p.BoundingBox.Length);
|
||||||
|
var gapThreshold = refDim * 0.5;
|
||||||
|
|
||||||
|
var clusters = new List<List<Part>>();
|
||||||
|
var current = new List<Part> { sorted[0] };
|
||||||
|
|
||||||
|
for (var i = 1; i < sorted.Count; i++)
|
||||||
|
{
|
||||||
|
var prevCenter = horizontal
|
||||||
|
? sorted[i - 1].BoundingBox.Center.X
|
||||||
|
: sorted[i - 1].BoundingBox.Center.Y;
|
||||||
|
var currCenter = horizontal
|
||||||
|
? sorted[i].BoundingBox.Center.X
|
||||||
|
: sorted[i].BoundingBox.Center.Y;
|
||||||
|
|
||||||
|
if (currCenter - prevCenter > gapThreshold)
|
||||||
|
{
|
||||||
|
clusters.Add(current);
|
||||||
|
current = new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
current.Add(sorted[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
clusters.Add(current);
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
|
||||||
@@ -77,17 +78,16 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var bboxDim = GetDimension(partA.BoundingBox, direction);
|
var bboxDim = GetDimension(partA.BoundingBox, direction);
|
||||||
var pushDir = GetPushDirection(direction);
|
var pushDir = GetPushDirection(direction);
|
||||||
var opposite = Helper.OppositeDirection(pushDir);
|
|
||||||
|
|
||||||
var locationB = partA.Location + MakeOffset(direction, bboxDim);
|
var locationBOffset = MakeOffset(direction, bboxDim);
|
||||||
|
|
||||||
var movingLines = boundary.GetLines(locationB, pushDir);
|
// Use the most efficient array-based overload to avoid all allocations.
|
||||||
var stationaryLines = boundary.GetLines(partA.Location, opposite);
|
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||||
var slideDistance = Helper.DirectionalDistance(movingLines, stationaryLines, pushDir);
|
boundary.GetEdges(pushDir), partA.Location + locationBOffset,
|
||||||
|
boundary.GetEdges(SpatialQuery.OppositeDirection(pushDir)), partA.Location,
|
||||||
|
pushDir);
|
||||||
|
|
||||||
var copyDist = ComputeCopyDistance(bboxDim, slideDistance);
|
return ComputeCopyDistance(bboxDim, slideDistance);
|
||||||
//System.Diagnostics.Debug.WriteLine($"[FindCopyDistance] dir={direction} bboxDim={bboxDim:F4} slide={slideDistance:F4} copyDist={copyDist:F4} spacing={PartSpacing:F4} locA={partA.Location} locB={locationB} movingEdges={movingLines.Count} stationaryEdges={stationaryLines.Count}");
|
|
||||||
return copyDist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -103,11 +103,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
var bboxDim = GetDimension(patternA.BoundingBox, direction);
|
var bboxDim = GetDimension(patternA.BoundingBox, direction);
|
||||||
var pushDir = GetPushDirection(direction);
|
var pushDir = GetPushDirection(direction);
|
||||||
var opposite = Helper.OppositeDirection(pushDir);
|
var opposite = SpatialQuery.OppositeDirection(pushDir);
|
||||||
|
|
||||||
// Compute a starting offset large enough that every part-pair in
|
// Compute a starting offset large enough that every part-pair in
|
||||||
// patternB has its offset geometry beyond patternA's offset geometry.
|
// patternB has its offset geometry beyond patternA's offset geometry.
|
||||||
// max(aUpper_i - bLower_j) = max(aUpper) - min(bLower).
|
|
||||||
var maxUpper = double.MinValue;
|
var maxUpper = double.MinValue;
|
||||||
var minLower = double.MaxValue;
|
var minLower = double.MaxValue;
|
||||||
|
|
||||||
@@ -126,22 +125,28 @@ namespace OpenNest
|
|||||||
|
|
||||||
var offset = MakeOffset(direction, startOffset);
|
var offset = MakeOffset(direction, startOffset);
|
||||||
|
|
||||||
// Pre-compute stationary lines for patternA parts.
|
// Pre-cache edge arrays.
|
||||||
var stationaryCache = new List<Line>[patternA.Parts.Count];
|
var movingEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
|
||||||
|
var stationaryEdges = new (Vector start, Vector end)[patternA.Parts.Count][];
|
||||||
|
|
||||||
for (var i = 0; i < patternA.Parts.Count; i++)
|
for (var i = 0; i < patternA.Parts.Count; i++)
|
||||||
stationaryCache[i] = boundaries[i].GetLines(patternA.Parts[i].Location, opposite);
|
{
|
||||||
|
movingEdges[i] = boundaries[i].GetEdges(pushDir);
|
||||||
|
stationaryEdges[i] = boundaries[i].GetEdges(opposite);
|
||||||
|
}
|
||||||
|
|
||||||
var maxCopyDistance = 0.0;
|
var maxCopyDistance = 0.0;
|
||||||
|
|
||||||
for (var j = 0; j < patternA.Parts.Count; j++)
|
for (var j = 0; j < patternA.Parts.Count; j++)
|
||||||
{
|
{
|
||||||
var locationB = patternA.Parts[j].Location + offset;
|
var locationB = patternA.Parts[j].Location + offset;
|
||||||
var movingLines = boundaries[j].GetLines(locationB, pushDir);
|
|
||||||
|
|
||||||
for (var i = 0; i < patternA.Parts.Count; i++)
|
for (var i = 0; i < patternA.Parts.Count; i++)
|
||||||
{
|
{
|
||||||
var slideDistance = Helper.DirectionalDistance(movingLines, stationaryCache[i], pushDir);
|
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||||
|
movingEdges[j], locationB,
|
||||||
|
stationaryEdges[i], patternA.Parts[i].Location,
|
||||||
|
pushDir);
|
||||||
|
|
||||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||||
continue;
|
continue;
|
||||||
@@ -153,9 +158,7 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: if no pair interacted (shouldn't happen for real parts),
|
if (maxCopyDistance < Tolerance.Epsilon)
|
||||||
// use the simple bounding-box + spacing distance.
|
|
||||||
if (maxCopyDistance <= 0)
|
|
||||||
return bboxDim + PartSpacing;
|
return bboxDim + PartSpacing;
|
||||||
|
|
||||||
return maxCopyDistance;
|
return maxCopyDistance;
|
||||||
@@ -166,19 +169,8 @@ namespace OpenNest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary)
|
private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary)
|
||||||
{
|
{
|
||||||
var bboxDim = GetDimension(patternA.BoundingBox, direction);
|
var template = patternA.Parts[0];
|
||||||
var pushDir = GetPushDirection(direction);
|
return FindCopyDistance(template, direction, boundary);
|
||||||
var opposite = Helper.OppositeDirection(pushDir);
|
|
||||||
|
|
||||||
var offset = MakeOffset(direction, bboxDim);
|
|
||||||
|
|
||||||
var movingLines = GetOffsetPatternLines(patternA, offset, boundary, pushDir);
|
|
||||||
var stationaryLines = GetPatternLines(patternA, boundary, opposite);
|
|
||||||
var slideDistance = Helper.DirectionalDistance(movingLines, stationaryLines, pushDir);
|
|
||||||
|
|
||||||
var copyDist = ComputeCopyDistance(bboxDim, slideDistance);
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[FindSinglePartPatternCopyDist] dir={direction} bboxDim={bboxDim:F4} slide={slideDistance:F4} copyDist={copyDist:F4} spacing={PartSpacing:F4} patternParts={patternA.Parts.Count} movingEdges={movingLines.Count} stationaryEdges={stationaryLines.Count}");
|
|
||||||
return copyDist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -330,54 +322,46 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively fills the work area. At depth 0, tiles the pattern along the
|
/// Fills the work area by tiling the pattern along the primary axis to form
|
||||||
/// primary axis, then recurses perpendicular. At depth 1, tiles and returns.
|
/// a row, then tiling that row along the perpendicular axis to form a grid.
|
||||||
/// After the grid is formed, fills the remaining strip with individual parts.
|
/// After the grid is formed, fills the remaining strip with individual parts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private List<Part> FillRecursive(Pattern pattern, NestDirection direction, int depth)
|
private List<Part> FillGrid(Pattern pattern, NestDirection direction)
|
||||||
{
|
{
|
||||||
|
var perpAxis = PerpendicularAxis(direction);
|
||||||
var boundaries = CreateBoundaries(pattern);
|
var boundaries = CreateBoundaries(pattern);
|
||||||
var result = new List<Part>(pattern.Parts);
|
|
||||||
result.AddRange(TilePattern(pattern, direction, boundaries));
|
|
||||||
|
|
||||||
if (depth == 0 && result.Count > pattern.Parts.Count)
|
// Step 1: Tile along primary axis
|
||||||
|
var row = new List<Part>(pattern.Parts);
|
||||||
|
row.AddRange(TilePattern(pattern, direction, boundaries));
|
||||||
|
|
||||||
|
// If primary tiling didn't produce copies, just tile along perpendicular
|
||||||
|
if (row.Count <= pattern.Parts.Count)
|
||||||
{
|
{
|
||||||
var rowPattern = new Pattern();
|
row.AddRange(TilePattern(pattern, perpAxis, boundaries));
|
||||||
rowPattern.Parts.AddRange(result);
|
return row;
|
||||||
rowPattern.UpdateBounds();
|
|
||||||
var perpAxis = PerpendicularAxis(direction);
|
|
||||||
var gridResult = FillRecursive(rowPattern, perpAxis, depth + 1);
|
|
||||||
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[FillRecursive] Grid: {gridResult.Count} parts, rowSize={rowPattern.Parts.Count}, dir={direction}");
|
|
||||||
|
|
||||||
// Fill the remaining strip (after the last full row/column)
|
|
||||||
// with individual parts from the seed pattern.
|
|
||||||
var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction);
|
|
||||||
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[FillRecursive] Remainder: {remaining.Count} parts");
|
|
||||||
|
|
||||||
if (remaining.Count > 0)
|
|
||||||
gridResult.AddRange(remaining);
|
|
||||||
|
|
||||||
// Try one fewer row/column — the larger remainder strip may
|
|
||||||
// fit more parts than the extra row contained.
|
|
||||||
var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction);
|
|
||||||
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[FillRecursive] TryFewerRows: {fewerResult?.Count ?? -1} vs grid+remainder={gridResult.Count}");
|
|
||||||
|
|
||||||
if (fewerResult != null && fewerResult.Count > gridResult.Count)
|
|
||||||
return fewerResult;
|
|
||||||
|
|
||||||
return gridResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth == 0)
|
// Step 2: Build row pattern and tile along perpendicular axis
|
||||||
{
|
var rowPattern = new Pattern();
|
||||||
// Single part didn't tile along primary — still try perpendicular.
|
rowPattern.Parts.AddRange(row);
|
||||||
return FillRecursive(pattern, PerpendicularAxis(direction), depth + 1);
|
rowPattern.UpdateBounds();
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
var rowBoundaries = CreateBoundaries(rowPattern);
|
||||||
|
var gridResult = new List<Part>(rowPattern.Parts);
|
||||||
|
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
|
||||||
|
|
||||||
|
// Step 3: Fill remaining strip
|
||||||
|
var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction);
|
||||||
|
if (remaining.Count > 0)
|
||||||
|
gridResult.AddRange(remaining);
|
||||||
|
|
||||||
|
// Step 4: Try fewer rows optimization
|
||||||
|
var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction);
|
||||||
|
if (fewerResult != null && fewerResult.Count > gridResult.Count)
|
||||||
|
return fewerResult;
|
||||||
|
|
||||||
|
return gridResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -390,37 +374,16 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var rowPartCount = rowPattern.Parts.Count;
|
var rowPartCount = rowPattern.Parts.Count;
|
||||||
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[TryFewerRows] fullResult={fullResult.Count}, rowPartCount={rowPartCount}, tiledAxis={tiledAxis}");
|
|
||||||
|
|
||||||
// Need at least 2 rows for this to make sense (remove 1, keep 1+).
|
|
||||||
if (fullResult.Count < rowPartCount * 2)
|
if (fullResult.Count < rowPartCount * 2)
|
||||||
{
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Skipped: too few parts for 2 rows");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the last row's worth of parts.
|
|
||||||
var fewerParts = new List<Part>(fullResult.Count - rowPartCount);
|
var fewerParts = new List<Part>(fullResult.Count - rowPartCount);
|
||||||
|
|
||||||
for (var i = 0; i < fullResult.Count - rowPartCount; i++)
|
for (var i = 0; i < fullResult.Count - rowPartCount; i++)
|
||||||
fewerParts.Add(fullResult[i]);
|
fewerParts.Add(fullResult[i]);
|
||||||
|
|
||||||
// Find the top/right edge of the kept parts for logging.
|
|
||||||
var edge = double.MinValue;
|
|
||||||
foreach (var part in fewerParts)
|
|
||||||
{
|
|
||||||
var e = tiledAxis == NestDirection.Vertical
|
|
||||||
? part.BoundingBox.Top
|
|
||||||
: part.BoundingBox.Right;
|
|
||||||
if (e > edge) edge = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Kept {fewerParts.Count} parts, edge={edge:F2}, workArea={WorkArea}");
|
|
||||||
|
|
||||||
var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis);
|
var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis);
|
||||||
|
|
||||||
//System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Remainder fill: {remaining.Count} parts (need > {rowPartCount} to improve)");
|
|
||||||
|
|
||||||
if (remaining.Count <= rowPartCount)
|
if (remaining.Count <= rowPartCount)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -438,7 +401,18 @@ namespace OpenNest
|
|||||||
List<Part> placedParts, Pattern seedPattern,
|
List<Part> placedParts, Pattern seedPattern,
|
||||||
NestDirection tiledAxis, NestDirection primaryAxis)
|
NestDirection tiledAxis, NestDirection primaryAxis)
|
||||||
{
|
{
|
||||||
// Find the furthest edge of placed parts along the tiled axis.
|
var placedEdge = FindPlacedEdge(placedParts, tiledAxis);
|
||||||
|
var remainingStrip = BuildRemainingStrip(placedEdge, tiledAxis);
|
||||||
|
|
||||||
|
if (remainingStrip == null)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
var rotations = BuildRotationSet(seedPattern);
|
||||||
|
return FindBestFill(rotations, remainingStrip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double FindPlacedEdge(List<Part> placedParts, NestDirection tiledAxis)
|
||||||
|
{
|
||||||
var placedEdge = double.MinValue;
|
var placedEdge = double.MinValue;
|
||||||
|
|
||||||
foreach (var part in placedParts)
|
foreach (var part in placedParts)
|
||||||
@@ -451,18 +425,20 @@ namespace OpenNest
|
|||||||
placedEdge = edge;
|
placedEdge = edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the remaining strip with a spacing gap from the last tiled row.
|
return placedEdge;
|
||||||
Box remainingStrip;
|
}
|
||||||
|
|
||||||
|
private Box BuildRemainingStrip(double placedEdge, NestDirection tiledAxis)
|
||||||
|
{
|
||||||
if (tiledAxis == NestDirection.Vertical)
|
if (tiledAxis == NestDirection.Vertical)
|
||||||
{
|
{
|
||||||
var bottom = placedEdge + PartSpacing;
|
var bottom = placedEdge + PartSpacing;
|
||||||
var height = WorkArea.Top - bottom;
|
var height = WorkArea.Top - bottom;
|
||||||
|
|
||||||
if (height <= Tolerance.Epsilon)
|
if (height <= Tolerance.Epsilon)
|
||||||
return new List<Part>();
|
return null;
|
||||||
|
|
||||||
remainingStrip = new Box(WorkArea.X, bottom, WorkArea.Width, height);
|
return new Box(WorkArea.X, bottom, WorkArea.Width, height);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -470,18 +446,20 @@ namespace OpenNest
|
|||||||
var width = WorkArea.Right - left;
|
var width = WorkArea.Right - left;
|
||||||
|
|
||||||
if (width <= Tolerance.Epsilon)
|
if (width <= Tolerance.Epsilon)
|
||||||
return new List<Part>();
|
return null;
|
||||||
|
|
||||||
remainingStrip = new Box(left, WorkArea.Y, width, WorkArea.Length);
|
return new Box(left, WorkArea.Y, width, WorkArea.Length);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build rotation set: always try cardinal orientations (0° and 90°),
|
/// <summary>
|
||||||
// plus any unique rotations from the seed pattern.
|
/// Builds a set of (drawing, rotation) candidates: cardinal orientations
|
||||||
var filler = new FillLinear(remainingStrip, PartSpacing);
|
/// (0° and 90°) for each unique drawing, plus any seed pattern rotations
|
||||||
List<Part> best = null;
|
/// not already covered.
|
||||||
|
/// </summary>
|
||||||
|
private static List<(Drawing drawing, double rotation)> BuildRotationSet(Pattern seedPattern)
|
||||||
|
{
|
||||||
var rotations = new List<(Drawing drawing, double rotation)>();
|
var rotations = new List<(Drawing drawing, double rotation)>();
|
||||||
|
|
||||||
// Cardinal rotations for each unique drawing.
|
|
||||||
var drawings = new List<Drawing>();
|
var drawings = new List<Drawing>();
|
||||||
|
|
||||||
foreach (var seedPart in seedPattern.Parts)
|
foreach (var seedPart in seedPattern.Parts)
|
||||||
@@ -507,7 +485,6 @@ namespace OpenNest
|
|||||||
rotations.Add((drawing, Angle.HalfPI));
|
rotations.Add((drawing, Angle.HalfPI));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add seed pattern rotations that aren't already covered.
|
|
||||||
foreach (var seedPart in seedPattern.Parts)
|
foreach (var seedPart in seedPattern.Parts)
|
||||||
{
|
{
|
||||||
var skip = false;
|
var skip = false;
|
||||||
@@ -525,13 +502,22 @@ namespace OpenNest
|
|||||||
rotations.Add((seedPart.BaseDrawing, seedPart.Rotation));
|
rotations.Add((seedPart.BaseDrawing, seedPart.Rotation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return rotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries all rotation candidates in both directions in parallel, returns the
|
||||||
|
/// fill with the most parts.
|
||||||
|
/// </summary>
|
||||||
|
private List<Part> FindBestFill(List<(Drawing drawing, double rotation)> rotations, Box strip)
|
||||||
|
{
|
||||||
var bag = new System.Collections.Concurrent.ConcurrentBag<List<Part>>();
|
var bag = new System.Collections.Concurrent.ConcurrentBag<List<Part>>();
|
||||||
|
|
||||||
System.Threading.Tasks.Parallel.ForEach(rotations, entry =>
|
Parallel.ForEach(rotations, entry =>
|
||||||
{
|
{
|
||||||
var localFiller = new FillLinear(remainingStrip, PartSpacing);
|
var filler = new FillLinear(strip, PartSpacing);
|
||||||
var h = localFiller.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal);
|
var h = filler.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal);
|
||||||
var v = localFiller.Fill(entry.drawing, entry.rotation, NestDirection.Vertical);
|
var v = filler.Fill(entry.drawing, entry.rotation, NestDirection.Vertical);
|
||||||
|
|
||||||
if (h != null && h.Count > 0)
|
if (h != null && h.Count > 0)
|
||||||
bag.Add(h);
|
bag.Add(h);
|
||||||
@@ -540,6 +526,8 @@ namespace OpenNest
|
|||||||
bag.Add(v);
|
bag.Add(v);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
List<Part> best = null;
|
||||||
|
|
||||||
foreach (var candidate in bag)
|
foreach (var candidate in bag)
|
||||||
{
|
{
|
||||||
if (best == null || candidate.Count > best.Count)
|
if (best == null || candidate.Count > best.Count)
|
||||||
@@ -604,7 +592,7 @@ namespace OpenNest
|
|||||||
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
|
|
||||||
return FillRecursive(basePattern, primaryAxis, depth: 0);
|
return FillGrid(basePattern, primaryAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -618,7 +606,7 @@ namespace OpenNest
|
|||||||
if (seed.Parts.Count == 0)
|
if (seed.Parts.Count == 0)
|
||||||
return new List<Part>();
|
return new List<Part>();
|
||||||
|
|
||||||
return FillRecursive(seed, primaryAxis, depth: 0);
|
return FillGrid(seed, primaryAxis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,39 +41,32 @@ namespace OpenNest
|
|||||||
return default;
|
return default;
|
||||||
|
|
||||||
var totalPartArea = 0.0;
|
var totalPartArea = 0.0;
|
||||||
|
var minX = double.MaxValue;
|
||||||
|
var minY = double.MaxValue;
|
||||||
|
var maxX = double.MinValue;
|
||||||
|
var maxY = double.MinValue;
|
||||||
|
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
totalPartArea += part.BaseDrawing.Area;
|
totalPartArea += part.BaseDrawing.Area;
|
||||||
|
var bb = part.BoundingBox;
|
||||||
|
|
||||||
var bbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
if (bb.Left < minX) minX = bb.Left;
|
||||||
var bboxArea = bbox.Area();
|
if (bb.Bottom < minY) minY = bb.Bottom;
|
||||||
|
if (bb.Right > maxX) maxX = bb.Right;
|
||||||
|
if (bb.Top > maxY) maxY = bb.Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bboxArea = (maxX - minX) * (maxY - minY);
|
||||||
var density = bboxArea > 0 ? totalPartArea / bboxArea : 0;
|
var density = bboxArea > 0 ? totalPartArea / bboxArea : 0;
|
||||||
|
|
||||||
var usableRemnantArea = ComputeUsableRemnantArea(parts, workArea);
|
var usableRemnantArea = ComputeUsableRemnantArea(maxX, maxY, workArea);
|
||||||
|
|
||||||
return new FillScore(parts.Count, usableRemnantArea, density);
|
return new FillScore(parts.Count, usableRemnantArea, density);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static double ComputeUsableRemnantArea(double maxRight, double maxTop, Box workArea)
|
||||||
/// Finds the largest usable remnant (short side >= MinRemnantDimension)
|
|
||||||
/// by checking right and top edge strips between placed parts and the work area boundary.
|
|
||||||
/// </summary>
|
|
||||||
private static double ComputeUsableRemnantArea(List<Part> parts, Box workArea)
|
|
||||||
{
|
{
|
||||||
var maxRight = double.MinValue;
|
|
||||||
var maxTop = double.MinValue;
|
|
||||||
|
|
||||||
foreach (var part in parts)
|
|
||||||
{
|
|
||||||
var bb = part.BoundingBox;
|
|
||||||
|
|
||||||
if (bb.Right > maxRight)
|
|
||||||
maxRight = bb.Right;
|
|
||||||
|
|
||||||
if (bb.Top > maxTop)
|
|
||||||
maxTop = bb.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
var largest = 0.0;
|
var largest = 0.0;
|
||||||
|
|
||||||
// Right strip
|
// Right strip
|
||||||
|
|||||||
119
OpenNest.Engine/ML/AnglePredictor.cs
Normal file
119
OpenNest.Engine/ML/AnglePredictor.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
public static class AnglePredictor
|
||||||
|
{
|
||||||
|
private static InferenceSession _session;
|
||||||
|
private static volatile bool _loadAttempted;
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
public static List<double> PredictAngles(
|
||||||
|
PartFeatures features, double sheetWidth, double sheetHeight,
|
||||||
|
double threshold = 0.3)
|
||||||
|
{
|
||||||
|
var session = GetSession();
|
||||||
|
if (session == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var input = new float[11];
|
||||||
|
input[0] = (float)features.Area;
|
||||||
|
input[1] = (float)features.Convexity;
|
||||||
|
input[2] = (float)features.AspectRatio;
|
||||||
|
input[3] = (float)features.BoundingBoxFill;
|
||||||
|
input[4] = (float)features.Circularity;
|
||||||
|
input[5] = (float)features.PerimeterToAreaRatio;
|
||||||
|
input[6] = features.VertexCount;
|
||||||
|
input[7] = (float)sheetWidth;
|
||||||
|
input[8] = (float)sheetHeight;
|
||||||
|
input[9] = (float)(sheetWidth / (sheetHeight > 0 ? sheetHeight : 1.0));
|
||||||
|
input[10] = (float)(features.Area / (sheetWidth * sheetHeight));
|
||||||
|
|
||||||
|
var tensor = new DenseTensor<float>(input, new[] { 1, 11 });
|
||||||
|
var inputs = new List<NamedOnnxValue>
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor("features", tensor)
|
||||||
|
};
|
||||||
|
|
||||||
|
using var results = session.Run(inputs);
|
||||||
|
var probabilities = results.First().AsEnumerable<float>().ToArray();
|
||||||
|
|
||||||
|
var angles = new List<(double angleDeg, float prob)>();
|
||||||
|
for (var i = 0; i < 36 && i < probabilities.Length; i++)
|
||||||
|
{
|
||||||
|
if (probabilities[i] >= threshold)
|
||||||
|
angles.Add((i * 5.0, probabilities[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimum 3 angles — take top by probability if fewer pass threshold.
|
||||||
|
if (angles.Count < 3)
|
||||||
|
{
|
||||||
|
angles = probabilities
|
||||||
|
.Select((p, i) => (angleDeg: i * 5.0, prob: p))
|
||||||
|
.OrderByDescending(x => x.prob)
|
||||||
|
.Take(3)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always include 0 and 90 as safety fallback.
|
||||||
|
var result = angles.Select(a => Angle.ToRadians(a.angleDeg)).ToList();
|
||||||
|
|
||||||
|
if (!result.Any(a => a.IsEqualTo(0)))
|
||||||
|
result.Add(0);
|
||||||
|
if (!result.Any(a => a.IsEqualTo(Angle.HalfPI)))
|
||||||
|
result.Add(Angle.HalfPI);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[AnglePredictor] Inference failed: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InferenceSession GetSession()
|
||||||
|
{
|
||||||
|
if (_loadAttempted)
|
||||||
|
return _session;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_loadAttempted)
|
||||||
|
return _session;
|
||||||
|
|
||||||
|
_loadAttempted = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(typeof(AnglePredictor).Assembly.Location);
|
||||||
|
var modelPath = Path.Combine(dir, "Models", "angle_predictor.onnx");
|
||||||
|
|
||||||
|
if (!File.Exists(modelPath))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[AnglePredictor] Model not found: {modelPath}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_session = new InferenceSession(modelPath);
|
||||||
|
Debug.WriteLine("[AnglePredictor] Model loaded successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[AnglePredictor] Failed to load model: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
OpenNest.Engine/ML/BruteForceRunner.cs
Normal file
80
OpenNest.Engine/ML/BruteForceRunner.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.ML
|
||||||
|
{
|
||||||
|
public class BruteForceResult
|
||||||
|
{
|
||||||
|
public int PartCount { get; set; }
|
||||||
|
public double Utilization { get; set; }
|
||||||
|
public long TimeMs { get; set; }
|
||||||
|
public string LayoutData { get; set; }
|
||||||
|
public List<Part> PlacedParts { get; set; }
|
||||||
|
public string WinnerEngine { get; set; } = "";
|
||||||
|
public long WinnerTimeMs { get; set; }
|
||||||
|
public string RunnerUpEngine { get; set; } = "";
|
||||||
|
public int RunnerUpPartCount { get; set; }
|
||||||
|
public long RunnerUpTimeMs { get; set; }
|
||||||
|
public string ThirdPlaceEngine { get; set; } = "";
|
||||||
|
public int ThirdPlacePartCount { get; set; }
|
||||||
|
public long ThirdPlaceTimeMs { get; set; }
|
||||||
|
public List<AngleResult> AngleResults { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BruteForceRunner
|
||||||
|
{
|
||||||
|
public static BruteForceResult Run(Drawing drawing, Plate plate, bool forceFullAngleSweep = false)
|
||||||
|
{
|
||||||
|
var engine = new DefaultNestEngine(plate);
|
||||||
|
engine.ForceFullAngleSweep = forceFullAngleSweep;
|
||||||
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Rank phase results — winner is explicit, runners-up sorted by count.
|
||||||
|
var winner = engine.PhaseResults
|
||||||
|
.FirstOrDefault(r => r.Phase == engine.WinnerPhase);
|
||||||
|
var runnerUps = engine.PhaseResults
|
||||||
|
.Where(r => r.PartCount > 0 && r.Phase != engine.WinnerPhase)
|
||||||
|
.OrderByDescending(r => r.PartCount)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new BruteForceResult
|
||||||
|
{
|
||||||
|
PartCount = parts.Count,
|
||||||
|
Utilization = CalculateUtilization(parts, plate.Area()),
|
||||||
|
TimeMs = sw.ElapsedMilliseconds,
|
||||||
|
LayoutData = SerializeLayout(parts),
|
||||||
|
PlacedParts = parts,
|
||||||
|
WinnerEngine = engine.WinnerPhase.ToString(),
|
||||||
|
WinnerTimeMs = winner?.TimeMs ?? 0,
|
||||||
|
RunnerUpEngine = runnerUps.Count > 0 ? runnerUps[0].Phase.ToString() : "",
|
||||||
|
RunnerUpPartCount = runnerUps.Count > 0 ? runnerUps[0].PartCount : 0,
|
||||||
|
RunnerUpTimeMs = runnerUps.Count > 0 ? runnerUps[0].TimeMs : 0,
|
||||||
|
ThirdPlaceEngine = runnerUps.Count > 1 ? runnerUps[1].Phase.ToString() : "",
|
||||||
|
ThirdPlacePartCount = runnerUps.Count > 1 ? runnerUps[1].PartCount : 0,
|
||||||
|
ThirdPlaceTimeMs = runnerUps.Count > 1 ? runnerUps[1].TimeMs : 0,
|
||||||
|
AngleResults = engine.AngleResults.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SerializeLayout(List<Part> parts)
|
||||||
|
{
|
||||||
|
var data = parts.Select(p => new { X = p.Location.X, Y = p.Location.Y, R = p.Rotation }).ToList();
|
||||||
|
return System.Text.Json.JsonSerializer.Serialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double CalculateUtilization(List<Part> parts, double plateArea)
|
||||||
|
{
|
||||||
|
if (plateArea <= 0) return 0;
|
||||||
|
return parts.Sum(p => p.BaseDrawing.Area) / plateArea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
OpenNest.Engine/ML/FeatureExtractor.cs
Normal file
90
OpenNest.Engine/ML/FeatureExtractor.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.ML
|
||||||
|
{
|
||||||
|
public class PartFeatures
|
||||||
|
{
|
||||||
|
// --- Geometric Features ---
|
||||||
|
public double Area { get; set; }
|
||||||
|
public double Convexity { get; set; } // Area / Convex Hull Area
|
||||||
|
public double AspectRatio { get; set; } // Width / Length
|
||||||
|
public double BoundingBoxFill { get; set; } // Area / (Width * Length)
|
||||||
|
public double Circularity { get; set; } // 4 * PI * Area / Perimeter^2
|
||||||
|
public double PerimeterToAreaRatio { get; set; } // Perimeter / Area — spacing sensitivity
|
||||||
|
public int VertexCount { get; set; }
|
||||||
|
|
||||||
|
// --- Normalized Bitmask (32x32 = 1024 features) ---
|
||||||
|
public byte[] Bitmask { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Area:F2},{Convexity:F4},{AspectRatio:F4},{BoundingBoxFill:F4},{Circularity:F4},{PerimeterToAreaRatio:F4},{VertexCount}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FeatureExtractor
|
||||||
|
{
|
||||||
|
public static PartFeatures Extract(Drawing drawing)
|
||||||
|
{
|
||||||
|
var entities = OpenNest.Converters.ConvertProgram.ToGeometry(drawing.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var profile = new ShapeProfile(entities);
|
||||||
|
var perimeter = profile.Perimeter;
|
||||||
|
|
||||||
|
if (perimeter == null) return null;
|
||||||
|
|
||||||
|
var polygon = perimeter.ToPolygonWithTolerance(0.01);
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
var bb = polygon.BoundingBox;
|
||||||
|
|
||||||
|
var hull = ConvexHull.Compute(polygon.Vertices);
|
||||||
|
var hullArea = hull.Area();
|
||||||
|
|
||||||
|
var features = new PartFeatures
|
||||||
|
{
|
||||||
|
Area = drawing.Area,
|
||||||
|
Convexity = drawing.Area / (hullArea > 0 ? hullArea : 1.0),
|
||||||
|
AspectRatio = bb.Width / (bb.Length > 0 ? bb.Length : 1.0),
|
||||||
|
BoundingBoxFill = drawing.Area / (bb.Area() > 0 ? bb.Area() : 1.0),
|
||||||
|
VertexCount = polygon.Vertices.Count,
|
||||||
|
Bitmask = GenerateBitmask(polygon, 32)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Circularity = 4 * PI * Area / Perimeter^2
|
||||||
|
var perimeterLen = polygon.Perimeter();
|
||||||
|
features.Circularity = (4 * System.Math.PI * drawing.Area) / (perimeterLen * perimeterLen);
|
||||||
|
features.PerimeterToAreaRatio = drawing.Area > 0 ? perimeterLen / drawing.Area : 0;
|
||||||
|
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GenerateBitmask(Polygon polygon, int size)
|
||||||
|
{
|
||||||
|
var mask = new byte[size * size];
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
var bb = polygon.BoundingBox;
|
||||||
|
|
||||||
|
for (int y = 0; y < size; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < size; x++)
|
||||||
|
{
|
||||||
|
// Map grid coordinate (0..size) to bounding box coordinate
|
||||||
|
var px = bb.Left + (x + 0.5) * (bb.Width / size);
|
||||||
|
var py = bb.Bottom + (y + 0.5) * (bb.Length / size);
|
||||||
|
|
||||||
|
if (polygon.ContainsPoint(new Vector(px, py)))
|
||||||
|
{
|
||||||
|
mask[y * size + x] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
OpenNest.Engine/Models/.gitkeep
Normal file
0
OpenNest.Engine/Models/.gitkeep
Normal file
File diff suppressed because it is too large
Load Diff
319
OpenNest.Engine/NestEngineBase.cs
Normal file
319
OpenNest.Engine/NestEngineBase.cs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public abstract class NestEngineBase
|
||||||
|
{
|
||||||
|
protected NestEngineBase(Plate plate)
|
||||||
|
{
|
||||||
|
Plate = plate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plate Plate { get; set; }
|
||||||
|
|
||||||
|
public int PlateNumber { get; set; }
|
||||||
|
|
||||||
|
public NestDirection NestDirection { get; set; }
|
||||||
|
|
||||||
|
public NestPhase WinnerPhase { get; protected set; }
|
||||||
|
|
||||||
|
public List<PhaseResult> PhaseResults { get; } = new();
|
||||||
|
|
||||||
|
public List<AngleResult> AngleResults { get; } = new();
|
||||||
|
|
||||||
|
public abstract string Name { get; }
|
||||||
|
|
||||||
|
public abstract string Description { get; }
|
||||||
|
|
||||||
|
// --- Virtual methods (side-effect-free, return parts) ---
|
||||||
|
|
||||||
|
public virtual List<Part> Fill(NestItem item, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
return new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
return new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual List<Part> PackArea(Box box, List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
return new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Nest: multi-item strategy (virtual, side-effect-free) ---
|
||||||
|
|
||||||
|
public virtual List<Part> Nest(List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
var workArea = Plate.WorkArea();
|
||||||
|
var allParts = new List<Part>();
|
||||||
|
|
||||||
|
var fillItems = items
|
||||||
|
.Where(i => i.Quantity != 1)
|
||||||
|
.OrderBy(i => i.Priority)
|
||||||
|
.ThenByDescending(i => i.Drawing.Area)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var packItems = items
|
||||||
|
.Where(i => i.Quantity == 1)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Phase 1: Fill multi-quantity drawings sequentially.
|
||||||
|
foreach (var item in fillItems)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (item.Quantity <= 0 || workArea.Width <= 0 || workArea.Length <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var parts = FillExact(
|
||||||
|
new NestItem { Drawing = item.Drawing, Quantity = item.Quantity },
|
||||||
|
workArea, progress, token);
|
||||||
|
|
||||||
|
if (parts.Count > 0)
|
||||||
|
{
|
||||||
|
allParts.AddRange(parts);
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||||
|
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
|
||||||
|
workArea = ComputeRemainderWithin(workArea, placedBox, Plate.PartSpacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Pack single-quantity items into remaining space.
|
||||||
|
packItems = packItems.Where(i => i.Quantity > 0).ToList();
|
||||||
|
|
||||||
|
if (packItems.Count > 0 && workArea.Width > 0 && workArea.Length > 0
|
||||||
|
&& !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var packParts = PackArea(workArea, packItems, progress, token);
|
||||||
|
|
||||||
|
if (packParts.Count > 0)
|
||||||
|
{
|
||||||
|
allParts.AddRange(packParts);
|
||||||
|
|
||||||
|
foreach (var item in packItems)
|
||||||
|
{
|
||||||
|
var placed = packParts.Count(p =>
|
||||||
|
p.BaseDrawing.Name == item.Drawing.Name);
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Box ComputeRemainderWithin(Box workArea, Box usedBox, double spacing)
|
||||||
|
{
|
||||||
|
var hWidth = workArea.Right - usedBox.Right - spacing;
|
||||||
|
var hStrip = hWidth > 0
|
||||||
|
? new Box(usedBox.Right + spacing, workArea.Y, hWidth, workArea.Length)
|
||||||
|
: Box.Empty;
|
||||||
|
|
||||||
|
var vHeight = workArea.Top - usedBox.Top - spacing;
|
||||||
|
var vStrip = vHeight > 0
|
||||||
|
? new Box(workArea.X, usedBox.Top + spacing, workArea.Width, vHeight)
|
||||||
|
: Box.Empty;
|
||||||
|
|
||||||
|
return hStrip.Area() >= vStrip.Area() ? hStrip : vStrip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FillExact (non-virtual, delegates to virtual Fill) ---
|
||||||
|
|
||||||
|
public List<Part> FillExact(NestItem item, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
return Fill(item, workArea, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Convenience overloads (mutate plate, return bool) ---
|
||||||
|
|
||||||
|
public bool Fill(NestItem item)
|
||||||
|
{
|
||||||
|
return Fill(item, Plate.WorkArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Fill(NestItem item, Box workArea)
|
||||||
|
{
|
||||||
|
var parts = Fill(item, workArea, null, CancellationToken.None);
|
||||||
|
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Plate.Parts.AddRange(parts);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Fill(List<Part> groupParts)
|
||||||
|
{
|
||||||
|
return Fill(groupParts, Plate.WorkArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Fill(List<Part> groupParts, Box workArea)
|
||||||
|
{
|
||||||
|
var parts = Fill(groupParts, workArea, null, CancellationToken.None);
|
||||||
|
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Plate.Parts.AddRange(parts);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Pack(List<NestItem> items)
|
||||||
|
{
|
||||||
|
var workArea = Plate.WorkArea();
|
||||||
|
var parts = PackArea(workArea, items, null, CancellationToken.None);
|
||||||
|
|
||||||
|
if (parts == null || parts.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Plate.Parts.AddRange(parts);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Protected utilities ---
|
||||||
|
|
||||||
|
protected static void ReportProgress(
|
||||||
|
IProgress<NestProgress> progress,
|
||||||
|
NestPhase phase,
|
||||||
|
int plateNumber,
|
||||||
|
List<Part> best,
|
||||||
|
Box workArea,
|
||||||
|
string description)
|
||||||
|
{
|
||||||
|
if (progress == null || best == null || best.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var score = FillScore.Compute(best, workArea);
|
||||||
|
var clonedParts = new List<Part>(best.Count);
|
||||||
|
var totalPartArea = 0.0;
|
||||||
|
|
||||||
|
foreach (var part in best)
|
||||||
|
{
|
||||||
|
clonedParts.Add((Part)part.Clone());
|
||||||
|
totalPartArea += part.BaseDrawing.Area;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = best.GetBoundingBox();
|
||||||
|
|
||||||
|
var msg = $"[Progress] Phase={phase}, Plate={plateNumber}, Parts={score.Count}, " +
|
||||||
|
$"Density={score.Density:P1}, Nested={bounds.Width:F1}x{bounds.Length:F1}, " +
|
||||||
|
$"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(
|
||||||
|
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"),
|
||||||
|
$"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); } catch { }
|
||||||
|
|
||||||
|
progress.Report(new NestProgress
|
||||||
|
{
|
||||||
|
Phase = phase,
|
||||||
|
PlateNumber = plateNumber,
|
||||||
|
BestPartCount = score.Count,
|
||||||
|
BestDensity = score.Density,
|
||||||
|
NestedWidth = bounds.Width,
|
||||||
|
NestedLength = bounds.Length,
|
||||||
|
NestedArea = totalPartArea,
|
||||||
|
UsableRemnantArea = workArea.Area() - totalPartArea,
|
||||||
|
BestParts = clonedParts,
|
||||||
|
Description = description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string BuildProgressSummary()
|
||||||
|
{
|
||||||
|
if (PhaseResults.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var parts = new List<string>(PhaseResults.Count);
|
||||||
|
|
||||||
|
foreach (var r in PhaseResults)
|
||||||
|
parts.Add($"{FormatPhaseName(r.Phase)}: {r.PartCount}");
|
||||||
|
|
||||||
|
return string.Join(" | ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea)
|
||||||
|
{
|
||||||
|
if (candidate == null || candidate.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (current == null || current.Count == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsBetterValidFill(List<Part> candidate, List<Part> current, Box workArea)
|
||||||
|
{
|
||||||
|
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsBetterFill(candidate, current, workArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static bool HasOverlaps(List<Part> parts, double spacing)
|
||||||
|
{
|
||||||
|
if (parts == null || parts.Count <= 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var box1 = parts[i].BoundingBox;
|
||||||
|
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
var box2 = parts[j].BoundingBox;
|
||||||
|
|
||||||
|
if (box1.Right < box2.Left || box2.Right < box1.Left ||
|
||||||
|
box1.Top < box2.Bottom || box2.Top < box1.Bottom)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
List<Vector> pts;
|
||||||
|
|
||||||
|
if (parts[i].Intersects(parts[j], out pts))
|
||||||
|
{
|
||||||
|
var b1 = parts[i].BoundingBox;
|
||||||
|
var b2 = parts[j].BoundingBox;
|
||||||
|
Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" +
|
||||||
|
$" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" +
|
||||||
|
$" intersections={pts?.Count ?? 0}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static string FormatPhaseName(NestPhase phase)
|
||||||
|
{
|
||||||
|
switch (phase)
|
||||||
|
{
|
||||||
|
case NestPhase.Pairs: return "Pairs";
|
||||||
|
case NestPhase.Linear: return "Linear";
|
||||||
|
case NestPhase.RectBestFit: return "BestFit";
|
||||||
|
case NestPhase.Remainder: return "Remainder";
|
||||||
|
default: return phase.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
OpenNest.Engine/NestEngineInfo.cs
Normal file
18
OpenNest.Engine/NestEngineInfo.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class NestEngineInfo
|
||||||
|
{
|
||||||
|
public NestEngineInfo(string name, string description, Func<Plate, NestEngineBase> factory)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
Factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public Func<Plate, NestEngineBase> Factory { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
100
OpenNest.Engine/NestEngineRegistry.cs
Normal file
100
OpenNest.Engine/NestEngineRegistry.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public static class NestEngineRegistry
|
||||||
|
{
|
||||||
|
private static readonly List<NestEngineInfo> engines = new();
|
||||||
|
|
||||||
|
static NestEngineRegistry()
|
||||||
|
{
|
||||||
|
Register("Default",
|
||||||
|
"Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
||||||
|
plate => new DefaultNestEngine(plate));
|
||||||
|
|
||||||
|
Register("Strip",
|
||||||
|
"Strip-based nesting for mixed-drawing layouts",
|
||||||
|
plate => new StripNestEngine(plate));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<NestEngineInfo> AvailableEngines => engines;
|
||||||
|
|
||||||
|
public static string ActiveEngineName { get; set; } = "Default";
|
||||||
|
|
||||||
|
public static NestEngineBase Create(Plate plate)
|
||||||
|
{
|
||||||
|
var info = engines.FirstOrDefault(e =>
|
||||||
|
e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default");
|
||||||
|
info = engines[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.Factory(plate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Register(string name, string description, Func<Plate, NestEngineBase> factory)
|
||||||
|
{
|
||||||
|
if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestEngineRegistry] Duplicate engine '{name}' skipped");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
engines.Add(new NestEngineInfo(name, description, factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LoadPlugins(string directory)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var dll in Directory.GetFiles(directory, "*.dll"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.LoadFrom(dll);
|
||||||
|
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (type.IsAbstract || !typeof(NestEngineBase).IsAssignableFrom(type))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var ctor = type.GetConstructor(new[] { typeof(Plate) });
|
||||||
|
|
||||||
|
if (ctor == null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary instance to read Name and Description.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tempPlate = new Plate();
|
||||||
|
var instance = (NestEngineBase)ctor.Invoke(new object[] { tempPlate });
|
||||||
|
Register(instance.Name, instance.Description,
|
||||||
|
plate => (NestEngineBase)ctor.Invoke(new object[] { plate }));
|
||||||
|
Debug.WriteLine($"[NestEngineRegistry] Loaded plugin engine: {instance.Name}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,38 @@ namespace OpenNest
|
|||||||
Remainder
|
Remainder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PhaseResult
|
||||||
|
{
|
||||||
|
public NestPhase Phase { get; set; }
|
||||||
|
public int PartCount { get; set; }
|
||||||
|
public long TimeMs { get; set; }
|
||||||
|
|
||||||
|
public PhaseResult(NestPhase phase, int partCount, long timeMs)
|
||||||
|
{
|
||||||
|
Phase = phase;
|
||||||
|
PartCount = partCount;
|
||||||
|
TimeMs = timeMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AngleResult
|
||||||
|
{
|
||||||
|
public double AngleDeg { get; set; }
|
||||||
|
public NestDirection Direction { get; set; }
|
||||||
|
public int PartCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class NestProgress
|
public class NestProgress
|
||||||
{
|
{
|
||||||
public NestPhase Phase { get; set; }
|
public NestPhase Phase { get; set; }
|
||||||
public int PlateNumber { get; set; }
|
public int PlateNumber { get; set; }
|
||||||
public int BestPartCount { get; set; }
|
public int BestPartCount { get; set; }
|
||||||
public double BestDensity { get; set; }
|
public double BestDensity { get; set; }
|
||||||
|
public double NestedWidth { get; set; }
|
||||||
|
public double NestedLength { get; set; }
|
||||||
|
public double NestedArea { get; set; }
|
||||||
public double UsableRemnantArea { get; set; }
|
public double UsableRemnantArea { get; set; }
|
||||||
public List<Part> BestParts { get; set; }
|
public List<Part> BestParts { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.17.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Models\**" CopyToOutputDirectory="PreserveNewest" Condition="Exists('Models')" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -23,22 +23,26 @@ namespace OpenNest
|
|||||||
|
|
||||||
public PartBoundary(Part part, double spacing)
|
public PartBoundary(Part part, double spacing)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
var shapes = Helper.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var definedShape = new ShapeProfile(entities);
|
||||||
|
var perimeter = definedShape.Perimeter;
|
||||||
_polygons = new List<Polygon>();
|
_polygons = new List<Polygon>();
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
if (perimeter != null)
|
||||||
{
|
{
|
||||||
var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
var offsetEntity = perimeter.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
if (offsetEntity == null)
|
if (offsetEntity != null)
|
||||||
continue;
|
{
|
||||||
|
// Circumscribe arcs so polygon vertices are always outside
|
||||||
// Circumscribe arcs so polygon vertices are always outside
|
// the true arc — guarantees the boundary never under-estimates.
|
||||||
// the true arc — guarantees the boundary never under-estimates.
|
var polygon = offsetEntity.ToPolygonWithTolerance(PolygonTolerance, circumscribe: true);
|
||||||
var polygon = offsetEntity.ToPolygonWithTolerance(PolygonTolerance, circumscribe: true);
|
polygon.RemoveSelfIntersections();
|
||||||
polygon.RemoveSelfIntersections();
|
_polygons.Add(polygon);
|
||||||
_polygons.Add(polygon);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PrecomputeDirectionalEdges(
|
PrecomputeDirectionalEdges(
|
||||||
@@ -89,10 +93,10 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
leftEdges = left.ToArray();
|
leftEdges = left.OrderBy(e => System.Math.Min(e.Item1.Y, e.Item2.Y)).ToArray();
|
||||||
rightEdges = right.ToArray();
|
rightEdges = right.OrderBy(e => System.Math.Min(e.Item1.Y, e.Item2.Y)).ToArray();
|
||||||
upEdges = up.ToArray();
|
upEdges = up.OrderBy(e => System.Math.Min(e.Item1.X, e.Item2.X)).ToArray();
|
||||||
downEdges = down.ToArray();
|
downEdges = down.OrderBy(e => System.Math.Min(e.Item1.X, e.Item2.X)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -148,5 +152,14 @@ namespace OpenNest
|
|||||||
default: return _leftEdges;
|
default: return _leftEdges;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the pre-computed edge arrays for the given direction.
|
||||||
|
/// These are in part-local coordinates (no translation applied).
|
||||||
|
/// </summary>
|
||||||
|
public (Vector start, Vector end)[] GetEdges(PushDirection direction)
|
||||||
|
{
|
||||||
|
return GetDirectionalEdges(direction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
OpenNest.Engine/PlateProcessor.cs
Normal file
120
OpenNest.Engine/PlateProcessor.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine
|
||||||
|
{
|
||||||
|
public class PlateProcessor
|
||||||
|
{
|
||||||
|
public IPartSequencer Sequencer { get; set; }
|
||||||
|
public ContourCuttingStrategy CuttingStrategy { get; set; }
|
||||||
|
public IRapidPlanner RapidPlanner { get; set; }
|
||||||
|
|
||||||
|
public PlateResult Process(Plate plate)
|
||||||
|
{
|
||||||
|
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
var results = new List<ProcessedPart>(sequenced.Count);
|
||||||
|
var cutAreas = new List<Shape>();
|
||||||
|
var currentPoint = PlateHelper.GetExitPoint(plate);
|
||||||
|
|
||||||
|
foreach (var sp in sequenced)
|
||||||
|
{
|
||||||
|
var part = sp.Part;
|
||||||
|
|
||||||
|
// Compute approach point in part-local space
|
||||||
|
var localApproach = ToPartLocal(currentPoint, part);
|
||||||
|
|
||||||
|
Program processedProgram;
|
||||||
|
Vector lastCutLocal;
|
||||||
|
|
||||||
|
if (!part.HasManualLeadIns && CuttingStrategy != null)
|
||||||
|
{
|
||||||
|
var cuttingResult = CuttingStrategy.Apply(part.Program, localApproach);
|
||||||
|
processedProgram = cuttingResult.Program;
|
||||||
|
lastCutLocal = cuttingResult.LastCutPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
processedProgram = part.Program;
|
||||||
|
lastCutLocal = GetProgramEndPoint(part.Program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pierce point: program start point in plate space
|
||||||
|
var pierceLocal = GetProgramStartPoint(part.Program);
|
||||||
|
var piercePoint = ToPlateSpace(pierceLocal, part);
|
||||||
|
|
||||||
|
// Plan rapid from currentPoint to pierce point
|
||||||
|
var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas);
|
||||||
|
|
||||||
|
results.Add(new ProcessedPart
|
||||||
|
{
|
||||||
|
Part = part,
|
||||||
|
ProcessedProgram = processedProgram,
|
||||||
|
RapidPath = rapidPath
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update cut areas with part perimeter
|
||||||
|
var perimeter = GetPartPerimeter(part);
|
||||||
|
if (perimeter != null)
|
||||||
|
cutAreas.Add(perimeter);
|
||||||
|
|
||||||
|
// Update current point to last cut point in plate space
|
||||||
|
currentPoint = ToPlateSpace(lastCutLocal, part);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlateResult { Parts = results };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector ToPartLocal(Vector platePoint, Part part)
|
||||||
|
{
|
||||||
|
return platePoint - part.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector ToPlateSpace(Vector localPoint, Part part)
|
||||||
|
{
|
||||||
|
return localPoint + part.Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetProgramStartPoint(Program program)
|
||||||
|
{
|
||||||
|
if (program.Codes.Count == 0)
|
||||||
|
return Vector.Zero;
|
||||||
|
|
||||||
|
var first = program.Codes[0];
|
||||||
|
if (first is Motion motion)
|
||||||
|
return motion.EndPoint;
|
||||||
|
|
||||||
|
return Vector.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetProgramEndPoint(Program program)
|
||||||
|
{
|
||||||
|
for (var i = program.Codes.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (program.Codes[i] is Motion motion)
|
||||||
|
return motion.EndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Vector.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Shape GetPartPerimeter(Part part)
|
||||||
|
{
|
||||||
|
var entities = part.Program.ToGeometry();
|
||||||
|
if (entities == null || entities.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var profile = new ShapeProfile(entities);
|
||||||
|
var perimeter = profile.Perimeter;
|
||||||
|
if (perimeter == null || perimeter.Entities.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
perimeter.Offset(part.Location);
|
||||||
|
return perimeter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
OpenNest.Engine/PlateResult.cs
Normal file
18
OpenNest.Engine/PlateResult.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine
|
||||||
|
{
|
||||||
|
public class PlateResult
|
||||||
|
{
|
||||||
|
public List<ProcessedPart> Parts { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ProcessedPart
|
||||||
|
{
|
||||||
|
public Part Part { get; init; }
|
||||||
|
public Program ProcessedProgram { get; init; }
|
||||||
|
public RapidPath RapidPath { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
44
OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs
Normal file
44
OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public class DirectRapidPlanner : IRapidPlanner
|
||||||
|
{
|
||||||
|
public RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas)
|
||||||
|
{
|
||||||
|
var travelLine = new Line(from, to);
|
||||||
|
|
||||||
|
foreach (var cutArea in cutAreas)
|
||||||
|
{
|
||||||
|
if (TravelLineIntersectsShape(travelLine, cutArea))
|
||||||
|
{
|
||||||
|
return new RapidPath
|
||||||
|
{
|
||||||
|
HeadUp = true,
|
||||||
|
Waypoints = new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RapidPath
|
||||||
|
{
|
||||||
|
HeadUp = false,
|
||||||
|
Waypoints = new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TravelLineIntersectsShape(Line travelLine, Shape shape)
|
||||||
|
{
|
||||||
|
foreach (var entity in shape.Entities)
|
||||||
|
{
|
||||||
|
if (entity is Line edge)
|
||||||
|
{
|
||||||
|
if (travelLine.Intersects(edge, out _))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
OpenNest.Engine/RapidPlanning/IRapidPlanner.cs
Normal file
10
OpenNest.Engine/RapidPlanning/IRapidPlanner.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public interface IRapidPlanner
|
||||||
|
{
|
||||||
|
RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
OpenNest.Engine/RapidPlanning/RapidPath.cs
Normal file
11
OpenNest.Engine/RapidPlanning/RapidPath.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public readonly struct RapidPath
|
||||||
|
{
|
||||||
|
public bool HeadUp { get; init; }
|
||||||
|
public List<Vector> Waypoints { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
17
OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs
Normal file
17
OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.RapidPlanning
|
||||||
|
{
|
||||||
|
public class SafeHeightRapidPlanner : IRapidPlanner
|
||||||
|
{
|
||||||
|
public RapidPath Plan(Vector from, Vector to, IReadOnlyList<Shape> cutAreas)
|
||||||
|
{
|
||||||
|
return new RapidPath
|
||||||
|
{
|
||||||
|
HeadUp = true,
|
||||||
|
Waypoints = new List<Vector>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace OpenNest
|
|||||||
var entities = ConvertProgram.ToGeometry(item.Drawing.Program)
|
var entities = ConvertProgram.ToGeometry(item.Drawing.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
|
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -65,7 +65,7 @@ namespace OpenNest
|
|||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
|
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
@@ -80,6 +80,11 @@ namespace OpenNest
|
|||||||
return new List<double> { 0 };
|
return new List<double> { 0 };
|
||||||
|
|
||||||
var hull = ConvexHull.Compute(points);
|
var hull = ConvexHull.Compute(points);
|
||||||
|
return GetHullEdgeAngles(hull);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<double> GetHullEdgeAngles(Polygon hull)
|
||||||
|
{
|
||||||
var vertices = hull.Vertices;
|
var vertices = hull.Vertices;
|
||||||
var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
|
var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
|
||||||
|
|
||||||
|
|||||||
96
OpenNest.Engine/Sequencing/AdvancedSequencer.cs
Normal file
96
OpenNest.Engine/Sequencing/AdvancedSequencer.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class AdvancedSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
private readonly SequenceParameters _parameters;
|
||||||
|
|
||||||
|
public AdvancedSequencer(SequenceParameters parameters)
|
||||||
|
{
|
||||||
|
_parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
if (parts.Count == 0)
|
||||||
|
return new List<SequencedPart>();
|
||||||
|
|
||||||
|
var exit = PlateHelper.GetExitPoint(plate);
|
||||||
|
|
||||||
|
// Group parts into rows by Y proximity
|
||||||
|
var rows = GroupIntoRows(parts, _parameters.MinDistanceBetweenRowsColumns);
|
||||||
|
|
||||||
|
// Sort rows bottom-to-top (ascending Y)
|
||||||
|
rows.Sort((a, b) => a.RowY.CompareTo(b.RowY));
|
||||||
|
|
||||||
|
// Determine initial direction based on exit point
|
||||||
|
var leftToRight = exit.X > plate.Size.Width * 0.5;
|
||||||
|
|
||||||
|
var result = new List<SequencedPart>(parts.Count);
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var sorted = leftToRight
|
||||||
|
? row.Parts.OrderBy(p => p.BoundingBox.Center.X).ToList()
|
||||||
|
: row.Parts.OrderByDescending(p => p.BoundingBox.Center.X).ToList();
|
||||||
|
|
||||||
|
foreach (var p in sorted)
|
||||||
|
result.Add(new SequencedPart { Part = p });
|
||||||
|
|
||||||
|
if (_parameters.AlternateRowsColumns)
|
||||||
|
leftToRight = !leftToRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PartRow> GroupIntoRows(IReadOnlyList<Part> parts, double minDistance)
|
||||||
|
{
|
||||||
|
// Sort parts by Y center
|
||||||
|
var sorted = parts
|
||||||
|
.OrderBy(p => p.BoundingBox.Center.Y)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var rows = new List<PartRow>();
|
||||||
|
|
||||||
|
foreach (var part in sorted)
|
||||||
|
{
|
||||||
|
var y = part.BoundingBox.Center.Y;
|
||||||
|
var placed = false;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (System.Math.Abs(y - row.RowY) <= minDistance + Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
row.Parts.Add(part);
|
||||||
|
placed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!placed)
|
||||||
|
{
|
||||||
|
var row = new PartRow(y);
|
||||||
|
row.Parts.Add(part);
|
||||||
|
rows.Add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PartRow
|
||||||
|
{
|
||||||
|
public double RowY { get; }
|
||||||
|
public List<Part> Parts { get; } = new List<Part>();
|
||||||
|
|
||||||
|
public PartRow(double rowY)
|
||||||
|
{
|
||||||
|
RowY = rowY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
OpenNest.Engine/Sequencing/BottomSideSequencer.cs
Normal file
17
OpenNest.Engine/Sequencing/BottomSideSequencer.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class BottomSideSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return parts
|
||||||
|
.OrderBy(p => p.Location.Y)
|
||||||
|
.ThenBy(p => p.Location.X)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
OpenNest.Engine/Sequencing/EdgeStartSequencer.cs
Normal file
36
OpenNest.Engine/Sequencing/EdgeStartSequencer.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class EdgeStartSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
// Plate(width, length) stores Size with Width/Length swapped internally.
|
||||||
|
// Reconstruct the logical plate box using the BoundingBox origin and the
|
||||||
|
// corrected extents: Size.Length = X-extent, Size.Width = Y-extent.
|
||||||
|
var origin = plate.BoundingBox(false);
|
||||||
|
var plateBox = new OpenNest.Geometry.Box(
|
||||||
|
origin.X, origin.Y,
|
||||||
|
plate.Size.Length,
|
||||||
|
plate.Size.Width);
|
||||||
|
|
||||||
|
return parts
|
||||||
|
.OrderBy(p => MinEdgeDistance(p.BoundingBox.Center, plateBox))
|
||||||
|
.ThenBy(p => p.Location.X)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double MinEdgeDistance(OpenNest.Geometry.Vector center, OpenNest.Geometry.Box plateBox)
|
||||||
|
{
|
||||||
|
var distLeft = center.X - plateBox.Left;
|
||||||
|
var distRight = plateBox.Right - center.X;
|
||||||
|
var distBottom = center.Y - plateBox.Bottom;
|
||||||
|
var distTop = plateBox.Top - center.Y;
|
||||||
|
|
||||||
|
return System.Math.Min(System.Math.Min(distLeft, distRight), System.Math.Min(distBottom, distTop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
OpenNest.Engine/Sequencing/IPartSequencer.cs
Normal file
14
OpenNest.Engine/Sequencing/IPartSequencer.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public readonly struct SequencedPart
|
||||||
|
{
|
||||||
|
public Part Part { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPartSequencer
|
||||||
|
{
|
||||||
|
List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
OpenNest.Engine/Sequencing/LeastCodeSequencer.cs
Normal file
139
OpenNest.Engine/Sequencing/LeastCodeSequencer.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class LeastCodeSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
private readonly int _maxIterations;
|
||||||
|
|
||||||
|
public LeastCodeSequencer(int maxIterations = 100)
|
||||||
|
{
|
||||||
|
_maxIterations = maxIterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
if (parts.Count == 0)
|
||||||
|
return new List<SequencedPart>();
|
||||||
|
|
||||||
|
var exit = PlateHelper.GetExitPoint(plate);
|
||||||
|
var ordered = NearestNeighbor(parts, exit);
|
||||||
|
TwoOpt(ordered, exit);
|
||||||
|
|
||||||
|
var result = new List<SequencedPart>(ordered.Count);
|
||||||
|
foreach (var p in ordered)
|
||||||
|
result.Add(new SequencedPart { Part = p });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Part> NearestNeighbor(IReadOnlyList<Part> parts, OpenNest.Geometry.Vector exit)
|
||||||
|
{
|
||||||
|
var remaining = new List<Part>(parts);
|
||||||
|
var ordered = new List<Part>(parts.Count);
|
||||||
|
|
||||||
|
var current = exit;
|
||||||
|
while (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
var bestIdx = 0;
|
||||||
|
var bestDist = Distance(current, Center(remaining[0]));
|
||||||
|
|
||||||
|
for (var i = 1; i < remaining.Count; i++)
|
||||||
|
{
|
||||||
|
var d = Distance(current, Center(remaining[i]));
|
||||||
|
if (d < bestDist - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
bestDist = d;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = remaining[bestIdx];
|
||||||
|
ordered.Add(next);
|
||||||
|
remaining.RemoveAt(bestIdx);
|
||||||
|
current = Center(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TwoOpt(List<Part> ordered, OpenNest.Geometry.Vector exit)
|
||||||
|
{
|
||||||
|
var n = ordered.Count;
|
||||||
|
if (n < 3)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var iter = 0; iter < _maxIterations; iter++)
|
||||||
|
{
|
||||||
|
var improved = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < n - 1; i++)
|
||||||
|
{
|
||||||
|
for (var j = i + 1; j < n; j++)
|
||||||
|
{
|
||||||
|
var before = RouteDistance(ordered, exit, i, j);
|
||||||
|
Reverse(ordered, i, j);
|
||||||
|
var after = RouteDistance(ordered, exit, i, j);
|
||||||
|
|
||||||
|
if (after < before - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
improved = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Revert
|
||||||
|
Reverse(ordered, i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!improved)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the total distance of the route starting from exit through all parts.
|
||||||
|
/// Only the segment around the reversed segment [i..j] needs to be checked,
|
||||||
|
/// but here we compute the full route cost for correctness.
|
||||||
|
/// </summary>
|
||||||
|
private static double RouteDistance(List<Part> ordered, OpenNest.Geometry.Vector exit, int i, int j)
|
||||||
|
{
|
||||||
|
// Full route distance: exit -> ordered[0] -> ... -> ordered[n-1]
|
||||||
|
var total = 0.0;
|
||||||
|
var prev = exit;
|
||||||
|
foreach (var p in ordered)
|
||||||
|
{
|
||||||
|
var c = Center(p);
|
||||||
|
total += Distance(prev, c);
|
||||||
|
prev = c;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Reverse(List<Part> list, int i, int j)
|
||||||
|
{
|
||||||
|
while (i < j)
|
||||||
|
{
|
||||||
|
var tmp = list[i];
|
||||||
|
list[i] = list[j];
|
||||||
|
list[j] = tmp;
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenNest.Geometry.Vector Center(Part part)
|
||||||
|
{
|
||||||
|
return part.BoundingBox.Center;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double Distance(OpenNest.Geometry.Vector a, OpenNest.Geometry.Vector b)
|
||||||
|
{
|
||||||
|
var dx = b.X - a.X;
|
||||||
|
var dy = b.Y - a.Y;
|
||||||
|
return System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
OpenNest.Engine/Sequencing/LeftSideSequencer.cs
Normal file
17
OpenNest.Engine/Sequencing/LeftSideSequencer.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class LeftSideSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return parts
|
||||||
|
.OrderBy(p => p.Location.X)
|
||||||
|
.ThenBy(p => p.Location.Y)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
OpenNest.Engine/Sequencing/PartSequencerFactory.cs
Normal file
23
OpenNest.Engine/Sequencing/PartSequencerFactory.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public static class PartSequencerFactory
|
||||||
|
{
|
||||||
|
public static IPartSequencer Create(SequenceParameters parameters)
|
||||||
|
{
|
||||||
|
return parameters.Method switch
|
||||||
|
{
|
||||||
|
SequenceMethod.RightSide => new RightSideSequencer(),
|
||||||
|
SequenceMethod.LeftSide => new LeftSideSequencer(),
|
||||||
|
SequenceMethod.BottomSide => new BottomSideSequencer(),
|
||||||
|
SequenceMethod.EdgeStart => new EdgeStartSequencer(),
|
||||||
|
SequenceMethod.LeastCode => new LeastCodeSequencer(),
|
||||||
|
SequenceMethod.Advanced => new AdvancedSequencer(parameters),
|
||||||
|
_ => throw new NotSupportedException(
|
||||||
|
$"Sequence method '{parameters.Method}' is not supported.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
OpenNest.Engine/Sequencing/PlateHelper.cs
Normal file
22
OpenNest.Engine/Sequencing/PlateHelper.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
internal static class PlateHelper
|
||||||
|
{
|
||||||
|
public static Vector GetExitPoint(Plate plate)
|
||||||
|
{
|
||||||
|
var w = plate.Size.Width;
|
||||||
|
var l = plate.Size.Length;
|
||||||
|
|
||||||
|
return plate.Quadrant switch
|
||||||
|
{
|
||||||
|
1 => new Vector(w, l),
|
||||||
|
2 => new Vector(0, l),
|
||||||
|
3 => new Vector(0, 0),
|
||||||
|
4 => new Vector(w, 0),
|
||||||
|
_ => new Vector(w, l)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
OpenNest.Engine/Sequencing/RightSideSequencer.cs
Normal file
17
OpenNest.Engine/Sequencing/RightSideSequencer.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Sequencing
|
||||||
|
{
|
||||||
|
public class RightSideSequencer : IPartSequencer
|
||||||
|
{
|
||||||
|
public List<SequencedPart> Sequence(IReadOnlyList<Part> parts, Plate plate)
|
||||||
|
{
|
||||||
|
return parts
|
||||||
|
.OrderByDescending(p => p.Location.X)
|
||||||
|
.ThenBy(p => p.Location.Y)
|
||||||
|
.Select(p => new SequencedPart { Part = p })
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
OpenNest.Engine/StripDirection.cs
Normal file
8
OpenNest.Engine/StripDirection.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public enum StripDirection
|
||||||
|
{
|
||||||
|
Bottom,
|
||||||
|
Left
|
||||||
|
}
|
||||||
|
}
|
||||||
375
OpenNest.Engine/StripNestEngine.cs
Normal file
375
OpenNest.Engine/StripNestEngine.cs
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class StripNestEngine : NestEngineBase
|
||||||
|
{
|
||||||
|
private const int MaxShrinkIterations = 20;
|
||||||
|
|
||||||
|
public StripNestEngine(Plate plate) : base(plate)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Name => "Strip";
|
||||||
|
|
||||||
|
public override string Description => "Strip-based nesting for mixed-drawing layouts";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single-item fill delegates to DefaultNestEngine.
|
||||||
|
/// The strip strategy adds value for multi-drawing nesting, not single-item fills.
|
||||||
|
/// </summary>
|
||||||
|
public override List<Part> Fill(NestItem item, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
return inner.Fill(item, workArea, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Group-parts fill delegates to DefaultNestEngine.
|
||||||
|
/// </summary>
|
||||||
|
public override List<Part> Fill(List<Part> groupParts, Box workArea,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
return inner.Fill(groupParts, workArea, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pack delegates to DefaultNestEngine.
|
||||||
|
/// </summary>
|
||||||
|
public override List<Part> PackArea(Box box, List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
return inner.PackArea(box, items, progress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects the item that consumes the most plate area (bounding box area x quantity).
|
||||||
|
/// Returns the index into the items list.
|
||||||
|
/// </summary>
|
||||||
|
private static int SelectStripItemIndex(List<NestItem> items, Box workArea)
|
||||||
|
{
|
||||||
|
var bestIndex = 0;
|
||||||
|
var bestArea = 0.0;
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
var bbox = items[i].Drawing.Program.BoundingBox();
|
||||||
|
var qty = items[i].Quantity > 0
|
||||||
|
? items[i].Quantity
|
||||||
|
: (int)(workArea.Area() / bbox.Area());
|
||||||
|
var totalArea = bbox.Area() * qty;
|
||||||
|
|
||||||
|
if (totalArea > bestArea)
|
||||||
|
{
|
||||||
|
bestArea = totalArea;
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates the strip dimension (height for bottom, width for left) needed
|
||||||
|
/// to fit the target quantity. Tries 0 deg and 90 deg rotations and picks the shorter.
|
||||||
|
/// This is only an estimate for the shrink loop starting point — the actual fill
|
||||||
|
/// uses DefaultNestEngine.Fill which tries many rotation angles internally.
|
||||||
|
/// </summary>
|
||||||
|
private static double EstimateStripDimension(NestItem item, double stripLength, double maxDimension)
|
||||||
|
{
|
||||||
|
var bbox = item.Drawing.Program.BoundingBox();
|
||||||
|
var qty = item.Quantity > 0
|
||||||
|
? item.Quantity
|
||||||
|
: System.Math.Max(1, (int)(stripLength * maxDimension / bbox.Area()));
|
||||||
|
|
||||||
|
// At 0 deg: parts per row along strip length, strip dimension is bbox.Length
|
||||||
|
var perRow0 = (int)(stripLength / bbox.Width);
|
||||||
|
var rows0 = perRow0 > 0 ? (int)System.Math.Ceiling((double)qty / perRow0) : int.MaxValue;
|
||||||
|
var dim0 = rows0 * bbox.Length;
|
||||||
|
|
||||||
|
// At 90 deg: rotated bounding box (Width and Length swap)
|
||||||
|
var perRow90 = (int)(stripLength / bbox.Length);
|
||||||
|
var rows90 = perRow90 > 0 ? (int)System.Math.Ceiling((double)qty / perRow90) : int.MaxValue;
|
||||||
|
var dim90 = rows90 * bbox.Width;
|
||||||
|
|
||||||
|
var estimate = System.Math.Min(dim0, dim90);
|
||||||
|
|
||||||
|
// Clamp to available dimension
|
||||||
|
return System.Math.Min(estimate, maxDimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multi-drawing strip nesting strategy.
|
||||||
|
/// Picks the largest-area drawing for strip treatment, finds the tightest strip
|
||||||
|
/// in both bottom and left orientations, fills remnants with remaining drawings,
|
||||||
|
/// and returns the denser result.
|
||||||
|
/// </summary>
|
||||||
|
public override List<Part> Nest(List<NestItem> items,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
var workArea = Plate.WorkArea();
|
||||||
|
|
||||||
|
// Select which item gets the strip treatment.
|
||||||
|
var stripIndex = SelectStripItemIndex(items, workArea);
|
||||||
|
var stripItem = items[stripIndex];
|
||||||
|
var remainderItems = items.Where((_, i) => i != stripIndex).ToList();
|
||||||
|
|
||||||
|
// Try both orientations.
|
||||||
|
var bottomResult = TryOrientation(StripDirection.Bottom, stripItem, remainderItems, workArea, progress, token);
|
||||||
|
var leftResult = TryOrientation(StripDirection.Left, stripItem, remainderItems, workArea, progress, token);
|
||||||
|
|
||||||
|
// Pick the better result.
|
||||||
|
var winner = bottomResult.Score >= leftResult.Score
|
||||||
|
? bottomResult.Parts
|
||||||
|
: leftResult.Parts;
|
||||||
|
|
||||||
|
// Deduct placed quantities from the original items.
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Quantity <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var placed = winner.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StripNestResult TryOrientation(StripDirection direction, NestItem stripItem,
|
||||||
|
List<NestItem> remainderItems, Box workArea, IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = new StripNestResult { Direction = direction };
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// Estimate initial strip dimension.
|
||||||
|
var stripLength = direction == StripDirection.Bottom ? workArea.Width : workArea.Length;
|
||||||
|
var maxDimension = direction == StripDirection.Bottom ? workArea.Length : workArea.Width;
|
||||||
|
var estimatedDim = EstimateStripDimension(stripItem, stripLength, maxDimension);
|
||||||
|
|
||||||
|
// Create the initial strip box.
|
||||||
|
var stripBox = direction == StripDirection.Bottom
|
||||||
|
? new Box(workArea.X, workArea.Y, workArea.Width, estimatedDim)
|
||||||
|
: new Box(workArea.X, workArea.Y, estimatedDim, workArea.Length);
|
||||||
|
|
||||||
|
// Initial fill using DefaultNestEngine (composition, not inheritance).
|
||||||
|
var inner = new DefaultNestEngine(Plate);
|
||||||
|
var stripParts = inner.Fill(
|
||||||
|
new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity },
|
||||||
|
stripBox, progress, token);
|
||||||
|
|
||||||
|
if (stripParts == null || stripParts.Count == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// Measure actual strip dimension from placed parts.
|
||||||
|
var placedBox = stripParts.Cast<IBoundable>().GetBoundingBox();
|
||||||
|
var actualDim = direction == StripDirection.Bottom
|
||||||
|
? placedBox.Top - workArea.Y
|
||||||
|
: placedBox.Right - workArea.X;
|
||||||
|
|
||||||
|
var bestParts = stripParts;
|
||||||
|
var bestDim = actualDim;
|
||||||
|
var targetCount = stripParts.Count;
|
||||||
|
|
||||||
|
// Shrink loop: reduce strip dimension by PartSpacing until count drops.
|
||||||
|
for (var i = 0; i < MaxShrinkIterations; i++)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var trialDim = bestDim - Plate.PartSpacing;
|
||||||
|
if (trialDim <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var trialBox = direction == StripDirection.Bottom
|
||||||
|
? new Box(workArea.X, workArea.Y, workArea.Width, trialDim)
|
||||||
|
: new Box(workArea.X, workArea.Y, trialDim, workArea.Length);
|
||||||
|
|
||||||
|
var trialInner = new DefaultNestEngine(Plate);
|
||||||
|
var trialParts = trialInner.Fill(
|
||||||
|
new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity },
|
||||||
|
trialBox, progress, token);
|
||||||
|
|
||||||
|
if (trialParts == null || trialParts.Count < targetCount)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Same count in a tighter strip — keep going.
|
||||||
|
bestParts = trialParts;
|
||||||
|
var trialPlacedBox = trialParts.Cast<IBoundable>().GetBoundingBox();
|
||||||
|
bestDim = direction == StripDirection.Bottom
|
||||||
|
? trialPlacedBox.Top - workArea.Y
|
||||||
|
: trialPlacedBox.Right - workArea.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build remnant box with spacing gap.
|
||||||
|
var spacing = Plate.PartSpacing;
|
||||||
|
var remnantBox = direction == StripDirection.Bottom
|
||||||
|
? new Box(workArea.X, workArea.Y + bestDim + spacing,
|
||||||
|
workArea.Width, workArea.Length - bestDim - spacing)
|
||||||
|
: new Box(workArea.X + bestDim + spacing, workArea.Y,
|
||||||
|
workArea.Width - bestDim - spacing, workArea.Length);
|
||||||
|
|
||||||
|
// Collect all parts.
|
||||||
|
var allParts = new List<Part>(bestParts);
|
||||||
|
|
||||||
|
// If strip item was only partially placed, add leftovers to remainder.
|
||||||
|
var placed = bestParts.Count;
|
||||||
|
var leftover = stripItem.Quantity > 0 ? stripItem.Quantity - placed : 0;
|
||||||
|
var effectiveRemainder = new List<NestItem>(remainderItems);
|
||||||
|
|
||||||
|
if (leftover > 0)
|
||||||
|
{
|
||||||
|
effectiveRemainder.Add(new NestItem
|
||||||
|
{
|
||||||
|
Drawing = stripItem.Drawing,
|
||||||
|
Quantity = leftover
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort remainder by descending bounding box area x quantity.
|
||||||
|
effectiveRemainder = effectiveRemainder
|
||||||
|
.OrderByDescending(i =>
|
||||||
|
{
|
||||||
|
var bb = i.Drawing.Program.BoundingBox();
|
||||||
|
return bb.Area() * (i.Quantity > 0 ? i.Quantity : 1);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Fill remnant with remainder items using free-rectangle tracking.
|
||||||
|
// After each fill, the consumed box is split into two non-overlapping
|
||||||
|
// sub-rectangles (guillotine cut) so no usable area is lost.
|
||||||
|
if (remnantBox.Width > 0 && remnantBox.Length > 0)
|
||||||
|
{
|
||||||
|
var freeBoxes = new List<Box> { remnantBox };
|
||||||
|
var remnantProgress = progress != null
|
||||||
|
? new AccumulatingProgress(progress, allParts)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
foreach (var item in effectiveRemainder)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested || freeBoxes.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var itemBbox = item.Drawing.Program.BoundingBox();
|
||||||
|
var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length);
|
||||||
|
|
||||||
|
// Try free boxes from largest to smallest.
|
||||||
|
freeBoxes.Sort((a, b) => b.Area().CompareTo(a.Area()));
|
||||||
|
|
||||||
|
for (var i = 0; i < freeBoxes.Count; i++)
|
||||||
|
{
|
||||||
|
var box = freeBoxes[i];
|
||||||
|
|
||||||
|
if (System.Math.Min(box.Width, box.Length) < minItemDim)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var remnantInner = new DefaultNestEngine(Plate);
|
||||||
|
var remnantParts = remnantInner.Fill(
|
||||||
|
new NestItem { Drawing = item.Drawing, Quantity = item.Quantity },
|
||||||
|
box, remnantProgress, token);
|
||||||
|
|
||||||
|
if (remnantParts != null && remnantParts.Count > 0)
|
||||||
|
{
|
||||||
|
allParts.AddRange(remnantParts);
|
||||||
|
freeBoxes.RemoveAt(i);
|
||||||
|
|
||||||
|
var usedBox = remnantParts.Cast<IBoundable>().GetBoundingBox();
|
||||||
|
SplitFreeBox(box, usedBox, spacing, freeBoxes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Parts = allParts;
|
||||||
|
result.StripBox = direction == StripDirection.Bottom
|
||||||
|
? new Box(workArea.X, workArea.Y, workArea.Width, bestDim)
|
||||||
|
: new Box(workArea.X, workArea.Y, bestDim, workArea.Length);
|
||||||
|
result.RemnantBox = remnantBox;
|
||||||
|
result.Score = FillScore.Compute(allParts, workArea);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SplitFreeBox(Box parent, Box used, double spacing, List<Box> freeBoxes)
|
||||||
|
{
|
||||||
|
var hWidth = parent.Right - used.Right - spacing;
|
||||||
|
var vHeight = parent.Top - used.Top - spacing;
|
||||||
|
|
||||||
|
if (hWidth > spacing && vHeight > spacing)
|
||||||
|
{
|
||||||
|
// Guillotine split: give the overlapping corner to the larger strip.
|
||||||
|
var hFullArea = hWidth * parent.Length;
|
||||||
|
var vFullArea = parent.Width * vHeight;
|
||||||
|
|
||||||
|
if (hFullArea >= vFullArea)
|
||||||
|
{
|
||||||
|
// hStrip gets full height; vStrip truncated to left of split line.
|
||||||
|
freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, parent.Length));
|
||||||
|
var vWidth = used.Right + spacing - parent.X;
|
||||||
|
if (vWidth > spacing)
|
||||||
|
freeBoxes.Add(new Box(parent.X, used.Top + spacing, vWidth, vHeight));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// vStrip gets full width; hStrip truncated below split line.
|
||||||
|
freeBoxes.Add(new Box(parent.X, used.Top + spacing, parent.Width, vHeight));
|
||||||
|
var hHeight = used.Top + spacing - parent.Y;
|
||||||
|
if (hHeight > spacing)
|
||||||
|
freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, hHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (hWidth > spacing)
|
||||||
|
{
|
||||||
|
freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, parent.Length));
|
||||||
|
}
|
||||||
|
else if (vHeight > spacing)
|
||||||
|
{
|
||||||
|
freeBoxes.Add(new Box(parent.X, used.Top + spacing, parent.Width, vHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps an IProgress to prepend previously placed parts to each report,
|
||||||
|
/// so the UI shows the full picture (strip + remnant) during remnant fills.
|
||||||
|
/// </summary>
|
||||||
|
private class AccumulatingProgress : IProgress<NestProgress>
|
||||||
|
{
|
||||||
|
private readonly IProgress<NestProgress> inner;
|
||||||
|
private readonly List<Part> previousParts;
|
||||||
|
|
||||||
|
public AccumulatingProgress(IProgress<NestProgress> inner, List<Part> previousParts)
|
||||||
|
{
|
||||||
|
this.inner = inner;
|
||||||
|
this.previousParts = previousParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Report(NestProgress value)
|
||||||
|
{
|
||||||
|
if (value.BestParts != null && previousParts.Count > 0)
|
||||||
|
{
|
||||||
|
var combined = new List<Part>(previousParts.Count + value.BestParts.Count);
|
||||||
|
combined.AddRange(previousParts);
|
||||||
|
combined.AddRange(value.BestParts);
|
||||||
|
value.BestParts = combined;
|
||||||
|
value.BestPartCount = combined.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.Report(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
OpenNest.Engine/StripNestResult.cs
Normal file
14
OpenNest.Engine/StripNestResult.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
internal class StripNestResult
|
||||||
|
{
|
||||||
|
public List<Part> Parts { get; set; } = new();
|
||||||
|
public Box StripBox { get; set; }
|
||||||
|
public Box RemnantBox { get; set; }
|
||||||
|
public FillScore Score { get; set; }
|
||||||
|
public StripDirection Direction { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ namespace OpenNest.Gpu
|
|||||||
private static bool _probed;
|
private static bool _probed;
|
||||||
private static bool _gpuAvailable;
|
private static bool _gpuAvailable;
|
||||||
private static string _deviceName;
|
private static string _deviceName;
|
||||||
|
private static GpuSlideComputer _slideComputer;
|
||||||
|
private static readonly object _slideLock = new object();
|
||||||
|
|
||||||
public static bool GpuAvailable
|
public static bool GpuAvailable
|
||||||
{
|
{
|
||||||
@@ -46,6 +48,29 @@ namespace OpenNest.Gpu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ISlideComputer CreateSlideComputer()
|
||||||
|
{
|
||||||
|
if (!GpuAvailable)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
lock (_slideLock)
|
||||||
|
{
|
||||||
|
if (_slideComputer != null)
|
||||||
|
return _slideComputer;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_slideComputer = new GpuSlideComputer();
|
||||||
|
return _slideComputer;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[GpuEvaluatorFactory] GPU slide computer failed: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void Probe()
|
private static void Probe()
|
||||||
{
|
{
|
||||||
_probed = true;
|
_probed = true;
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ namespace OpenNest.Gpu
|
|||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
|
|||||||
460
OpenNest.Gpu/GpuSlideComputer.cs
Normal file
460
OpenNest.Gpu/GpuSlideComputer.cs
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
using System;
|
||||||
|
using ILGPU;
|
||||||
|
using ILGPU.Runtime;
|
||||||
|
using ILGPU.Algorithms;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
|
||||||
|
namespace OpenNest.Gpu
|
||||||
|
{
|
||||||
|
public class GpuSlideComputer : ISlideComputer
|
||||||
|
{
|
||||||
|
private readonly Context _context;
|
||||||
|
private readonly Accelerator _accelerator;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
// ── Kernels ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private readonly Action<Index1D,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // stationaryPrep
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // movingPrep
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // offsets
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // results
|
||||||
|
int, int, int> _kernel;
|
||||||
|
|
||||||
|
private readonly Action<Index1D,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // stationaryPrep
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // movingPrep
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // offsets
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // results
|
||||||
|
ArrayView1D<int, Stride1D.Dense>, // directions
|
||||||
|
int, int> _kernelMultiDir;
|
||||||
|
|
||||||
|
private readonly Action<Index1D,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // raw
|
||||||
|
ArrayView1D<double, Stride1D.Dense>, // prepared
|
||||||
|
int> _prepareKernel;
|
||||||
|
|
||||||
|
// ── Buffers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private MemoryBuffer1D<double, Stride1D.Dense>? _gpuStationaryRaw;
|
||||||
|
private MemoryBuffer1D<double, Stride1D.Dense>? _gpuStationaryPrep;
|
||||||
|
private double[]? _lastStationaryData; // Keep CPU copy/ref for content check
|
||||||
|
|
||||||
|
private MemoryBuffer1D<double, Stride1D.Dense>? _gpuMovingRaw;
|
||||||
|
private MemoryBuffer1D<double, Stride1D.Dense>? _gpuMovingPrep;
|
||||||
|
private double[]? _lastMovingData; // Keep CPU copy/ref for content check
|
||||||
|
|
||||||
|
private MemoryBuffer1D<double, Stride1D.Dense>? _gpuOffsets;
|
||||||
|
private MemoryBuffer1D<double, Stride1D.Dense>? _gpuResults;
|
||||||
|
private MemoryBuffer1D<int, Stride1D.Dense>? _gpuDirs;
|
||||||
|
private int _offsetCapacity;
|
||||||
|
|
||||||
|
public GpuSlideComputer()
|
||||||
|
{
|
||||||
|
_context = Context.CreateDefault();
|
||||||
|
_accelerator = _context.GetPreferredDevice(preferCPU: false)
|
||||||
|
.CreateAccelerator(_context);
|
||||||
|
|
||||||
|
_kernel = _accelerator.LoadAutoGroupedStreamKernel<
|
||||||
|
Index1D,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
int, int, int>(SlideKernel);
|
||||||
|
|
||||||
|
_kernelMultiDir = _accelerator.LoadAutoGroupedStreamKernel<
|
||||||
|
Index1D,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<int, Stride1D.Dense>,
|
||||||
|
int, int>(SlideKernelMultiDir);
|
||||||
|
|
||||||
|
_prepareKernel = _accelerator.LoadAutoGroupedStreamKernel<
|
||||||
|
Index1D,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
ArrayView1D<double, Stride1D.Dense>,
|
||||||
|
int>(PrepareKernel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double[] ComputeBatch(
|
||||||
|
double[] stationarySegments, int stationaryCount,
|
||||||
|
double[] movingTemplateSegments, int movingCount,
|
||||||
|
double[] offsets, int offsetCount,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
var results = new double[offsetCount];
|
||||||
|
if (offsetCount == 0 || stationaryCount == 0 || movingCount == 0)
|
||||||
|
{
|
||||||
|
Array.Fill(results, double.MaxValue);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
EnsureStationary(stationarySegments, stationaryCount);
|
||||||
|
EnsureMoving(movingTemplateSegments, movingCount);
|
||||||
|
EnsureOffsetBuffers(offsetCount);
|
||||||
|
|
||||||
|
_gpuOffsets!.View.SubView(0, offsetCount * 2).CopyFromCPU(offsets);
|
||||||
|
|
||||||
|
_kernel(offsetCount,
|
||||||
|
_gpuStationaryPrep!.View, _gpuMovingPrep!.View,
|
||||||
|
_gpuOffsets.View, _gpuResults!.View,
|
||||||
|
stationaryCount, movingCount, (int)direction);
|
||||||
|
|
||||||
|
_accelerator.Synchronize();
|
||||||
|
_gpuResults.View.SubView(0, offsetCount).CopyToCPU(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double[] ComputeBatchMultiDir(
|
||||||
|
double[] stationarySegments, int stationaryCount,
|
||||||
|
double[] movingTemplateSegments, int movingCount,
|
||||||
|
double[] offsets, int offsetCount,
|
||||||
|
int[] directions)
|
||||||
|
{
|
||||||
|
var results = new double[offsetCount];
|
||||||
|
if (offsetCount == 0 || stationaryCount == 0 || movingCount == 0)
|
||||||
|
{
|
||||||
|
Array.Fill(results, double.MaxValue);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
EnsureStationary(stationarySegments, stationaryCount);
|
||||||
|
EnsureMoving(movingTemplateSegments, movingCount);
|
||||||
|
EnsureOffsetBuffers(offsetCount);
|
||||||
|
|
||||||
|
_gpuOffsets!.View.SubView(0, offsetCount * 2).CopyFromCPU(offsets);
|
||||||
|
_gpuDirs!.View.SubView(0, offsetCount).CopyFromCPU(directions);
|
||||||
|
|
||||||
|
_kernelMultiDir(offsetCount,
|
||||||
|
_gpuStationaryPrep!.View, _gpuMovingPrep!.View,
|
||||||
|
_gpuOffsets.View, _gpuResults!.View, _gpuDirs.View,
|
||||||
|
stationaryCount, movingCount);
|
||||||
|
|
||||||
|
_accelerator.Synchronize();
|
||||||
|
_gpuResults.View.SubView(0, offsetCount).CopyToCPU(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvalidateStationary() => _lastStationaryData = null;
|
||||||
|
public void InvalidateMoving() => _lastMovingData = null;
|
||||||
|
|
||||||
|
private void EnsureStationary(double[] data, int count)
|
||||||
|
{
|
||||||
|
// Fast check: if same object or content is identical, skip upload
|
||||||
|
if (_gpuStationaryPrep != null &&
|
||||||
|
_lastStationaryData != null &&
|
||||||
|
_lastStationaryData.Length == data.Length)
|
||||||
|
{
|
||||||
|
// Reference equality or content equality
|
||||||
|
if (_lastStationaryData == data ||
|
||||||
|
new ReadOnlySpan<double>(_lastStationaryData).SequenceEqual(new ReadOnlySpan<double>(data)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_gpuStationaryRaw?.Dispose();
|
||||||
|
_gpuStationaryPrep?.Dispose();
|
||||||
|
|
||||||
|
_gpuStationaryRaw = _accelerator.Allocate1D(data);
|
||||||
|
_gpuStationaryPrep = _accelerator.Allocate1D<double>(count * 10);
|
||||||
|
|
||||||
|
_prepareKernel(count, _gpuStationaryRaw.View, _gpuStationaryPrep.View, count);
|
||||||
|
_accelerator.Synchronize();
|
||||||
|
|
||||||
|
_lastStationaryData = data; // store reference for next comparison
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureMoving(double[] data, int count)
|
||||||
|
{
|
||||||
|
if (_gpuMovingPrep != null &&
|
||||||
|
_lastMovingData != null &&
|
||||||
|
_lastMovingData.Length == data.Length)
|
||||||
|
{
|
||||||
|
if (_lastMovingData == data ||
|
||||||
|
new ReadOnlySpan<double>(_lastMovingData).SequenceEqual(new ReadOnlySpan<double>(data)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_gpuMovingRaw?.Dispose();
|
||||||
|
_gpuMovingPrep?.Dispose();
|
||||||
|
|
||||||
|
_gpuMovingRaw = _accelerator.Allocate1D(data);
|
||||||
|
_gpuMovingPrep = _accelerator.Allocate1D<double>(count * 10);
|
||||||
|
|
||||||
|
_prepareKernel(count, _gpuMovingRaw.View, _gpuMovingPrep.View, count);
|
||||||
|
_accelerator.Synchronize();
|
||||||
|
|
||||||
|
_lastMovingData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureOffsetBuffers(int offsetCount)
|
||||||
|
{
|
||||||
|
if (_offsetCapacity >= offsetCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var newCapacity = System.Math.Max(offsetCount, _offsetCapacity * 3 / 2);
|
||||||
|
|
||||||
|
_gpuOffsets?.Dispose();
|
||||||
|
_gpuResults?.Dispose();
|
||||||
|
_gpuDirs?.Dispose();
|
||||||
|
|
||||||
|
_gpuOffsets = _accelerator.Allocate1D<double>(newCapacity * 2);
|
||||||
|
_gpuResults = _accelerator.Allocate1D<double>(newCapacity);
|
||||||
|
_gpuDirs = _accelerator.Allocate1D<int>(newCapacity);
|
||||||
|
|
||||||
|
_offsetCapacity = newCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Preparation Kernel ───────────────────────────────────────
|
||||||
|
|
||||||
|
private static void PrepareKernel(
|
||||||
|
Index1D index,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> raw,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> prepared,
|
||||||
|
int count)
|
||||||
|
{
|
||||||
|
if (index >= count) return;
|
||||||
|
var x1 = raw[index * 4 + 0];
|
||||||
|
var y1 = raw[index * 4 + 1];
|
||||||
|
var x2 = raw[index * 4 + 2];
|
||||||
|
var y2 = raw[index * 4 + 3];
|
||||||
|
|
||||||
|
prepared[index * 10 + 0] = x1;
|
||||||
|
prepared[index * 10 + 1] = y1;
|
||||||
|
prepared[index * 10 + 2] = x2;
|
||||||
|
prepared[index * 10 + 3] = y2;
|
||||||
|
|
||||||
|
var dx = x2 - x1;
|
||||||
|
var dy = y2 - y1;
|
||||||
|
|
||||||
|
// invD is used for parameter 't'. We use a small epsilon for stability.
|
||||||
|
prepared[index * 10 + 4] = (XMath.Abs(dx) < 1e-9) ? 0 : 1.0 / dx;
|
||||||
|
prepared[index * 10 + 5] = (XMath.Abs(dy) < 1e-9) ? 0 : 1.0 / dy;
|
||||||
|
|
||||||
|
prepared[index * 10 + 6] = XMath.Min(x1, x2);
|
||||||
|
prepared[index * 10 + 7] = XMath.Max(x1, x2);
|
||||||
|
prepared[index * 10 + 8] = XMath.Min(y1, y2);
|
||||||
|
prepared[index * 10 + 9] = XMath.Max(y1, y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main Slide Kernels ───────────────────────────────────────
|
||||||
|
|
||||||
|
private static void SlideKernel(
|
||||||
|
Index1D index,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> stationaryPrep,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> movingPrep,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> offsets,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> results,
|
||||||
|
int sCount, int mCount, int direction)
|
||||||
|
{
|
||||||
|
if (index >= results.Length) return;
|
||||||
|
|
||||||
|
var dx = offsets[index * 2];
|
||||||
|
var dy = offsets[index * 2 + 1];
|
||||||
|
|
||||||
|
results[index] = ComputeSlideLean(
|
||||||
|
stationaryPrep, movingPrep, dx, dy, sCount, mCount, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SlideKernelMultiDir(
|
||||||
|
Index1D index,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> stationaryPrep,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> movingPrep,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> offsets,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> results,
|
||||||
|
ArrayView1D<int, Stride1D.Dense> directions,
|
||||||
|
int sCount, int mCount)
|
||||||
|
{
|
||||||
|
if (index >= results.Length) return;
|
||||||
|
|
||||||
|
var dx = offsets[index * 2];
|
||||||
|
var dy = offsets[index * 2 + 1];
|
||||||
|
var dir = directions[index];
|
||||||
|
|
||||||
|
results[index] = ComputeSlideLean(
|
||||||
|
stationaryPrep, movingPrep, dx, dy, sCount, mCount, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ComputeSlideLean(
|
||||||
|
ArrayView1D<double, Stride1D.Dense> sPrep,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> mPrep,
|
||||||
|
double dx, double dy, int sCount, int mCount, int direction)
|
||||||
|
{
|
||||||
|
const double eps = 0.00001;
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var horizontal = direction >= 2;
|
||||||
|
var oppDir = direction ^ 1;
|
||||||
|
|
||||||
|
// ── Forward Pass: moving vertices vs stationary edges ─────
|
||||||
|
for (int i = 0; i < mCount; i++)
|
||||||
|
{
|
||||||
|
var m1x = mPrep[i * 10 + 0] + dx;
|
||||||
|
var m1y = mPrep[i * 10 + 1] + dy;
|
||||||
|
var m2x = mPrep[i * 10 + 2] + dx;
|
||||||
|
var m2y = mPrep[i * 10 + 3] + dy;
|
||||||
|
|
||||||
|
for (int j = 0; j < sCount; j++)
|
||||||
|
{
|
||||||
|
var sMin = horizontal ? sPrep[j * 10 + 8] : sPrep[j * 10 + 6];
|
||||||
|
var sMax = horizontal ? sPrep[j * 10 + 9] : sPrep[j * 10 + 7];
|
||||||
|
|
||||||
|
// Test moving vertex 1 against stationary edge j
|
||||||
|
var mv1 = horizontal ? m1y : m1x;
|
||||||
|
if (mv1 >= sMin - eps && mv1 <= sMax + eps)
|
||||||
|
{
|
||||||
|
var d = RayEdgeLean(m1x, m1y, sPrep, j, direction, eps);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test moving vertex 2 against stationary edge j
|
||||||
|
var mv2 = horizontal ? m2y : m2x;
|
||||||
|
if (mv2 >= sMin - eps && mv2 <= sMax + eps)
|
||||||
|
{
|
||||||
|
var d = RayEdgeLean(m2x, m2y, sPrep, j, direction, eps);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reverse Pass: stationary vertices vs moving edges ─────
|
||||||
|
for (int i = 0; i < sCount; i++)
|
||||||
|
{
|
||||||
|
var s1x = sPrep[i * 10 + 0];
|
||||||
|
var s1y = sPrep[i * 10 + 1];
|
||||||
|
var s2x = sPrep[i * 10 + 2];
|
||||||
|
var s2y = sPrep[i * 10 + 3];
|
||||||
|
|
||||||
|
for (int j = 0; j < mCount; j++)
|
||||||
|
{
|
||||||
|
var mMin = horizontal ? (mPrep[j * 10 + 8] + dy) : (mPrep[j * 10 + 6] + dx);
|
||||||
|
var mMax = horizontal ? (mPrep[j * 10 + 9] + dy) : (mPrep[j * 10 + 7] + dx);
|
||||||
|
|
||||||
|
// Test stationary vertex 1 against moving edge j
|
||||||
|
var sv1 = horizontal ? s1y : s1x;
|
||||||
|
if (sv1 >= mMin - eps && sv1 <= mMax + eps)
|
||||||
|
{
|
||||||
|
var d = RayEdgeLeanMoving(s1x, s1y, mPrep, j, dx, dy, oppDir, eps);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stationary vertex 2 against moving edge j
|
||||||
|
var sv2 = horizontal ? s2y : s2x;
|
||||||
|
if (sv2 >= mMin - eps && sv2 <= mMax + eps)
|
||||||
|
{
|
||||||
|
var d = RayEdgeLeanMoving(s2x, s2y, mPrep, j, dx, dy, oppDir, eps);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double RayEdgeLean(
|
||||||
|
double vx, double vy,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> sPrep, int j,
|
||||||
|
int direction, double eps)
|
||||||
|
{
|
||||||
|
var p1x = sPrep[j * 10 + 0];
|
||||||
|
var p1y = sPrep[j * 10 + 1];
|
||||||
|
var p2x = sPrep[j * 10 + 2];
|
||||||
|
var p2y = sPrep[j * 10 + 3];
|
||||||
|
|
||||||
|
if (direction >= 2) // Horizontal (Left=2, Right=3)
|
||||||
|
{
|
||||||
|
var invDy = sPrep[j * 10 + 5];
|
||||||
|
if (invDy == 0) return double.MaxValue;
|
||||||
|
|
||||||
|
var t = (vy - p1y) * invDy;
|
||||||
|
if (t < -eps || t > 1.0 + eps) return double.MaxValue;
|
||||||
|
|
||||||
|
var ix = p1x + t * (p2x - p1x);
|
||||||
|
var dist = (direction == 2) ? (vx - ix) : (ix - vx);
|
||||||
|
|
||||||
|
if (dist > eps) return dist;
|
||||||
|
return (dist >= -eps) ? 0.0 : double.MaxValue;
|
||||||
|
}
|
||||||
|
else // Vertical (Up=0, Down=1)
|
||||||
|
{
|
||||||
|
var invDx = sPrep[j * 10 + 4];
|
||||||
|
if (invDx == 0) return double.MaxValue;
|
||||||
|
|
||||||
|
var t = (vx - p1x) * invDx;
|
||||||
|
if (t < -eps || t > 1.0 + eps) return double.MaxValue;
|
||||||
|
|
||||||
|
var iy = p1y + t * (p2y - p1y);
|
||||||
|
var dist = (direction == 1) ? (vy - iy) : (iy - vy);
|
||||||
|
|
||||||
|
if (dist > eps) return dist;
|
||||||
|
return (dist >= -eps) ? 0.0 : double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double RayEdgeLeanMoving(
|
||||||
|
double vx, double vy,
|
||||||
|
ArrayView1D<double, Stride1D.Dense> mPrep, int j,
|
||||||
|
double dx, double dy, int direction, double eps)
|
||||||
|
{
|
||||||
|
var p1x = mPrep[j * 10 + 0] + dx;
|
||||||
|
var p1y = mPrep[j * 10 + 1] + dy;
|
||||||
|
var p2x = mPrep[j * 10 + 2] + dx;
|
||||||
|
var p2y = mPrep[j * 10 + 3] + dy;
|
||||||
|
|
||||||
|
if (direction >= 2) // Horizontal
|
||||||
|
{
|
||||||
|
var invDy = mPrep[j * 10 + 5];
|
||||||
|
if (invDy == 0) return double.MaxValue;
|
||||||
|
|
||||||
|
var t = (vy - p1y) * invDy;
|
||||||
|
if (t < -eps || t > 1.0 + eps) return double.MaxValue;
|
||||||
|
|
||||||
|
var ix = p1x + t * (p2x - p1x);
|
||||||
|
var dist = (direction == 2) ? (vx - ix) : (ix - vx);
|
||||||
|
|
||||||
|
if (dist > eps) return dist;
|
||||||
|
return (dist >= -eps) ? 0.0 : double.MaxValue;
|
||||||
|
}
|
||||||
|
else // Vertical
|
||||||
|
{
|
||||||
|
var invDx = mPrep[j * 10 + 4];
|
||||||
|
if (invDx == 0) return double.MaxValue;
|
||||||
|
|
||||||
|
var t = (vx - p1x) * invDx;
|
||||||
|
if (t < -eps || t > 1.0 + eps) return double.MaxValue;
|
||||||
|
|
||||||
|
var iy = p1y + t * (p2y - p1y);
|
||||||
|
var dist = (direction == 1) ? (vy - iy) : (iy - vy);
|
||||||
|
|
||||||
|
if (dist > eps) return dist;
|
||||||
|
return (dist >= -eps) ? 0.0 : double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_gpuStationaryRaw?.Dispose();
|
||||||
|
_gpuStationaryPrep?.Dispose();
|
||||||
|
_gpuMovingRaw?.Dispose();
|
||||||
|
_gpuMovingPrep?.Dispose();
|
||||||
|
_gpuOffsets?.Dispose();
|
||||||
|
_gpuResults?.Dispose();
|
||||||
|
_gpuDirs?.Dispose();
|
||||||
|
_accelerator?.Dispose();
|
||||||
|
_context?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ namespace OpenNest.Gpu
|
|||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program)
|
var entities = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
var polygons = new List<Polygon>();
|
var polygons = new List<Polygon>();
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ namespace OpenNest.Gpu
|
|||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
var polygons = new List<Polygon>();
|
var polygons = new List<Polygon>();
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ namespace OpenNest.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Helper.Optimize(lines);
|
GeometryOptimizer.Optimize(lines);
|
||||||
Helper.Optimize(arcs);
|
GeometryOptimizer.Optimize(arcs);
|
||||||
|
|
||||||
entities.AddRange(lines);
|
entities.AddRange(lines);
|
||||||
entities.AddRange(arcs);
|
entities.AddRange(arcs);
|
||||||
|
|||||||
@@ -122,5 +122,32 @@ namespace OpenNest.IO
|
|||||||
public double X { get; init; }
|
public double X { get; init; }
|
||||||
public double Y { get; init; }
|
public double Y { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record BestFitSetDto
|
||||||
|
{
|
||||||
|
public double PlateWidth { get; init; }
|
||||||
|
public double PlateHeight { get; init; }
|
||||||
|
public double Spacing { get; init; }
|
||||||
|
public List<BestFitResultDto> Results { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BestFitResultDto
|
||||||
|
{
|
||||||
|
public double Part1Rotation { get; init; }
|
||||||
|
public double Part2Rotation { get; init; }
|
||||||
|
public double Part2OffsetX { get; init; }
|
||||||
|
public double Part2OffsetY { get; init; }
|
||||||
|
public int StrategyType { get; init; }
|
||||||
|
public int TestNumber { get; init; }
|
||||||
|
public double CandidateSpacing { get; init; }
|
||||||
|
public double RotatedArea { get; init; }
|
||||||
|
public double BoundingWidth { get; init; }
|
||||||
|
public double BoundingHeight { get; init; }
|
||||||
|
public double OptimalRotation { get; init; }
|
||||||
|
public bool Keep { get; init; }
|
||||||
|
public string Reason { get; init; } = "";
|
||||||
|
public double TrueArea { get; init; }
|
||||||
|
public List<double> HullAngles { get; init; } = new();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.IO.Compression;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using static OpenNest.IO.NestFormat;
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
var programs = ReadPrograms(dto.Drawings.Count);
|
var programs = ReadPrograms(dto.Drawings.Count);
|
||||||
var drawingMap = BuildDrawings(dto, programs);
|
var drawingMap = BuildDrawings(dto, programs);
|
||||||
|
ReadBestFits(drawingMap);
|
||||||
var nest = BuildNest(dto, drawingMap);
|
var nest = BuildNest(dto, drawingMap);
|
||||||
|
|
||||||
zipArchive.Dispose();
|
zipArchive.Dispose();
|
||||||
@@ -97,6 +99,54 @@ namespace OpenNest.IO
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReadBestFits(Dictionary<int, Drawing> drawingMap)
|
||||||
|
{
|
||||||
|
foreach (var kvp in drawingMap)
|
||||||
|
{
|
||||||
|
var entry = zipArchive.GetEntry($"bestfits/bestfit-{kvp.Key}");
|
||||||
|
if (entry == null) continue;
|
||||||
|
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var reader = new StreamReader(entryStream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
|
||||||
|
var sets = JsonSerializer.Deserialize<List<BestFitSetDto>>(json, JsonOptions);
|
||||||
|
if (sets == null) continue;
|
||||||
|
|
||||||
|
PopulateBestFitSets(kvp.Value, sets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateBestFitSets(Drawing drawing, List<BestFitSetDto> sets)
|
||||||
|
{
|
||||||
|
foreach (var set in sets)
|
||||||
|
{
|
||||||
|
var results = set.Results.Select(r => new BestFitResult
|
||||||
|
{
|
||||||
|
Candidate = new PairCandidate
|
||||||
|
{
|
||||||
|
Drawing = drawing,
|
||||||
|
Part1Rotation = r.Part1Rotation,
|
||||||
|
Part2Rotation = r.Part2Rotation,
|
||||||
|
Part2Offset = new Vector(r.Part2OffsetX, r.Part2OffsetY),
|
||||||
|
StrategyType = r.StrategyType,
|
||||||
|
TestNumber = r.TestNumber,
|
||||||
|
Spacing = r.CandidateSpacing
|
||||||
|
},
|
||||||
|
RotatedArea = r.RotatedArea,
|
||||||
|
BoundingWidth = r.BoundingWidth,
|
||||||
|
BoundingHeight = r.BoundingHeight,
|
||||||
|
OptimalRotation = r.OptimalRotation,
|
||||||
|
Keep = r.Keep,
|
||||||
|
Reason = r.Reason,
|
||||||
|
TrueArea = r.TrueArea,
|
||||||
|
HullAngles = r.HullAngles
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
BestFitCache.Populate(drawing, set.PlateWidth, set.PlateHeight, set.Spacing, results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Nest BuildNest(NestDto dto, Dictionary<int, Drawing> drawingMap)
|
private Nest BuildNest(NestDto dto, Dictionary<int, Drawing> drawingMap)
|
||||||
{
|
{
|
||||||
var nest = new Nest();
|
var nest = new Nest();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using static OpenNest.IO.NestFormat;
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
WriteNestJson(zipArchive);
|
WriteNestJson(zipArchive);
|
||||||
WritePrograms(zipArchive);
|
WritePrograms(zipArchive);
|
||||||
|
WriteBestFits(zipArchive);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -185,6 +188,70 @@ namespace OpenNest.IO
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<BestFitSetDto> BuildBestFitDtos(Drawing drawing)
|
||||||
|
{
|
||||||
|
var allBestFits = BestFitCache.GetAllForDrawing(drawing);
|
||||||
|
var sets = new List<BestFitSetDto>();
|
||||||
|
|
||||||
|
// Only save best-fit sets for plate sizes actually used in this nest.
|
||||||
|
var plateSizes = new HashSet<(double, double, double)>();
|
||||||
|
foreach (var plate in nest.Plates)
|
||||||
|
plateSizes.Add((plate.Size.Width, plate.Size.Length, plate.PartSpacing));
|
||||||
|
|
||||||
|
foreach (var kvp in allBestFits)
|
||||||
|
{
|
||||||
|
if (!plateSizes.Contains((kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var results = kvp.Value
|
||||||
|
.Where(r => r.Keep)
|
||||||
|
.Select(r => new BestFitResultDto
|
||||||
|
{
|
||||||
|
Part1Rotation = r.Candidate.Part1Rotation,
|
||||||
|
Part2Rotation = r.Candidate.Part2Rotation,
|
||||||
|
Part2OffsetX = r.Candidate.Part2Offset.X,
|
||||||
|
Part2OffsetY = r.Candidate.Part2Offset.Y,
|
||||||
|
StrategyType = r.Candidate.StrategyType,
|
||||||
|
TestNumber = r.Candidate.TestNumber,
|
||||||
|
CandidateSpacing = r.Candidate.Spacing,
|
||||||
|
RotatedArea = r.RotatedArea,
|
||||||
|
BoundingWidth = r.BoundingWidth,
|
||||||
|
BoundingHeight = r.BoundingHeight,
|
||||||
|
OptimalRotation = r.OptimalRotation,
|
||||||
|
Keep = r.Keep,
|
||||||
|
Reason = r.Reason ?? "",
|
||||||
|
TrueArea = r.TrueArea,
|
||||||
|
HullAngles = r.HullAngles ?? new List<double>()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
sets.Add(new BestFitSetDto
|
||||||
|
{
|
||||||
|
PlateWidth = kvp.Key.PlateWidth,
|
||||||
|
PlateHeight = kvp.Key.PlateHeight,
|
||||||
|
Spacing = kvp.Key.Spacing,
|
||||||
|
Results = results
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteBestFits(ZipArchive zipArchive)
|
||||||
|
{
|
||||||
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
var sets = BuildBestFitDtos(kvp.Value);
|
||||||
|
if (sets.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(sets, JsonOptions);
|
||||||
|
var entry = zipArchive.CreateEntry($"bestfits/bestfit-{kvp.Key}");
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||||
|
writer.Write(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void WritePrograms(ZipArchive zipArchive)
|
private void WritePrograms(ZipArchive zipArchive)
|
||||||
{
|
{
|
||||||
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
return $"Error: drawing '{drawingName}' not found";
|
return $"Error: drawing '{drawingName}' not found";
|
||||||
|
|
||||||
var countBefore = plate.Parts.Count;
|
var countBefore = plate.Parts.Count;
|
||||||
var engine = new NestEngine(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
||||||
var success = engine.Fill(item);
|
var success = engine.Fill(item);
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
return $"Error: drawing '{drawingName}' not found";
|
return $"Error: drawing '{drawingName}' not found";
|
||||||
|
|
||||||
var countBefore = plate.Parts.Count;
|
var countBefore = plate.Parts.Count;
|
||||||
var engine = new NestEngine(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
||||||
var area = new Box(x, y, width, height);
|
var area = new Box(x, y, width, height);
|
||||||
var success = engine.Fill(item, area);
|
var success = engine.Fill(item, area);
|
||||||
@@ -111,7 +111,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
sb.AppendLine($"Found {remnants.Count} remnant area(s) on plate {plateIndex}");
|
sb.AppendLine($"Found {remnants.Count} remnant area(s) on plate {plateIndex}");
|
||||||
|
|
||||||
var totalAdded = 0;
|
var totalAdded = 0;
|
||||||
var engine = new NestEngine(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
|
||||||
for (var i = 0; i < remnants.Count; i++)
|
for (var i = 0; i < remnants.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
}
|
}
|
||||||
|
|
||||||
var countBefore = plate.Parts.Count;
|
var countBefore = plate.Parts.Count;
|
||||||
var engine = new NestEngine(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var success = engine.Pack(items);
|
var success = engine.Pack(items);
|
||||||
var countAfter = plate.Parts.Count;
|
var countAfter = plate.Parts.Count;
|
||||||
var added = countAfter - countBefore;
|
var added = countAfter - countBefore;
|
||||||
@@ -193,7 +193,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool(Name = "autonest_plate")]
|
[McpServerTool(Name = "autonest_plate")]
|
||||||
[Description("NFP-based mixed-part autonesting. Places multiple different drawings on a plate with geometry-aware collision avoidance and simulated annealing optimization. Produces tighter layouts than pack_plate by allowing parts to interlock.")]
|
[Description("Mixed-part autonesting. Fills the plate with multiple different drawings using iterative per-drawing fills with remainder-strip packing.")]
|
||||||
public string AutoNestPlate(
|
public string AutoNestPlate(
|
||||||
[Description("Index of the plate")] int plateIndex,
|
[Description("Index of the plate")] int plateIndex,
|
||||||
[Description("Comma-separated drawing names")] string drawingNames,
|
[Description("Comma-separated drawing names")] string drawingNames,
|
||||||
@@ -233,16 +233,18 @@ namespace OpenNest.Mcp.Tools
|
|||||||
items.Add(new NestItem { Drawing = drawing, Quantity = qtys[i] });
|
items.Add(new NestItem { Drawing = drawing, Quantity = qtys[i] });
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = NestEngine.AutoNest(items, plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
plate.Parts.AddRange(parts);
|
var nestParts = engine.Nest(items, null, CancellationToken.None);
|
||||||
|
plate.Parts.AddRange(nestParts);
|
||||||
|
var totalPlaced = nestParts.Count;
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"AutoNest plate {plateIndex}: {(parts.Count > 0 ? "success" : "no parts placed")}");
|
sb.AppendLine($"AutoNest plate {plateIndex} ({engine.Name} engine): {(totalPlaced > 0 ? "success" : "no parts placed")}");
|
||||||
sb.AppendLine($" Parts placed: {parts.Count}");
|
sb.AppendLine($" Parts placed: {totalPlaced}");
|
||||||
sb.AppendLine($" Total parts: {plate.Parts.Count}");
|
sb.AppendLine($" Total parts: {plate.Parts.Count}");
|
||||||
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
||||||
|
|
||||||
var groups = parts.GroupBy(p => p.BaseDrawing.Name);
|
var groups = plate.Parts.GroupBy(p => p.BaseDrawing.Name);
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
sb.AppendLine($" {group.Key}: {group.Count()}");
|
sb.AppendLine($" {group.Key}: {group.Count()}");
|
||||||
|
|
||||||
|
|||||||
23
OpenNest.Tests/CuttingResultTests.cs
Normal file
23
OpenNest.Tests/CuttingResultTests.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class CuttingResultTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CuttingResult_StoresValues()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(1, 2)));
|
||||||
|
var point = new Vector(3, 4);
|
||||||
|
|
||||||
|
var result = new CuttingResult { Program = pgm, LastCutPoint = point };
|
||||||
|
|
||||||
|
Assert.Same(pgm, result.Program);
|
||||||
|
Assert.Equal(3, result.LastCutPoint.X);
|
||||||
|
Assert.Equal(4, result.LastCutPoint.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
OpenNest.Tests/OpenNest.Tests.csproj
Normal file
29
OpenNest.Tests/OpenNest.Tests.csproj
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.5.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
32
OpenNest.Tests/PartFlagTests.cs
Normal file
32
OpenNest.Tests/PartFlagTests.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class PartFlagTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HasManualLeadIns_DefaultsFalse()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("test", pgm);
|
||||||
|
var part = new Part(drawing);
|
||||||
|
|
||||||
|
Assert.False(part.HasManualLeadIns);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HasManualLeadIns_CanBeSet()
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("test", pgm);
|
||||||
|
var part = new Part(drawing);
|
||||||
|
|
||||||
|
part.HasManualLeadIns = true;
|
||||||
|
|
||||||
|
Assert.True(part.HasManualLeadIns);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
OpenNest.Tests/PlateProcessorTests.cs
Normal file
132
OpenNest.Tests/PlateProcessorTests.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
public class PlateProcessorTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y, size: 2);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Process_ReturnsAllParts()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
plate.Parts.Add(MakePartAt(10, 10));
|
||||||
|
plate.Parts.Add(MakePartAt(30, 30));
|
||||||
|
plate.Parts.Add(MakePartAt(50, 50));
|
||||||
|
|
||||||
|
var processor = new PlateProcessor
|
||||||
|
{
|
||||||
|
Sequencer = new RightSideSequencer(),
|
||||||
|
RapidPlanner = new SafeHeightRapidPlanner()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = processor.Process(plate);
|
||||||
|
|
||||||
|
Assert.Equal(3, result.Parts.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Process_PreservesSequenceOrder()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var left = MakePartAt(5, 10);
|
||||||
|
var right = MakePartAt(50, 10);
|
||||||
|
plate.Parts.Add(left);
|
||||||
|
plate.Parts.Add(right);
|
||||||
|
|
||||||
|
var processor = new PlateProcessor
|
||||||
|
{
|
||||||
|
Sequencer = new RightSideSequencer(),
|
||||||
|
RapidPlanner = new SafeHeightRapidPlanner()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = processor.Process(plate);
|
||||||
|
|
||||||
|
Assert.Same(right, result.Parts[0].Part);
|
||||||
|
Assert.Same(left, result.Parts[1].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Process_SkipsCuttingStrategy_WhenManualLeadIns()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var part = MakePartAt(10, 10);
|
||||||
|
part.HasManualLeadIns = true;
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
|
var processor = new PlateProcessor
|
||||||
|
{
|
||||||
|
Sequencer = new LeftSideSequencer(),
|
||||||
|
CuttingStrategy = new ContourCuttingStrategy
|
||||||
|
{
|
||||||
|
Parameters = new CuttingParameters()
|
||||||
|
},
|
||||||
|
RapidPlanner = new SafeHeightRapidPlanner()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = processor.Process(plate);
|
||||||
|
|
||||||
|
Assert.Same(part.Program, result.Parts[0].ProcessedProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Process_DoesNotMutatePart()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var part = MakePartAt(10, 10);
|
||||||
|
var originalProgram = part.Program;
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
|
var processor = new PlateProcessor
|
||||||
|
{
|
||||||
|
Sequencer = new LeftSideSequencer(),
|
||||||
|
RapidPlanner = new SafeHeightRapidPlanner()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = processor.Process(plate);
|
||||||
|
|
||||||
|
Assert.Same(originalProgram, part.Program);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Process_NoCuttingStrategy_PassesProgramThrough()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var part = MakePartAt(10, 10);
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
|
var processor = new PlateProcessor
|
||||||
|
{
|
||||||
|
Sequencer = new LeftSideSequencer(),
|
||||||
|
RapidPlanner = new SafeHeightRapidPlanner()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = processor.Process(plate);
|
||||||
|
|
||||||
|
Assert.Same(part.Program, result.Parts[0].ProcessedProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Process_EmptyPlate_ReturnsEmptyResult()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
|
||||||
|
var processor = new PlateProcessor
|
||||||
|
{
|
||||||
|
Sequencer = new LeftSideSequencer(),
|
||||||
|
RapidPlanner = new SafeHeightRapidPlanner()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = processor.Process(plate);
|
||||||
|
|
||||||
|
Assert.Empty(result.Parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs
Normal file
56
OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.RapidPlanning;
|
||||||
|
|
||||||
|
public class DirectRapidPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NoCutAreas_ReturnsHeadDown()
|
||||||
|
{
|
||||||
|
var planner = new DirectRapidPlanner();
|
||||||
|
var result = planner.Plan(new Vector(0, 0), new Vector(10, 10), new List<Shape>());
|
||||||
|
|
||||||
|
Assert.False(result.HeadUp);
|
||||||
|
Assert.Empty(result.Waypoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearPath_ReturnsHeadDown()
|
||||||
|
{
|
||||||
|
var planner = new DirectRapidPlanner();
|
||||||
|
|
||||||
|
var cutArea = new Shape();
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(50, 0), new Vector(50, 10)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(50, 10), new Vector(60, 10)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(60, 10), new Vector(60, 0)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(60, 0), new Vector(50, 0)));
|
||||||
|
|
||||||
|
var result = planner.Plan(
|
||||||
|
new Vector(0, 0), new Vector(10, 10),
|
||||||
|
new List<Shape> { cutArea });
|
||||||
|
|
||||||
|
Assert.False(result.HeadUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BlockedPath_ReturnsHeadUp()
|
||||||
|
{
|
||||||
|
var planner = new DirectRapidPlanner();
|
||||||
|
|
||||||
|
var cutArea = new Shape();
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(5, 0), new Vector(5, 20)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(5, 20), new Vector(6, 20)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(6, 20), new Vector(6, 0)));
|
||||||
|
cutArea.Entities.Add(new Line(new Vector(6, 0), new Vector(5, 0)));
|
||||||
|
|
||||||
|
var result = planner.Plan(
|
||||||
|
new Vector(0, 10), new Vector(10, 10),
|
||||||
|
new List<Shape> { cutArea });
|
||||||
|
|
||||||
|
Assert.True(result.HeadUp);
|
||||||
|
Assert.Empty(result.Waypoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs
Normal file
39
OpenNest.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Engine.RapidPlanning;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.RapidPlanning;
|
||||||
|
|
||||||
|
public class SafeHeightRapidPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AlwaysReturnsHeadUp()
|
||||||
|
{
|
||||||
|
var planner = new SafeHeightRapidPlanner();
|
||||||
|
var from = new Vector(10, 10);
|
||||||
|
var to = new Vector(50, 50);
|
||||||
|
var cutAreas = new List<Shape>();
|
||||||
|
|
||||||
|
var result = planner.Plan(from, to, cutAreas);
|
||||||
|
|
||||||
|
Assert.True(result.HeadUp);
|
||||||
|
Assert.Empty(result.Waypoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnsHeadUp_EvenWithCutAreas()
|
||||||
|
{
|
||||||
|
var planner = new SafeHeightRapidPlanner();
|
||||||
|
var from = new Vector(0, 0);
|
||||||
|
var to = new Vector(10, 10);
|
||||||
|
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(new Line(new Vector(5, 0), new Vector(5, 20)));
|
||||||
|
var cutAreas = new List<Shape> { shape };
|
||||||
|
|
||||||
|
var result = planner.Plan(from, to, cutAreas);
|
||||||
|
|
||||||
|
Assert.True(result.HeadUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs
Normal file
69
OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class AdvancedSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GroupsIntoRows_NoAlternate()
|
||||||
|
{
|
||||||
|
var plate = new Plate(100, 100);
|
||||||
|
var row1a = MakePartAt(10, 10);
|
||||||
|
var row1b = MakePartAt(30, 10);
|
||||||
|
var row2a = MakePartAt(10, 50);
|
||||||
|
var row2b = MakePartAt(30, 50);
|
||||||
|
plate.Parts.Add(row1a);
|
||||||
|
plate.Parts.Add(row1b);
|
||||||
|
plate.Parts.Add(row2a);
|
||||||
|
plate.Parts.Add(row2b);
|
||||||
|
|
||||||
|
var parameters = new SequenceParameters
|
||||||
|
{
|
||||||
|
Method = SequenceMethod.Advanced,
|
||||||
|
MinDistanceBetweenRowsColumns = 5.0,
|
||||||
|
AlternateRowsColumns = false
|
||||||
|
};
|
||||||
|
var sequencer = new AdvancedSequencer(parameters);
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(row1a, result[0].Part);
|
||||||
|
Assert.Same(row1b, result[1].Part);
|
||||||
|
Assert.Same(row2a, result[2].Part);
|
||||||
|
Assert.Same(row2b, result[3].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SerpentineAlternatesDirection()
|
||||||
|
{
|
||||||
|
var plate = new Plate(100, 100);
|
||||||
|
var r1Left = MakePartAt(10, 10);
|
||||||
|
var r1Right = MakePartAt(30, 10);
|
||||||
|
var r2Left = MakePartAt(10, 50);
|
||||||
|
var r2Right = MakePartAt(30, 50);
|
||||||
|
plate.Parts.Add(r1Left);
|
||||||
|
plate.Parts.Add(r1Right);
|
||||||
|
plate.Parts.Add(r2Left);
|
||||||
|
plate.Parts.Add(r2Right);
|
||||||
|
|
||||||
|
var parameters = new SequenceParameters
|
||||||
|
{
|
||||||
|
Method = SequenceMethod.Advanced,
|
||||||
|
MinDistanceBetweenRowsColumns = 5.0,
|
||||||
|
AlternateRowsColumns = true
|
||||||
|
};
|
||||||
|
var sequencer = new AdvancedSequencer(parameters);
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(r1Left, result[0].Part);
|
||||||
|
Assert.Same(r1Right, result[1].Part);
|
||||||
|
Assert.Same(r2Right, result[2].Part);
|
||||||
|
Assert.Same(r2Left, result[3].Part);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs
Normal file
75
OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class DirectionalSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
private static Plate MakePlate(params Part[] parts) => TestHelpers.MakePlate(60, 120, parts);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RightSide_SortsXDescending()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(10, 5);
|
||||||
|
var b = MakePartAt(30, 5);
|
||||||
|
var c = MakePartAt(20, 5);
|
||||||
|
var plate = MakePlate(a, b, c);
|
||||||
|
|
||||||
|
var sequencer = new RightSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(b, result[0].Part);
|
||||||
|
Assert.Same(c, result[1].Part);
|
||||||
|
Assert.Same(a, result[2].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LeftSide_SortsXAscending()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(10, 5);
|
||||||
|
var b = MakePartAt(30, 5);
|
||||||
|
var c = MakePartAt(20, 5);
|
||||||
|
var plate = MakePlate(a, b, c);
|
||||||
|
|
||||||
|
var sequencer = new LeftSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(a, result[0].Part);
|
||||||
|
Assert.Same(c, result[1].Part);
|
||||||
|
Assert.Same(b, result[2].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BottomSide_SortsYAscending()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(5, 20);
|
||||||
|
var b = MakePartAt(5, 5);
|
||||||
|
var c = MakePartAt(5, 10);
|
||||||
|
var plate = MakePlate(a, b, c);
|
||||||
|
|
||||||
|
var sequencer = new BottomSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(b, result[0].Part);
|
||||||
|
Assert.Same(c, result[1].Part);
|
||||||
|
Assert.Same(a, result[2].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RightSide_TiesBrokenByPerpendicularAxis()
|
||||||
|
{
|
||||||
|
var a = MakePartAt(10, 20);
|
||||||
|
var b = MakePartAt(10, 5);
|
||||||
|
var plate = MakePlate(a, b);
|
||||||
|
|
||||||
|
var sequencer = new RightSideSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(b, result[0].Part);
|
||||||
|
Assert.Same(a, result[1].Part);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs
Normal file
31
OpenNest.Tests/Sequencing/EdgeStartSequencerTests.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class EdgeStartSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SortsByDistanceFromNearestEdge()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var edgePart = MakePartAt(1, 1);
|
||||||
|
var centerPart = MakePartAt(25, 55);
|
||||||
|
var midPart = MakePartAt(10, 10);
|
||||||
|
plate.Parts.Add(edgePart);
|
||||||
|
plate.Parts.Add(centerPart);
|
||||||
|
plate.Parts.Add(midPart);
|
||||||
|
|
||||||
|
var sequencer = new EdgeStartSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Same(edgePart, result[0].Part);
|
||||||
|
Assert.Same(midPart, result[1].Part);
|
||||||
|
Assert.Same(centerPart, result[2].Part);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs
Normal file
61
OpenNest.Tests/Sequencing/LeastCodeSequencerTests.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class LeastCodeSequencerTests
|
||||||
|
{
|
||||||
|
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NearestNeighbor_FromExitPoint()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
var farPart = MakePartAt(5, 5);
|
||||||
|
var nearPart = MakePartAt(55, 115);
|
||||||
|
plate.Parts.Add(farPart);
|
||||||
|
plate.Parts.Add(nearPart);
|
||||||
|
|
||||||
|
var sequencer = new LeastCodeSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
// nearPart is closer to exit point, should come first
|
||||||
|
Assert.Same(nearPart, result[0].Part);
|
||||||
|
Assert.Same(farPart, result[1].Part);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreservesAllParts()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120);
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
plate.Parts.Add(MakePartAt(i * 5, i * 10));
|
||||||
|
|
||||||
|
var sequencer = new LeastCodeSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Equal(10, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoOpt_ImprovesSolution()
|
||||||
|
{
|
||||||
|
var plate = new Plate(100, 100);
|
||||||
|
var a = MakePartAt(90, 90);
|
||||||
|
var b = MakePartAt(10, 80);
|
||||||
|
var c = MakePartAt(80, 10);
|
||||||
|
var d = MakePartAt(5, 5);
|
||||||
|
plate.Parts.Add(a);
|
||||||
|
plate.Parts.Add(b);
|
||||||
|
plate.Parts.Add(c);
|
||||||
|
plate.Parts.Add(d);
|
||||||
|
|
||||||
|
var sequencer = new LeastCodeSequencer();
|
||||||
|
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||||
|
|
||||||
|
Assert.Equal(4, result.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs
Normal file
30
OpenNest.Tests/Sequencing/PartSequencerFactoryTests.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using OpenNest.CNC.CuttingStrategy;
|
||||||
|
using OpenNest.Engine.Sequencing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Sequencing;
|
||||||
|
|
||||||
|
public class PartSequencerFactoryTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(SequenceMethod.RightSide, typeof(RightSideSequencer))]
|
||||||
|
[InlineData(SequenceMethod.LeftSide, typeof(LeftSideSequencer))]
|
||||||
|
[InlineData(SequenceMethod.BottomSide, typeof(BottomSideSequencer))]
|
||||||
|
[InlineData(SequenceMethod.EdgeStart, typeof(EdgeStartSequencer))]
|
||||||
|
[InlineData(SequenceMethod.LeastCode, typeof(LeastCodeSequencer))]
|
||||||
|
[InlineData(SequenceMethod.Advanced, typeof(AdvancedSequencer))]
|
||||||
|
public void Create_ReturnsCorrectType(SequenceMethod method, Type expectedType)
|
||||||
|
{
|
||||||
|
var parameters = new SequenceParameters { Method = method };
|
||||||
|
var sequencer = PartSequencerFactory.Create(parameters);
|
||||||
|
Assert.IsType(expectedType, sequencer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_RightSideAlt_Throws()
|
||||||
|
{
|
||||||
|
var parameters = new SequenceParameters { Method = SequenceMethod.RightSideAlt };
|
||||||
|
Assert.Throws<NotSupportedException>(() => PartSequencerFactory.Create(parameters));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
OpenNest.Tests/TestHelpers.cs
Normal file
27
OpenNest.Tests/TestHelpers.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests;
|
||||||
|
|
||||||
|
internal static class TestHelpers
|
||||||
|
{
|
||||||
|
public static Part MakePartAt(double x, double y, double size = 1)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
|
||||||
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("test", pgm);
|
||||||
|
return new Part(drawing, new Vector(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Plate MakePlate(double width = 60, double length = 120, params Part[] parts)
|
||||||
|
{
|
||||||
|
var plate = new Plate(width, length);
|
||||||
|
foreach (var p in parts)
|
||||||
|
plate.Parts.Add(p);
|
||||||
|
return plate;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
OpenNest.Training/Data/TrainingAngleResult.cs
Normal file
20
OpenNest.Training/Data/TrainingAngleResult.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
[Table("AngleResults")]
|
||||||
|
public class TrainingAngleResult
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public long RunId { get; set; }
|
||||||
|
public double AngleDeg { get; set; }
|
||||||
|
public string Direction { get; set; }
|
||||||
|
public int PartCount { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(RunId))]
|
||||||
|
public TrainingRun Run { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
47
OpenNest.Training/Data/TrainingDbContext.cs
Normal file
47
OpenNest.Training/Data/TrainingDbContext.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
public class TrainingDbContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<TrainingPart> Parts { get; set; }
|
||||||
|
public DbSet<TrainingRun> Runs { get; set; }
|
||||||
|
public DbSet<TrainingAngleResult> AngleResults { get; set; }
|
||||||
|
|
||||||
|
private readonly string _dbPath;
|
||||||
|
|
||||||
|
public TrainingDbContext(string dbPath)
|
||||||
|
{
|
||||||
|
_dbPath = dbPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
|
{
|
||||||
|
options.UseSqlite($"Data Source={_dbPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<TrainingPart>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(p => p.FileName).HasDatabaseName("idx_parts_filename");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TrainingRun>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(r => r.PartId).HasDatabaseName("idx_runs_partid");
|
||||||
|
e.HasOne(r => r.Part)
|
||||||
|
.WithMany(p => p.Runs)
|
||||||
|
.HasForeignKey(r => r.PartId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TrainingAngleResult>(e =>
|
||||||
|
{
|
||||||
|
e.HasIndex(a => a.RunId).HasDatabaseName("idx_angleresults_runid");
|
||||||
|
e.HasOne(a => a.Run)
|
||||||
|
.WithMany(r => r.AngleResults)
|
||||||
|
.HasForeignKey(a => a.RunId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
OpenNest.Training/Data/TrainingPart.cs
Normal file
28
OpenNest.Training/Data/TrainingPart.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
[Table("Parts")]
|
||||||
|
public class TrainingPart
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(260)]
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
public double Area { get; set; }
|
||||||
|
public double Convexity { get; set; }
|
||||||
|
public double AspectRatio { get; set; }
|
||||||
|
public double BBFill { get; set; }
|
||||||
|
public double Circularity { get; set; }
|
||||||
|
public double PerimeterToAreaRatio { get; set; }
|
||||||
|
public int VertexCount { get; set; }
|
||||||
|
public byte[] Bitmask { get; set; }
|
||||||
|
public string GeometryData { get; set; }
|
||||||
|
|
||||||
|
public List<TrainingRun> Runs { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
OpenNest.Training/Data/TrainingRun.cs
Normal file
36
OpenNest.Training/Data/TrainingRun.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace OpenNest.Training.Data
|
||||||
|
{
|
||||||
|
[Table("Runs")]
|
||||||
|
public class TrainingRun
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public long PartId { get; set; }
|
||||||
|
public double SheetWidth { get; set; }
|
||||||
|
public double SheetHeight { get; set; }
|
||||||
|
public double Spacing { get; set; }
|
||||||
|
public int PartCount { get; set; }
|
||||||
|
public double Utilization { get; set; }
|
||||||
|
public long TimeMs { get; set; }
|
||||||
|
public string LayoutData { get; set; }
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
public string WinnerEngine { get; set; } = "";
|
||||||
|
public long WinnerTimeMs { get; set; }
|
||||||
|
public string RunnerUpEngine { get; set; } = "";
|
||||||
|
public int RunnerUpPartCount { get; set; }
|
||||||
|
public long RunnerUpTimeMs { get; set; }
|
||||||
|
public string ThirdPlaceEngine { get; set; } = "";
|
||||||
|
public int ThirdPlacePartCount { get; set; }
|
||||||
|
public long ThirdPlaceTimeMs { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(PartId))]
|
||||||
|
public TrainingPart Part { get; set; }
|
||||||
|
|
||||||
|
public List<TrainingAngleResult> AngleResults { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
OpenNest.Training/OpenNest.Training.csproj
Normal file
19
OpenNest.Training/OpenNest.Training.csproj
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<RootNamespace>OpenNest.Training</RootNamespace>
|
||||||
|
<AssemblyName>OpenNest.Training</AssemblyName>
|
||||||
|
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
305
OpenNest.Training/Program.cs
Normal file
305
OpenNest.Training/Program.cs
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using Color = System.Drawing.Color;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
|
using OpenNest.Engine.ML;
|
||||||
|
using OpenNest.Gpu;
|
||||||
|
using OpenNest.Training;
|
||||||
|
|
||||||
|
// Parse arguments.
|
||||||
|
var dbPath = "OpenNestTraining";
|
||||||
|
var saveNestsDir = (string)null;
|
||||||
|
var templateFile = (string)null;
|
||||||
|
var spacing = 0.5;
|
||||||
|
var collectDir = (string)null;
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--db" when i + 1 < args.Length:
|
||||||
|
dbPath = args[++i];
|
||||||
|
break;
|
||||||
|
case "--save-nests" when i + 1 < args.Length:
|
||||||
|
saveNestsDir = args[++i];
|
||||||
|
break;
|
||||||
|
case "--template" when i + 1 < args.Length:
|
||||||
|
templateFile = args[++i];
|
||||||
|
break;
|
||||||
|
case "--spacing" when i + 1 < args.Length:
|
||||||
|
spacing = double.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
PrintUsage();
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
if (!args[i].StartsWith("--") && collectDir == null)
|
||||||
|
collectDir = args[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(collectDir) || !Directory.Exists(collectDir))
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize GPU if available.
|
||||||
|
if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
|
{
|
||||||
|
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||||
|
Console.WriteLine($"GPU: {GpuEvaluatorFactory.DeviceName}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("GPU: not available (using CPU)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunDataCollection(collectDir, dbPath, saveNestsDir, spacing, templateFile);
|
||||||
|
|
||||||
|
int RunDataCollection(string dir, string dbPath, string saveDir, double s, string template)
|
||||||
|
{
|
||||||
|
// Load template nest for plate defaults if provided.
|
||||||
|
Nest templateNest = null;
|
||||||
|
if (template != null)
|
||||||
|
{
|
||||||
|
if (!File.Exists(template))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: Template not found: {template}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
templateNest = new NestReader(template).Read();
|
||||||
|
Console.WriteLine($"Using template: {template}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var PartColors = new[]
|
||||||
|
{
|
||||||
|
Color.FromArgb(205, 92, 92),
|
||||||
|
Color.FromArgb(148, 103, 189),
|
||||||
|
Color.FromArgb(75, 180, 175),
|
||||||
|
Color.FromArgb(210, 190, 75),
|
||||||
|
Color.FromArgb(190, 85, 175),
|
||||||
|
Color.FromArgb(185, 115, 85),
|
||||||
|
Color.FromArgb(120, 100, 190),
|
||||||
|
Color.FromArgb(200, 100, 140),
|
||||||
|
Color.FromArgb(80, 175, 155),
|
||||||
|
Color.FromArgb(195, 160, 85),
|
||||||
|
Color.FromArgb(175, 95, 160),
|
||||||
|
Color.FromArgb(215, 130, 130),
|
||||||
|
};
|
||||||
|
|
||||||
|
var sheetSuite = new[]
|
||||||
|
{
|
||||||
|
new Size(96, 48), new Size(120, 48), new Size(144, 48),
|
||||||
|
new Size(96, 60), new Size(120, 60), new Size(144, 60),
|
||||||
|
new Size(96, 72), new Size(120, 72), new Size(144, 72),
|
||||||
|
new Size(48, 24), new Size(120, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
var dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories);
|
||||||
|
Console.WriteLine($"Found {dxfFiles.Length} DXF files");
|
||||||
|
var resolvedDb = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) ? dbPath : dbPath + ".db";
|
||||||
|
Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}");
|
||||||
|
Console.WriteLine($"Sheet sizes: {sheetSuite.Length} configurations");
|
||||||
|
Console.WriteLine($"Spacing: {s:F2}");
|
||||||
|
if (saveDir != null) Console.WriteLine($"Saving nests to: {saveDir}");
|
||||||
|
Console.WriteLine("---");
|
||||||
|
|
||||||
|
using var db = new TrainingDatabase(dbPath);
|
||||||
|
|
||||||
|
var backfilled = db.BackfillPerimeterToAreaRatio();
|
||||||
|
if (backfilled > 0)
|
||||||
|
Console.WriteLine($"Backfilled PerimeterToAreaRatio for {backfilled} existing parts");
|
||||||
|
|
||||||
|
var importer = new DxfImporter();
|
||||||
|
var colorIndex = 0;
|
||||||
|
var processed = 0;
|
||||||
|
var skippedGeometry = 0;
|
||||||
|
var skippedFeatures = 0;
|
||||||
|
var skippedExisting = 0;
|
||||||
|
var totalRuns = 0;
|
||||||
|
var totalSw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
foreach (var file in dxfFiles)
|
||||||
|
{
|
||||||
|
var fileNum = processed + skippedGeometry + skippedFeatures + skippedExisting + 1;
|
||||||
|
var partNo = Path.GetFileNameWithoutExtension(file);
|
||||||
|
Console.Write($"[{fileNum}/{dxfFiles.Length}] {partNo}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existingRuns = db.RunCount(Path.GetFileName(file));
|
||||||
|
if (existingRuns >= sheetSuite.Length)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" - SKIP (all sizes done)");
|
||||||
|
skippedExisting++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!importer.GetGeometry(file, out var entities))
|
||||||
|
{
|
||||||
|
Console.WriteLine(" - SKIP (no geometry)");
|
||||||
|
skippedGeometry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawing = new Drawing(Path.GetFileName(file));
|
||||||
|
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(entities);
|
||||||
|
drawing.UpdateArea();
|
||||||
|
drawing.Color = PartColors[colorIndex % PartColors.Length];
|
||||||
|
colorIndex++;
|
||||||
|
|
||||||
|
var features = FeatureExtractor.Extract(drawing);
|
||||||
|
if (features == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" - SKIP (feature extraction failed)");
|
||||||
|
skippedFeatures++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($" (area={features.Area:F1}, verts={features.VertexCount})");
|
||||||
|
|
||||||
|
// Precompute best-fits once for all sheet sizes.
|
||||||
|
var sizes = sheetSuite.Select(sz => (sz.Width, sz.Length)).ToList();
|
||||||
|
var bfSw = Stopwatch.StartNew();
|
||||||
|
BestFitCache.ComputeForSizes(drawing, s, sizes);
|
||||||
|
bfSw.Stop();
|
||||||
|
Console.WriteLine($" Best-fits computed in {bfSw.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
var partId = db.GetOrAddPart(Path.GetFileName(file), features, drawing.Program.ToString());
|
||||||
|
var partSw = Stopwatch.StartNew();
|
||||||
|
var runsThisPart = 0;
|
||||||
|
var bestUtil = 0.0;
|
||||||
|
var bestCount = 0;
|
||||||
|
|
||||||
|
foreach (var size in sheetSuite)
|
||||||
|
{
|
||||||
|
if (db.HasRun(Path.GetFileName(file), size.Width, size.Length, s))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {size.Length}x{size.Width} - skip (exists)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plate runPlate;
|
||||||
|
if (templateNest != null)
|
||||||
|
{
|
||||||
|
runPlate = templateNest.PlateDefaults.CreateNew();
|
||||||
|
runPlate.Size = size;
|
||||||
|
runPlate.PartSpacing = s;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runPlate = new Plate { Size = size, PartSpacing = s };
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeSw = Stopwatch.StartNew();
|
||||||
|
var result = BruteForceRunner.Run(drawing, runPlate, forceFullAngleSweep: true);
|
||||||
|
sizeSw.Stop();
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {size.Length}x{size.Width} - no fit");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Utilization > bestUtil)
|
||||||
|
{
|
||||||
|
bestUtil = result.Utilization;
|
||||||
|
bestCount = result.PartCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
var engineInfo = $"{result.WinnerEngine}({result.WinnerTimeMs}ms)";
|
||||||
|
if (!string.IsNullOrEmpty(result.RunnerUpEngine))
|
||||||
|
engineInfo += $", 2nd={result.RunnerUpEngine}({result.RunnerUpPartCount}pcs/{result.RunnerUpTimeMs}ms)";
|
||||||
|
if (!string.IsNullOrEmpty(result.ThirdPlaceEngine))
|
||||||
|
engineInfo += $", 3rd={result.ThirdPlaceEngine}({result.ThirdPlacePartCount}pcs/{result.ThirdPlaceTimeMs}ms)";
|
||||||
|
Console.WriteLine($" {size.Length}x{size.Width} - {result.PartCount}pcs, {result.Utilization:P1}, {sizeSw.ElapsedMilliseconds}ms [{engineInfo}] angles={result.AngleResults.Count}");
|
||||||
|
|
||||||
|
string savedFilePath = null;
|
||||||
|
if (saveDir != null)
|
||||||
|
{
|
||||||
|
// Deterministic bucket (00-FF) based on filename hash
|
||||||
|
uint hash = 0;
|
||||||
|
foreach (char c in partNo) hash = (hash * 31) + c;
|
||||||
|
var bucket = (hash % 256).ToString("X2");
|
||||||
|
|
||||||
|
var partDir = Path.Combine(saveDir, bucket, partNo);
|
||||||
|
Directory.CreateDirectory(partDir);
|
||||||
|
|
||||||
|
var nestName = $"{partNo}-{size.Length}x{size.Width}-{result.PartCount}pcs";
|
||||||
|
var fileName = nestName + ".zip";
|
||||||
|
savedFilePath = Path.Combine(partDir, fileName);
|
||||||
|
|
||||||
|
// Create nest from template or from scratch
|
||||||
|
Nest nestObj;
|
||||||
|
if (templateNest != null)
|
||||||
|
{
|
||||||
|
nestObj = new Nest(nestName)
|
||||||
|
{
|
||||||
|
Units = templateNest.Units,
|
||||||
|
DateCreated = DateTime.Now
|
||||||
|
};
|
||||||
|
nestObj.PlateDefaults.SetFromExisting(templateNest.PlateDefaults.CreateNew());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nestObj = new Nest(nestName) { Units = Units.Inches, DateCreated = DateTime.Now };
|
||||||
|
}
|
||||||
|
|
||||||
|
nestObj.Drawings.Add(drawing);
|
||||||
|
var plateObj = nestObj.CreatePlate();
|
||||||
|
plateObj.Size = size;
|
||||||
|
plateObj.PartSpacing = s;
|
||||||
|
plateObj.Parts.AddRange(result.PlacedParts);
|
||||||
|
|
||||||
|
var writer = new NestWriter(nestObj);
|
||||||
|
writer.Write(savedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.AddRun(partId, size.Width, size.Length, s, result, savedFilePath, result.AngleResults);
|
||||||
|
runsThisPart++;
|
||||||
|
totalRuns++;
|
||||||
|
}
|
||||||
|
|
||||||
|
BestFitCache.Invalidate(drawing);
|
||||||
|
partSw.Stop();
|
||||||
|
processed++;
|
||||||
|
Console.WriteLine($" Total: {runsThisPart} runs, best={bestCount}pcs @ {bestUtil:P1}, {partSw.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.Error.WriteLine($" ERROR: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSw.Stop();
|
||||||
|
Console.WriteLine("---");
|
||||||
|
Console.WriteLine($"Processed: {processed} parts, {totalRuns} total runs");
|
||||||
|
Console.WriteLine($"Skipped: {skippedExisting} (existing) + {skippedGeometry} (no geometry) + {skippedFeatures} (no features)");
|
||||||
|
Console.WriteLine($"Time: {totalSw.Elapsed:h\\:mm\\:ss}");
|
||||||
|
Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintUsage()
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Usage: OpenNest.Training <dxf-dir> [options]");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Arguments:");
|
||||||
|
Console.Error.WriteLine(" dxf-dir Directory containing DXF files to process");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Options:");
|
||||||
|
Console.Error.WriteLine(" --spacing <value> Part spacing (default: 0.5)");
|
||||||
|
Console.Error.WriteLine(" --db <path> SQLite database path (default: OpenNestTraining.db)");
|
||||||
|
Console.Error.WriteLine(" --save-nests <dir> Directory to save individual .zip nests for each winner");
|
||||||
|
Console.Error.WriteLine(" --template <path> Nest template (.nstdot) for plate defaults");
|
||||||
|
Console.Error.WriteLine(" -h, --help Show this help");
|
||||||
|
}
|
||||||
201
OpenNest.Training/TrainingDatabase.cs
Normal file
201
OpenNest.Training/TrainingDatabase.cs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OpenNest.Engine.ML;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using OpenNest.Training.Data;
|
||||||
|
|
||||||
|
namespace OpenNest.Training
|
||||||
|
{
|
||||||
|
public class TrainingDatabase : IDisposable
|
||||||
|
{
|
||||||
|
private readonly TrainingDbContext _db;
|
||||||
|
|
||||||
|
public TrainingDatabase(string dbPath)
|
||||||
|
{
|
||||||
|
if (!dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase))
|
||||||
|
dbPath += ".db";
|
||||||
|
|
||||||
|
_db = new TrainingDbContext(dbPath);
|
||||||
|
_db.Database.EnsureCreated();
|
||||||
|
MigrateSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GetOrAddPart(string fileName, PartFeatures features, string geometryData)
|
||||||
|
{
|
||||||
|
var existing = _db.Parts.FirstOrDefault(p => p.FileName == fileName);
|
||||||
|
if (existing != null) return existing.Id;
|
||||||
|
|
||||||
|
var part = new TrainingPart
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Area = features.Area,
|
||||||
|
Convexity = features.Convexity,
|
||||||
|
AspectRatio = features.AspectRatio,
|
||||||
|
BBFill = features.BoundingBoxFill,
|
||||||
|
Circularity = features.Circularity,
|
||||||
|
PerimeterToAreaRatio = features.PerimeterToAreaRatio,
|
||||||
|
VertexCount = features.VertexCount,
|
||||||
|
Bitmask = features.Bitmask,
|
||||||
|
GeometryData = geometryData
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Parts.Add(part);
|
||||||
|
_db.SaveChanges();
|
||||||
|
return part.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasRun(string fileName, double sheetWidth, double sheetHeight, double spacing)
|
||||||
|
{
|
||||||
|
return _db.Runs.Any(r =>
|
||||||
|
r.Part.FileName == fileName &&
|
||||||
|
r.SheetWidth == sheetWidth &&
|
||||||
|
r.SheetHeight == sheetHeight &&
|
||||||
|
r.Spacing == spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RunCount(string fileName)
|
||||||
|
{
|
||||||
|
return _db.Runs.Count(r => r.Part.FileName == fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRun(long partId, double w, double h, double s, BruteForceResult result, string filePath, List<AngleResult> angleResults = null)
|
||||||
|
{
|
||||||
|
var run = new TrainingRun
|
||||||
|
{
|
||||||
|
PartId = partId,
|
||||||
|
SheetWidth = w,
|
||||||
|
SheetHeight = h,
|
||||||
|
Spacing = s,
|
||||||
|
PartCount = result.PartCount,
|
||||||
|
Utilization = result.Utilization,
|
||||||
|
TimeMs = result.TimeMs,
|
||||||
|
LayoutData = result.LayoutData ?? "",
|
||||||
|
FilePath = filePath ?? "",
|
||||||
|
WinnerEngine = result.WinnerEngine ?? "",
|
||||||
|
WinnerTimeMs = result.WinnerTimeMs,
|
||||||
|
RunnerUpEngine = result.RunnerUpEngine ?? "",
|
||||||
|
RunnerUpPartCount = result.RunnerUpPartCount,
|
||||||
|
RunnerUpTimeMs = result.RunnerUpTimeMs,
|
||||||
|
ThirdPlaceEngine = result.ThirdPlaceEngine ?? "",
|
||||||
|
ThirdPlacePartCount = result.ThirdPlacePartCount,
|
||||||
|
ThirdPlaceTimeMs = result.ThirdPlaceTimeMs
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.Runs.Add(run);
|
||||||
|
|
||||||
|
if (angleResults != null && angleResults.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var ar in angleResults)
|
||||||
|
{
|
||||||
|
_db.AngleResults.Add(new Data.TrainingAngleResult
|
||||||
|
{
|
||||||
|
Run = run,
|
||||||
|
AngleDeg = ar.AngleDeg,
|
||||||
|
Direction = ar.Direction.ToString(),
|
||||||
|
PartCount = ar.PartCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BackfillPerimeterToAreaRatio()
|
||||||
|
{
|
||||||
|
var partsToFix = _db.Parts
|
||||||
|
.Where(p => p.PerimeterToAreaRatio == 0)
|
||||||
|
.Select(p => new { p.Id, p.GeometryData })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (partsToFix.Count == 0) return 0;
|
||||||
|
|
||||||
|
var updated = 0;
|
||||||
|
foreach (var item in partsToFix)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream(Encoding.UTF8.GetBytes(item.GeometryData));
|
||||||
|
var programReader = new ProgramReader(stream);
|
||||||
|
var program = programReader.Read();
|
||||||
|
|
||||||
|
var drawing = new Drawing("backfill") { Program = program };
|
||||||
|
drawing.UpdateArea();
|
||||||
|
|
||||||
|
var features = FeatureExtractor.Extract(drawing);
|
||||||
|
if (features == null) continue;
|
||||||
|
|
||||||
|
var part = _db.Parts.Find(item.Id);
|
||||||
|
part.PerimeterToAreaRatio = features.PerimeterToAreaRatio;
|
||||||
|
_db.SaveChanges();
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Skip parts that fail to reconstruct.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MigrateSchema()
|
||||||
|
{
|
||||||
|
var columns = new[]
|
||||||
|
{
|
||||||
|
("WinnerEngine", "TEXT NOT NULL DEFAULT ''"),
|
||||||
|
("WinnerTimeMs", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
("RunnerUpEngine", "TEXT NOT NULL DEFAULT ''"),
|
||||||
|
("RunnerUpPartCount", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
("RunnerUpTimeMs", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
("ThirdPlaceEngine", "TEXT NOT NULL DEFAULT ''"),
|
||||||
|
("ThirdPlacePartCount", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
("ThirdPlaceTimeMs", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (name, type) in columns)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Database.ExecuteSqlRaw($"ALTER TABLE Runs ADD COLUMN {name} {type}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Column already exists.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Database.ExecuteSqlRaw(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS AngleResults (
|
||||||
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
RunId INTEGER NOT NULL,
|
||||||
|
AngleDeg REAL NOT NULL,
|
||||||
|
Direction TEXT NOT NULL,
|
||||||
|
PartCount INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (RunId) REFERENCES Runs(Id)
|
||||||
|
)");
|
||||||
|
_db.Database.ExecuteSqlRaw(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_angleresults_runid ON AngleResults (RunId)");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Table already exists or other non-fatal issue.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveChanges()
|
||||||
|
{
|
||||||
|
_db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_db?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
OpenNest.Training/notebooks/requirements.txt
Normal file
7
OpenNest.Training/notebooks/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pandas>=2.0
|
||||||
|
scikit-learn>=1.3
|
||||||
|
xgboost>=2.0
|
||||||
|
onnxmltools>=1.12
|
||||||
|
skl2onnx>=1.16
|
||||||
|
matplotlib>=3.7
|
||||||
|
jupyter>=1.0
|
||||||
264
OpenNest.Training/notebooks/train_angle_model.ipynb
Normal file
264
OpenNest.Training/notebooks/train_angle_model.ipynb
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5,
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "a1b2c3d4-0001-0000-0000-000000000001",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Angle Prediction Model Training\n",
|
||||||
|
"Trains an XGBoost multi-label classifier to predict which rotation angles are competitive for a given part geometry and sheet size.\n",
|
||||||
|
"\n",
|
||||||
|
"**Input:** SQLite database from OpenNest.Training data collection runs\n",
|
||||||
|
"**Output:** `angle_predictor.onnx` model file for `OpenNest.Engine/Models/`"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0002-0000-0000-000000000002",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import sqlite3\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"\n",
|
||||||
|
"DB_PATH = \"../OpenNestTraining.db\" # Adjust to your database location\n",
|
||||||
|
"OUTPUT_PATH = \"../../OpenNest.Engine/Models/angle_predictor.onnx\"\n",
|
||||||
|
"COMPETITIVE_THRESHOLD = 0.95 # Angle is \"competitive\" if >= 95% of best"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0003-0000-0000-000000000003",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Extract training data from SQLite\n",
|
||||||
|
"conn = sqlite3.connect(DB_PATH)\n",
|
||||||
|
"\n",
|
||||||
|
"query = \"\"\"\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" p.Area, p.Convexity, p.AspectRatio, p.BBFill, p.Circularity,\n",
|
||||||
|
" p.PerimeterToAreaRatio, p.VertexCount,\n",
|
||||||
|
" r.SheetWidth, r.SheetHeight, r.Id as RunId,\n",
|
||||||
|
" a.AngleDeg, a.Direction, a.PartCount\n",
|
||||||
|
"FROM AngleResults a\n",
|
||||||
|
"JOIN Runs r ON a.RunId = r.Id\n",
|
||||||
|
"JOIN Parts p ON r.PartId = p.Id\n",
|
||||||
|
"WHERE a.PartCount > 0\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"df = pd.read_sql_query(query, conn)\n",
|
||||||
|
"conn.close()\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Loaded {len(df)} angle result rows\")\n",
|
||||||
|
"print(f\"Unique runs: {df['RunId'].nunique()}\")\n",
|
||||||
|
"print(f\"Angle range: {df['AngleDeg'].min()}-{df['AngleDeg'].max()}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0004-0000-0000-000000000004",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# For each run, find best PartCount (max of H and V per angle),\n",
|
||||||
|
"# then label angles within 95% of best as positive.\n",
|
||||||
|
"\n",
|
||||||
|
"# Best count per angle per run (max of H and V)\n",
|
||||||
|
"angle_best = df.groupby(['RunId', 'AngleDeg'])['PartCount'].max().reset_index()\n",
|
||||||
|
"angle_best.columns = ['RunId', 'AngleDeg', 'BestCount']\n",
|
||||||
|
"\n",
|
||||||
|
"# Best count per run (overall best angle)\n",
|
||||||
|
"run_best = angle_best.groupby('RunId')['BestCount'].max().reset_index()\n",
|
||||||
|
"run_best.columns = ['RunId', 'RunBest']\n",
|
||||||
|
"\n",
|
||||||
|
"# Merge and compute labels\n",
|
||||||
|
"labels = angle_best.merge(run_best, on='RunId')\n",
|
||||||
|
"labels['IsCompetitive'] = (labels['BestCount'] >= labels['RunBest'] * COMPETITIVE_THRESHOLD).astype(int)\n",
|
||||||
|
"\n",
|
||||||
|
"# Pivot to 36-column binary label matrix\n",
|
||||||
|
"label_matrix = labels.pivot_table(\n",
|
||||||
|
" index='RunId', columns='AngleDeg', values='IsCompetitive', fill_value=0\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"# Ensure all 36 angle columns exist (0, 5, 10, ..., 175)\n",
|
||||||
|
"all_angles = [i * 5 for i in range(36)]\n",
|
||||||
|
"for a in all_angles:\n",
|
||||||
|
" if a not in label_matrix.columns:\n",
|
||||||
|
" label_matrix[a] = 0\n",
|
||||||
|
"label_matrix = label_matrix[all_angles]\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Label matrix: {label_matrix.shape}\")\n",
|
||||||
|
"print(f\"Average competitive angles per run: {label_matrix.sum(axis=1).mean():.1f}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0005-0000-0000-000000000005",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Build feature matrix - one row per run\n",
|
||||||
|
"features_query = \"\"\"\n",
|
||||||
|
"SELECT DISTINCT\n",
|
||||||
|
" r.Id as RunId, p.FileName,\n",
|
||||||
|
" p.Area, p.Convexity, p.AspectRatio, p.BBFill, p.Circularity,\n",
|
||||||
|
" p.PerimeterToAreaRatio, p.VertexCount,\n",
|
||||||
|
" r.SheetWidth, r.SheetHeight\n",
|
||||||
|
"FROM Runs r\n",
|
||||||
|
"JOIN Parts p ON r.PartId = p.Id\n",
|
||||||
|
"WHERE r.Id IN ({})\n",
|
||||||
|
"\"\"\".format(','.join(str(x) for x in label_matrix.index))\n",
|
||||||
|
"\n",
|
||||||
|
"conn = sqlite3.connect(DB_PATH)\n",
|
||||||
|
"features_df = pd.read_sql_query(features_query, conn)\n",
|
||||||
|
"conn.close()\n",
|
||||||
|
"\n",
|
||||||
|
"features_df = features_df.set_index('RunId')\n",
|
||||||
|
"\n",
|
||||||
|
"# Derived features\n",
|
||||||
|
"features_df['SheetAspectRatio'] = features_df['SheetWidth'] / features_df['SheetHeight']\n",
|
||||||
|
"features_df['PartToSheetAreaRatio'] = features_df['Area'] / (features_df['SheetWidth'] * features_df['SheetHeight'])\n",
|
||||||
|
"\n",
|
||||||
|
"# Filter outliers (title blocks, etc.)\n",
|
||||||
|
"mask = (features_df['BBFill'] >= 0.01) & (features_df['Area'] > 0.1)\n",
|
||||||
|
"print(f\"Filtering: {(~mask).sum()} outlier runs removed\")\n",
|
||||||
|
"features_df = features_df[mask]\n",
|
||||||
|
"label_matrix = label_matrix.loc[features_df.index]\n",
|
||||||
|
"\n",
|
||||||
|
"feature_cols = ['Area', 'Convexity', 'AspectRatio', 'BBFill', 'Circularity',\n",
|
||||||
|
" 'PerimeterToAreaRatio', 'VertexCount',\n",
|
||||||
|
" 'SheetWidth', 'SheetHeight', 'SheetAspectRatio', 'PartToSheetAreaRatio']\n",
|
||||||
|
"\n",
|
||||||
|
"X = features_df[feature_cols].values\n",
|
||||||
|
"y = label_matrix.values\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Features: {X.shape}, Labels: {y.shape}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0006-0000-0000-000000000006",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from sklearn.model_selection import GroupShuffleSplit\n",
|
||||||
|
"from sklearn.multioutput import MultiOutputClassifier\n",
|
||||||
|
"import xgboost as xgb\n",
|
||||||
|
"\n",
|
||||||
|
"# Split by part (all sheet sizes for a part stay in the same split)\n",
|
||||||
|
"groups = features_df['FileName']\n",
|
||||||
|
"splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\n",
|
||||||
|
"train_idx, test_idx = next(splitter.split(X, y, groups))\n",
|
||||||
|
"\n",
|
||||||
|
"X_train, X_test = X[train_idx], X[test_idx]\n",
|
||||||
|
"y_train, y_test = y[train_idx], y[test_idx]\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Train: {len(train_idx)}, Test: {len(test_idx)}\")\n",
|
||||||
|
"\n",
|
||||||
|
"# Train XGBoost multi-label classifier\n",
|
||||||
|
"base_clf = xgb.XGBClassifier(\n",
|
||||||
|
" n_estimators=200,\n",
|
||||||
|
" max_depth=6,\n",
|
||||||
|
" learning_rate=0.1,\n",
|
||||||
|
" use_label_encoder=False,\n",
|
||||||
|
" eval_metric='logloss',\n",
|
||||||
|
" random_state=42\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"clf = MultiOutputClassifier(base_clf, n_jobs=-1)\n",
|
||||||
|
"clf.fit(X_train, y_train)\n",
|
||||||
|
"print(\"Training complete\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0007-0000-0000-000000000007",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from sklearn.metrics import recall_score, precision_score\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"y_pred = clf.predict(X_test)\n",
|
||||||
|
"y_prob = np.array([est.predict_proba(X_test)[:, 1] for est in clf.estimators_]).T\n",
|
||||||
|
"\n",
|
||||||
|
"# Per-angle metrics\n",
|
||||||
|
"recalls = []\n",
|
||||||
|
"precisions = []\n",
|
||||||
|
"for i in range(36):\n",
|
||||||
|
" if y_test[:, i].sum() > 0:\n",
|
||||||
|
" recalls.append(recall_score(y_test[:, i], y_pred[:, i], zero_division=0))\n",
|
||||||
|
" precisions.append(precision_score(y_test[:, i], y_pred[:, i], zero_division=0))\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Mean recall: {np.mean(recalls):.3f}\")\n",
|
||||||
|
"print(f\"Mean precision: {np.mean(precisions):.3f}\")\n",
|
||||||
|
"\n",
|
||||||
|
"# Average angles predicted per run\n",
|
||||||
|
"avg_predicted = y_pred.sum(axis=1).mean()\n",
|
||||||
|
"print(f\"Avg angles predicted per run: {avg_predicted:.1f}\")\n",
|
||||||
|
"\n",
|
||||||
|
"# Plot\n",
|
||||||
|
"fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
|
||||||
|
"axes[0].bar(range(len(recalls)), recalls)\n",
|
||||||
|
"axes[0].set_title('Recall per Angle Bin')\n",
|
||||||
|
"axes[0].set_xlabel('Angle (5-deg bins)')\n",
|
||||||
|
"axes[0].axhline(y=0.95, color='r', linestyle='--', label='Target 95%')\n",
|
||||||
|
"axes[0].legend()\n",
|
||||||
|
"\n",
|
||||||
|
"axes[1].bar(range(len(precisions)), precisions)\n",
|
||||||
|
"axes[1].set_title('Precision per Angle Bin')\n",
|
||||||
|
"axes[1].set_xlabel('Angle (5-deg bins)')\n",
|
||||||
|
"axes[1].axhline(y=0.60, color='r', linestyle='--', label='Target 60%')\n",
|
||||||
|
"axes[1].legend()\n",
|
||||||
|
"\n",
|
||||||
|
"plt.tight_layout()\n",
|
||||||
|
"plt.show()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a1b2c3d4-0008-0000-0000-000000000008",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from skl2onnx import convert_sklearn\n",
|
||||||
|
"from skl2onnx.common.data_types import FloatTensorType\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"\n",
|
||||||
|
"initial_type = [('features', FloatTensorType([None, 11]))]\n",
|
||||||
|
"onnx_model = convert_sklearn(clf, initial_types=initial_type)\n",
|
||||||
|
"\n",
|
||||||
|
"output_path = Path(OUTPUT_PATH)\n",
|
||||||
|
"output_path.parent.mkdir(parents=True, exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"with open(output_path, 'wb') as f:\n",
|
||||||
|
" f.write(onnx_model.SerializeToString())\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Model saved to {output_path} ({output_path.stat().st_size / 1024:.0f} KB)\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
28
OpenNest.sln
28
OpenNest.sln
@@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Mcp", "OpenNest.Mc
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNest.Console\OpenNest.Console.csproj", "{58E00A25-86B5-42C7-87B5-DE4AD22381EA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Console", "OpenNest.Console\OpenNest.Console.csproj", "{58E00A25-86B5-42C7-87B5-DE4AD22381EA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Training", "OpenNest.Training\OpenNest.Training.csproj", "{249BF728-25DD-4863-8266-207ACD26E964}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Tests", "OpenNest.Tests\OpenNest.Tests.csproj", "{03539EB7-9DB2-4634-A6FD-F094B9603596}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -111,6 +115,30 @@ Global
|
|||||||
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x64.Build.0 = Release|Any CPU
|
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.ActiveCfg = Release|Any CPU
|
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.Build.0 = Release|Any CPU
|
{58E00A25-86B5-42C7-87B5-DE4AD22381EA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{249BF728-25DD-4863-8266-207ACD26E964}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{03539EB7-9DB2-4634-A6FD-F094B9603596}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -186,8 +186,8 @@ namespace OpenNest.Actions
|
|||||||
boxes.Add(part.BoundingBox.Offset(plate.PartSpacing));
|
boxes.Add(part.BoundingBox.Offset(plate.PartSpacing));
|
||||||
|
|
||||||
var pt = plateView.CurrentPoint;
|
var pt = plateView.CurrentPoint;
|
||||||
var vertical = Helper.GetLargestBoxVertically(pt, bounds, boxes);
|
var vertical = SpatialQuery.GetLargestBoxVertically(pt, bounds, boxes);
|
||||||
var horizontal = Helper.GetLargestBoxHorizontally(pt, bounds, boxes);
|
var horizontal = SpatialQuery.GetLargestBoxHorizontally(pt, bounds, boxes);
|
||||||
|
|
||||||
var bestArea = vertical;
|
var bestArea = vertical;
|
||||||
if (horizontal.Area() > vertical.Area())
|
if (horizontal.Area() > vertical.Area())
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var engine = new NestEngine(plateView.Plate);
|
var engine = NestEngineRegistry.Create(plateView.Plate);
|
||||||
var parts = await Task.Run(() =>
|
var parts = await Task.Run(() =>
|
||||||
engine.Fill(new NestItem { Drawing = drawing },
|
engine.Fill(new NestItem { Drawing = drawing },
|
||||||
SelectedArea, progress, cts.Token));
|
SelectedArea, progress, cts.Token));
|
||||||
@@ -61,7 +61,7 @@ namespace OpenNest.Actions
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var engine = new NestEngine(plateView.Plate);
|
var engine = NestEngineRegistry.Create(plateView.Plate);
|
||||||
engine.Fill(new NestItem { Drawing = drawing }, SelectedArea);
|
engine.Fill(new NestItem { Drawing = drawing }, SelectedArea);
|
||||||
plateView.Invalidate();
|
plateView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user