Compare commits
157 Commits
ccd230568e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a548d5329a | |||
| 07012033c7 | |||
| 92b17b2963 | |||
| b6ee04f038 | |||
| 8ffdacd6c0 | |||
| ccd402c50f | |||
| b1e872577c | |||
| 9903478d3e | |||
| 93a8981d0a | |||
| 00e7866506 | |||
| 560105f952 | |||
| 266f8a83e6 | |||
| 0b7697e9c0 | |||
| 83124eb38d | |||
| 24beb8ada1 | |||
| ee83f17afe | |||
| 99546e7eef | |||
| 4586a53590 | |||
| 1a41eeb81d | |||
| f894ffd27c | |||
| 0ec22f2207 | |||
| 3f3d95a5e4 | |||
| 811d23510e | |||
| 0597a11a23 | |||
| 2ae1d513cf | |||
| 904d30d05d | |||
| e9678c73b2 | |||
| 4060430757 | |||
| de527cd668 | |||
| 9887cb1aa3 | |||
| cdf8e4e40e | |||
| 4f21fb91a1 | |||
| 7f96d632f3 | |||
| 38dcaf16d3 | |||
| 8c57e43221 | |||
| bc78ddc49c | |||
| c88cec2beb | |||
| b7c7cecd75 | |||
| 4d0d8c453b | |||
| 5f4288a786 | |||
| 707ddb80d9 | |||
| 71f28600d1 | |||
| d39b0ae540 | |||
| ee5c77c645 | |||
| 4615bcb40d | |||
| 7843de145b | |||
| 2d1f2217e5 | |||
| ae88c34361 | |||
| 708d895a04 | |||
| 884817c5f9 | |||
| cf1c5fe120 | |||
| a04586f7df | |||
| 069e966453 | |||
| d9d275b675 | |||
| 9411dd0fdd | |||
| facd07d7de | |||
| 2ed02c2dae | |||
| 3756ea255e | |||
| 33ba40e203 | |||
| 6d66636e3d | |||
| 85278bbb75 | |||
| f0a3547bd1 | |||
| fe2a293128 | |||
| 11f605801f | |||
| 8dc12972f5 | |||
| 8a0ebf8c18 | |||
| c552372f81 | |||
| 683cb3c180 | |||
| 2cb2808c79 | |||
| e969260f3d | |||
| 8bfc13d529 | |||
| ca35945c13 | |||
| fab2214149 | |||
| e3b89f2660 | |||
| 1e9640d4fc | |||
| 116a386152 | |||
| 8957b20bac | |||
| c31ef9f80c | |||
| 3b6e4bdd3a | |||
| ef737ffa6d | |||
| 1bc635acde | |||
| ed555ba56a | |||
| 20aa172f46 | |||
| 9a58782c46 | |||
| e656956c1c | |||
| f13443b6b3 | |||
| a7688f4c9d | |||
| e324e15fc0 | |||
| d7cc08dff7 | |||
| 1c8b35bcfb | |||
| 84679b40ce | |||
| b6bd7eda6e | |||
| cfe8a38620 | |||
| 4be0b0db09 | |||
| 2f5d20f972 | |||
| 0f953b8701 | |||
| 62ec6484c8 | |||
| 0472c12113 | |||
| a9a9dc8a0a | |||
| 4fc8f1f6cf | |||
| 231f97fafc | |||
| 76e30d91c0 | |||
| e789fe312d | |||
| f73bb2bc2f | |||
| 0da970ec9a | |||
| 62f00055b7 | |||
| e695e29355 | |||
| 9012a9fc1c | |||
| b009f195be | |||
| dddc890a96 | |||
| 794ef16629 | |||
| d1d47b5223 | |||
| 24ed878d8e | |||
| c2b8400986 | |||
| 0a33047ad6 | |||
| c98e024f9c | |||
| d6d7ba8480 | |||
| b6cde145e1 | |||
| 9a4f20ca00 | |||
| b5af5a118d | |||
| 60a557bd37 | |||
| 97ab33c899 | |||
| a1810db96d | |||
| 39d656ad21 | |||
| 1d9bcc63d2 | |||
| 6102dd5b85 | |||
| 495ee6f0c3 | |||
| 0e1e619f0a | |||
| 0cba528591 | |||
| 442501828a | |||
| 202f49f368 | |||
| 7bbfe06494 | |||
| 267254dcae | |||
| 5668748f37 | |||
| b7de61e4d1 | |||
| c4d5cfd17b | |||
| 1f965897f2 | |||
| 46fe48870c | |||
| c287e3ec32 | |||
| 4348e5c427 | |||
| e6a7d9b047 | |||
| ddf1686ea5 | |||
| 501fbda762 | |||
| a83efd0b01 | |||
| a1139efecb | |||
| d8373ab135 | |||
| f0b9b51229 | |||
| 76a338f3d0 | |||
| 0ac7b9babd | |||
| f336af5d65 | |||
| 3d6be3900e | |||
| 285e7082fb | |||
| 207cef5423 | |||
| c3b3f24704 | |||
| 6229e5e49d | |||
| 07465d6f0c | |||
| d2eeb23107 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -208,3 +208,7 @@ FakesAssemblies/
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
.superpowers/
|
||||
|
||||
# Launch settings
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
33
CLAUDE.md
33
CLAUDE.md
@@ -35,18 +35,18 @@ Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
### OpenNest.Engine (class library, depends on Core)
|
||||
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/`.
|
||||
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`.
|
||||
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
|
||||
- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
|
||||
- **BestFit/**: NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
|
||||
- **RectanglePacking/**: `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
|
||||
- **CirclePacking/**: Alternative packing for circular parts.
|
||||
- **ML/**: `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
|
||||
- `FillLinear`: Grid-based fill with directional sliding.
|
||||
- `Compactor`: Post-fill gravity compaction — pushes parts toward a plate edge to close gaps.
|
||||
- `FillScore`: Lexicographic comparison struct for fill results (count > utilization > compactness).
|
||||
- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`.
|
||||
- **Strategies/** (`namespace OpenNest.Engine.Strategies`): Pluggable fill strategy layer — `IFillStrategy` interface, `FillContext`, `FillStrategyRegistry` (auto-discovers strategies via reflection, supports plugin DLLs), `FillHelpers`. Built-in strategies: `LinearFillStrategy`, `PairsFillStrategy`, `RectBestFitStrategy`, `ExtentsFillStrategy`.
|
||||
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
|
||||
- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
|
||||
- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts.
|
||||
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): NFP-based nesting (not yet integrated) — `AutoNester` (mixed-part nesting with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`NestResult`.
|
||||
- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
|
||||
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
||||
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
|
||||
- `RotationAnalysis`: Analyzes part geometry to determine valid rotation angles.
|
||||
|
||||
### OpenNest.IO (class library, depends on Core)
|
||||
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
||||
@@ -85,11 +85,9 @@ The UI application with MDI interface.
|
||||
## File Format
|
||||
|
||||
Nest files (`.nest`, ZIP-based) use v2 JSON format:
|
||||
- `info.json` — nest metadata and plate defaults
|
||||
- `drawing-info.json` — drawing metadata (name, material, quantities, colors)
|
||||
- `plate-info.json` — plate metadata (size, material, spacing)
|
||||
- `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)
|
||||
- `nest.json` — single JSON file containing all nest metadata: nest info (name, units, customer, dates, notes), plate defaults (size, thickness, quadrant, spacing, material, edge spacing), drawings array (id, name, color, quantity, priority, rotation constraints, material, source), and plates array (id, size, material, edge spacing, parts with drawingId/x/y/rotation)
|
||||
- `programs/program-N` — G-code text for each drawing's cut program (N = drawing id)
|
||||
- `bestfits/bestfit-N` — JSON array of best-fit pair evaluation results per drawing, keyed by plate size/spacing (optional, only present if best-fit data was computed)
|
||||
|
||||
## Tool Preferences
|
||||
|
||||
@@ -99,9 +97,16 @@ Always use Roslyn Bridge MCP tools (`mcp__RoslynBridge__*`) as the primary metho
|
||||
|
||||
- Always use `var` instead of explicit types (e.g., `var parts = new List<Part>();` not `List<Part> parts = new List<Part>();`).
|
||||
|
||||
## Documentation Maintenance
|
||||
|
||||
Always keep `README.md` and `CLAUDE.md` up to date when making changes that affect project structure, architecture, build instructions, dependencies, or key patterns. If you add a new project, change a namespace, modify the build process, or alter significant behavior, update both files as part of the same change.
|
||||
|
||||
**Do not commit** design specs, implementation plans, or other temporary planning documents (`docs/superpowers/` etc.) to the repository. These are working documents only — keep them local and untracked.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
||||
- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Nfp` (NFP-based nesting, not yet integrated), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`.
|
||||
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
|
||||
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
|
||||
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
|
||||
|
||||
15
OpenNest.Api/NestRequest.cs
Normal file
15
OpenNest.Api/NestRequest.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public class NestRequest
|
||||
{
|
||||
public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
|
||||
public Size SheetSize { get; init; } = new(60, 120);
|
||||
public string Material { get; init; } = "Steel, A1011 HR";
|
||||
public double Thickness { get; init; } = 0.06;
|
||||
public double Spacing { get; init; } = 0.1;
|
||||
public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
|
||||
public CutParameters Cutting { get; init; } = CutParameters.Default;
|
||||
}
|
||||
9
OpenNest.Api/NestRequestPart.cs
Normal file
9
OpenNest.Api/NestRequestPart.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public class NestRequestPart
|
||||
{
|
||||
public string DxfPath { get; init; }
|
||||
public int Quantity { get; init; } = 1;
|
||||
public bool AllowRotation { get; init; } = true;
|
||||
public int Priority { get; init; } = 0;
|
||||
}
|
||||
112
OpenNest.Api/NestResponse.cs
Normal file
112
OpenNest.Api/NestResponse.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public class NestResponse
|
||||
{
|
||||
public int SheetCount { get; init; }
|
||||
public double Utilization { get; init; }
|
||||
public TimeSpan CutTime { get; init; }
|
||||
public TimeSpan Elapsed { get; init; }
|
||||
public Nest Nest { get; init; }
|
||||
public NestRequest Request { get; init; }
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
IncludeFields = true // Required for OpenNest.Geometry.Size (public fields)
|
||||
};
|
||||
|
||||
public async Task SaveAsync(string path)
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Create);
|
||||
using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
|
||||
|
||||
// Write request.json
|
||||
var requestEntry = zip.CreateEntry("request.json");
|
||||
await using (var stream = requestEntry.Open())
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
|
||||
}
|
||||
|
||||
// Write response.json (metrics only)
|
||||
var metrics = new
|
||||
{
|
||||
SheetCount,
|
||||
Utilization,
|
||||
CutTimeTicks = CutTime.Ticks,
|
||||
ElapsedTicks = Elapsed.Ticks
|
||||
};
|
||||
var responseEntry = zip.CreateEntry("response.json");
|
||||
await using (var stream = responseEntry.Open())
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, metrics, JsonOptions);
|
||||
}
|
||||
|
||||
// Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry
|
||||
var nestEntry = zip.CreateEntry("nest.nest");
|
||||
using var nestMs = new MemoryStream();
|
||||
var writer = new NestWriter(Nest);
|
||||
writer.Write(nestMs);
|
||||
nestMs.Position = 0;
|
||||
await using (var stream = nestEntry.Open())
|
||||
{
|
||||
await nestMs.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<NestResponse> LoadAsync(string path)
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
|
||||
|
||||
// Read request.json
|
||||
var requestEntry = zip.GetEntry("request.json")
|
||||
?? throw new InvalidOperationException("Missing request.json in .nestquote file");
|
||||
NestRequest request;
|
||||
await using (var stream = requestEntry.Open())
|
||||
{
|
||||
request = await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions);
|
||||
}
|
||||
|
||||
// Read response.json
|
||||
var responseEntry = zip.GetEntry("response.json")
|
||||
?? throw new InvalidOperationException("Missing response.json in .nestquote file");
|
||||
JsonElement metricsJson;
|
||||
await using (var stream = responseEntry.Open())
|
||||
{
|
||||
metricsJson = await JsonSerializer.DeserializeAsync<JsonElement>(stream, JsonOptions);
|
||||
}
|
||||
|
||||
// Read embedded nest.nest via NestReader(Stream)
|
||||
var nestEntry = zip.GetEntry("nest.nest")
|
||||
?? throw new InvalidOperationException("Missing nest.nest in .nestquote file");
|
||||
Nest nest;
|
||||
using (var nestMs = new MemoryStream())
|
||||
{
|
||||
await using (var stream = nestEntry.Open())
|
||||
{
|
||||
await stream.CopyToAsync(nestMs);
|
||||
}
|
||||
nestMs.Position = 0;
|
||||
var reader = new NestReader(nestMs);
|
||||
nest = reader.Read();
|
||||
}
|
||||
|
||||
return new NestResponse
|
||||
{
|
||||
SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(),
|
||||
Utilization = metricsJson.GetProperty("utilization").GetDouble(),
|
||||
CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()),
|
||||
Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()),
|
||||
Nest = nest,
|
||||
Request = request
|
||||
};
|
||||
}
|
||||
}
|
||||
131
OpenNest.Api/NestRunner.cs
Normal file
131
OpenNest.Api/NestRunner.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public static class NestRunner
|
||||
{
|
||||
public static Task<NestResponse> RunAsync(
|
||||
NestRequest request,
|
||||
IProgress<NestProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request.Parts.Count == 0)
|
||||
throw new ArgumentException("Request must contain at least one part.", nameof(request));
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// 1. Import DXFs → Drawings
|
||||
var drawings = new List<Drawing>();
|
||||
var importer = new DxfImporter();
|
||||
|
||||
foreach (var part in request.Parts)
|
||||
{
|
||||
if (!File.Exists(part.DxfPath))
|
||||
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
|
||||
|
||||
if (!importer.GetGeometry(part.DxfPath, out var geometry) || geometry.Count == 0)
|
||||
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
var name = Path.GetFileNameWithoutExtension(part.DxfPath);
|
||||
var drawing = new Drawing(name);
|
||||
drawing.Program = pgm;
|
||||
drawings.Add(drawing);
|
||||
}
|
||||
|
||||
// 2. Build NestItems
|
||||
var items = new List<NestItem>();
|
||||
for (var i = 0; i < request.Parts.Count; i++)
|
||||
{
|
||||
var part = request.Parts[i];
|
||||
items.Add(new NestItem
|
||||
{
|
||||
Drawing = drawings[i],
|
||||
Quantity = part.Quantity,
|
||||
Priority = part.Priority,
|
||||
StepAngle = part.AllowRotation ? 0 : OpenNest.Math.Angle.TwoPI,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Multi-plate loop
|
||||
var nest = new Nest();
|
||||
var remaining = items.Select(item => item.Quantity).ToList();
|
||||
|
||||
while (remaining.Any(q => q > 0))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var plate = new Plate(request.SheetSize)
|
||||
{
|
||||
Thickness = request.Thickness,
|
||||
PartSpacing = request.Spacing,
|
||||
Material = new Material(request.Material)
|
||||
};
|
||||
|
||||
// Build items for this pass with remaining quantities
|
||||
var passItems = new List<NestItem>();
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (remaining[i] <= 0) continue;
|
||||
passItems.Add(new NestItem
|
||||
{
|
||||
Drawing = items[i].Drawing,
|
||||
Quantity = remaining[i],
|
||||
Priority = items[i].Priority,
|
||||
StepAngle = items[i].StepAngle,
|
||||
});
|
||||
}
|
||||
|
||||
// Run engine
|
||||
var engine = NestEngineRegistry.Create(plate);
|
||||
var parts = engine.Nest(passItems, progress, token);
|
||||
|
||||
if (parts.Count == 0)
|
||||
break; // No progress — part doesn't fit on fresh sheet
|
||||
|
||||
// Add parts to plate and nest
|
||||
foreach (var p in parts)
|
||||
plate.Parts.Add(p);
|
||||
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
// Deduct placed quantities
|
||||
foreach (var p in parts)
|
||||
{
|
||||
var idx = drawings.IndexOf(p.BaseDrawing);
|
||||
if (idx >= 0)
|
||||
remaining[idx]--;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Compute timing
|
||||
var timingInfo = Timing.GetTimingInfo(nest);
|
||||
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// 5. Build response
|
||||
var response = new NestResponse
|
||||
{
|
||||
SheetCount = nest.Plates.Count,
|
||||
Utilization = nest.Plates.Count > 0
|
||||
? nest.Plates.Average(p => p.Utilization())
|
||||
: 0,
|
||||
CutTime = cutTime,
|
||||
Elapsed = sw.Elapsed,
|
||||
Nest = nest,
|
||||
Request = request
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
3
OpenNest.Api/NestStrategy.cs
Normal file
3
OpenNest.Api/NestStrategy.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public enum NestStrategy { Auto }
|
||||
12
OpenNest.Api/OpenNest.Api.csproj
Normal file
12
OpenNest.Api/OpenNest.Api.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>OpenNest.Api</RootNamespace>
|
||||
<AssemblyName>OpenNest.Api</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,13 +1,13 @@
|
||||
using OpenNest;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
return NestConsole.Run(args);
|
||||
|
||||
@@ -152,7 +152,8 @@ static class NestConsole
|
||||
static Nest LoadOrCreateNest(Options options)
|
||||
{
|
||||
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
||||
f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||
var dxfFiles = options.InputFiles.Where(f =>
|
||||
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
@@ -190,7 +191,7 @@ static class NestConsole
|
||||
// DXF-only mode: create a fresh nest.
|
||||
if (dxfFiles.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine("Error: no nest (.zip) or DXF (.dxf) files specified");
|
||||
Console.Error.WriteLine("Error: no nest (.nest) or DXF (.dxf) files specified");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -272,7 +273,9 @@ static class NestConsole
|
||||
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));
|
||||
var hasDxfOnly = !options.InputFiles.Any(f =>
|
||||
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (options.PlateSize.HasValue && !hasDxfOnly)
|
||||
plate.Size = options.PlateSize.Value;
|
||||
@@ -373,7 +376,7 @@ static class NestConsole
|
||||
var firstInput = options.InputFiles[0];
|
||||
var outputFile = options.OutputFile ?? Path.Combine(
|
||||
Path.GetDirectoryName(firstInput),
|
||||
$"{Path.GetFileNameWithoutExtension(firstInput)}-result.zip");
|
||||
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}");
|
||||
|
||||
new NestWriter(nest).Write(outputFile);
|
||||
Console.WriteLine($"Saved: {outputFile}");
|
||||
@@ -384,12 +387,12 @@ static class NestConsole
|
||||
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine("Arguments:");
|
||||
Console.Error.WriteLine(" input-files One or more .zip nest files or .dxf drawing files");
|
||||
Console.Error.WriteLine(" input-files One or more .nest 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 WxL Import DXF, create plate, and fill");
|
||||
Console.Error.WriteLine(" <nest.zip> <part.dxf> Load nest and add imported DXF drawings");
|
||||
Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)");
|
||||
Console.Error.WriteLine(" <part.dxf> --size WxL Import DXF, create plate, and fill");
|
||||
Console.Error.WriteLine(" <nest.nest> <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)");
|
||||
@@ -397,7 +400,7 @@ static class NestConsole
|
||||
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
||||
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
||||
Console.Error.WriteLine(" --size <WxL> Override plate size (e.g. 60x120); required for DXF-only mode");
|
||||
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.zip)");
|
||||
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.nest)");
|
||||
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");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Converters
|
||||
{
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Converters
|
||||
{
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public class CutParameters
|
||||
{
|
||||
public class CutParameters
|
||||
public double Feedrate { get; set; }
|
||||
public double RapidTravelRate { get; set; }
|
||||
public TimeSpan PierceTime { get; set; }
|
||||
public double LeadInLength { get; set; }
|
||||
public string PostProcessor { get; set; }
|
||||
public Units Units { get; set; }
|
||||
|
||||
public static CutParameters Default => new()
|
||||
{
|
||||
public double Feedrate { get; set; }
|
||||
|
||||
public double RapidTravelRate { get; set; }
|
||||
|
||||
public TimeSpan PierceTime { get; set; }
|
||||
|
||||
public Units Units { get; set; }
|
||||
}
|
||||
Feedrate = 100,
|
||||
RapidTravelRate = 300,
|
||||
PierceTime = TimeSpan.FromSeconds(0.5),
|
||||
Units = OpenNest.Units.Inches
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
public int Remaining
|
||||
{
|
||||
get
|
||||
get
|
||||
{
|
||||
var x = Required - Nested;
|
||||
return x < 0 ? 0: x;
|
||||
return x < 0 ? 0 : x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -74,6 +74,16 @@ namespace OpenNest.Geometry
|
||||
Location += voffset;
|
||||
}
|
||||
|
||||
public Box Translate(double x, double y)
|
||||
{
|
||||
return new Box(X + x, Y + y, Width, Length);
|
||||
}
|
||||
|
||||
public Box Translate(Vector offset)
|
||||
{
|
||||
return new Box(X + offset.X, Y + offset.Y, Width, Length);
|
||||
}
|
||||
|
||||
public double Left
|
||||
{
|
||||
get { return X; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace OpenNest.Geometry
|
||||
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
||||
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
||||
result.Close();
|
||||
result.UpdateBounds();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -62,36 +63,20 @@ namespace OpenNest.Geometry
|
||||
/// Returns the polygon representing valid placement positions, or an empty
|
||||
/// polygon if no valid position exists.
|
||||
/// </summary>
|
||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
|
||||
{
|
||||
if (ifp.Vertices.Count < 3)
|
||||
return new Polygon();
|
||||
|
||||
if (nfps == null || nfps.Length == 0)
|
||||
if (nfpPaths == null || nfpPaths.Count == 0)
|
||||
return ifp;
|
||||
|
||||
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
||||
var ifpPaths = new PathsD { ifpPath };
|
||||
|
||||
// Union all NFPs.
|
||||
var nfpPaths = new PathsD();
|
||||
|
||||
foreach (var nfp in nfps)
|
||||
{
|
||||
if (nfp.Vertices.Count >= 3)
|
||||
{
|
||||
var path = NoFitPolygon.ToClipperPath(nfp);
|
||||
nfpPaths.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (nfpPaths.Count == 0)
|
||||
return ifp;
|
||||
|
||||
var nfpUnion = Clipper.Union(nfpPaths, FillRule.NonZero);
|
||||
|
||||
// Subtract the NFP union from the IFP.
|
||||
var feasible = Clipper.Difference(ifpPaths, nfpUnion, FillRule.NonZero);
|
||||
// Subtract the NFPs from the IFP.
|
||||
// Clipper2 handles the implicit union of the clip paths.
|
||||
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
|
||||
|
||||
if (feasible.Count == 0)
|
||||
return new Polygon();
|
||||
@@ -118,6 +103,25 @@ namespace OpenNest.Geometry
|
||||
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the feasible region for placing a part given already-placed parts.
|
||||
/// (Legacy overload for backward compatibility).
|
||||
/// </summary>
|
||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
||||
{
|
||||
if (nfps == null || nfps.Length == 0)
|
||||
return ifp;
|
||||
|
||||
var nfpPaths = new PathsD(nfps.Length);
|
||||
foreach (var nfp in nfps)
|
||||
{
|
||||
if (nfp.Vertices.Count >= 3)
|
||||
nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp));
|
||||
}
|
||||
|
||||
return ComputeFeasibleRegion(ifp, nfpPaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the bottom-left-most point on a polygon boundary.
|
||||
/// "Bottom-left" means: minimize Y first, then minimize X.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
@@ -23,8 +23,20 @@ namespace OpenNest.Geometry
|
||||
return MinkowskiSum(stationary, reflected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized version of Compute for polygons known to be convex.
|
||||
/// Bypasses expensive triangulation and Clipper unions.
|
||||
/// </summary>
|
||||
public static Polygon ComputeConvex(Polygon stationary, Polygon orbiting)
|
||||
{
|
||||
var reflected = Reflect(orbiting);
|
||||
return ConvexMinkowskiSum(stationary, reflected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflects a polygon through the origin (negates all vertex coordinates).
|
||||
/// Point reflection (negating both axes) is equivalent to 180° rotation,
|
||||
/// which preserves winding order. No reversal needed.
|
||||
/// </summary>
|
||||
private static Polygon Reflect(Polygon polygon)
|
||||
{
|
||||
@@ -33,8 +45,6 @@ namespace OpenNest.Geometry
|
||||
foreach (var v in polygon.Vertices)
|
||||
result.Vertices.Add(new Vector(-v.X, -v.Y));
|
||||
|
||||
// Reflecting reverses winding order — reverse to maintain CCW.
|
||||
result.Vertices.Reverse();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -79,19 +89,24 @@ namespace OpenNest.Geometry
|
||||
/// edge vectors sorted by angle. O(n+m) where n and m are vertex counts.
|
||||
/// Both polygons must have CCW winding.
|
||||
/// </summary>
|
||||
internal static Polygon ConvexMinkowskiSum(Polygon a, Polygon b)
|
||||
public static Polygon ConvexMinkowskiSum(Polygon a, Polygon b)
|
||||
{
|
||||
var edgesA = GetEdgeVectors(a);
|
||||
var edgesB = GetEdgeVectors(b);
|
||||
|
||||
// Find bottom-most (then left-most) vertex for each polygon as starting point.
|
||||
// Find indices of bottom-left vertices for both.
|
||||
var startA = FindBottomLeft(a);
|
||||
var startB = FindBottomLeft(b);
|
||||
|
||||
var result = new Polygon();
|
||||
|
||||
// The starting point of the Minkowski sum A + B is the sum of the
|
||||
// starting points of A and B. For NFP = A + (-B), this is
|
||||
// startA + startReflectedB.
|
||||
var current = new Vector(
|
||||
a.Vertices[startA].X + b.Vertices[startB].X,
|
||||
a.Vertices[startA].Y + b.Vertices[startB].Y);
|
||||
|
||||
result.Vertices.Add(current);
|
||||
|
||||
var ia = 0;
|
||||
@@ -99,7 +114,6 @@ namespace OpenNest.Geometry
|
||||
var na = edgesA.Count;
|
||||
var nb = edgesB.Count;
|
||||
|
||||
// Reorder edges to start from the bottom-left vertex.
|
||||
var orderedA = ReorderEdges(edgesA, startA);
|
||||
var orderedB = ReorderEdges(edgesB, startB);
|
||||
|
||||
@@ -118,7 +132,10 @@ namespace OpenNest.Geometry
|
||||
else
|
||||
{
|
||||
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
|
||||
if (angleA < 0) angleA += Angle.TwoPI;
|
||||
|
||||
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
|
||||
if (angleB < 0) angleB += Angle.TwoPI;
|
||||
|
||||
if (angleA < angleB)
|
||||
{
|
||||
@@ -130,7 +147,6 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same angle — merge both edges.
|
||||
edge = new Vector(
|
||||
orderedA[ia].X + orderedB[ib].X,
|
||||
orderedA[ia].Y + orderedB[ib].Y);
|
||||
@@ -144,6 +160,7 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
result.Close();
|
||||
result.UpdateBounds();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -251,9 +268,9 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an OpenNest Polygon to a Clipper2 PathD.
|
||||
/// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset.
|
||||
/// </summary>
|
||||
internal static PathD ToClipperPath(Polygon polygon)
|
||||
public static PathD ToClipperPath(Polygon polygon, Vector offset = default)
|
||||
{
|
||||
var path = new PathD();
|
||||
var verts = polygon.Vertices;
|
||||
@@ -264,7 +281,7 @@ namespace OpenNest.Geometry
|
||||
n--;
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
path.Add(new PointD(verts[i].X, verts[i].Y));
|
||||
path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y));
|
||||
|
||||
return path;
|
||||
}
|
||||
@@ -272,7 +289,7 @@ namespace OpenNest.Geometry
|
||||
/// <summary>
|
||||
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
||||
/// </summary>
|
||||
internal static Polygon FromClipperPath(PathD path)
|
||||
public static Polygon FromClipperPath(PathD path)
|
||||
{
|
||||
var polygon = new Polygon();
|
||||
|
||||
@@ -280,6 +297,7 @@ namespace OpenNest.Geometry
|
||||
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
||||
|
||||
polygon.Close();
|
||||
polygon.UpdateBounds();
|
||||
return polygon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Width} x {Length}";
|
||||
|
||||
|
||||
public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
@@ -30,47 +29,81 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
case PushDirection.Left:
|
||||
case PushDirection.Right:
|
||||
{
|
||||
var dy = p2y - p1y;
|
||||
if (System.Math.Abs(dy) < Tolerance.Epsilon)
|
||||
{
|
||||
var dy = p2y - p1y;
|
||||
if (System.Math.Abs(dy) < Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var t = (vy - p1y) / dy;
|
||||
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var ix = p1x + t * (p2x - p1x);
|
||||
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
|
||||
|
||||
if (dist > Tolerance.Epsilon) return dist;
|
||||
if (dist >= -Tolerance.Epsilon) return 0;
|
||||
return double.MaxValue;
|
||||
|
||||
var t = (vy - p1y) / dy;
|
||||
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var ix = p1x + t * (p2x - p1x);
|
||||
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
|
||||
|
||||
if (dist > Tolerance.Epsilon) return dist;
|
||||
if (dist >= -Tolerance.Epsilon) return 0;
|
||||
return double.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
case PushDirection.Down:
|
||||
case PushDirection.Up:
|
||||
{
|
||||
var dx = p2x - p1x;
|
||||
if (System.Math.Abs(dx) < Tolerance.Epsilon)
|
||||
{
|
||||
var dx = p2x - p1x;
|
||||
if (System.Math.Abs(dx) < Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var t = (vx - p1x) / dx;
|
||||
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var iy = p1y + t * (p2y - p1y);
|
||||
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
|
||||
|
||||
if (dist > Tolerance.Epsilon) return dist;
|
||||
if (dist >= -Tolerance.Epsilon) return 0;
|
||||
return double.MaxValue;
|
||||
|
||||
var t = (vx - p1x) / dx;
|
||||
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var iy = p1y + t * (p2y - p1y);
|
||||
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
|
||||
|
||||
if (dist > Tolerance.Epsilon) return dist;
|
||||
if (dist >= -Tolerance.Epsilon) return 0;
|
||||
return double.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return double.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generalized ray-edge distance along an arbitrary unit direction vector.
|
||||
/// Returns double.MaxValue if the ray does not hit the segment.
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(
|
||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static double RayEdgeDistance(
|
||||
double vx, double vy,
|
||||
double p1x, double p1y, double p2x, double p2y,
|
||||
double dirX, double dirY)
|
||||
{
|
||||
var ex = p2x - p1x;
|
||||
var ey = p2y - p1y;
|
||||
|
||||
var det = ex * dirY - ey * dirX;
|
||||
if (System.Math.Abs(det) < Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var dvx = p1x - vx;
|
||||
var dvy = p1y - vy;
|
||||
|
||||
var t = (ex * dvy - ey * dvx) / det;
|
||||
if (t < -Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
var s = (dirX * dvy - dirY * dvx) / det;
|
||||
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
|
||||
return double.MaxValue;
|
||||
|
||||
if (t > Tolerance.Epsilon) return t;
|
||||
if (t >= -Tolerance.Epsilon) return 0;
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum translation distance along a push direction before
|
||||
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||
@@ -329,10 +362,10 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: return box.Left - boundary.Left;
|
||||
case PushDirection.Left: return box.Left - boundary.Left;
|
||||
case PushDirection.Right: return boundary.Right - box.Right;
|
||||
case PushDirection.Up: return boundary.Top - box.Top;
|
||||
case PushDirection.Down: return box.Bottom - boundary.Bottom;
|
||||
case PushDirection.Up: return boundary.Top - box.Top;
|
||||
case PushDirection.Down: return box.Bottom - boundary.Bottom;
|
||||
default: return double.MaxValue;
|
||||
}
|
||||
}
|
||||
@@ -341,10 +374,10 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: return new Vector(-distance, 0);
|
||||
case PushDirection.Left: return new Vector(-distance, 0);
|
||||
case PushDirection.Right: return new Vector(distance, 0);
|
||||
case PushDirection.Up: return new Vector(0, distance);
|
||||
case PushDirection.Down: return new Vector(0, -distance);
|
||||
case PushDirection.Up: return new Vector(0, distance);
|
||||
case PushDirection.Down: return new Vector(0, -distance);
|
||||
default: return new Vector();
|
||||
}
|
||||
}
|
||||
@@ -353,14 +386,143 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: return from.Left - to.Right;
|
||||
case PushDirection.Left: return from.Left - to.Right;
|
||||
case PushDirection.Right: return to.Left - from.Right;
|
||||
case PushDirection.Up: return to.Bottom - from.Top;
|
||||
case PushDirection.Down: return from.Bottom - to.Top;
|
||||
case PushDirection.Up: return to.Bottom - from.Top;
|
||||
case PushDirection.Down: return from.Bottom - to.Top;
|
||||
default: return double.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
#region Generalized direction (Vector) overloads
|
||||
|
||||
/// <summary>
|
||||
/// Computes how far a box can travel along the given unit direction
|
||||
/// before exiting the boundary box.
|
||||
/// </summary>
|
||||
public static double EdgeDistance(Box box, Box boundary, Vector direction)
|
||||
{
|
||||
var dist = double.MaxValue;
|
||||
|
||||
if (direction.X < -Tolerance.Epsilon)
|
||||
{
|
||||
var d = (box.Left - boundary.Left) / -direction.X;
|
||||
if (d < dist) dist = d;
|
||||
}
|
||||
else if (direction.X > Tolerance.Epsilon)
|
||||
{
|
||||
var d = (boundary.Right - box.Right) / direction.X;
|
||||
if (d < dist) dist = d;
|
||||
}
|
||||
|
||||
if (direction.Y < -Tolerance.Epsilon)
|
||||
{
|
||||
var d = (box.Bottom - boundary.Bottom) / -direction.Y;
|
||||
if (d < dist) dist = d;
|
||||
}
|
||||
else if (direction.Y > Tolerance.Epsilon)
|
||||
{
|
||||
var d = (boundary.Top - box.Top) / direction.Y;
|
||||
if (d < dist) dist = d;
|
||||
}
|
||||
|
||||
return dist < 0 ? 0 : dist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the directional gap between two boxes along an arbitrary unit direction.
|
||||
/// Positive means 'to' is ahead of 'from' in the push direction.
|
||||
/// </summary>
|
||||
public static double DirectionalGap(Box from, Box to, Vector direction)
|
||||
{
|
||||
var fromMax = BoxProjectionMax(from, direction.X, direction.Y);
|
||||
var toMin = BoxProjectionMin(to, direction.X, direction.Y);
|
||||
return toMin - fromMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if two boxes overlap when projected onto the axis
|
||||
/// perpendicular to the given unit direction.
|
||||
/// </summary>
|
||||
public static bool PerpendicularOverlap(Box a, Box b, Vector direction)
|
||||
{
|
||||
var px = -direction.Y;
|
||||
var py = direction.X;
|
||||
|
||||
var aMin = BoxProjectionMin(a, px, py);
|
||||
var aMax = BoxProjectionMax(a, px, py);
|
||||
var bMin = BoxProjectionMin(b, px, py);
|
||||
var bMax = BoxProjectionMax(b, px, py);
|
||||
|
||||
return aMin <= bMax + Tolerance.Epsilon && bMin <= aMax + Tolerance.Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||
/// before any edge of movingLines contacts any edge of stationaryLines.
|
||||
/// </summary>
|
||||
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction)
|
||||
{
|
||||
var minDist = double.MaxValue;
|
||||
var dirX = direction.X;
|
||||
var dirY = direction.Y;
|
||||
|
||||
var movingVertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < movingLines.Count; i++)
|
||||
{
|
||||
movingVertices.Add(movingLines[i].pt1);
|
||||
movingVertices.Add(movingLines[i].pt2);
|
||||
}
|
||||
|
||||
foreach (var mv in movingVertices)
|
||||
{
|
||||
for (var i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
var e = stationaryLines[i];
|
||||
var d = RayEdgeDistance(mv.X, mv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, dirX, dirY);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
||||
}
|
||||
|
||||
foreach (var sv in stationaryVertices)
|
||||
{
|
||||
for (var i = 0; i < movingLines.Count; i++)
|
||||
{
|
||||
var e = movingLines[i];
|
||||
var d = RayEdgeDistance(sv.X, sv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, oppX, oppY);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
|
||||
private static double BoxProjectionMin(Box box, double dx, double dy)
|
||||
{
|
||||
var x = dx >= 0 ? box.Left : box.Right;
|
||||
var y = dy >= 0 ? box.Bottom : box.Top;
|
||||
return x * dx + y * dy;
|
||||
}
|
||||
|
||||
private static double BoxProjectionMax(Box box, double dx, double dy)
|
||||
{
|
||||
var x = dx >= 0 ? box.Right : box.Left;
|
||||
var y = dy >= 0 ? box.Top : box.Bottom;
|
||||
return x * dx + y * dy;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static double ClosestDistanceLeft(Box box, List<Box> boxes)
|
||||
{
|
||||
var closestDistance = double.MaxValue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Math
|
||||
namespace OpenNest.Math
|
||||
{
|
||||
public static class Angle
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Math
|
||||
namespace OpenNest.Math
|
||||
{
|
||||
public static class Tolerance
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace OpenNest.Math
|
||||
namespace OpenNest.Math
|
||||
{
|
||||
public static class Trigonometry
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -20,6 +20,7 @@ namespace OpenNest
|
||||
public class Part : IPart, IBoundable
|
||||
{
|
||||
private Vector location;
|
||||
private bool ownsProgram;
|
||||
|
||||
public readonly Drawing BaseDrawing;
|
||||
|
||||
@@ -32,6 +33,7 @@ namespace OpenNest
|
||||
{
|
||||
BaseDrawing = baseDrawing;
|
||||
Program = baseDrawing.Program.Clone() as Program;
|
||||
ownsProgram = true;
|
||||
this.location = location;
|
||||
UpdateBounds();
|
||||
}
|
||||
@@ -67,6 +69,7 @@ namespace OpenNest
|
||||
/// <param name="angle">Angle of rotation in radians.</param>
|
||||
public void Rotate(double angle)
|
||||
{
|
||||
EnsureOwnedProgram();
|
||||
Program.Rotate(angle);
|
||||
location = Location.Rotate(angle);
|
||||
UpdateBounds();
|
||||
@@ -79,6 +82,7 @@ namespace OpenNest
|
||||
/// <param name="origin">The origin to rotate the part around.</param>
|
||||
public void Rotate(double angle, Vector origin)
|
||||
{
|
||||
EnsureOwnedProgram();
|
||||
Program.Rotate(angle);
|
||||
location = Location.Rotate(angle, origin);
|
||||
UpdateBounds();
|
||||
@@ -222,6 +226,15 @@ namespace OpenNest
|
||||
return part;
|
||||
}
|
||||
|
||||
private void EnsureOwnedProgram()
|
||||
{
|
||||
if (!ownsProgram)
|
||||
{
|
||||
Program = Program.Clone() as Program;
|
||||
ownsProgram = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Part(Drawing baseDrawing, Program program, Vector location, Box boundingBox)
|
||||
{
|
||||
BaseDrawing = baseDrawing;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -85,6 +85,73 @@ namespace OpenNest
|
||||
return lines;
|
||||
}
|
||||
|
||||
public static List<Line> GetPartLines(Part part, Vector 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, Vector 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 vector.
|
||||
/// </summary>
|
||||
private static List<Line> GetDirectionalLines(Polygon polygon, Vector direction)
|
||||
{
|
||||
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 (var i = 1; i < polygon.Vertices.Count; i++)
|
||||
{
|
||||
var current = polygon.Vertices[i];
|
||||
var edx = current.X - last.X;
|
||||
var edy = current.Y - last.Y;
|
||||
|
||||
var keep = sign * (edy * direction.X - edx * direction.Y) > 0;
|
||||
|
||||
if (keep)
|
||||
lines.Add(new Line(last, current));
|
||||
|
||||
last = current;
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns only polygon edges whose outward normal faces the specified direction.
|
||||
/// </summary>
|
||||
@@ -107,10 +174,10 @@ namespace OpenNest
|
||||
|
||||
switch (facingDirection)
|
||||
{
|
||||
case PushDirection.Left: keep = -sign * dy > 0; break;
|
||||
case PushDirection.Right: keep = sign * dy > 0; break;
|
||||
case PushDirection.Up: keep = -sign * dx > 0; break;
|
||||
case PushDirection.Down: keep = sign * dx > 0; break;
|
||||
case PushDirection.Left: keep = -sign * dy > 0; break;
|
||||
case PushDirection.Right: keep = sign * dy > 0; break;
|
||||
case PushDirection.Up: keep = -sign * dx > 0; break;
|
||||
case PushDirection.Down: keep = sign * dx > 0; break;
|
||||
default: keep = true; break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Collections;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OpenNest.Api;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -83,7 +84,7 @@ namespace OpenNest
|
||||
time += TimeSpan.FromSeconds(info.TravelDistance / cutParams.RapidTravelRate);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
time += TimeSpan.FromTicks(info.PierceCount * cutParams.PierceTime.Ticks);
|
||||
|
||||
return time;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace OpenNest
|
||||
case Units.Millimeters:
|
||||
return "mm";
|
||||
|
||||
default:
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace OpenNest
|
||||
case Units.Millimeters:
|
||||
return "millimeters";
|
||||
|
||||
default:
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace OpenNest
|
||||
case Units.Millimeters:
|
||||
return "sec";
|
||||
|
||||
default:
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds candidate rotation angles for single-item fill. Encapsulates the
|
||||
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
|
||||
/// known-good pruning across fills.
|
||||
/// </summary>
|
||||
public class AngleCandidateBuilder
|
||||
{
|
||||
private readonly HashSet<double> knownGoodAngles = new();
|
||||
|
||||
public bool ForceFullSweep { get; set; }
|
||||
|
||||
public List<double> Build(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
var testPart = new Part(item.Drawing);
|
||||
if (!bestRotation.IsEqualTo(0))
|
||||
testPart.Rotate(bestRotation);
|
||||
testPart.UpdateBounds();
|
||||
|
||||
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
|
||||
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
||||
var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep;
|
||||
|
||||
if (needsSweep)
|
||||
{
|
||||
var step = Angle.ToRadians(5);
|
||||
for (var a = 0.0; a < System.Math.PI; a += step)
|
||||
{
|
||||
if (!angles.Any(existing => existing.IsEqualTo(a)))
|
||||
angles.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ForceFullSweep && angles.Count > 2)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features != null)
|
||||
{
|
||||
var predicted = AnglePredictor.PredictAngles(
|
||||
features, workArea.Width, workArea.Length);
|
||||
|
||||
if (predicted != null)
|
||||
{
|
||||
var mlAngles = new List<double>(predicted);
|
||||
|
||||
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
|
||||
mlAngles.Add(bestRotation);
|
||||
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
|
||||
mlAngles.Add(bestRotation + Angle.HalfPI);
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
|
||||
angles = mlAngles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
|
||||
{
|
||||
var pruned = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
foreach (var a in knownGoodAngles)
|
||||
{
|
||||
if (!pruned.Any(existing => existing.IsEqualTo(a)))
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
|
||||
return pruned;
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records angles that produced results. These are used to prune
|
||||
/// subsequent Build() calls.
|
||||
/// </summary>
|
||||
public void RecordProductive(List<AngleResult> angleResults)
|
||||
{
|
||||
foreach (var ar in angleResults)
|
||||
{
|
||||
if (ar.PartCount > 0)
|
||||
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using System;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
internal static class BestCombination
|
||||
{
|
||||
public static bool FindFrom2(double length1, double length2, double overallLength, out int count1, out int count2)
|
||||
{
|
||||
overallLength += Tolerance.Epsilon;
|
||||
|
||||
if (length1 > overallLength)
|
||||
{
|
||||
if (length2 > overallLength)
|
||||
{
|
||||
count1 = 0;
|
||||
count2 = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
count1 = 0;
|
||||
count2 = (int)System.Math.Floor(overallLength / length2);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (length2 > overallLength)
|
||||
{
|
||||
count1 = (int)System.Math.Floor(overallLength / length1);
|
||||
count2 = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
var maxCountLength1 = (int)System.Math.Floor(overallLength / length1);
|
||||
|
||||
count1 = maxCountLength1;
|
||||
count2 = 0;
|
||||
|
||||
var remnant = overallLength - maxCountLength1 * length1;
|
||||
|
||||
if (remnant.IsEqualTo(0))
|
||||
return true;
|
||||
|
||||
for (int countLength1 = 0; countLength1 <= maxCountLength1; ++countLength1)
|
||||
{
|
||||
var remnant1 = overallLength - countLength1 * length1;
|
||||
|
||||
if (remnant1 >= length2)
|
||||
{
|
||||
var countLength2 = (int)System.Math.Floor(remnant1 / length2);
|
||||
var remnant2 = remnant1 - length2 * countLength2;
|
||||
|
||||
if (!(remnant2 < remnant))
|
||||
continue;
|
||||
|
||||
count1 = countLength1;
|
||||
count2 = countLength2;
|
||||
|
||||
if (remnant2.IsEqualTo(0))
|
||||
break;
|
||||
|
||||
remnant = remnant2;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(remnant1 < remnant))
|
||||
continue;
|
||||
|
||||
count1 = countLength1;
|
||||
count2 = 0;
|
||||
|
||||
if (remnant1.IsEqualTo(0))
|
||||
break;
|
||||
|
||||
remnant = remnant1;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.BestFit.Tiling;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class BestFitFinder
|
||||
{
|
||||
private readonly IPairEvaluator _evaluator;
|
||||
private readonly ISlideComputer _slideComputer;
|
||||
private readonly IDistanceComputer _distanceComputer;
|
||||
private readonly BestFitFilter _filter;
|
||||
|
||||
public BestFitFinder(double maxPlateWidth, double maxPlateHeight,
|
||||
IPairEvaluator evaluator = null, ISlideComputer slideComputer = null)
|
||||
{
|
||||
_evaluator = evaluator ?? new PairEvaluator();
|
||||
_slideComputer = slideComputer;
|
||||
_distanceComputer = slideComputer != null
|
||||
? (IDistanceComputer)new GpuDistanceComputer(slideComputer)
|
||||
: new CpuDistanceComputer();
|
||||
var plateAspect = System.Math.Max(maxPlateWidth, maxPlateHeight) /
|
||||
System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001);
|
||||
_filter = new BestFitFilter
|
||||
@@ -36,7 +38,7 @@ namespace OpenNest.Engine.BestFit
|
||||
double stepSize = 0.25,
|
||||
BestFitSortField sortBy = BestFitSortField.Area)
|
||||
{
|
||||
var strategies = BuildStrategies(drawing);
|
||||
var strategies = BuildStrategies(drawing, spacing);
|
||||
|
||||
var candidateBags = new ConcurrentBag<List<PairCandidate>>();
|
||||
|
||||
@@ -75,16 +77,16 @@ namespace OpenNest.Engine.BestFit
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<IBestFitStrategy> BuildStrategies(Drawing drawing)
|
||||
private List<IBestFitStrategy> BuildStrategies(Drawing drawing, double spacing)
|
||||
{
|
||||
var angles = GetRotationAngles(drawing);
|
||||
var strategies = new List<IBestFitStrategy>();
|
||||
var type = 1;
|
||||
var index = 1;
|
||||
|
||||
foreach (var angle in angles)
|
||||
{
|
||||
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
|
||||
strategies.Add(new RotationSlideStrategy(angle, type++, desc, _slideComputer));
|
||||
strategies.Add(new RotationSlideStrategy(angle, index++, desc, _distanceComputer));
|
||||
}
|
||||
|
||||
return strategies;
|
||||
@@ -226,7 +228,7 @@ namespace OpenNest.Engine.BestFit
|
||||
case BestFitSortField.ShortestSide:
|
||||
return results.OrderBy(r => r.ShortestSide).ToList();
|
||||
case BestFitSortField.Type:
|
||||
return results.OrderBy(r => r.Candidate.StrategyType)
|
||||
return results.OrderBy(r => r.Candidate.StrategyIndex)
|
||||
.ThenBy(r => r.Candidate.TestNumber).ToList();
|
||||
case BestFitSortField.OriginalSequence:
|
||||
return results.OrderBy(r => r.Candidate.TestNumber).ToList();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
|
||||
152
OpenNest.Engine/BestFit/CpuDistanceComputer.cs
Normal file
152
OpenNest.Engine/BestFit/CpuDistanceComputer.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class CpuDistanceComputer : IDistanceComputer
|
||||
{
|
||||
public double[] ComputeDistances(
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets)
|
||||
{
|
||||
var count = offsets.Length;
|
||||
var results = new double[count];
|
||||
|
||||
var allMovingVerts = ExtractUniqueVertices(movingTemplateLines);
|
||||
var allStationaryVerts = ExtractUniqueVertices(stationaryLines);
|
||||
|
||||
// Pre-filter vertices per unique direction (typically 4 cardinal directions).
|
||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
var key = (offset.DirX, offset.DirY);
|
||||
if (vertexCache.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true);
|
||||
var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false);
|
||||
vertexCache[key] = (leading, facing);
|
||||
}
|
||||
|
||||
System.Threading.Tasks.Parallel.For(0, count, i =>
|
||||
{
|
||||
var offset = offsets[i];
|
||||
var dirX = offset.DirX;
|
||||
var dirY = offset.DirY;
|
||||
var oppX = -dirX;
|
||||
var oppY = -dirY;
|
||||
|
||||
var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)];
|
||||
|
||||
var minDist = double.MaxValue;
|
||||
|
||||
// Case 1: Leading moving vertices → stationary edges
|
||||
for (var v = 0; v < leadingMoving.Length; v++)
|
||||
{
|
||||
var vx = leadingMoving[v].X + offset.Dx;
|
||||
var vy = leadingMoving[v].Y + offset.Dy;
|
||||
|
||||
for (var j = 0; j < stationaryLines.Count; j++)
|
||||
{
|
||||
var e = stationaryLines[j];
|
||||
var d = SpatialQuery.RayEdgeDistance(
|
||||
vx, vy,
|
||||
e.StartPoint.X, e.StartPoint.Y,
|
||||
e.EndPoint.X, e.EndPoint.Y,
|
||||
dirX, dirY);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Facing stationary vertices → moving edges (opposite direction)
|
||||
for (var v = 0; v < facingStationary.Length; v++)
|
||||
{
|
||||
var svx = facingStationary[v].X;
|
||||
var svy = facingStationary[v].Y;
|
||||
|
||||
for (var j = 0; j < movingTemplateLines.Count; j++)
|
||||
{
|
||||
var e = movingTemplateLines[j];
|
||||
var d = SpatialQuery.RayEdgeDistance(
|
||||
svx, svy,
|
||||
e.StartPoint.X + offset.Dx, e.StartPoint.Y + offset.Dy,
|
||||
e.EndPoint.X + offset.Dx, e.EndPoint.Y + offset.Dy,
|
||||
oppX, oppY);
|
||||
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
if (d <= 0) { results[i] = 0; return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static Vector[] ExtractUniqueVertices(List<Line> lines)
|
||||
{
|
||||
var vertices = new HashSet<Vector>();
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
{
|
||||
vertices.Add(lines[i].StartPoint);
|
||||
vertices.Add(lines[i].EndPoint);
|
||||
}
|
||||
return vertices.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters vertices by their projection onto the push direction.
|
||||
/// keepHigh=true returns the leading half (front face, closest to target).
|
||||
/// keepHigh=false returns the facing half (side facing the approaching part).
|
||||
/// </summary>
|
||||
private static Vector[] FilterVerticesByProjection(
|
||||
Vector[] vertices, double dirX, double dirY, bool keepHigh)
|
||||
{
|
||||
if (vertices.Length == 0)
|
||||
return vertices;
|
||||
|
||||
var projections = new double[vertices.Length];
|
||||
var min = double.MaxValue;
|
||||
var max = double.MinValue;
|
||||
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
projections[i] = vertices[i].X * dirX + vertices[i].Y * dirY;
|
||||
if (projections[i] < min) min = projections[i];
|
||||
if (projections[i] > max) max = projections[i];
|
||||
}
|
||||
|
||||
var midpoint = (min + max) / 2;
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
if (keepHigh ? projections[i] >= midpoint : projections[i] <= midpoint)
|
||||
count++;
|
||||
}
|
||||
|
||||
var result = new Vector[count];
|
||||
var idx = 0;
|
||||
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
if (keepHigh ? projections[i] >= midpoint : projections[i] <= midpoint)
|
||||
result[idx++] = vertices[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
OpenNest.Engine/BestFit/GpuDistanceComputer.cs
Normal file
51
OpenNest.Engine/BestFit/GpuDistanceComputer.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class GpuDistanceComputer : IDistanceComputer
|
||||
{
|
||||
private readonly ISlideComputer _slideComputer;
|
||||
|
||||
public GpuDistanceComputer(ISlideComputer slideComputer)
|
||||
{
|
||||
_slideComputer = slideComputer;
|
||||
}
|
||||
|
||||
public double[] ComputeDistances(
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets)
|
||||
{
|
||||
var stationarySegments = SpatialQuery.FlattenLines(stationaryLines);
|
||||
var movingSegments = SpatialQuery.FlattenLines(movingTemplateLines);
|
||||
var count = offsets.Length;
|
||||
var flatOffsets = new double[count * 2];
|
||||
var directions = new int[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
flatOffsets[i * 2] = offsets[i].Dx;
|
||||
flatOffsets[i * 2 + 1] = offsets[i].Dy;
|
||||
directions[i] = DirectionVectorToInt(offsets[i].DirX, offsets[i].DirY);
|
||||
}
|
||||
|
||||
return _slideComputer.ComputeBatchMultiDir(
|
||||
stationarySegments, stationaryLines.Count,
|
||||
movingSegments, movingTemplateLines.Count,
|
||||
flatOffsets, count, directions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a unit direction vector to a PushDirection int for the GPU interface.
|
||||
/// Left=0, Down=1, Right=2, Up=3.
|
||||
/// </summary>
|
||||
private static int DirectionVectorToInt(double dirX, double dirY)
|
||||
{
|
||||
if (dirX < -0.5) return (int)PushDirection.Left;
|
||||
if (dirX > 0.5) return (int)PushDirection.Right;
|
||||
if (dirY < -0.5) return (int)PushDirection.Down;
|
||||
return (int)PushDirection.Up;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public interface IBestFitStrategy
|
||||
{
|
||||
int Type { get; }
|
||||
int StrategyIndex { get; }
|
||||
string Description { get; }
|
||||
List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize);
|
||||
}
|
||||
|
||||
13
OpenNest.Engine/BestFit/IDistanceComputer.cs
Normal file
13
OpenNest.Engine/BestFit/IDistanceComputer.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public interface IDistanceComputer
|
||||
{
|
||||
double[] ComputeDistances(
|
||||
List<Line> stationaryLines,
|
||||
List<Line> movingTemplateLines,
|
||||
SlideOffset[] offsets);
|
||||
}
|
||||
}
|
||||
179
OpenNest.Engine/BestFit/NfpSlideStrategy.cs
Normal file
179
OpenNest.Engine/BestFit/NfpSlideStrategy.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class NfpSlideStrategy : IBestFitStrategy
|
||||
{
|
||||
private static readonly string LogPath = Path.Combine(
|
||||
System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop),
|
||||
"nfp-slide-debug.log");
|
||||
|
||||
private static readonly object LogLock = new object();
|
||||
|
||||
private readonly double _part2Rotation;
|
||||
private readonly Polygon _stationaryPerimeter;
|
||||
private readonly Polygon _stationaryHull;
|
||||
private readonly Vector _correction;
|
||||
|
||||
public NfpSlideStrategy(double part2Rotation, int type, string description,
|
||||
Polygon stationaryPerimeter, Polygon stationaryHull, Vector correction)
|
||||
{
|
||||
_part2Rotation = part2Rotation;
|
||||
StrategyIndex = type;
|
||||
Description = description;
|
||||
_stationaryPerimeter = stationaryPerimeter;
|
||||
_stationaryHull = stationaryHull;
|
||||
_correction = correction;
|
||||
}
|
||||
|
||||
public int StrategyIndex { get; }
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an NfpSlideStrategy by extracting polygon data from a drawing.
|
||||
/// Returns null if the drawing has no valid perimeter.
|
||||
/// </summary>
|
||||
public static NfpSlideStrategy Create(Drawing drawing, double part2Rotation,
|
||||
int type, string description, double spacing)
|
||||
{
|
||||
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, spacing / 2);
|
||||
|
||||
if (result.Polygon == null)
|
||||
return null;
|
||||
|
||||
var hull = ConvexHull.Compute(result.Polygon.Vertices);
|
||||
|
||||
Log($"=== Create: drawing={drawing.Name}, rotation={Angle.ToDegrees(part2Rotation):F1}deg ===");
|
||||
Log($" Perimeter: {result.Polygon.Vertices.Count} verts, bounds={FormatBounds(result.Polygon)}");
|
||||
Log($" Hull: {hull.Vertices.Count} verts, bounds={FormatBounds(hull)}");
|
||||
Log($" Correction: ({result.Correction.X:F4}, {result.Correction.Y:F4})");
|
||||
Log($" ProgramBBox: {drawing.Program.BoundingBox()}");
|
||||
|
||||
return new NfpSlideStrategy(part2Rotation, type, description,
|
||||
result.Polygon, hull, result.Correction);
|
||||
}
|
||||
|
||||
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
|
||||
{
|
||||
var candidates = new List<PairCandidate>();
|
||||
|
||||
if (stepSize <= 0)
|
||||
return candidates;
|
||||
|
||||
Log($"--- GenerateCandidates: drawing={drawing.Name}, part2Rot={Angle.ToDegrees(_part2Rotation):F1}deg, spacing={spacing}, stepSize={stepSize} ---");
|
||||
|
||||
// Orbiting polygon: same shape rotated to Part2's angle.
|
||||
var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true);
|
||||
var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices);
|
||||
|
||||
Log($" Stationary hull: {_stationaryHull.Vertices.Count} verts, bounds={FormatBounds(_stationaryHull)}");
|
||||
Log($" Orbiting perimeter (rotated): {orbitingPerimeter.Vertices.Count} verts, bounds={FormatBounds(orbitingPerimeter)}");
|
||||
Log($" Orbiting hull: {orbitingPoly.Vertices.Count} verts, bounds={FormatBounds(orbitingPoly)}");
|
||||
|
||||
var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly);
|
||||
|
||||
if (nfp == null || nfp.Vertices.Count < 3)
|
||||
{
|
||||
Log($" NFP failed or degenerate (verts={nfp?.Vertices.Count ?? 0})");
|
||||
return candidates;
|
||||
}
|
||||
|
||||
var verts = nfp.Vertices;
|
||||
var vertCount = nfp.IsClosed() ? verts.Count - 1 : verts.Count;
|
||||
|
||||
Log($" NFP: {verts.Count} verts (closed={nfp.IsClosed()}, walking {vertCount}), bounds={FormatBounds(nfp)}");
|
||||
Log($" Correction: ({_correction.X:F4}, {_correction.Y:F4})");
|
||||
|
||||
// Log NFP vertices
|
||||
for (var v = 0; v < vertCount; v++)
|
||||
Log($" NFP vert[{v}]: ({verts[v].X:F4}, {verts[v].Y:F4}) -> corrected: ({verts[v].X - _correction.X:F4}, {verts[v].Y - _correction.Y:F4})");
|
||||
|
||||
// Compare with what RotationSlideStrategy would produce
|
||||
var part1 = Part.CreateAtOrigin(drawing);
|
||||
var part2 = Part.CreateAtOrigin(drawing, _part2Rotation);
|
||||
Log($" Part1 (rot=0): loc=({part1.Location.X:F4}, {part1.Location.Y:F4}), bbox={part1.BoundingBox}");
|
||||
Log($" Part2 (rot={Angle.ToDegrees(_part2Rotation):F1}): loc=({part2.Location.X:F4}, {part2.Location.Y:F4}), bbox={part2.BoundingBox}");
|
||||
|
||||
var testNumber = 0;
|
||||
|
||||
for (var i = 0; i < vertCount; i++)
|
||||
{
|
||||
var offset = ApplyCorrection(verts[i], _correction);
|
||||
candidates.Add(MakeCandidate(drawing, offset, spacing, testNumber++));
|
||||
|
||||
// Add edge samples for long edges.
|
||||
var next = (i + 1) % vertCount;
|
||||
var dx = verts[next].X - verts[i].X;
|
||||
var dy = verts[next].Y - verts[i].Y;
|
||||
var edgeLength = System.Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (edgeLength > stepSize)
|
||||
{
|
||||
var steps = (int)(edgeLength / stepSize);
|
||||
for (var s = 1; s < steps; s++)
|
||||
{
|
||||
var t = (double)s / steps;
|
||||
var sample = new Vector(
|
||||
verts[i].X + dx * t,
|
||||
verts[i].Y + dy * t);
|
||||
var sampleOffset = ApplyCorrection(sample, _correction);
|
||||
candidates.Add(MakeCandidate(drawing, sampleOffset, spacing, testNumber++));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log overlap check for vertex candidates (first few)
|
||||
var checkCount = System.Math.Min(vertCount, 8);
|
||||
for (var c = 0; c < checkCount; c++)
|
||||
{
|
||||
var cand = candidates[c];
|
||||
var p2 = Part.CreateAtOrigin(drawing, cand.Part2Rotation);
|
||||
p2.Location = cand.Part2Offset;
|
||||
var overlaps = part1.Intersects(p2, out _);
|
||||
Log($" Candidate[{c}]: offset=({cand.Part2Offset.X:F4}, {cand.Part2Offset.Y:F4}), overlaps={overlaps}");
|
||||
}
|
||||
|
||||
Log($" Total candidates: {candidates.Count}");
|
||||
Log("");
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static Vector ApplyCorrection(Vector nfpVertex, Vector correction)
|
||||
{
|
||||
return new Vector(nfpVertex.X - correction.X, nfpVertex.Y - correction.Y);
|
||||
}
|
||||
|
||||
private PairCandidate MakeCandidate(Drawing drawing, Vector offset, double spacing, int testNumber)
|
||||
{
|
||||
return new PairCandidate
|
||||
{
|
||||
Drawing = drawing,
|
||||
Part1Rotation = 0,
|
||||
Part2Rotation = _part2Rotation,
|
||||
Part2Offset = offset,
|
||||
StrategyIndex = StrategyIndex,
|
||||
TestNumber = testNumber,
|
||||
Spacing = spacing
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatBounds(Polygon polygon)
|
||||
{
|
||||
polygon.UpdateBounds();
|
||||
var bb = polygon.BoundingBox;
|
||||
return $"[({bb.Left:F4}, {bb.Bottom:F4})-({bb.Right:F4}, {bb.Top:F4}), {bb.Width:F2}x{bb.Length:F2}]";
|
||||
}
|
||||
|
||||
private static void Log(string message)
|
||||
{
|
||||
lock (LogLock)
|
||||
{
|
||||
File.AppendAllText(LogPath, message + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace OpenNest.Engine.BestFit
|
||||
public double Part1Rotation { get; set; }
|
||||
public double Part2Rotation { get; set; }
|
||||
public Vector Part2Offset { get; set; }
|
||||
public int StrategyType { get; set; }
|
||||
public int StrategyIndex { get; set; }
|
||||
public int TestNumber { get; set; }
|
||||
public double Spacing { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -66,6 +68,15 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
var trueArea = drawing.Area * 2;
|
||||
|
||||
// Normalize to landscape (width >= height) for consistent display.
|
||||
if (bestHeight > bestWidth)
|
||||
{
|
||||
var tmp = bestWidth;
|
||||
bestWidth = bestHeight;
|
||||
bestHeight = tmp;
|
||||
bestRotation += Angle.HalfPI;
|
||||
}
|
||||
|
||||
return new BestFitResult
|
||||
{
|
||||
Candidate = candidate,
|
||||
|
||||
77
OpenNest.Engine/BestFit/PolygonHelper.cs
Normal file
77
OpenNest.Engine/BestFit/PolygonHelper.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public static class PolygonHelper
|
||||
{
|
||||
public static PolygonExtractionResult ExtractPerimeterPolygon(Drawing drawing, double halfSpacing)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
|
||||
if (entities.Count == 0)
|
||||
return new PolygonExtractionResult(null, Vector.Zero);
|
||||
|
||||
var definedShape = new ShapeProfile(entities);
|
||||
var perimeter = definedShape.Perimeter;
|
||||
|
||||
if (perimeter == null)
|
||||
return new PolygonExtractionResult(null, Vector.Zero);
|
||||
|
||||
// Inflate by half-spacing if spacing is non-zero.
|
||||
// OffsetSide.Right = outward for CCW perimeters (standard for outer contours).
|
||||
var inflated = halfSpacing > 0
|
||||
? (perimeter.OffsetEntity(halfSpacing, OffsetSide.Right) as Shape ?? perimeter)
|
||||
: perimeter;
|
||||
|
||||
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||
var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true);
|
||||
|
||||
if (polygon.Vertices.Count < 3)
|
||||
return new PolygonExtractionResult(null, Vector.Zero);
|
||||
|
||||
// Normalize: move polygon to origin.
|
||||
polygon.UpdateBounds();
|
||||
var bb = polygon.BoundingBox;
|
||||
polygon.Offset(-bb.Left, -bb.Bottom);
|
||||
|
||||
// No correction needed: BestFitFinder always pairs the same drawing with
|
||||
// itself, so the polygon-to-part offset is identical for both parts and
|
||||
// cancels out in the NFP displacement.
|
||||
return new PolygonExtractionResult(polygon, Vector.Zero);
|
||||
}
|
||||
|
||||
public static Polygon RotatePolygon(Polygon polygon, double angle, bool reNormalize = true)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
if (reNormalize)
|
||||
{
|
||||
// Re-normalize to origin.
|
||||
result.UpdateBounds();
|
||||
var bb = result.BoundingBox;
|
||||
result.Offset(-bb.Left, -bb.Bottom);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public record PolygonExtractionResult(Polygon Polygon, Vector Correction);
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class RotationSlideStrategy : IBestFitStrategy
|
||||
{
|
||||
private readonly ISlideComputer _slideComputer;
|
||||
private readonly IDistanceComputer _distanceComputer;
|
||||
|
||||
private static readonly PushDirection[] AllDirections =
|
||||
private static readonly (double DirX, double DirY)[] PushDirections =
|
||||
{
|
||||
PushDirection.Left, PushDirection.Down, PushDirection.Right, PushDirection.Up
|
||||
(-1, 0), // Left
|
||||
(0, -1), // Down
|
||||
(1, 0), // Right
|
||||
(0, 1) // Up
|
||||
};
|
||||
|
||||
public RotationSlideStrategy(double part2Rotation, int type, string description,
|
||||
ISlideComputer slideComputer = null)
|
||||
public RotationSlideStrategy(double part2Rotation, int strategyIndex, string description,
|
||||
IDistanceComputer distanceComputer)
|
||||
{
|
||||
Part2Rotation = part2Rotation;
|
||||
Type = type;
|
||||
StrategyIndex = strategyIndex;
|
||||
Description = description;
|
||||
_slideComputer = slideComputer;
|
||||
_distanceComputer = distanceComputer;
|
||||
}
|
||||
|
||||
public double Part2Rotation { get; }
|
||||
public int Type { get; }
|
||||
public int StrategyIndex { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
|
||||
@@ -40,36 +42,25 @@ namespace OpenNest.Engine.BestFit
|
||||
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>();
|
||||
var offsets = BuildOffsets(bbox1, bbox2, spacing, stepSize);
|
||||
|
||||
foreach (var pushDir in AllDirections)
|
||||
BuildOffsets(bbox1, bbox2, spacing, stepSize, pushDir, allDx, allDy, allDirs);
|
||||
|
||||
if (allDx.Count == 0)
|
||||
if (offsets.Length == 0)
|
||||
return candidates;
|
||||
|
||||
// Compute all distances — single GPU dispatch or CPU loop
|
||||
var distances = ComputeAllDistances(
|
||||
part1Lines, part2TemplateLines, allDx, allDy, allDirs);
|
||||
var distances = _distanceComputer.ComputeDistances(
|
||||
part1Lines, part2TemplateLines, offsets);
|
||||
|
||||
// Create candidates from valid results
|
||||
var testNumber = 0;
|
||||
|
||||
for (var i = 0; i < allDx.Count; i++)
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
var slideDist = distances[i];
|
||||
if (slideDist >= double.MaxValue || slideDist < 0)
|
||||
continue;
|
||||
|
||||
var dx = allDx[i];
|
||||
var dy = allDy[i];
|
||||
var pushVector = GetPushVector(allDirs[i], slideDist);
|
||||
var finalPosition = new Vector(
|
||||
part2Template.Location.X + dx + pushVector.X,
|
||||
part2Template.Location.Y + dy + pushVector.Y);
|
||||
part2Template.Location.X + offsets[i].Dx + offsets[i].DirX * slideDist,
|
||||
part2Template.Location.Y + offsets[i].Dy + offsets[i].DirY * slideDist);
|
||||
|
||||
candidates.Add(new PairCandidate
|
||||
{
|
||||
@@ -77,7 +68,7 @@ namespace OpenNest.Engine.BestFit
|
||||
Part1Rotation = 0,
|
||||
Part2Rotation = Part2Rotation,
|
||||
Part2Offset = finalPosition,
|
||||
StrategyType = Type,
|
||||
StrategyIndex = StrategyIndex,
|
||||
TestNumber = testNumber++,
|
||||
Spacing = spacing
|
||||
});
|
||||
@@ -86,158 +77,44 @@ namespace OpenNest.Engine.BestFit
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static void BuildOffsets(
|
||||
Box bbox1, Box bbox2, double spacing, double stepSize,
|
||||
PushDirection pushDir, List<double> allDx, List<double> allDy,
|
||||
List<PushDirection> allDirs)
|
||||
private static SlideOffset[] BuildOffsets(Box bbox1, Box bbox2, double spacing, double stepSize)
|
||||
{
|
||||
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
||||
var offsets = new List<SlideOffset>();
|
||||
|
||||
double perpMin, perpMax, pushStartOffset;
|
||||
|
||||
if (isHorizontalPush)
|
||||
foreach (var (dirX, dirY) in PushDirections)
|
||||
{
|
||||
perpMin = -(bbox2.Length + spacing);
|
||||
perpMax = bbox1.Length + bbox2.Length + spacing;
|
||||
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
perpMin = -(bbox2.Width + spacing);
|
||||
perpMax = bbox1.Width + bbox2.Width + spacing;
|
||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||
}
|
||||
var isHorizontalPush = System.Math.Abs(dirX) > System.Math.Abs(dirY);
|
||||
|
||||
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
|
||||
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
||||
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
||||
double perpMin, perpMax, pushStartOffset;
|
||||
|
||||
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
||||
{
|
||||
allDx.Add(isHorizontalPush ? startPos : offset);
|
||||
allDy.Add(isHorizontalPush ? offset : startPos);
|
||||
allDirs.Add(pushDir);
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
if (isHorizontalPush)
|
||||
{
|
||||
offsets[i * 2] = allDx[i];
|
||||
offsets[i * 2 + 1] = allDy[i];
|
||||
directions[i] = (int)allDirs[i];
|
||||
perpMin = -(bbox2.Length + spacing);
|
||||
perpMax = bbox1.Length + bbox2.Length + spacing;
|
||||
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
||||
}
|
||||
|
||||
return _slideComputer.ComputeBatchMultiDir(
|
||||
stationarySegments, part1Lines.Count,
|
||||
movingSegments, part2TemplateLines.Count,
|
||||
offsets, count, directions);
|
||||
}
|
||||
|
||||
var results = new double[count];
|
||||
|
||||
// Pre-calculate moving vertices in local space.
|
||||
var movingVerticesLocal = new HashSet<Vector>();
|
||||
for (var i = 0; i < part2TemplateLines.Count; i++)
|
||||
{
|
||||
movingVerticesLocal.Add(part2TemplateLines[i].StartPoint);
|
||||
movingVerticesLocal.Add(part2TemplateLines[i].EndPoint);
|
||||
}
|
||||
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;
|
||||
perpMin = -(bbox2.Width + spacing);
|
||||
perpMax = bbox1.Width + bbox2.Width + spacing;
|
||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||
}
|
||||
|
||||
// Case 2: Stationary vertices -> Moving edges (translated)
|
||||
foreach (var sv in stationaryVerticesArray)
|
||||
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
|
||||
|
||||
// Start on the opposite side of the push direction.
|
||||
var pushComponent = isHorizontalPush ? dirX : dirY;
|
||||
var startPos = pushComponent < 0 ? pushStartOffset : -pushStartOffset;
|
||||
|
||||
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
||||
{
|
||||
var d = SpatialQuery.OneWayDistance(sv, mEdges, movingOffset, opposite);
|
||||
if (d < minDist) minDist = d;
|
||||
var dx = isHorizontalPush ? startPos : offset;
|
||||
var dy = isHorizontalPush ? offset : startPos;
|
||||
offsets.Add(new SlideOffset(dx, dy, dirX, dirY));
|
||||
}
|
||||
|
||||
results[i] = minDist;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static Vector GetPushVector(PushDirection direction, double distance)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case PushDirection.Left: return new Vector(-distance, 0);
|
||||
case PushDirection.Right: return new Vector(distance, 0);
|
||||
case PushDirection.Down: return new Vector(0, -distance);
|
||||
case PushDirection.Up: return new Vector(0, distance);
|
||||
default: return Vector.Zero;
|
||||
}
|
||||
|
||||
return offsets.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
OpenNest.Engine/BestFit/SlideOffset.cs
Normal file
18
OpenNest.Engine/BestFit/SlideOffset.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public readonly struct SlideOffset
|
||||
{
|
||||
public double Dx { get; }
|
||||
public double Dy { get; }
|
||||
public double DirX { get; }
|
||||
public double DirY { get; }
|
||||
|
||||
public SlideOffset(double dx, double dy, double dirX, double dirY)
|
||||
{
|
||||
Dx = dx;
|
||||
Dy = dy;
|
||||
DirX = dirX;
|
||||
DirY = dirY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.BestFit.Tiling
|
||||
{
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// NFP-based Bottom-Left Fill (BLF) placement engine.
|
||||
/// Places parts one at a time using feasible regions computed from
|
||||
/// the Inner-Fit Polygon minus the union of No-Fit Polygons.
|
||||
/// </summary>
|
||||
public class BottomLeftFill
|
||||
{
|
||||
private readonly Box workArea;
|
||||
private readonly NfpCache nfpCache;
|
||||
|
||||
public BottomLeftFill(Box workArea, NfpCache nfpCache)
|
||||
{
|
||||
this.workArea = workArea;
|
||||
this.nfpCache = nfpCache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Places parts according to the given sequence using NFP-based BLF.
|
||||
/// Each entry is (drawingId, rotation) determining what to place and how.
|
||||
/// Returns the list of successfully placed parts with their positions.
|
||||
/// </summary>
|
||||
public List<PlacedPart> Fill(List<(int drawingId, double rotation, Drawing drawing)> sequence)
|
||||
{
|
||||
var placedParts = new List<PlacedPart>();
|
||||
|
||||
foreach (var (drawingId, rotation, drawing) in sequence)
|
||||
{
|
||||
var polygon = nfpCache.GetPolygon(drawingId, rotation);
|
||||
|
||||
if (polygon == null || polygon.Vertices.Count < 3)
|
||||
continue;
|
||||
|
||||
// Compute IFP for this part inside the work area.
|
||||
var ifp = InnerFitPolygon.Compute(workArea, polygon);
|
||||
|
||||
if (ifp.Vertices.Count < 3)
|
||||
continue;
|
||||
|
||||
// Compute NFPs against all already-placed parts.
|
||||
var nfps = new Polygon[placedParts.Count];
|
||||
|
||||
for (var i = 0; i < placedParts.Count; i++)
|
||||
{
|
||||
var placed = placedParts[i];
|
||||
var nfp = nfpCache.Get(placed.DrawingId, placed.Rotation, drawingId, rotation);
|
||||
|
||||
// Translate NFP to the placed part's position.
|
||||
var translated = TranslatePolygon(nfp, placed.Position);
|
||||
nfps[i] = translated;
|
||||
}
|
||||
|
||||
// Compute feasible region and find bottom-left point.
|
||||
var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfps);
|
||||
var point = InnerFitPolygon.FindBottomLeftPoint(feasible);
|
||||
|
||||
if (double.IsNaN(point.X))
|
||||
continue;
|
||||
|
||||
placedParts.Add(new PlacedPart
|
||||
{
|
||||
DrawingId = drawingId,
|
||||
Rotation = rotation,
|
||||
Position = point,
|
||||
Drawing = drawing
|
||||
});
|
||||
}
|
||||
|
||||
return placedParts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts placed parts to OpenNest Part instances positioned on the plate.
|
||||
/// </summary>
|
||||
public static List<Part> ToNestParts(List<PlacedPart> placedParts)
|
||||
{
|
||||
var parts = new List<Part>(placedParts.Count);
|
||||
|
||||
foreach (var placed in placedParts)
|
||||
{
|
||||
var part = new Part(placed.Drawing);
|
||||
|
||||
if (placed.Rotation != 0)
|
||||
part.Rotate(placed.Rotation);
|
||||
|
||||
part.Location = placed.Position;
|
||||
parts.Add(part);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a translated copy of a polygon.
|
||||
/// </summary>
|
||||
private static Polygon TranslatePolygon(Polygon polygon, Vector offset)
|
||||
{
|
||||
var result = new Polygon();
|
||||
|
||||
foreach (var v in polygon.Vertices)
|
||||
result.Vertices.Add(new Vector(v.X + offset.X, v.Y + offset.Y));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a part that has been placed by the BLF algorithm.
|
||||
/// </summary>
|
||||
public class PlacedPart
|
||||
{
|
||||
public int DrawingId { get; set; }
|
||||
public double Rotation { get; set; }
|
||||
public Vector Position { get; set; }
|
||||
public Drawing Drawing { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
|
||||
namespace OpenNest.CirclePacking
|
||||
{
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes a group of parts left and down to close gaps after placement.
|
||||
/// Uses the same directional-distance logic as PlateView.PushSelected
|
||||
/// but operates on Part objects directly.
|
||||
/// </summary>
|
||||
public static class Compactor
|
||||
{
|
||||
private const double ChordTolerance = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// Compacts movingParts toward the bottom-left of the plate work area.
|
||||
/// Everything already on the plate (excluding movingParts) is treated
|
||||
/// as stationary obstacles.
|
||||
/// </summary>
|
||||
private const double RepeatThreshold = 0.01;
|
||||
private const int MaxIterations = 20;
|
||||
|
||||
public static void Compact(List<Part> movingParts, Plate plate)
|
||||
{
|
||||
if (movingParts == null || movingParts.Count == 0)
|
||||
return;
|
||||
|
||||
var savedPositions = SavePositions(movingParts);
|
||||
|
||||
// Try left-first.
|
||||
var leftFirst = CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
|
||||
|
||||
// Restore and try down-first.
|
||||
RestorePositions(movingParts, savedPositions);
|
||||
var downFirst = CompactLoop(movingParts, plate, PushDirection.Down, PushDirection.Left);
|
||||
|
||||
// Keep left-first if it traveled further.
|
||||
if (leftFirst > downFirst)
|
||||
{
|
||||
RestorePositions(movingParts, savedPositions);
|
||||
CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down);
|
||||
}
|
||||
}
|
||||
|
||||
private static double CompactLoop(List<Part> parts, Plate plate,
|
||||
PushDirection first, PushDirection second)
|
||||
{
|
||||
var total = 0.0;
|
||||
|
||||
for (var i = 0; i < MaxIterations; i++)
|
||||
{
|
||||
var a = Push(parts, plate, first);
|
||||
var b = Push(parts, plate, second);
|
||||
total += a + b;
|
||||
|
||||
if (a <= RepeatThreshold && b <= RepeatThreshold)
|
||||
break;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static Vector[] SavePositions(List<Part> parts)
|
||||
{
|
||||
var positions = new Vector[parts.Count];
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
positions[i] = parts[i].Location;
|
||||
return positions;
|
||||
}
|
||||
|
||||
private static void RestorePositions(List<Part> parts, Vector[] positions)
|
||||
{
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
parts[i].Location = positions[i];
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
var obstacleLines = new List<Line>[obstacleParts.Count];
|
||||
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var halfSpacing = partSpacing / 2;
|
||||
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||
var distance = double.MaxValue;
|
||||
|
||||
// BB gap at which offset geometries are expected to be touching.
|
||||
var contactGap = (halfSpacing + ChordTolerance) * 2;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
List<Line> movingLines = null;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
// Use the reverse-direction gap to check if the obstacle is entirely
|
||||
// behind the moving part. The forward gap (gap < 0) is unreliable for
|
||||
// irregular shapes whose bounding boxes overlap even when the actual
|
||||
// geometry still has a valid contact in the push direction.
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
var perpOverlap = isHorizontal
|
||||
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
||||
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
||||
|
||||
if (!perpOverlap)
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
||||
|
||||
obstacleLines[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = SpatialQuery.DirectionToOffset(direction, distance);
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compacts parts individually toward the bottom-left of the work area.
|
||||
/// Each part is pushed against all others as obstacles, closing geometry-based gaps.
|
||||
/// Does not require parts to be on a plate.
|
||||
/// </summary>
|
||||
public static void CompactIndividual(List<Part> parts, Box workArea, double partSpacing)
|
||||
{
|
||||
if (parts == null || parts.Count < 2)
|
||||
return;
|
||||
|
||||
var savedPositions = SavePositions(parts);
|
||||
|
||||
var leftFirst = CompactIndividualLoop(parts, workArea, partSpacing,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
|
||||
RestorePositions(parts, savedPositions);
|
||||
var downFirst = CompactIndividualLoop(parts, workArea, partSpacing,
|
||||
PushDirection.Down, PushDirection.Left);
|
||||
|
||||
if (leftFirst > downFirst)
|
||||
{
|
||||
RestorePositions(parts, savedPositions);
|
||||
CompactIndividualLoop(parts, workArea, partSpacing,
|
||||
PushDirection.Left, PushDirection.Down);
|
||||
}
|
||||
}
|
||||
|
||||
private static double CompactIndividualLoop(List<Part> parts, Box workArea,
|
||||
double partSpacing, PushDirection first, PushDirection second)
|
||||
{
|
||||
var total = 0.0;
|
||||
|
||||
for (var pass = 0; pass < MaxIterations; pass++)
|
||||
{
|
||||
var moved = 0.0;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var single = new List<Part>(1) { part };
|
||||
var obstacles = new List<Part>(parts.Count - 1);
|
||||
foreach (var p in parts)
|
||||
if (p != part) obstacles.Add(p);
|
||||
|
||||
moved += Push(single, obstacles, workArea, partSpacing, first);
|
||||
moved += Push(single, obstacles, workArea, partSpacing, second);
|
||||
}
|
||||
|
||||
total += moved;
|
||||
if (moved <= RepeatThreshold)
|
||||
break;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.RectanglePacking;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
@@ -27,6 +26,16 @@ namespace OpenNest
|
||||
set => angleBuilder.ForceFullSweep = value;
|
||||
}
|
||||
|
||||
public override List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
return angleBuilder.Build(item, bestRotation, workArea);
|
||||
}
|
||||
|
||||
protected override void RecordProductiveAngles(List<AngleResult> angleResults)
|
||||
{
|
||||
angleBuilder.RecordProductive(angleResults);
|
||||
}
|
||||
|
||||
// --- Public Fill API ---
|
||||
|
||||
public override List<Part> Fill(NestItem item, Box workArea,
|
||||
@@ -34,147 +43,73 @@ namespace OpenNest
|
||||
{
|
||||
PhaseResults.Clear();
|
||||
AngleResults.Clear();
|
||||
var best = FindBestFill(item, workArea, progress, token);
|
||||
|
||||
if (best == null || best.Count == 0)
|
||||
return new List<Part>();
|
||||
var context = new FillContext
|
||||
{
|
||||
Item = item,
|
||||
WorkArea = workArea,
|
||||
Plate = Plate,
|
||||
PlateNumber = PlateNumber,
|
||||
Token = token,
|
||||
Progress = progress,
|
||||
Policy = BuildPolicy(),
|
||||
};
|
||||
|
||||
RunPipeline(context);
|
||||
|
||||
// PhaseResults already synced during RunPipeline.
|
||||
AngleResults.AddRange(context.AngleResults);
|
||||
WinnerPhase = context.WinnerPhase;
|
||||
|
||||
var best = context.CurrentBest ?? new List<Part>();
|
||||
|
||||
if (item.Quantity > 0 && best.Count > item.Quantity)
|
||||
best = best.Take(item.Quantity).ToList();
|
||||
best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width);
|
||||
|
||||
ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
Phase = WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = best,
|
||||
WorkArea = workArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
});
|
||||
|
||||
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.Length, Plate.Size.Width, 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>();
|
||||
|
||||
// Single part: delegate to the strategy pipeline.
|
||||
if (groupParts.Count == 1)
|
||||
{
|
||||
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
|
||||
return Fill(nestItem, workArea, progress, token);
|
||||
}
|
||||
|
||||
// Multi-part group: linear pattern fill only.
|
||||
PhaseResults.Clear();
|
||||
var engine = new FillLinear(workArea, Plate.PartSpacing);
|
||||
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
|
||||
var best = FillPattern(engine, groupParts, angles, workArea);
|
||||
var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea, Comparer);
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0));
|
||||
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
|
||||
|
||||
ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
|
||||
if (groupParts.Count == 1)
|
||||
ReportProgress(progress, new ProgressReport
|
||||
{
|
||||
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 pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing);
|
||||
var pairResult = pairFiller.Fill(nestItem, workArea, PlateNumber, token, progress);
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Pairs, pairResult.Count, 0));
|
||||
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
|
||||
|
||||
if (IsBetterFill(pairResult, best, workArea))
|
||||
{
|
||||
best = pairResult;
|
||||
ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var extentsFiller = new FillExtents(workArea, Plate.PartSpacing);
|
||||
var extentsAngles2 = new[] { groupParts[0].Rotation, groupParts[0].Rotation + Angle.HalfPI };
|
||||
List<Part> bestExtents2 = null;
|
||||
|
||||
foreach (var angle in extentsAngles2)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var result = extentsFiller.Fill(groupParts[0].BaseDrawing, angle, PlateNumber, token, progress);
|
||||
if (result != null && result.Count > (bestExtents2?.Count ?? 0))
|
||||
bestExtents2 = result;
|
||||
}
|
||||
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents2?.Count ?? 0, 0));
|
||||
Debug.WriteLine($"[Fill(groupParts,Box)] Extents: {bestExtents2?.Count ?? 0} parts");
|
||||
|
||||
if (IsBetterFill(bestExtents2, best, workArea))
|
||||
{
|
||||
best = bestExtents2;
|
||||
ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("[Fill(groupParts,Box)] Cancelled, returning current best");
|
||||
}
|
||||
}
|
||||
Phase = NestPhase.Linear,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = best,
|
||||
WorkArea = workArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
});
|
||||
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
@@ -193,204 +128,61 @@ namespace OpenNest
|
||||
return BinConverter.ToParts(bin, items);
|
||||
}
|
||||
|
||||
// --- FindBestFill: core orchestration ---
|
||||
// --- RunPipeline: strategy-based orchestration ---
|
||||
|
||||
private List<Part> FindBestFill(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress = null, CancellationToken token = default)
|
||||
protected virtual void RunPipeline(FillContext context)
|
||||
{
|
||||
List<Part> best = null;
|
||||
var bestRotation = RotationAnalysis.FindBestRotation(context.Item);
|
||||
context.SharedState["BestRotation"] = bestRotation;
|
||||
|
||||
var angles = BuildAngles(context.Item, bestRotation, context.WorkArea);
|
||||
context.SharedState["AngleCandidates"] = angles;
|
||||
|
||||
try
|
||||
{
|
||||
var bestRotation = RotationAnalysis.FindBestRotation(item);
|
||||
var angles = angleBuilder.Build(item, bestRotation, workArea);
|
||||
|
||||
// Pairs phase
|
||||
var pairSw = Stopwatch.StartNew();
|
||||
var pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing);
|
||||
var pairResult = pairFiller.Fill(item, workArea, PlateNumber, 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++)
|
||||
foreach (var strategy in FillStrategyRegistry.Strategies)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
context.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 sw = Stopwatch.StartNew();
|
||||
var result = strategy.Fill(context);
|
||||
sw.Stop();
|
||||
|
||||
var angleDeg = Angle.ToDegrees(angle);
|
||||
if (h != null && h.Count > 0)
|
||||
var phaseResult = new PhaseResult(
|
||||
strategy.Phase, result?.Count ?? 0, sw.ElapsedMilliseconds);
|
||||
context.PhaseResults.Add(phaseResult);
|
||||
|
||||
// Keep engine's PhaseResults in sync so BuildProgressSummary() works
|
||||
// during progress reporting.
|
||||
PhaseResults.Add(phaseResult);
|
||||
|
||||
if (context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea))
|
||||
{
|
||||
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;
|
||||
}
|
||||
context.CurrentBest = result;
|
||||
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
|
||||
context.WinnerPhase = strategy.Phase;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
angleBuilder.RecordProductive(AngleResults);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// Extents phase
|
||||
token.ThrowIfCancellationRequested();
|
||||
var extentsSw = Stopwatch.StartNew();
|
||||
var extentsFiller = new FillExtents(workArea, Plate.PartSpacing);
|
||||
List<Part> bestExtents = null;
|
||||
var extentsAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
foreach (var angle in extentsAngles)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var extentsResult = extentsFiller.Fill(item.Drawing, angle, PlateNumber, token, progress);
|
||||
if (bestExtents == null || (extentsResult != null && extentsResult.Count > (bestExtents?.Count ?? 0)))
|
||||
bestExtents = extentsResult;
|
||||
}
|
||||
|
||||
extentsSw.Stop();
|
||||
var extentsScore = bestExtents != null ? FillScore.Compute(bestExtents, workArea) : default;
|
||||
Debug.WriteLine($"[FindBestFill] Extents: {extentsScore.Count} parts");
|
||||
PhaseResults.Add(new PhaseResult(NestPhase.Extents, bestExtents?.Count ?? 0, extentsSw.ElapsedMilliseconds));
|
||||
|
||||
var bestScore2 = FillScore.Compute(best, workArea);
|
||||
if (extentsScore > bestScore2)
|
||||
{
|
||||
best = bestExtents;
|
||||
WinnerPhase = NestPhase.Extents;
|
||||
ReportProgress(progress, NestPhase.Extents, PlateNumber, best, workArea, BuildProgressSummary());
|
||||
if (context.CurrentBest != null && context.CurrentBest.Count > 0)
|
||||
{
|
||||
ReportProgress(context.Progress, new ProgressReport
|
||||
{
|
||||
Phase = context.WinnerPhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = context.CurrentBest,
|
||||
WorkArea = context.WorkArea,
|
||||
Description = BuildProgressSummary(),
|
||||
IsOverallBest = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("[FindBestFill] Cancelled, returning current best");
|
||||
Debug.WriteLine("[RunPipeline] Cancelled, returning current best");
|
||||
}
|
||||
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
// --- 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 });
|
||||
}
|
||||
|
||||
// --- Pattern helpers ---
|
||||
|
||||
internal static 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;
|
||||
}
|
||||
|
||||
internal static 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;
|
||||
RecordProductiveAngles(context.AngleResults);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps an IProgress to prepend previously placed parts to each report,
|
||||
@@ -26,7 +26,6 @@ namespace OpenNest
|
||||
combined.AddRange(previousParts);
|
||||
combined.AddRange(value.BestParts);
|
||||
value.BestParts = combined;
|
||||
value.BestPartCount = combined.Count;
|
||||
}
|
||||
|
||||
inner.Report(value);
|
||||
102
OpenNest.Engine/Fill/AngleCandidateBuilder.cs
Normal file
102
OpenNest.Engine/Fill/AngleCandidateBuilder.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using OpenNest.Engine.ML;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds candidate rotation angles for single-item fill. Encapsulates the
|
||||
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
|
||||
/// known-good pruning across fills.
|
||||
/// </summary>
|
||||
public class AngleCandidateBuilder
|
||||
{
|
||||
private readonly HashSet<double> knownGoodAngles = new();
|
||||
|
||||
public bool ForceFullSweep { get; set; }
|
||||
|
||||
public List<double> Build(NestItem item, double bestRotation, Box workArea)
|
||||
{
|
||||
var baseAngles = new[] { bestRotation, bestRotation + Angle.HalfPI };
|
||||
|
||||
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
|
||||
return BuildPrunedList(baseAngles);
|
||||
|
||||
var angles = new List<double>(baseAngles);
|
||||
|
||||
if (ForceFullSweep)
|
||||
AddSweepAngles(angles);
|
||||
|
||||
if (!ForceFullSweep && angles.Count > 2)
|
||||
angles = ApplyMlPrediction(item, workArea, baseAngles, angles);
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
private static void AddSweepAngles(List<double> angles)
|
||||
{
|
||||
var step = Angle.ToRadians(5);
|
||||
for (var a = 0.0; a < System.Math.PI; a += step)
|
||||
{
|
||||
if (!ContainsAngle(angles, a))
|
||||
angles.Add(a);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<double> ApplyMlPrediction(
|
||||
NestItem item, Box workArea, double[] baseAngles, List<double> fallback)
|
||||
{
|
||||
var features = FeatureExtractor.Extract(item.Drawing);
|
||||
if (features == null)
|
||||
return fallback;
|
||||
|
||||
var predicted = AnglePredictor.PredictAngles(features, workArea.Width, workArea.Length);
|
||||
if (predicted == null)
|
||||
return fallback;
|
||||
|
||||
var mlAngles = new List<double>(predicted);
|
||||
foreach (var b in baseAngles)
|
||||
{
|
||||
if (!ContainsAngle(mlAngles, b))
|
||||
mlAngles.Add(b);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} angles -> {mlAngles.Count} predicted");
|
||||
return mlAngles;
|
||||
}
|
||||
|
||||
private List<double> BuildPrunedList(double[] baseAngles)
|
||||
{
|
||||
var pruned = new List<double>(baseAngles);
|
||||
foreach (var a in knownGoodAngles)
|
||||
{
|
||||
if (!ContainsAngle(pruned, a))
|
||||
pruned.Add(a);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)");
|
||||
return pruned;
|
||||
}
|
||||
|
||||
private static bool ContainsAngle(List<double> angles, double angle)
|
||||
{
|
||||
return angles.Any(existing => existing.IsEqualTo(angle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records angles that produced results. These are used to prune
|
||||
/// subsequent Build() calls.
|
||||
/// </summary>
|
||||
public void RecordProductive(List<AngleResult> angleResults)
|
||||
{
|
||||
foreach (var ar in angleResults)
|
||||
{
|
||||
if (ar.PartCount > 0)
|
||||
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
OpenNest.Engine/Fill/BestCombination.cs
Normal file
36
OpenNest.Engine/Fill/BestCombination.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
internal static class BestCombination
|
||||
{
|
||||
public static bool FindFrom2(double length1, double length2, double overallLength, out int count1, out int count2)
|
||||
{
|
||||
overallLength += Tolerance.Epsilon;
|
||||
count1 = 0;
|
||||
count2 = 0;
|
||||
|
||||
var maxCount1 = (int)System.Math.Floor(overallLength / length1);
|
||||
var bestRemnant = overallLength + 1;
|
||||
|
||||
for (var c1 = 0; c1 <= maxCount1; c1++)
|
||||
{
|
||||
var remaining = overallLength - c1 * length1;
|
||||
var c2 = (int)System.Math.Floor(remaining / length2);
|
||||
var remnant = remaining - c2 * length2;
|
||||
|
||||
if (!(remnant < bestRemnant))
|
||||
continue;
|
||||
|
||||
count1 = c1;
|
||||
count2 = c2;
|
||||
bestRemnant = remnant;
|
||||
|
||||
if (remnant.IsEqualTo(0))
|
||||
break;
|
||||
}
|
||||
|
||||
return count1 > 0 || count2 > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
201
OpenNest.Engine/Fill/Compactor.cs
Normal file
201
OpenNest.Engine/Fill/Compactor.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes a group of parts left and down to close gaps after placement.
|
||||
/// Uses the same directional-distance logic as PlateView.PushSelected
|
||||
/// but operates on Part objects directly.
|
||||
/// </summary>
|
||||
public static class Compactor
|
||||
{
|
||||
private const double ChordTolerance = 0.001;
|
||||
|
||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||
/// </summary>
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, Vector direction)
|
||||
{
|
||||
var opposite = -direction;
|
||||
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
var obstacleLines = new List<Line>[obstacleParts.Count];
|
||||
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var halfSpacing = partSpacing / 2;
|
||||
var distance = double.MaxValue;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
List<Line> movingLines = null;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
if (gap >= distance)
|
||||
continue;
|
||||
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(moving, direction, ChordTolerance);
|
||||
|
||||
obstacleLines[i] ??= halfSpacing > 0
|
||||
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance);
|
||||
|
||||
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = direction * distance;
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static double Push(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
{
|
||||
var vector = SpatialQuery.DirectionToOffset(direction, 1.0);
|
||||
return Push(movingParts, obstacleParts, workArea, partSpacing, vector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes movingParts using bounding-box distances only (no geometry lines).
|
||||
/// Much faster but less precise — use as a coarse positioning pass before
|
||||
/// a full geometry Push.
|
||||
/// </summary>
|
||||
public static double PushBoundingBox(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var obstacleParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
||||
}
|
||||
|
||||
public static double PushBoundingBox(List<Part> movingParts, List<Part> obstacleParts,
|
||||
Box workArea, double partSpacing, PushDirection direction)
|
||||
{
|
||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
||||
for (var i = 0; i < obstacleParts.Count; i++)
|
||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
||||
|
||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||
var distance = double.MaxValue;
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction);
|
||||
if (edgeDist <= 0)
|
||||
distance = 0;
|
||||
else if (edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
var movingBox = moving.BoundingBox;
|
||||
|
||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||
{
|
||||
var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite);
|
||||
if (reverseGap > 0)
|
||||
continue;
|
||||
|
||||
var perpOverlap = isHorizontal
|
||||
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
||||
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
||||
|
||||
if (!perpOverlap)
|
||||
continue;
|
||||
|
||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||
var d = gap - partSpacing;
|
||||
if (d < 0) d = 0;
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = SpatialQuery.DirectionToOffset(direction, distance);
|
||||
foreach (var moving in movingParts)
|
||||
moving.Offset(offset);
|
||||
return distance;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repeatedly pushes parts left then down until total movement per
|
||||
/// iteration falls below the given threshold.
|
||||
/// </summary>
|
||||
public static void Settle(List<Part> parts, Box workArea, double partSpacing,
|
||||
double threshold = 0.01, int maxIterations = 20)
|
||||
{
|
||||
if (parts.Count < 2)
|
||||
return;
|
||||
|
||||
var noObstacles = new List<Part>();
|
||||
|
||||
for (var i = 0; i < maxIterations; i++)
|
||||
{
|
||||
var moved = 0.0;
|
||||
moved += Push(parts, noObstacles, workArea, partSpacing, PushDirection.Left);
|
||||
moved += Push(parts, noObstacles, workArea, partSpacing, PushDirection.Down);
|
||||
|
||||
if (moved < threshold)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
OpenNest.Engine/Fill/DefaultFillComparer.cs
Normal file
23
OpenNest.Engine/Fill/DefaultFillComparer.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Ranks fill results by count first, then density.
|
||||
/// This is the original scoring logic used by DefaultNestEngine.
|
||||
/// </summary>
|
||||
public class DefaultFillComparer : IFillComparer
|
||||
{
|
||||
public bool IsBetter(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user