Compare commits
102
Commits
a68e252ac7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695ccc0a3b | ||
|
|
98d0124172 | ||
|
|
4161e6d1c7 | ||
|
|
896ed2026a | ||
|
|
7f63c725e6 | ||
|
|
ec5f57171f | ||
|
|
dd1a958f5c | ||
|
|
ae2b0beb45 | ||
|
|
6ab45e6de7 | ||
|
|
029299ccf8 | ||
|
|
4cdb39870b | ||
|
|
95833236cf | ||
|
|
1b5e1b14a6 | ||
|
|
e3d10e95ae | ||
|
|
22d21924b8 | ||
|
|
87f7ddad1a | ||
|
|
23fcec4b6c | ||
|
|
d6d819bfed | ||
|
|
c45909e397 | ||
|
|
1c363504f5 | ||
|
|
dc9e83ef17 | ||
|
|
1a7ed3c759 | ||
|
|
b122298f57 | ||
|
|
2ecd94c705 | ||
|
|
26c17a386b | ||
|
|
4dd00c14c4 | ||
|
|
5064340eb3 | ||
|
|
062c7fa08f | ||
|
|
9af97c70b0 | ||
|
|
01789c5929 | ||
|
|
dceb5f7d18 | ||
|
|
10fe00d8ab | ||
|
|
9b9386b029 | ||
|
|
12f97474b7 | ||
|
|
a6bc9d8be6 | ||
|
|
7964c87eb9 | ||
|
|
45a59c6b02 | ||
|
|
1c8305e8a1 | ||
|
|
828500f984 | ||
|
|
8e4735d25d | ||
|
|
5bbb7b7461 | ||
|
|
57e9f625b6 | ||
|
|
e65f849c7a | ||
|
|
98c4929d14 | ||
|
|
014c071716 | ||
|
|
da1f8120f1 | ||
|
|
0512f3f84a | ||
|
|
b14cb9d10a | ||
|
|
a0417a6077 | ||
|
|
56f39556d1 | ||
|
|
86f6c9efa1 | ||
|
|
6f250b5730 | ||
|
|
c28bc0da21 | ||
|
|
88966d118c | ||
|
|
f36e124039 | ||
|
|
856dbfd8af | ||
|
|
eb8fbec1aa | ||
|
|
073ead9b79 | ||
|
|
bb671cb176 | ||
|
|
6bcea7919a | ||
|
|
ed908b9706 | ||
|
|
e69ec07830 | ||
|
|
eafa0fab01 | ||
|
|
9b9e510510 | ||
|
|
451841401a | ||
|
|
fd3375cde6 | ||
|
|
42bbde7433 | ||
|
|
64d38c452c | ||
|
|
630d514b0e | ||
|
|
a764a70e52 | ||
|
|
2a855139d2 | ||
|
|
f23f0fa566 | ||
|
|
6f38c11383 | ||
|
|
f34b3c4449 | ||
|
|
365825bc5a | ||
|
|
02fc0ea3db | ||
|
|
451876c02f | ||
|
|
5061b41a5d | ||
|
|
e7cbd99db6 | ||
|
|
27684c3782 | ||
|
|
54694f9b17 | ||
|
|
de1248589a | ||
|
|
a9ebd8bb55 | ||
|
|
aec0523062 | ||
|
|
8e6fa677fb | ||
|
|
589d341455 | ||
|
|
1a05391d94 | ||
|
|
f5d27652f4 | ||
|
|
ea4bd836cd | ||
|
|
9b69c67572 | ||
|
|
aa88eee484 | ||
|
|
e0e3b96bed | ||
|
|
424ff15ebc | ||
|
|
9888fe6083 | ||
|
|
a2dcfc7484 | ||
|
|
ae704478af | ||
|
|
ecca71e185 | ||
|
|
f0fe79f0f1 | ||
|
|
a9e0f8a1d4 | ||
|
|
20da5477b6 | ||
|
|
6a0fba0fec | ||
|
|
a085339ba9 |
@@ -0,0 +1,68 @@
|
|||||||
|
# Unified code style for OpenNest.
|
||||||
|
# Canonical formatter: dotnet format.
|
||||||
|
# Format only the files or directories being changed:
|
||||||
|
# dotnet format OpenNest.sln --include path/to/changed-file.cs
|
||||||
|
# dotnet format OpenNest.sln --include path/to/changed-file.cs --verify-no-changes
|
||||||
|
# These settings are used by dotnet format and IDE auto-formatting
|
||||||
|
# (VS / Rider / VS Code).
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{csproj,props,targets,xml,config,manifest}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{json,yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{cs,csx}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
# dotnet format cannot re-wrap source to this limit, but IDEs can surface it
|
||||||
|
# as a visual guide and analyzers can flag hard violations.
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# --- Using directives (System first, outside the namespace) ---
|
||||||
|
dotnet_sort_system_directives_first = true
|
||||||
|
csharp_using_directive_placement = outside_namespace:warning
|
||||||
|
|
||||||
|
# --- Brace placement: Allman (opening brace on its own line) ---
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
|
# --- Spacing ---
|
||||||
|
csharp_space_after_keywords_in_control_flow_statements = true
|
||||||
|
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_parentheses = false
|
||||||
|
csharp_space_before_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_after_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_around_binary_operators = before_and_after
|
||||||
|
csharp_space_after_cast = false
|
||||||
|
csharp_space_after_comma = true
|
||||||
|
csharp_space_before_comma = false
|
||||||
|
|
||||||
|
# --- Code style preferences ---
|
||||||
|
# Project rule: always use var for locals (see CLAUDE.md).
|
||||||
|
csharp_style_var_for_built_in_types = true:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||||
|
csharp_style_var_elsewhere = true:suggestion
|
||||||
|
csharp_prefer_braces = true:suggestion
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
csharp_style_namespace_declarations = file_scoped:silent
|
||||||
|
dotnet_style_prefer_auto_properties = true:suggestion
|
||||||
|
dotnet_style_object_initializer = true:suggestion
|
||||||
|
dotnet_style_collection_initializer = true:suggestion
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Commits whose changes git blame should skip (whitespace-only sweeps).
|
||||||
|
# Enable locally: git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
aec052306234e3c4313c0ee8905e2557d3c3671b
|
||||||
@@ -8,26 +8,30 @@ OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-windows`. Build with:
|
This is a .NET 8 solution using SDK-style `.csproj` files. The desktop app and Windows-dependent projects target `net8.0-windows`; the core libraries and `OpenNest.Console` target `net8.0`. Build the full solution on Windows with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet build OpenNest.sln
|
dotnet build OpenNest.sln
|
||||||
```
|
```
|
||||||
|
|
||||||
Cross-platform whole-job engine tests (net8.0, runs on Linux/macOS/Windows without the desktop project or DXF fixtures): `dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj`. The existing `OpenNest.Tests` suite targets `net8.0-windows` and requires a Windows runner; cross-compiling on Linux is not Windows runtime verification.
|
Cross-platform whole-job engine tests (net8.0, runs on Linux/macOS/Windows without the desktop project or DXF fixtures): `dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj`. The main `OpenNest.Tests` suite also targets `net8.0`: run `dotnet test OpenNest.Tests/OpenNest.Tests.csproj` independently on Linux/macOS/Windows. It must not reference the WinForms `OpenNest` project. The API, Data, Cincinnati, and GravographIS libraries target `net8.0`; post-processor build deployment still targets the desktop app's `net8.0-windows/Posts` directory. Optional CHR-font fixtures are configured through `OpenNest.Tests/test-config.json` and skip when absent.
|
||||||
|
|
||||||
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp), `Microsoft.ML.OnnxRuntime` (in OpenNest.Engine for ML angle prediction), `Microsoft.EntityFrameworkCore.Sqlite` (in OpenNest.Training).
|
`OpenNest.WinForms.Tests` contains the desktop-assembly-dependent `CadBendNoteTests` (`CadText`) and `CuttingParametersSerializerTests` (`CuttingParametersSerializer`). It targets `net8.0-windows`, references `OpenNest`, and requires a Windows runner: `dotnet test OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj`. Keep future desktop-dependent tests here rather than in `OpenNest.Tests`. Linux cross-compilation uses `dotnet build OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj -p:EnableWindowsTargeting=true`; cross-compilation is not Windows runtime verification.
|
||||||
|
|
||||||
|
Cross-platform CAD import tests: `dotnet test OpenNest.IO.Tests/OpenNest.IO.Tests.csproj`. These synthetic-DXF and bend-repair tests target `net8.0`, require no external fixtures, and are included in the solution. Build the headless console independently with `dotnet build OpenNest.Console/OpenNest.Console.csproj`.
|
||||||
|
|
||||||
|
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `Clipper2` 2.0.0 (region offsetting, in OpenNest.Core), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp), `Microsoft.ML.OnnxRuntime` (in OpenNest.Engine for ML angle prediction), `Microsoft.EntityFrameworkCore.Sqlite` (in OpenNest.Training).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Eight projects form a layered architecture:
|
Nine projects form a layered architecture:
|
||||||
|
|
||||||
### OpenNest.Core (class library)
|
### OpenNest.Core (class library)
|
||||||
Domain model, geometry, and CNC primitives organized into namespaces:
|
Domain model, geometry, and CNC primitives organized into namespaces:
|
||||||
|
|
||||||
- **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
|
- **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
|
||||||
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output.
|
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output.
|
||||||
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
|
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon` (convex NFP only), `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, `ClipperBridge` (Clipper2 region offsetting for CPU preparation only; see Key Patterns), and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction; deliberately hand-rolled as the reference for a future GPU kernel, with the port contract in its class summary).
|
||||||
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
||||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||||
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||||
@@ -37,19 +41,19 @@ Domain model, geometry, and CNC primitives organized into namespaces:
|
|||||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||||
|
|
||||||
### OpenNest.Engine (class library, depends on Core)
|
### OpenNest.Engine (class library, depends on Core)
|
||||||
Nesting algorithms provide both a legacy single-plate API and a whole-job API. The legacy path centers on `NestEngineBase`, `DefaultNestEngine` (formerly `NestEngine`), and the global `NestEngineRegistry`. New job callers use immutable, ID-based contracts in `Jobs/`: `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named strategy without reading or changing the process-global registry.
|
Nesting algorithms use the jobs-only API. `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named built-in placement strategy.
|
||||||
|
|
||||||
- **Whole-job API (`Jobs/`)**: `NestJob` owns part requirements, physical stock, and options for one material/thickness/unit system. `PartGeometrySnapshot` contains owned flat rapid/line/arc geometry; results contain stock IDs and placement poses (radians), not mutable desktop models. `NestJobPlacementValidator` validates contours, rotation, usable work area, overlap, and spacing before accounting commits. The runner selects valid trial candidates greedily by priority vector, sheet area, envelope, and input order; an incomplete result reports why but does not prove geometric impossibility. `DrawingJobMapper` and `NestResultMaterializer` are the domain-boundary adapters.
|
- **Whole-job API (`Jobs/`)**: `NestJob` owns part requirements, physical stock, and options for one material/thickness/unit system. `PartGeometrySnapshot` contains owned flat rapid/line/arc geometry; results contain stock IDs and placement poses (radians), not mutable desktop models. `NestJobPlacementValidator` validates contours, rotation, usable work area, overlap, and spacing before accounting commits. The runner selects valid trial candidates greedily by priority vector, sheet area, envelope, and input order; an incomplete result reports why but does not prove geometric impossibility. `DrawingJobMapper` and `NestResultMaterializer` are the domain-boundary adapters.
|
||||||
- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester` and `StripPlateNester` are migrated built-ins with run-scoped private geometry; `LegacyPlateNesterAdapter` remains for remnant strategies and legacy plugins/callers during rollout. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` and NFP/`AutoNester` retain legacy name-based helpers and are deliberately outside the runner path.
|
- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester`, `StripPlateNester`, and `RemnantPlateNester` are built-ins with run-scoped private geometry. `PlateFillService` is the public single-plate proposal service for interactive fill/group/pack flows; it returns parts without mutating caller-owned plates. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains name-based helpers and remains outside the runner path.
|
||||||
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. Existing desktop, CLI, and MCP callers remain on this compatibility path until separate migrations preserve their existing-plate, preview, and accept/cancel semantics.
|
- **Filler pipeline (`Jobs/Placement/Fillers/`)**: internal `DefaultPlateFiller`, `StripPlateFiller`, and policy-backed `RemnantPlateFiller` implement the standard single-plate geometry pipeline. `Default` runs the Linear, Pairs, RectBestFit, and Extents phases; remnant variants preserve their distinct comparer, direction, trim-axis, and angle-ordering policies.
|
||||||
- **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`.
|
- **Engine registration**: `NestingEngineRegistry` holds whole-job `INestingEngine` implementations including the four fixed strategies and `StockLadder`. It loads plug-ins that implement `INestingEngine` and have a public parameterless constructor. Plug-ins for the removed single-plate inheritance API are not binary compatible.
|
||||||
- **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.
|
- **Plugin engines**: independent `INestingEngine` plugins are class libraries that reference `OpenNest.Engine` and are built outside `OpenNest.sln`. The desktop app and `OpenNest.Benchmark` load them from an `Engines/` folder next to their build output (e.g. `OpenNest.Benchmark/bin/<Config>/net8.0/Engines/`). Do not add engine projects to this repo.
|
||||||
|
- **IFillComparer**: Interface enabling filler-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), and `HorizontalRemnantComparer` (minimize Y-extent) are grouped into `FillPolicy` on `FillContext`.
|
||||||
- **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`.
|
- **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`.
|
- **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.
|
- **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.
|
- **RectanglePacking/** (`namespace OpenNest.Engine.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.
|
- **CirclePacking/** (`namespace OpenNest.Engine.CirclePacking`): Alternative packing for circular parts.
|
||||||
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): Internal NFP-based single-part placement utilities — `AutoNester` (NFP placement with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`OptimizationResult`. Not exposed as a nest engine; used internally for individual part placement.
|
|
||||||
- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
|
- **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.
|
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
||||||
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
|
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
|
||||||
@@ -63,9 +67,10 @@ File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
|||||||
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
||||||
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
|
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
|
||||||
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
|
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
|
||||||
|
- `Bending/BendRepair` — conservative opt-in repair configured by `CadImportOptions.BendRepair`. Requires explicit inches/mm source units and an endpoint movement limit above 0.001 and at most 3.175 physical mm. Only unambiguous paired ETCH/SCRIBE ticks may move along the existing bend axis; cut geometry and unrelated marks must remain unchanged. Opt-in imports preserve source marks without blanket etch regeneration and expose per-bend outcomes in `CadImportResult.BendRepairReports`.
|
||||||
|
|
||||||
### OpenNest.Console (console app, depends on Core + Engine + IO)
|
### OpenNest.Console (console app, depends on Core + Engine + IO)
|
||||||
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`).
|
Command-line interface for batch nesting (`net8.0`). Supports DXF import, plate configuration, linear fill, and multi-drawing auto-nesting through the active engine's `Nest()` (`--autonest`). `--repair-bends-mm <limit> --cad-units inches|mm` opts newly imported DXFs into conservative bend repair and prints per-bend reports; it does not rescale coordinates or repair saved nests.
|
||||||
|
|
||||||
### OpenNest.Gpu (class library, depends on Core + Engine)
|
### OpenNest.Gpu (class library, depends on Core + Engine)
|
||||||
GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` implements `IPairEvaluator`, `GpuSlideComputer` implements `ISlideComputer`, and `PartBitmap` handles rasterization. `GpuEvaluatorFactory` provides factory methods.
|
GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` implements `IPairEvaluator`, `GpuSlideComputer` implements `ISlideComputer`, and `PartBitmap` handles rasterization. `GpuEvaluatorFactory` provides factory methods.
|
||||||
@@ -73,6 +78,18 @@ GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` impleme
|
|||||||
### OpenNest.Training (console app, depends on Core + Engine)
|
### OpenNest.Training (console app, depends on Core + Engine)
|
||||||
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
|
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
|
||||||
|
|
||||||
|
### OpenNest.Benchmark (console app, depends on Core + Engine + IO)
|
||||||
|
Compares registered `INestingEngine` implementations against each other on real `.nest` files. Each engine solves the whole job — it owns its own multi-plate/size strategy rather than being handed one already-sized plate at a time. Fully generic — it never hardcodes drawing geometry, just reads whatever drawings/quantities/plate settings each input file already has.
|
||||||
|
|
||||||
|
- `JobLoader` builds `BenchmarkJob`s from a `.nest` file or a folder of them via `NestReader`, using every drawing with `Quantity.Required > 0`. `--sheet-sizes` can sweep a fixed list of plate sizes instead of each file's own.
|
||||||
|
- `DxfManifestLoader` builds a `BenchmarkJob` from a JSON manifest (`sheetSizes`, `spacing`, `edgeSpacing`, `quadrant`, `parts[] { dxf, quantity, allowRotation }`) instead of a `.nest`, importing each DXF with `CadImporter.ImportDrawing`. DXF paths resolve relative to the manifest; sheet sizes are required (manifest or `--sheet-sizes`, which overrides). `allowRotation: false` locks rotation the same way `NestRunner` does. `JobLoader.Load` routes `*.json` inputs to it, and folder scans pick up `*.nest` plus `*.manifest.json` (plain `*.json` is ignored so `--output` reports are never read as manifests). Invalid manifests throw rather than being skipped.
|
||||||
|
- `BenchmarkJob.BuildNestJob(maxPlates)` converts the job into a `NestJob`: one `NestJobPart` per requested drawing (via `DrawingJobMapper.FromDrawing`) and one `NestPlateStock` per candidate sheet size (unlimited quantity — the engine decides how many of each size it uses).
|
||||||
|
- `BenchmarkRunner` fans the (job × engine) pairs out with `Parallel.ForEach` (`NoBuffering`, `MaxDegreeOfParallelism` from `--parallel`, CLI default 3, `Run`'s own default 1) and writes results by index so report order stays job-then-engine. Each solve builds its own `NestJob` snapshot and materialized drawings, so solves share no mutable drawing state. Concurrent solves compete for cores, so `Time(ms)` is only clean at `--parallel 1`. It calls each engine's `INestingEngine.Solve(NestJob)` once per job, under a wall-clock timeout so a runaway or hanging engine can't stall the whole benchmark run, then materializes the result back into legacy `Plate`/`Part` objects via `NestResultMaterializer` for scoring.
|
||||||
|
- `NestValidator` checks the returned layout: every part inside `Plate.WorkArea()`, every pair at least `Plate.PartSpacing` apart (checked geometrically: each part's perimeter inflated and cutouts shrunk by the spacing, tested against the other part's raw material with holes subtracted, so part-in-part inside a cutout is legal; an X-sorted bounding-box sweep prunes distant pairs), and no drawing over its requested quantity. `ValidateAgainstJob` also checks the raw `NestJobResult`: every sheet must match a stock entry the job offered (size, spacing, edge spacing, quadrant; finite quantity not overdrawn), and every placement rotation must satisfy its part's `RotationPolicy.Allows`. An invalid, throwing, or timed-out run places nothing for scoring.
|
||||||
|
- Ranking (`Report.Compare`): valid > invalid, fully placed > not, then lower `JobResult.Cost`, then fewer plates. Cost = salvage-credited sheet area (`StockLadderNestingEngine.EstimateNetArea` per plate, recomputed from job geometry) + `BenchmarkJob.UnplacedPartPenalty` (largest candidate sheet area) per unplaced part, so dropping hard parts never improves the score. The summary sums cost and areas across jobs (area-weighted, not a mean of per-job percentages). Without `--sheet-sizes`, `.nest` jobs only offer their original sizes, and the CLI warns that this hints engines. Numeric CLI and manifest sheet sizes parse with the invariant culture (`JobLoader.TryParseSheetSize`).
|
||||||
|
- `--engines Name1,Name2` filters to specific registered engines (default: all); `--csv <path>` writes a flat per-job CSV alongside the console report.
|
||||||
|
- `tools/PepNestExport` (outside the solution; references `PepLib.Core` from the sibling `PepApi.Core` repo) converts a PepApi year of PEP nests into `.nest` files that keep PEP's placements as the benchmark `Baseline`. PEP loop quirks: sub-loop calls continue the incremental position; lead-in/out, `DESTRUCT CUT` and non-cut moves must not reach the program as rapids (a program's bounding box counts rapid endpoints); contours may be broken by uncut micro-joint tabs (a rapid of up to 0.25 across the tab, at the seam or mid-contour, e.g. a cutout cut as two halves), which the export bridges only where the pieces chain into a closed loop; and one drawing can be placed through several loops with different origins.
|
||||||
|
|
||||||
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
||||||
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
||||||
|
|
||||||
@@ -114,13 +131,16 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
|
|||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
||||||
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
||||||
- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Nfp` (NFP-based nesting, not yet integrated), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`.
|
- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Jobs` (whole-job API, with `.Placement` and `.Adapters`), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`, `OpenNest.Engine.RectanglePacking`, `OpenNest.Engine.CirclePacking`. All Engine types live in namespaces matching their directory under `OpenNest.Engine/` (project files use `namespace X;` file-scoped or block style); consumers reference them via explicit `using OpenNest.Engine[.Sub];` directives.
|
||||||
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
|
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
|
||||||
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
|
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
|
||||||
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
|
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
|
||||||
- Nesting uses async progress/cancellation: `IProgress<NestProgress>` and `CancellationToken` flow through the engine to the UI's `NestProgressForm`.
|
- Nesting uses async progress/cancellation: `IProgress<NestProgress>` and `CancellationToken` flow through the engine to the UI's `NestProgressForm`.
|
||||||
|
- **Spacing offsets**: polygon consumers (`PolygonHelper`, `PartBoundary`, `NestValidator`, `CutOff`, the `LayoutPart` Draw Offset display) use `ClipperBridge.Offset`/`OffsetPerimeter`: one Clipper pass over the flattened region (perimeter positive, cutouts negative) with round joins at 1e-4 precision, so features narrower than twice the spacing collapse and closed-up holes disappear. `circumscribe: true` is the conservative mode (perimeter arcs circumscribed with endpoints kept on the arc, cutout arcs inscribed, inflation padded by the join chord error) and never under-estimates the spacing. `NestValidator` uses `OffsetForValidation` instead: the same flattening with fine joins and no padding, so a layout exactly at the spacing passes. `PartGeometry.GetOffsetPerimeterEntities`/`GetOffsetPartEntities` stay on the arc-preserving per-entity `Shape.OffsetOutward`/`OffsetInward` (internal) because directional-distance loops are much faster on native arcs; their chains are closed but may keep zero-area spikes inside the envelope. Clipper is allowed only for cached CPU preparation, never in per-pair hot loops.
|
||||||
|
- **Marks are not material**: scribe/etch moves are marked on the surface, never cut through, so they are left out of nesting. `SpecialLayers.IsMaterial(layer)` (excludes `Rapid` and `Scribe`) is the filter for every consumer that builds part material from a program: drawing area, canonical angle, part collision, `PartGeometry`, plate perimeters, best-fit/pair evaluation, rotation analysis, the GPU evaluators, and both validators (`NestJobPlacementValidator`, benchmark `NestValidator`). Cutting time, on-screen display, splitting, and post-processors still see marks. Older `.nest` files (e.g. `tools/PepNestExport` output) saved etch as cut moves while their source entities kept the `SCRIBE` layer; `NestReader` runs `ScribeLayerRepair` on load to move matching program moves back to `Scribe`.
|
||||||
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||||
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
||||||
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
||||||
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
||||||
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). Console, MCP, API, and Training projects use `ImportDrawing` for headless conversion. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
|
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). MCP, API, and Training projects use `ImportDrawing` for headless conversion. The console uses `Import` followed by `BuildDrawing` so it can report bend-repair outcomes. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
|
||||||
|
- **GravographIS engrave/cut passes**: The `OpenNest.Posts.GravographIS` post splits geometry by `LayerType` into ordered tool passes — engrave (`Scribe`) then cut (`Cut`/`Leadin`/`Leadout`); `Display` is skipped. `ConvertGeometry` tags DXF layers `ENGRAVE`/`ETCH` and the saved `SCRIBE` layer (lines, arcs, circles) as `Scribe`; the layer round-trips through `.nest` via `NestWriter`/`ProgramReader`. `NestPolylineExtractor.ExtractLayered` carries `LayerType` per polyline (splitting a continuous chain at any layer change); `GravographISPostProcessor.BuildPasses` groups them and `GravographISWriter.Write(IReadOnlyList<GravographPass>, …)` emits each pass at its own feed/depth, parking to origin and emitting an operator pause (motor off → aux off → `LB` console message → motor on) before any pass whose config has `PauseBefore`. Per-pass parameters live in `GravographISPostConfig` (an `IConfigurablePostProcessor` config with `Engrave`/`Cut` `LayerCutConfig` blocks), edited in the shared `PostProcessorConfigForm` PropertyGrid and persisted to JSON. The cut block pauses by default so the operator can swap/adjust the tool (the spring-floated spindle means programmed `DZ` depth is not the real cut depth).
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ namespace OpenNest.Api;
|
|||||||
public class NestRequest
|
public class NestRequest
|
||||||
{
|
{
|
||||||
public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
|
public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Explicit available physical stock. Null keeps the legacy unlimited SheetSize fallback;
|
/// Explicit available physical stock. Null keeps the legacy unlimited SheetSize fallback;
|
||||||
/// an empty list deliberately means no stock is available.
|
/// an empty list deliberately means no stock is available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<NestRequestPlate> Plates { get; init; }
|
public IReadOnlyList<NestRequestPlate> Plates { get; init; }
|
||||||
public Size SheetSize { get; init; } = new(60, 120);
|
public Size SheetSize { get; init; } = new(60, 120);
|
||||||
|
|
||||||
/// <summary>Built-in whole-job placement strategy. Explicit values take precedence over legacy Strategy.</summary>
|
/// <summary>Built-in whole-job placement strategy. Explicit values take precedence over legacy Strategy.</summary>
|
||||||
public string PlacementStrategy { get; init; } = "Default";
|
public string PlacementStrategy { get; init; } = "Default";
|
||||||
public string Material { get; init; } = "Steel, A1011 HR";
|
public string Material { get; init; } = "Steel, A1011 HR";
|
||||||
public double Thickness { get; init; } = 0.06;
|
public double Thickness { get; init; } = 0.06;
|
||||||
public double Spacing { get; init; } = 0.1;
|
public double Spacing { get; init; } = 0.1;
|
||||||
|
|
||||||
/// <summary>Legacy compatibility setting; Auto maps to the Default whole-job strategy.</summary>
|
/// <summary>Legacy compatibility setting; Auto maps to the Default whole-job strategy.</summary>
|
||||||
public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
|
public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
|
||||||
public CutParameters Cutting { get; init; } = CutParameters.Default;
|
public CutParameters Cutting { get; init; } = CutParameters.Default;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public class NestRequestPlate
|
|||||||
{
|
{
|
||||||
public string Id { get; init; }
|
public string Id { get; init; }
|
||||||
public Size Size { get; init; }
|
public Size Size { get; init; }
|
||||||
|
|
||||||
/// <summary>Available physical sheets; null means unlimited.</summary>
|
/// <summary>Available physical sheets; null means unlimited.</summary>
|
||||||
public int? Quantity { get; init; }
|
public int? Quantity { get; init; }
|
||||||
public double PartSpacing { get; init; }
|
public double PartSpacing { get; init; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
|
||||||
namespace OpenNest.Api;
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
@@ -25,10 +26,12 @@ public class NestResponse
|
|||||||
/// <summary>Zero identifies an archive written before response metadata was versioned.</summary>
|
/// <summary>Zero identifies an archive written before response metadata was versioned.</summary>
|
||||||
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
|
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
|
||||||
public int SheetCount { get; init; }
|
public int SheetCount { get; init; }
|
||||||
|
|
||||||
/// <summary>Placed-part area divided by total materialized physical-sheet area, as a 0.0–1.0 ratio.</summary>
|
/// <summary>Placed-part area divided by total materialized physical-sheet area, as a 0.0–1.0 ratio.</summary>
|
||||||
public double Utilization { get; init; }
|
public double Utilization { get; init; }
|
||||||
public TimeSpan CutTime { get; init; }
|
public TimeSpan CutTime { get; init; }
|
||||||
public TimeSpan Elapsed { get; init; }
|
public TimeSpan Elapsed { get; init; }
|
||||||
|
|
||||||
/// <summary>Null means an older archive did not record whole-job fulfillment status.</summary>
|
/// <summary>Null means an older archive did not record whole-job fulfillment status.</summary>
|
||||||
public NestJobStatus? Status { get; init; }
|
public NestJobStatus? Status { get; init; }
|
||||||
public NestJobStopReason? StopReason { get; init; }
|
public NestJobStopReason? StopReason { get; init; }
|
||||||
@@ -43,7 +46,7 @@ public class NestResponse
|
|||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
IncludeFields = true, // Required for OpenNest.Geometry.Size and Spacing public fields.
|
IncludeFields = true, // Required for OpenNest.Geometry.Size and Spacing public fields.
|
||||||
Converters = { new JsonStringEnumConverter() }
|
Converters = { new JsonStringEnumConverter() },
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task SaveAsync(string path)
|
public async Task SaveAsync(string path)
|
||||||
@@ -61,19 +64,27 @@ public class NestResponse
|
|||||||
var responseEntry = zip.CreateEntry("response.json");
|
var responseEntry = zip.CreateEntry("response.json");
|
||||||
await using (var stream = responseEntry.Open())
|
await using (var stream = responseEntry.Open())
|
||||||
{
|
{
|
||||||
await JsonSerializer.SerializeAsync(stream, new NestResponseArchiveDto
|
await JsonSerializer.SerializeAsync(
|
||||||
{
|
stream,
|
||||||
SchemaVersion = CurrentSchemaVersion,
|
new NestResponseArchiveDto
|
||||||
SheetCount = SheetCount,
|
{
|
||||||
Utilization = Utilization,
|
SchemaVersion = CurrentSchemaVersion,
|
||||||
CutTimeTicks = CutTime.Ticks,
|
SheetCount = SheetCount,
|
||||||
ElapsedTicks = Elapsed.Ticks,
|
Utilization = Utilization,
|
||||||
Status = Status,
|
CutTimeTicks = CutTime.Ticks,
|
||||||
StopReason = StopReason,
|
ElapsedTicks = Elapsed.Ticks,
|
||||||
Fulfillment = Fulfillment is null ? [] : new List<NestPartFulfillment>(Fulfillment),
|
Status = Status,
|
||||||
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
|
StopReason = StopReason,
|
||||||
PlateStockMappings = PlateStockMappings is null ? [] : new List<NestPlateStockMapping>(PlateStockMappings)
|
Fulfillment = Fulfillment is null
|
||||||
}, JsonOptions);
|
? []
|
||||||
|
: new List<NestPartFulfillment>(Fulfillment),
|
||||||
|
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
|
||||||
|
PlateStockMappings = PlateStockMappings is null
|
||||||
|
? []
|
||||||
|
: new List<NestPlateStockMapping>(PlateStockMappings),
|
||||||
|
},
|
||||||
|
JsonOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nestEntry = zip.CreateEntry("nest.nest");
|
var nestEntry = zip.CreateEntry("nest.nest");
|
||||||
@@ -91,16 +102,19 @@ public class NestResponse
|
|||||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
|
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
|
||||||
|
|
||||||
var requestEntry = zip.GetEntry("request.json")
|
var requestEntry =
|
||||||
|
zip.GetEntry("request.json")
|
||||||
?? throw new InvalidOperationException("Missing request.json in .nestquote file");
|
?? throw new InvalidOperationException("Missing request.json in .nestquote file");
|
||||||
NestRequest request;
|
NestRequest request;
|
||||||
await using (var stream = requestEntry.Open())
|
await using (var stream = requestEntry.Open())
|
||||||
{
|
{
|
||||||
request = await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions)
|
request =
|
||||||
|
await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions)
|
||||||
?? throw new InvalidOperationException("Invalid request.json in .nestquote file");
|
?? throw new InvalidOperationException("Invalid request.json in .nestquote file");
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseEntry = zip.GetEntry("response.json")
|
var responseEntry =
|
||||||
|
zip.GetEntry("response.json")
|
||||||
?? throw new InvalidOperationException("Missing response.json in .nestquote file");
|
?? throw new InvalidOperationException("Missing response.json in .nestquote file");
|
||||||
NestResponseArchiveDto archive;
|
NestResponseArchiveDto archive;
|
||||||
var hasSchemaVersion = false;
|
var hasSchemaVersion = false;
|
||||||
@@ -110,16 +124,19 @@ public class NestResponse
|
|||||||
{
|
{
|
||||||
var root = document.RootElement;
|
var root = document.RootElement;
|
||||||
hasSchemaVersion = root.TryGetProperty("schemaVersion", out _);
|
hasSchemaVersion = root.TryGetProperty("schemaVersion", out _);
|
||||||
hasStatusMetadata = root.TryGetProperty("status", out _) ||
|
hasStatusMetadata =
|
||||||
root.TryGetProperty("stopReason", out _) ||
|
root.TryGetProperty("status", out _)
|
||||||
root.TryGetProperty("fulfillment", out _) ||
|
|| root.TryGetProperty("stopReason", out _)
|
||||||
root.TryGetProperty("stockUsage", out _) ||
|
|| root.TryGetProperty("fulfillment", out _)
|
||||||
root.TryGetProperty("plateStockMappings", out _);
|
|| root.TryGetProperty("stockUsage", out _)
|
||||||
archive = root.Deserialize<NestResponseArchiveDto>(JsonOptions)
|
|| root.TryGetProperty("plateStockMappings", out _);
|
||||||
|
archive =
|
||||||
|
root.Deserialize<NestResponseArchiveDto>(JsonOptions)
|
||||||
?? throw new InvalidOperationException("Invalid response.json in .nestquote file");
|
?? throw new InvalidOperationException("Invalid response.json in .nestquote file");
|
||||||
}
|
}
|
||||||
|
|
||||||
var nestEntry = zip.GetEntry("nest.nest")
|
var nestEntry =
|
||||||
|
zip.GetEntry("nest.nest")
|
||||||
?? throw new InvalidOperationException("Missing nest.nest in .nestquote file");
|
?? throw new InvalidOperationException("Missing nest.nest in .nestquote file");
|
||||||
Nest nest;
|
Nest nest;
|
||||||
using (var nestMs = new MemoryStream())
|
using (var nestMs = new MemoryStream())
|
||||||
@@ -145,7 +162,7 @@ public class NestResponse
|
|||||||
StockUsage = hasStatusMetadata ? archive.StockUsage ?? [] : [],
|
StockUsage = hasStatusMetadata ? archive.StockUsage ?? [] : [],
|
||||||
PlateStockMappings = hasStatusMetadata ? archive.PlateStockMappings ?? [] : [],
|
PlateStockMappings = hasStatusMetadata ? archive.PlateStockMappings ?? [] : [],
|
||||||
Nest = nest,
|
Nest = nest,
|
||||||
Request = request
|
Request = request,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+107
-46
@@ -6,6 +6,9 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenNest.IO;
|
using OpenNest.IO;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
|
|
||||||
namespace OpenNest.Api;
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
@@ -16,10 +19,13 @@ public static class NestRunner
|
|||||||
public static Task<NestResponse> RunAsync(
|
public static Task<NestResponse> RunAsync(
|
||||||
NestRequest request,
|
NestRequest request,
|
||||||
IProgress<NestProgress> progress = null,
|
IProgress<NestProgress> progress = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default
|
||||||
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
var requestParts = request.Parts ?? throw new ArgumentException("Request parts must not be null.", nameof(request));
|
var requestParts =
|
||||||
|
request.Parts
|
||||||
|
?? throw new ArgumentException("Request parts must not be null.", nameof(request));
|
||||||
if (requestParts.Count == 0)
|
if (requestParts.Count == 0)
|
||||||
throw new ArgumentException("Request must contain at least one part.", nameof(request));
|
throw new ArgumentException("Request must contain at least one part.", nameof(request));
|
||||||
|
|
||||||
@@ -32,22 +38,32 @@ public static class NestRunner
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
if (!File.Exists(part.Request.DxfPath))
|
if (!File.Exists(part.Request.DxfPath))
|
||||||
throw new FileNotFoundException($"DXF file not found: {part.Request.DxfPath}", part.Request.DxfPath);
|
throw new FileNotFoundException(
|
||||||
|
$"DXF file not found: {part.Request.DxfPath}",
|
||||||
|
part.Request.DxfPath
|
||||||
|
);
|
||||||
|
|
||||||
if (!importedByPath.TryGetValue(part.Request.DxfPath, out var drawing))
|
if (!importedByPath.TryGetValue(part.Request.DxfPath, out var drawing))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
drawing = CadImporter.ImportDrawing(part.Request.DxfPath,
|
drawing = CadImporter.ImportDrawing(
|
||||||
new CadImportOptions { Quantity = part.Request.Quantity });
|
part.Request.DxfPath,
|
||||||
|
new CadImportOptions { Quantity = part.Request.Quantity }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Failed to import DXF: {part.Request.DxfPath}", exception);
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to import DXF: {part.Request.DxfPath}",
|
||||||
|
exception
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
|
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
|
||||||
throw new InvalidOperationException($"Failed to import DXF: {part.Request.DxfPath}");
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to import DXF: {part.Request.DxfPath}"
|
||||||
|
);
|
||||||
|
|
||||||
importedByPath.Add(part.Request.DxfPath, drawing);
|
importedByPath.Add(part.Request.DxfPath, drawing);
|
||||||
}
|
}
|
||||||
@@ -56,8 +72,11 @@ public static class NestRunner
|
|||||||
jobParts.Add(DrawingJobMapper.FromDrawing(part.Id, drawing, part.Request.Quantity));
|
jobParts.Add(DrawingJobMapper.FromDrawing(part.Id, drawing, part.Request.Quantity));
|
||||||
}
|
}
|
||||||
|
|
||||||
var job = new NestJob(jobParts, CreateStock(request),
|
var job = new NestJob(
|
||||||
new NestJobOptions(ResolvePlacementStrategy(request)));
|
jobParts,
|
||||||
|
CreateStock(request),
|
||||||
|
new NestJobOptions(ResolvePlacementStrategy(request))
|
||||||
|
);
|
||||||
var jobProgress = progress == null ? null : new JobProgressBridge(progress);
|
var jobProgress = progress == null ? null : new JobProgressBridge(progress);
|
||||||
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job, jobProgress, token);
|
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job, jobProgress, token);
|
||||||
|
|
||||||
@@ -71,35 +90,56 @@ public static class NestRunner
|
|||||||
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
|
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
|
|
||||||
return Task.FromResult(new NestResponse
|
return Task.FromResult(
|
||||||
{
|
new NestResponse
|
||||||
SheetCount = nest.Plates.Count,
|
{
|
||||||
Utilization = CalculateUtilization(nest),
|
SheetCount = nest.Plates.Count,
|
||||||
CutTime = cutTime,
|
Utilization = CalculateUtilization(nest),
|
||||||
Elapsed = sw.Elapsed,
|
CutTime = cutTime,
|
||||||
Status = result.Status,
|
Elapsed = sw.Elapsed,
|
||||||
StopReason = result.StopReason,
|
Status = result.Status,
|
||||||
Fulfillment = result.Fulfillment
|
StopReason = result.StopReason,
|
||||||
.Select(value => new NestPartFulfillment(value.PartId, value.Requested, value.Placed, value.Unplaced))
|
Fulfillment = result
|
||||||
.ToArray(),
|
.Fulfillment.Select(value => new NestPartFulfillment(
|
||||||
StockUsage = result.StockUsage
|
value.PartId,
|
||||||
.Select(value => new NestStockUsage(value.StockId, value.Used, value.Remaining))
|
value.Requested,
|
||||||
.ToArray(),
|
value.Placed,
|
||||||
PlateStockMappings = result.Plates
|
value.Unplaced
|
||||||
.Select(value => new NestPlateStockMapping(value.PlateIndex, value.StockId))
|
))
|
||||||
.ToArray(),
|
.ToArray(),
|
||||||
Nest = nest,
|
StockUsage = result
|
||||||
Request = request
|
.StockUsage.Select(value => new NestStockUsage(
|
||||||
});
|
value.StockId,
|
||||||
|
value.Used,
|
||||||
|
value.Remaining
|
||||||
|
))
|
||||||
|
.ToArray(),
|
||||||
|
PlateStockMappings = result
|
||||||
|
.Plates.Select(value => new NestPlateStockMapping(
|
||||||
|
value.PlateIndex,
|
||||||
|
value.StockId
|
||||||
|
))
|
||||||
|
.ToArray(),
|
||||||
|
Nest = nest,
|
||||||
|
Request = request,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<IdentifiedRequestPart> IdentifyParts(IReadOnlyList<NestRequestPart> requestParts)
|
private static IReadOnlyList<IdentifiedRequestPart> IdentifyParts(
|
||||||
|
IReadOnlyList<NestRequestPart> requestParts
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var identified = new List<IdentifiedRequestPart>(requestParts.Count);
|
var identified = new List<IdentifiedRequestPart>(requestParts.Count);
|
||||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||||
for (var index = 0; index < requestParts.Count; index++)
|
for (var index = 0; index < requestParts.Count; index++)
|
||||||
{
|
{
|
||||||
var part = requestParts[index] ?? throw new ArgumentException("Request parts must not contain null entries.", nameof(requestParts));
|
var part =
|
||||||
|
requestParts[index]
|
||||||
|
?? throw new ArgumentException(
|
||||||
|
"Request parts must not contain null entries.",
|
||||||
|
nameof(requestParts)
|
||||||
|
);
|
||||||
var id = part.Id ?? $"part-{index}";
|
var id = part.Id ?? $"part-{index}";
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
throw new ArgumentException("Part IDs must not be blank.", nameof(requestParts));
|
throw new ArgumentException("Part IDs must not be blank.", nameof(requestParts));
|
||||||
@@ -117,8 +157,12 @@ public static class NestRunner
|
|||||||
{
|
{
|
||||||
return
|
return
|
||||||
[
|
[
|
||||||
new NestPlateStock(LegacyStockId, request.SheetSize, quantity: null,
|
new NestPlateStock(
|
||||||
partSpacing: request.Spacing)
|
LegacyStockId,
|
||||||
|
request.SheetSize,
|
||||||
|
quantity: null,
|
||||||
|
partSpacing: request.Spacing
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +170,20 @@ public static class NestRunner
|
|||||||
foreach (var plate in request.Plates)
|
foreach (var plate in request.Plates)
|
||||||
{
|
{
|
||||||
if (plate is null)
|
if (plate is null)
|
||||||
throw new ArgumentException("Request plates must not contain null entries.", nameof(request));
|
throw new ArgumentException(
|
||||||
stock.Add(new NestPlateStock(plate.Id, plate.Size, plate.Quantity, plate.PartSpacing,
|
"Request plates must not contain null entries.",
|
||||||
plate.EdgeSpacing, plate.Quadrant));
|
nameof(request)
|
||||||
|
);
|
||||||
|
stock.Add(
|
||||||
|
new NestPlateStock(
|
||||||
|
plate.Id,
|
||||||
|
plate.Size,
|
||||||
|
plate.Quantity,
|
||||||
|
plate.PartSpacing,
|
||||||
|
plate.EdgeSpacing,
|
||||||
|
plate.Quadrant
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stock;
|
return stock;
|
||||||
@@ -147,25 +202,31 @@ public static class NestRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolvePlacementStrategy(NestRequest request) => request.PlacementStrategy ?? request.Strategy switch
|
private static string ResolvePlacementStrategy(NestRequest request) =>
|
||||||
{
|
request.PlacementStrategy
|
||||||
NestStrategy.Auto => "Default",
|
?? request.Strategy switch
|
||||||
_ => throw new NotSupportedException($"Unknown legacy nesting strategy: {request.Strategy}.")
|
{
|
||||||
};
|
NestStrategy.Auto => "Default",
|
||||||
|
_ => throw new NotSupportedException(
|
||||||
|
$"Unknown legacy nesting strategy: {request.Strategy}."
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
private static double CalculateUtilization(Nest nest)
|
private static double CalculateUtilization(Nest nest)
|
||||||
{
|
{
|
||||||
var sheetArea = nest.Plates.Sum(plate => plate.Area());
|
var sheetArea = nest.Plates.Sum(plate => plate.Area());
|
||||||
if (sheetArea == 0) return 0;
|
if (sheetArea == 0)
|
||||||
var placedArea = nest.Plates.Sum(plate => plate.Parts
|
return 0;
|
||||||
.Where(part => !part.BaseDrawing.IsCutOff)
|
var placedArea = nest.Plates.Sum(plate =>
|
||||||
.Sum(part => part.BaseDrawing.Area));
|
plate.Parts.Where(part => !part.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area)
|
||||||
|
);
|
||||||
return placedArea / sheetArea;
|
return placedArea / sheetArea;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record IdentifiedRequestPart(string Id, NestRequestPart Request);
|
private sealed record IdentifiedRequestPart(string Id, NestRequestPart Request);
|
||||||
|
|
||||||
private sealed class JobProgressBridge(IProgress<NestProgress> progress) : IProgress<NestJobProgress>
|
private sealed class JobProgressBridge(IProgress<NestProgress> progress)
|
||||||
|
: IProgress<NestJobProgress>
|
||||||
{
|
{
|
||||||
public void Report(NestJobProgress value)
|
public void Report(NestJobProgress value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
namespace OpenNest.Api;
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
public enum NestStrategy { Auto }
|
public enum NestStrategy
|
||||||
|
{
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RootNamespace>OpenNest.Api</RootNamespace>
|
<RootNamespace>OpenNest.Api</RootNamespace>
|
||||||
<AssemblyName>OpenNest.Api</AssemblyName>
|
<AssemblyName>OpenNest.Api</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// One request to nest a specific drawing, with the quantity and rotation
|
||||||
|
/// constraints pulled from its source .nest file.
|
||||||
|
/// </summary>
|
||||||
|
public class DrawingRequest
|
||||||
|
{
|
||||||
|
public Drawing Drawing { get; init; }
|
||||||
|
public int Quantity { get; init; }
|
||||||
|
public int Priority { get; init; }
|
||||||
|
public double StepAngle { get; init; }
|
||||||
|
public double RotationStart { get; init; }
|
||||||
|
public double RotationEnd { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An immutable specification for one benchmark job: the full set of
|
||||||
|
/// drawings/quantities that must be nested, and the pool of sheet sizes the
|
||||||
|
/// engine may draw from while doing it. A single run may use several
|
||||||
|
/// plates - possibly of different sizes - to place everything, the same
|
||||||
|
/// way a real production job spreads across whatever plates it needs
|
||||||
|
/// rather than being handed one fixed-size sheet.
|
||||||
|
/// </summary>
|
||||||
|
public class BenchmarkJob
|
||||||
|
{
|
||||||
|
public string SourceFile { get; init; }
|
||||||
|
public List<Size> CandidateSizes { get; init; }
|
||||||
|
public Spacing EdgeSpacing { get; init; }
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public int Quadrant { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Saved source-nest setting; manifests have no saved salvage rate and use zero.</summary>
|
||||||
|
public double SalvageRate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Original hand-authored placements, if the source was a .nest with any real parts.</summary>
|
||||||
|
public List<(Plate Plate, List<Part> Parts)> BaselinePlateRuns { get; init; }
|
||||||
|
public List<DrawingRequest> Requests { get; init; }
|
||||||
|
|
||||||
|
public string Name => Path.GetFileNameWithoutExtension(SourceFile);
|
||||||
|
|
||||||
|
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
|
||||||
|
|
||||||
|
/// <summary>Sheet area charged per unplaced part: the largest candidate
|
||||||
|
/// sheet. Any single part that fits the stock at all fits on one such
|
||||||
|
/// sheet, so placing a part is never scored worse than leaving it out.</summary>
|
||||||
|
public double UnplacedPartPenalty =>
|
||||||
|
CandidateSizes.Count == 0 ? 0 : CandidateSizes.Max(s => s.Width * s.Length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the whole-job request this job represents: one NestJobPart per
|
||||||
|
/// requested drawing, and one NestPlateStock per candidate sheet size
|
||||||
|
/// (unlimited quantity - the engine under test decides how many of each
|
||||||
|
/// size it actually uses, and how demand splits across plates). The
|
||||||
|
/// engine owns its own multi-plate/size strategy; this harness no
|
||||||
|
/// longer picks plate sizes on the engine's behalf.
|
||||||
|
/// </summary>
|
||||||
|
public NestJob BuildNestJob(
|
||||||
|
int maxPlates,
|
||||||
|
double? salvageRate = null,
|
||||||
|
double? minimumSalvageDimension = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var parts = Requests.Select(r =>
|
||||||
|
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)
|
||||||
|
);
|
||||||
|
var stock = CandidateSizes.Select(size => new NestPlateStock(
|
||||||
|
size.ToString(1),
|
||||||
|
size,
|
||||||
|
null,
|
||||||
|
PartSpacing,
|
||||||
|
EdgeSpacing,
|
||||||
|
Quadrant
|
||||||
|
));
|
||||||
|
return new NestJob(
|
||||||
|
parts,
|
||||||
|
stock,
|
||||||
|
new NestJobOptions(
|
||||||
|
"Default",
|
||||||
|
maxPlates,
|
||||||
|
salvageRate ?? SalvageRate,
|
||||||
|
minimumSalvageDimension ?? 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs every candidate engine against every job. Each engine is a full
|
||||||
|
/// INestingEngine: it owns its own plate/size selection and multi-plate
|
||||||
|
/// strategy for the whole job, rather than being handed one already-sized
|
||||||
|
/// plate at a time by this harness. A per-run timeout guards against a
|
||||||
|
/// runaway or hanging engine — cooperative cancellation, so it reliably
|
||||||
|
/// stops engines built on NestJobRunner (all four built-ins) but can't
|
||||||
|
/// forcibly interrupt an engine that never checks its token.
|
||||||
|
/// </summary>
|
||||||
|
public static class BenchmarkRunner
|
||||||
|
{
|
||||||
|
/// <summary>Physical-sheet cap passed to every job's NestJobOptions.MaxPlates.</summary>
|
||||||
|
private const int MaxPlates = 40;
|
||||||
|
|
||||||
|
/// <summary>Wall-clock budget for one engine solving one job.</summary>
|
||||||
|
private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
public static List<JobResult> Run(
|
||||||
|
List<BenchmarkJob> jobs,
|
||||||
|
IReadOnlyList<NestingEngineInfo> engines,
|
||||||
|
double? salvageRate = null,
|
||||||
|
double? minimumSalvageDimension = null,
|
||||||
|
string outputDirectory = null,
|
||||||
|
int maxParallelism = 1
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var pairs = jobs.SelectMany(job => engines.Select(engine => (Job: job, Engine: engine)))
|
||||||
|
.ToList();
|
||||||
|
var results = new JobResult[pairs.Count];
|
||||||
|
var options = new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = System.Math.Max(1, maxParallelism),
|
||||||
|
};
|
||||||
|
|
||||||
|
var baselineResults = new JobResult[jobs.Count];
|
||||||
|
Parallel.ForEach(
|
||||||
|
Partitioner.Create(
|
||||||
|
Enumerable.Range(0, jobs.Count),
|
||||||
|
EnumerablePartitionerOptions.NoBuffering
|
||||||
|
),
|
||||||
|
options,
|
||||||
|
i => baselineResults[i] = RunBaseline(jobs[i], salvageRate, minimumSalvageDimension)
|
||||||
|
);
|
||||||
|
|
||||||
|
// NoBuffering hands out one pair at a time: solves run for seconds to minutes,
|
||||||
|
// so chunked partitioning would leave workers idle behind a slow engine.
|
||||||
|
Parallel.ForEach(
|
||||||
|
Partitioner.Create(
|
||||||
|
Enumerable.Range(0, pairs.Count),
|
||||||
|
EnumerablePartitionerOptions.NoBuffering
|
||||||
|
),
|
||||||
|
options,
|
||||||
|
i =>
|
||||||
|
results[i] = RunOne(
|
||||||
|
pairs[i].Job,
|
||||||
|
pairs[i].Engine,
|
||||||
|
salvageRate,
|
||||||
|
minimumSalvageDimension,
|
||||||
|
outputDirectory
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Indexed writes keep the report in job-then-engine order whatever finishes first.
|
||||||
|
var ordered = new List<JobResult>(
|
||||||
|
results.Length + baselineResults.Count(result => result != null)
|
||||||
|
);
|
||||||
|
for (var jobIndex = 0; jobIndex < jobs.Count; jobIndex++)
|
||||||
|
{
|
||||||
|
if (baselineResults[jobIndex] != null)
|
||||||
|
ordered.Add(baselineResults[jobIndex]);
|
||||||
|
var firstResult = jobIndex * engines.Count;
|
||||||
|
for (var engineIndex = 0; engineIndex < engines.Count; engineIndex++)
|
||||||
|
ordered.Add(results[firstResult + engineIndex]);
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JobResult RunBaseline(
|
||||||
|
BenchmarkJob job,
|
||||||
|
double? salvageRate,
|
||||||
|
double? minimumSalvageDimension
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (job.BaselinePlateRuns == null)
|
||||||
|
return null;
|
||||||
|
var requested = job.TotalRequestedQuantity;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requirements = job.Requests.ToDictionary<
|
||||||
|
DrawingRequest,
|
||||||
|
Drawing,
|
||||||
|
(string Name, int Quantity)
|
||||||
|
>(
|
||||||
|
request => request.Drawing,
|
||||||
|
request => (request.Drawing.Name, request.Quantity),
|
||||||
|
ReferenceEqualityComparer.Instance
|
||||||
|
);
|
||||||
|
var partIds = job.Requests.ToDictionary<DrawingRequest, Drawing, string>(
|
||||||
|
request => request.Drawing,
|
||||||
|
request => request.Drawing.Id.ToString(),
|
||||||
|
ReferenceEqualityComparer.Instance
|
||||||
|
);
|
||||||
|
var validation = NestValidator.Validate(job.BaselinePlateRuns, requirements);
|
||||||
|
var benchmarkJob = job.BuildNestJob(
|
||||||
|
MaxPlates,
|
||||||
|
salvageRate,
|
||||||
|
minimumSalvageDimension
|
||||||
|
);
|
||||||
|
var instanceIndices = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
var plateResults = job
|
||||||
|
.BaselinePlateRuns.Select(
|
||||||
|
(run, index) =>
|
||||||
|
{
|
||||||
|
var stock = new NestPlateStock(
|
||||||
|
$"baseline-{index}",
|
||||||
|
run.Plate.Size,
|
||||||
|
1,
|
||||||
|
run.Plate.PartSpacing,
|
||||||
|
run.Plate.EdgeSpacing,
|
||||||
|
run.Plate.Quadrant
|
||||||
|
);
|
||||||
|
var placements = run
|
||||||
|
.Parts.Select(part =>
|
||||||
|
{
|
||||||
|
var partId = partIds[part.BaseDrawing];
|
||||||
|
instanceIndices.TryGetValue(partId, out var instanceIndex);
|
||||||
|
instanceIndices[partId] = instanceIndex + 1;
|
||||||
|
return new NestJobPlacement(
|
||||||
|
partId,
|
||||||
|
instanceIndex,
|
||||||
|
part.Location.X,
|
||||||
|
part.Location.Y,
|
||||||
|
part.Rotation
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
return new NestJobPlateResult(index, stock, placements);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
var baselineJob = new NestJob(
|
||||||
|
benchmarkJob.Parts,
|
||||||
|
plateResults.Select(result => result.Stock),
|
||||||
|
benchmarkJob.Options
|
||||||
|
);
|
||||||
|
var baselineJobResult = new NestJobResult(
|
||||||
|
NestJobStatus.Complete,
|
||||||
|
NestJobStopReason.Completed,
|
||||||
|
plateResults,
|
||||||
|
Array.Empty<PartFulfillment>(),
|
||||||
|
Array.Empty<StockUsage>()
|
||||||
|
);
|
||||||
|
NestValidator.ValidateAgainstJob(
|
||||||
|
baselineJob,
|
||||||
|
baselineJobResult,
|
||||||
|
job.Requests.ToDictionary(
|
||||||
|
request => request.Drawing.Id.ToString(),
|
||||||
|
request => request.Drawing.Name
|
||||||
|
),
|
||||||
|
validation
|
||||||
|
);
|
||||||
|
|
||||||
|
var plateRuns = job.BaselinePlateRuns;
|
||||||
|
var placedArea = validation.Valid
|
||||||
|
? plateRuns.Sum(run => run.Parts.Sum(part => part.BaseDrawing.Area))
|
||||||
|
: 0;
|
||||||
|
var plateArea = plateRuns.Sum(run => run.Plate.Area());
|
||||||
|
var netSheetArea = validation.Valid
|
||||||
|
? plateResults.Sum(result =>
|
||||||
|
NestJobCost.NetSheetArea(baselineJob, result)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
var sizeBreakdown = plateRuns
|
||||||
|
.GroupBy(run => run.Plate.Size.ToString(1))
|
||||||
|
.OrderByDescending(group => group.Count())
|
||||||
|
.ToDictionary(group => group.Key, group => group.Count());
|
||||||
|
|
||||||
|
return new JobResult
|
||||||
|
{
|
||||||
|
EngineName = "Baseline",
|
||||||
|
JobName = job.Name,
|
||||||
|
Valid = validation.Valid,
|
||||||
|
Violations = validation.Violations,
|
||||||
|
PartsPlaced = plateRuns.Sum(run => run.Parts.Count),
|
||||||
|
PartsRequested = requested,
|
||||||
|
PlacedArea = placedArea,
|
||||||
|
PlateArea = plateArea,
|
||||||
|
NetSheetArea = netSheetArea,
|
||||||
|
UnplacedPartPenalty = job.UnplacedPartPenalty,
|
||||||
|
PlatesUsed = plateRuns.Count,
|
||||||
|
SizeBreakdown = sizeBreakdown,
|
||||||
|
ElapsedMs = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new JobResult
|
||||||
|
{
|
||||||
|
EngineName = "Baseline",
|
||||||
|
JobName = job.Name,
|
||||||
|
Valid = false,
|
||||||
|
PartsRequested = requested,
|
||||||
|
UnplacedPartPenalty = job.UnplacedPartPenalty,
|
||||||
|
Error = $"{ex.GetType().Name}: {ex.Message}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JobResult RunOne(
|
||||||
|
BenchmarkJob job,
|
||||||
|
NestingEngineInfo engineInfo,
|
||||||
|
double? salvageRate,
|
||||||
|
double? minimumSalvageDimension,
|
||||||
|
string outputDirectory
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var requested = job.TotalRequestedQuantity;
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var nestJob = job.BuildNestJob(MaxPlates, salvageRate, minimumSalvageDimension);
|
||||||
|
var engine = engineInfo.Factory();
|
||||||
|
using var cts = new CancellationTokenSource(SolveTimeout);
|
||||||
|
var jobResult = engine.Solve(nestJob, null, cts.Token);
|
||||||
|
|
||||||
|
var materialized = NestResultMaterializer.Materialize(nestJob, jobResult);
|
||||||
|
var plateRuns = materialized
|
||||||
|
.Nest.Plates.Select(plate => (Plate: plate, Parts: plate.Parts.ToList()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var requirements = job.Requests.ToDictionary<
|
||||||
|
DrawingRequest,
|
||||||
|
Drawing,
|
||||||
|
(string Name, int Quantity)
|
||||||
|
>(
|
||||||
|
r => materialized.DrawingsByPartId[r.Drawing.Id.ToString()],
|
||||||
|
r => (r.Drawing.Name, r.Quantity),
|
||||||
|
ReferenceEqualityComparer.Instance
|
||||||
|
);
|
||||||
|
|
||||||
|
var validation = NestValidator.Validate(plateRuns, requirements);
|
||||||
|
NestValidator.ValidateAgainstJob(
|
||||||
|
nestJob,
|
||||||
|
jobResult,
|
||||||
|
job.Requests.ToDictionary(r => r.Drawing.Id.ToString(), r => r.Drawing.Name),
|
||||||
|
validation
|
||||||
|
);
|
||||||
|
var totalPlaced = plateRuns.Sum(pr => pr.Parts.Count);
|
||||||
|
var placedArea = validation.Valid
|
||||||
|
? plateRuns.Sum(pr => pr.Parts.Sum(p => p.BaseDrawing.Area))
|
||||||
|
: 0;
|
||||||
|
var plateArea = plateRuns.Sum(pr => pr.Plate.Area());
|
||||||
|
// Salvage credit is recomputed from the job's own geometry, never taken from the engine.
|
||||||
|
var netSheetArea = validation.Valid
|
||||||
|
? jobResult.Plates.Sum(p =>
|
||||||
|
NestJobCost.NetSheetArea(nestJob, p)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
var sizeBreakdown = plateRuns
|
||||||
|
.GroupBy(pr => pr.Plate.Size.ToString(1))
|
||||||
|
.OrderByDescending(g => g.Count())
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
if (validation.Valid && outputDirectory != null)
|
||||||
|
{
|
||||||
|
System.IO.Directory.CreateDirectory(outputDirectory);
|
||||||
|
// Keep names and job metadata for a useful inspectable output; never modify source.
|
||||||
|
// Manifest jobs have no source nest to copy from, so they keep the job's name.
|
||||||
|
if (job.SourceFile.EndsWith(".nest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var source = new OpenNest.IO.NestReader(job.SourceFile).Read();
|
||||||
|
materialized.Nest.Name = source.Name;
|
||||||
|
materialized.Nest.Units = source.Units;
|
||||||
|
materialized.Nest.Material = source.Material;
|
||||||
|
materialized.Nest.Thickness = source.Thickness;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
materialized.Nest.Name = job.Name;
|
||||||
|
}
|
||||||
|
materialized.Nest.SalvageRate = nestJob.Options.SalvageRate;
|
||||||
|
foreach (var request in job.Requests)
|
||||||
|
materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request
|
||||||
|
.Drawing
|
||||||
|
.Name;
|
||||||
|
var path = System.IO.Path.Combine(
|
||||||
|
outputDirectory,
|
||||||
|
$"{job.Name}-{engineInfo.Name}.nest"
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
System.IO.Path.GetFullPath(path)
|
||||||
|
== System.IO.Path.GetFullPath(job.SourceFile)
|
||||||
|
)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Output must not overwrite the source nest."
|
||||||
|
);
|
||||||
|
new OpenNest.IO.NestWriter(materialized.Nest).Write(path);
|
||||||
|
var report = new
|
||||||
|
{
|
||||||
|
Source = job.SourceFile,
|
||||||
|
Engine = engineInfo.Name,
|
||||||
|
jobResult.Status,
|
||||||
|
jobResult.StopReason,
|
||||||
|
Requested = requested,
|
||||||
|
Placed = totalPlaced,
|
||||||
|
SheetArea = plateArea,
|
||||||
|
PlacedArea = placedArea,
|
||||||
|
SalvageRate = nestJob.Options.SalvageRate,
|
||||||
|
MinimumSalvageDimension = nestJob.Options.MinimumSalvageDimension,
|
||||||
|
EstimatedNetArea = netSheetArea,
|
||||||
|
Fulfillment = jobResult.Fulfillment,
|
||||||
|
StockUsage = jobResult.StockUsage,
|
||||||
|
Plates = jobResult.Plates,
|
||||||
|
validation.Violations,
|
||||||
|
};
|
||||||
|
System.IO.File.WriteAllText(
|
||||||
|
System.IO.Path.ChangeExtension(path, ".json"),
|
||||||
|
System.Text.Json.JsonSerializer.Serialize(
|
||||||
|
report,
|
||||||
|
new System.Text.Json.JsonSerializerOptions { WriteIndented = true }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
return new JobResult
|
||||||
|
{
|
||||||
|
EngineName = engineInfo.Name,
|
||||||
|
JobName = job.Name,
|
||||||
|
Valid = validation.Valid,
|
||||||
|
Violations = validation.Violations,
|
||||||
|
PartsPlaced = totalPlaced,
|
||||||
|
PartsRequested = requested,
|
||||||
|
PlacedArea = placedArea,
|
||||||
|
PlateArea = plateArea,
|
||||||
|
NetSheetArea = netSheetArea,
|
||||||
|
UnplacedPartPenalty = job.UnplacedPartPenalty,
|
||||||
|
PlatesUsed = plateRuns.Count,
|
||||||
|
SizeBreakdown = sizeBreakdown,
|
||||||
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
return new JobResult
|
||||||
|
{
|
||||||
|
EngineName = engineInfo.Name,
|
||||||
|
JobName = job.Name,
|
||||||
|
Valid = false,
|
||||||
|
PartsRequested = requested,
|
||||||
|
UnplacedPartPenalty = job.UnplacedPartPenalty,
|
||||||
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
|
Error = $"Timed out after {SolveTimeout.TotalMinutes:F0} minute(s)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
return new JobResult
|
||||||
|
{
|
||||||
|
EngineName = engineInfo.Name,
|
||||||
|
JobName = job.Name,
|
||||||
|
Valid = false,
|
||||||
|
PartsRequested = requested,
|
||||||
|
UnplacedPartPenalty = job.UnplacedPartPenalty,
|
||||||
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
|
Error = $"{ex.GetType().Name}: {ex.Message}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a BenchmarkJob from a JSON manifest that lists DXF files and the
|
||||||
|
/// quantity of each to nest. DXF paths resolve relative to the manifest.
|
||||||
|
/// Sheet sizes come from the manifest or the caller's override; unlike a
|
||||||
|
/// .nest file there is no plate to inherit them from, so a job with none is
|
||||||
|
/// an error. Sheet sizes must use the same units as the DXFs.
|
||||||
|
/// </summary>
|
||||||
|
public static class DxfManifestLoader
|
||||||
|
{
|
||||||
|
/// <summary>Suffix a manifest needs to be picked up when scanning a folder.</summary>
|
||||||
|
public const string FolderSuffix = ".manifest.json";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static BenchmarkJob Load(
|
||||||
|
string manifestPath,
|
||||||
|
IReadOnlyList<Size> sheetSizeOverrides = null,
|
||||||
|
double? partSpacingOverride = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var manifest = ReadManifest(manifestPath);
|
||||||
|
var baseDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
|
||||||
|
|
||||||
|
if (manifest.Parts == null || manifest.Parts.Count == 0)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Manifest '{manifestPath}' has no parts. Add entries to \"parts\"."
|
||||||
|
);
|
||||||
|
|
||||||
|
var sizes = ResolveSheetSizes(manifest, sheetSizeOverrides, manifestPath);
|
||||||
|
var requests = manifest.Parts.Select(p => BuildRequest(p, baseDir)).ToList();
|
||||||
|
|
||||||
|
return new BenchmarkJob
|
||||||
|
{
|
||||||
|
SourceFile = manifestPath,
|
||||||
|
CandidateSizes = sizes,
|
||||||
|
EdgeSpacing = new Spacing(manifest.EdgeSpacing, manifest.EdgeSpacing),
|
||||||
|
PartSpacing = partSpacingOverride ?? manifest.Spacing,
|
||||||
|
Quadrant = manifest.Quadrant,
|
||||||
|
Requests = requests,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Manifest ReadManifest(string manifestPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<Manifest>(
|
||||||
|
File.ReadAllText(manifestPath),
|
||||||
|
JsonOptions
|
||||||
|
) ?? throw new InvalidOperationException("The manifest is empty.");
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Manifest '{manifestPath}' is not valid JSON: {ex.Message}",
|
||||||
|
ex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Size> ResolveSheetSizes(
|
||||||
|
Manifest manifest,
|
||||||
|
IReadOnlyList<Size> overrides,
|
||||||
|
string manifestPath
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (overrides != null && overrides.Count > 0)
|
||||||
|
return overrides.ToList();
|
||||||
|
|
||||||
|
var sizes = new List<Size>();
|
||||||
|
|
||||||
|
foreach (var text in manifest.SheetSizes ?? new List<string>())
|
||||||
|
{
|
||||||
|
if (!JobLoader.TryParseSheetSize(text, out var size))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Manifest '{manifestPath}': could not parse sheet size '{text}' (expected e.g. \"48x96\")."
|
||||||
|
);
|
||||||
|
|
||||||
|
sizes.Add(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizes.Count == 0)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Manifest '{manifestPath}' has no sheet sizes. Set \"sheetSizes\" or pass --sheet-sizes."
|
||||||
|
);
|
||||||
|
|
||||||
|
return sizes.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DrawingRequest BuildRequest(ManifestPart part, string baseDir)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(part.Dxf))
|
||||||
|
throw new InvalidOperationException("A manifest part is missing \"dxf\".");
|
||||||
|
|
||||||
|
if (part.Quantity <= 0)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Manifest part '{part.Dxf}': quantity must be greater than 0 (was {part.Quantity})."
|
||||||
|
);
|
||||||
|
|
||||||
|
var dxfPath = Path.GetFullPath(Path.Combine(baseDir, part.Dxf));
|
||||||
|
|
||||||
|
if (!File.Exists(dxfPath))
|
||||||
|
throw new FileNotFoundException($"DXF file not found: {dxfPath}", dxfPath);
|
||||||
|
|
||||||
|
Drawing drawing;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
drawing = CadImporter.ImportDrawing(
|
||||||
|
dxfPath,
|
||||||
|
new CadImportOptions { Quantity = part.Quantity }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to import DXF: {dxfPath}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
|
||||||
|
throw new InvalidOperationException($"Failed to import DXF: {dxfPath}");
|
||||||
|
|
||||||
|
// A zero legacy step means automatic rotation to DrawingJobMapper, so lock it explicitly.
|
||||||
|
if (!part.AllowRotation)
|
||||||
|
{
|
||||||
|
drawing.Constraints ??= new NestConstraints();
|
||||||
|
drawing.Constraints.StepAngle = OpenNest.Math.Angle.TwoPI;
|
||||||
|
drawing.Constraints.StartAngle = 0;
|
||||||
|
drawing.Constraints.EndAngle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var constraints = drawing.Constraints;
|
||||||
|
|
||||||
|
return new DrawingRequest
|
||||||
|
{
|
||||||
|
Drawing = drawing,
|
||||||
|
Quantity = part.Quantity,
|
||||||
|
Priority = drawing.Priority,
|
||||||
|
StepAngle = constraints?.StepAngle ?? 0,
|
||||||
|
RotationStart = constraints?.StartAngle ?? 0,
|
||||||
|
RotationEnd = constraints?.EndAngle ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Manifest
|
||||||
|
{
|
||||||
|
public List<string> SheetSizes { get; set; }
|
||||||
|
public double Spacing { get; set; }
|
||||||
|
public double EdgeSpacing { get; set; }
|
||||||
|
public int Quadrant { get; set; } = 1;
|
||||||
|
public List<ManifestPart> Parts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ManifestPart
|
||||||
|
{
|
||||||
|
public string Dxf { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public bool AllowRotation { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds BenchmarkJobs from .nest files on disk. Fully generic: works on
|
||||||
|
/// any valid .nest file, using whatever drawings/quantities/plate settings
|
||||||
|
/// it contains. One job per file, carrying the full pool of candidate
|
||||||
|
/// sheet sizes the engine may use across the whole nest - by default the
|
||||||
|
/// distinct sizes already present in that file, or a fixed override list
|
||||||
|
/// (e.g. a standard sheet-size lineup) applied to every file.
|
||||||
|
/// </summary>
|
||||||
|
public static class JobLoader
|
||||||
|
{
|
||||||
|
public static List<BenchmarkJob> Load(
|
||||||
|
string inputPath,
|
||||||
|
IReadOnlyList<Size> sheetSizeOverrides = null,
|
||||||
|
double? partSpacingOverride = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var files = ResolveFiles(inputPath);
|
||||||
|
var jobs = new List<BenchmarkJob>();
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Hand-written manifests fail loudly rather than being skipped like unreadable .nest files.
|
||||||
|
jobs.Add(DxfManifestLoader.Load(file, sheetSizeOverrides, partSpacingOverride));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Nest nest;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
nest = new NestReader(file).Read();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[JobLoader] Skipping '{file}': failed to read ({ex.Message})"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requests = BuildRequests(nest);
|
||||||
|
|
||||||
|
if (requests.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[JobLoader] Skipping '{file}': no drawings with quantity > 0"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var template = ResolvePlateTemplate(nest);
|
||||||
|
var sizes =
|
||||||
|
sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
|
||||||
|
? sheetSizeOverrides.ToList()
|
||||||
|
: ResolveSheetSizes(nest);
|
||||||
|
|
||||||
|
jobs.Add(
|
||||||
|
new BenchmarkJob
|
||||||
|
{
|
||||||
|
SourceFile = file,
|
||||||
|
CandidateSizes = sizes,
|
||||||
|
EdgeSpacing = template.EdgeSpacing,
|
||||||
|
PartSpacing = partSpacingOverride ?? template.PartSpacing,
|
||||||
|
Quadrant = template.Quadrant,
|
||||||
|
SalvageRate = nest.SalvageRate,
|
||||||
|
BaselinePlateRuns = BuildBaselinePlateRuns(nest, partSpacingOverride),
|
||||||
|
Requests = requests,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses "WxL" with invariant-culture numbers, so "48.5x96" means the
|
||||||
|
/// same thing on every machine (Size.Parse follows the current culture).</summary>
|
||||||
|
public static bool TryParseSheetSize(string text, out Size size)
|
||||||
|
{
|
||||||
|
size = default;
|
||||||
|
var dims = text?.Split('x', 'X');
|
||||||
|
|
||||||
|
if (dims == null || dims.Length != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var style = System.Globalization.NumberStyles.Float;
|
||||||
|
var culture = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!double.TryParse(dims[0].Trim(), style, culture, out var width)
|
||||||
|
|| !double.TryParse(dims[1].Trim(), style, culture, out var length)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
size = new Size(width, length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ResolveFiles(string inputPath)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(inputPath))
|
||||||
|
{
|
||||||
|
return Directory
|
||||||
|
.EnumerateFiles(inputPath, "*", SearchOption.AllDirectories)
|
||||||
|
.Where(f =>
|
||||||
|
f.EndsWith(".nest", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(
|
||||||
|
DxfManifestLoader.FolderSuffix,
|
||||||
|
StringComparison.OrdinalIgnoreCase
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(inputPath))
|
||||||
|
return new List<string> { inputPath };
|
||||||
|
|
||||||
|
throw new FileNotFoundException($"Benchmark input not found: {inputPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DrawingRequest> BuildRequests(Nest nest)
|
||||||
|
{
|
||||||
|
var requests = new List<DrawingRequest>();
|
||||||
|
|
||||||
|
foreach (var drawing in nest.Drawings)
|
||||||
|
{
|
||||||
|
var qty = drawing.Quantity.Required;
|
||||||
|
|
||||||
|
if (qty <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var constraints = drawing.Constraints;
|
||||||
|
|
||||||
|
requests.Add(
|
||||||
|
new DrawingRequest
|
||||||
|
{
|
||||||
|
Drawing = drawing,
|
||||||
|
Quantity = qty,
|
||||||
|
Priority = drawing.Priority,
|
||||||
|
StepAngle = constraints?.StepAngle ?? 0,
|
||||||
|
RotationStart = constraints?.StartAngle ?? 0,
|
||||||
|
RotationEnd = constraints?.EndAngle ?? 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<(Plate Plate, List<Part> Parts)> BuildBaselinePlateRuns(
|
||||||
|
Nest nest,
|
||||||
|
double? partSpacingOverride
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var runs = new List<(Plate Plate, List<Part> Parts)>();
|
||||||
|
foreach (var plate in nest.Plates ?? Enumerable.Empty<Plate>())
|
||||||
|
{
|
||||||
|
var parts = plate.Parts.Where(part => !part.BaseDrawing.IsCutOff).ToList();
|
||||||
|
if (parts.Count == 0)
|
||||||
|
continue;
|
||||||
|
var validationPlate = new Plate(new Size(plate.Size.Width, plate.Size.Length))
|
||||||
|
{
|
||||||
|
Quantity = 1,
|
||||||
|
Quadrant = plate.Quadrant,
|
||||||
|
PartSpacing = partSpacingOverride ?? plate.PartSpacing,
|
||||||
|
EdgeSpacing = new Spacing(
|
||||||
|
plate.EdgeSpacing.Left,
|
||||||
|
plate.EdgeSpacing.Bottom,
|
||||||
|
plate.EdgeSpacing.Right,
|
||||||
|
plate.EdgeSpacing.Top
|
||||||
|
),
|
||||||
|
};
|
||||||
|
for (var copy = 0; copy < plate.Quantity; copy++)
|
||||||
|
runs.Add((validationPlate, parts));
|
||||||
|
}
|
||||||
|
return runs.Count > 0 ? runs : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(
|
||||||
|
Nest nest
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var source = nest.Plates?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (source != null)
|
||||||
|
return (source.EdgeSpacing, source.PartSpacing, source.Quadrant);
|
||||||
|
|
||||||
|
var defaults = nest.PlateDefaults;
|
||||||
|
return (defaults.EdgeSpacing, defaults.PartSpacing, defaults.Quadrant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Size> ResolveSheetSizes(Nest nest)
|
||||||
|
{
|
||||||
|
var sizes = (nest.Plates ?? Enumerable.Empty<Plate>())
|
||||||
|
.Select(p => p.Size)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (sizes.Count == 0)
|
||||||
|
sizes.Add(nest.PlateDefaults.Size);
|
||||||
|
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Outcome of running one engine against one job. A job may span several
|
||||||
|
/// plates (PlatesUsed, SizeBreakdown), since the engine may need more than
|
||||||
|
/// one sheet - possibly of different sizes - to place everything asked of
|
||||||
|
/// it. An invalid or crashed run places nothing as far as scoring is
|
||||||
|
/// concerned: it earns no area and pays the unplaced penalty on every
|
||||||
|
/// requested part.
|
||||||
|
/// </summary>
|
||||||
|
public class JobResult
|
||||||
|
{
|
||||||
|
public string EngineName { get; init; }
|
||||||
|
public string JobName { get; init; }
|
||||||
|
public bool Valid { get; init; }
|
||||||
|
public List<string> Violations { get; init; } = new();
|
||||||
|
public string Error { get; init; }
|
||||||
|
public int PartsPlaced { get; init; }
|
||||||
|
public int PartsRequested { get; init; }
|
||||||
|
public double PlacedArea { get; init; }
|
||||||
|
public double PlateArea { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Sheet area consumed after crediting salvageable offcuts
|
||||||
|
/// (NestJobCost.NetSheetArea summed over every plate).
|
||||||
|
/// Equals PlateArea when salvage credit is disabled.</summary>
|
||||||
|
public double NetSheetArea { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Sheet area charged for each requested part that was not
|
||||||
|
/// placed: the largest candidate sheet's area, so leaving a part out
|
||||||
|
/// always costs at least as much as the extra sheet it would need.</summary>
|
||||||
|
public double UnplacedPartPenalty { get; init; }
|
||||||
|
|
||||||
|
public int PlatesUsed { get; init; }
|
||||||
|
public Dictionary<string, int> SizeBreakdown { get; init; } = new();
|
||||||
|
public long ElapsedMs { get; init; }
|
||||||
|
|
||||||
|
public bool Crashed => Error != null;
|
||||||
|
public bool FullyPlaced => Valid && PartsRequested > 0 && PartsPlaced >= PartsRequested;
|
||||||
|
|
||||||
|
/// <summary>Aggregate utilization across every plate the engine used:
|
||||||
|
/// total placed drawing area over total plate area, matching
|
||||||
|
/// Plate.Utilization()'s per-plate definition summed across the job.</summary>
|
||||||
|
public double Utilization => Valid && PlateArea > 0 ? PlacedArea / PlateArea : 0;
|
||||||
|
|
||||||
|
/// <summary>Placed area over salvage-credited sheet area.</summary>
|
||||||
|
public double NetUtilization =>
|
||||||
|
Valid && NetSheetArea > 0 ? PlacedArea / NetSheetArea : 0;
|
||||||
|
|
||||||
|
public int PartsUnplaced =>
|
||||||
|
Valid ? System.Math.Max(0, PartsRequested - PartsPlaced) : PartsRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ranking score, in sheet area (lower is better): net sheet area
|
||||||
|
/// consumed plus the unplaced penalty. An engine cannot improve it by
|
||||||
|
/// dropping awkward parts, and it sums honestly across jobs of
|
||||||
|
/// different sizes. Invalid runs consume no sheet but pay the penalty
|
||||||
|
/// on every requested part.
|
||||||
|
/// </summary>
|
||||||
|
public double Cost => (Valid ? NetSheetArea : 0) + PartsUnplaced * UnplacedPartPenalty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark;
|
||||||
|
|
||||||
|
/// <summary>Benchmark validation outcome.</summary>
|
||||||
|
public class ValidationResult
|
||||||
|
{
|
||||||
|
public bool Valid => Violations.Count == 0;
|
||||||
|
public List<string> Violations { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Compatibility wrapper over the shared layout validation contract.</summary>
|
||||||
|
public static class NestValidator
|
||||||
|
{
|
||||||
|
/// <summary>Checks materialized plates using drawing-reference requirement identity.</summary>
|
||||||
|
public static ValidationResult Validate(
|
||||||
|
List<(Plate Plate, List<Part> Parts)> plateRuns,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements)
|
||||||
|
{
|
||||||
|
var result = new ValidationResult();
|
||||||
|
result.Violations.AddRange(NestLayoutCheck.Validate(plateRuns, requirements));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Appends offered-stock, finite-stock and rotation-policy violations.</summary>
|
||||||
|
public static void ValidateAgainstJob(NestJob job, NestJobResult jobResult,
|
||||||
|
IReadOnlyDictionary<string, string> displayNames, ValidationResult result) =>
|
||||||
|
NestLayoutCheck.ValidateAgainstJob(job, jobResult, displayNames, result.Violations);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RootNamespace>OpenNest.Benchmark</RootNamespace>
|
||||||
|
<AssemblyName>OpenNest.Benchmark</AssemblyName>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest;
|
||||||
|
using OpenNest.Benchmark;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
return BenchmarkConsole.Run(args);
|
||||||
|
|
||||||
|
static class BenchmarkConsole
|
||||||
|
{
|
||||||
|
public static int Run(string[] args)
|
||||||
|
{
|
||||||
|
var options = ParseArgs(args);
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
return 0; // --help was requested
|
||||||
|
|
||||||
|
if (options.InputPath == null)
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BenchmarkJob> jobs;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jobs = JobLoader.Load(options.InputPath, options.SheetSizes, options.PartSpacing);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobs.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"No benchmark jobs found (no .nest files with any drawing quantity > 0, or *.manifest.json files)."
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enginesDir = Path.Combine(AppContext.BaseDirectory, "Engines");
|
||||||
|
NestingEngineRegistry.LoadPlugins(enginesDir);
|
||||||
|
|
||||||
|
var engines = NestingEngineRegistry.AvailableEngines;
|
||||||
|
|
||||||
|
if (options.EngineNames.Count > 0)
|
||||||
|
{
|
||||||
|
engines = engines
|
||||||
|
.Where(e =>
|
||||||
|
options.EngineNames.Any(n =>
|
||||||
|
n.Equals(e.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (engines.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"None of the requested engines are registered. Available: "
|
||||||
|
+ string.Join(
|
||||||
|
", ",
|
||||||
|
NestingEngineRegistry.AvailableEngines.Select(e => e.Name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Loaded {jobs.Count} job(s) from '{options.InputPath}'");
|
||||||
|
|
||||||
|
foreach (var job in jobs)
|
||||||
|
{
|
||||||
|
var sizes = string.Join(", ", job.CandidateSizes.Select(s => s.ToString(1)));
|
||||||
|
Console.WriteLine(
|
||||||
|
$" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.SheetSizes.Count == 0
|
||||||
|
&& jobs.Any(j => j.SourceFile.EndsWith(".nest", StringComparison.OrdinalIgnoreCase))
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"Warning: no --sheet-sizes given, so each .nest job only offers the sheet sizes its "
|
||||||
|
+ "original layout used - a hint toward that answer. Pass --sheet-sizes with the "
|
||||||
|
+ "sizes you actually stock for an unbiased comparison."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
|
||||||
|
|
||||||
|
var effectiveSalvageRates = jobs.Select(job => options.SalvageRate ?? job.SalvageRate);
|
||||||
|
if (
|
||||||
|
effectiveSalvageRates.Any(rate => rate > 0)
|
||||||
|
&& (options.MinimumSalvageDimension ?? 0) <= 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"Warning: salvage credit is disabled because --min-salvage-dimension was not set to a positive value."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var solves = jobs.Count * engines.Count;
|
||||||
|
|
||||||
|
if (options.Parallel > 1 && solves > 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"Running up to {options.Parallel} solves at a time; Time(ms) is measured under that "
|
||||||
|
+ "concurrent load. Use --parallel 1 for strictly isolated timings."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = BenchmarkRunner.Run(
|
||||||
|
jobs,
|
||||||
|
engines,
|
||||||
|
options.SalvageRate,
|
||||||
|
options.MinimumSalvageDimension,
|
||||||
|
options.OutputDirectory,
|
||||||
|
options.Parallel
|
||||||
|
);
|
||||||
|
|
||||||
|
Report.PrintDetailed(results);
|
||||||
|
Report.PrintSummary(results);
|
||||||
|
|
||||||
|
if (options.CsvPath != null)
|
||||||
|
{
|
||||||
|
Report.WriteCsv(options.CsvPath, results);
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Wrote CSV report to {options.CsvPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Options ParseArgs(string[] args)
|
||||||
|
{
|
||||||
|
var o = new Options();
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--sheet-sizes" when i + 1 < args.Length:
|
||||||
|
o.SheetSizes = ParseSheetSizes(args[++i]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--spacing" when i + 1 < args.Length:
|
||||||
|
o.PartSpacing = double.Parse(
|
||||||
|
args[++i],
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--engines" when i + 1 < args.Length:
|
||||||
|
o.EngineNames = args[++i]
|
||||||
|
.Split(
|
||||||
|
',',
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--csv" when i + 1 < args.Length:
|
||||||
|
o.CsvPath = args[++i];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--salvage-rate" when i + 1 < args.Length:
|
||||||
|
o.SalvageRate = double.Parse(
|
||||||
|
args[++i],
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "--min-salvage-dimension" when i + 1 < args.Length:
|
||||||
|
o.MinimumSalvageDimension = double.Parse(
|
||||||
|
args[++i],
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "--output" when i + 1 < args.Length:
|
||||||
|
o.OutputDirectory = args[++i];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--parallel" when i + 1 < args.Length:
|
||||||
|
if (int.TryParse(args[++i], out var parallel) && parallel >= 1)
|
||||||
|
o.Parallel = parallel;
|
||||||
|
else
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"Warning: --parallel needs a whole number >= 1, using {o.Parallel}"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--help":
|
||||||
|
PrintUsage();
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (!args[i].StartsWith("--"))
|
||||||
|
o.InputPath = args[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Size> ParseSheetSizes(string arg)
|
||||||
|
{
|
||||||
|
var sizes = new List<Size>();
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
var token in arg.Split(
|
||||||
|
',',
|
||||||
|
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (JobLoader.TryParseSheetSize(token, out var size))
|
||||||
|
sizes.Add(size);
|
||||||
|
else
|
||||||
|
Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintUsage()
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"For each .nest file, every drawing with quantity > 0 is nested (mixed together),"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"once per registered INestingEngine. Each engine is handed the full job - every"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"requested part and the whole pool of candidate sheet sizes - and owns its own"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"multi-plate/size strategy: how many plates it uses, of which sizes, and how"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"demand splits across them. Ranking: a run that places every requested part beats"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"one that does not; then lower cost = sheet area consumed (minus salvage credit for a"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"usable offcut) + the largest candidate sheet's area per unplaced part; then fewer"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"plates. An invalid layout (out of bounds, overlapping, over-quantity, off-stock, or"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"breaking a rotation constraint), a thrown exception, or a timeout places nothing."
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Usage:");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" OpenNest.Benchmark <file.nest | manifest.json | folder> [options]"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"A manifest.json builds a job straight from DXF files (paths relative to the manifest):"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" { \"sheetSizes\": [\"48x96\"], \"spacing\": 0.25, \"edgeSpacing\": 0.25, \"quadrant\": 1,"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" \"parts\": [ { \"dxf\": \"a.dxf\", \"quantity\": 12 }, { \"dxf\": \"b.dxf\", \"quantity\": 4, \"allowRotation\": false } ] }"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"Sheet sizes must use the same units as the DXFs. A folder is scanned for *.nest and"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"*.manifest.json files. --sheet-sizes and --spacing override the manifest."
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Options:");
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" (default: the distinct sizes already in each file,"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" which hints engines toward the original layout)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --spacing <value> Override part spacing for every job"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --engines Name1,Name2,... Only benchmark these registered engines (default: all)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --csv <path> Write a flat CSV of all results"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --salvage-rate <0..1> Fraction of eligible offcut area credited (default: saved .nest rate;"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" manifests 0; needs positive --min-salvage-dimension)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --min-salvage-dimension <value> Both offcut dimensions must qualify; positive value enables credit (default 0)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --output <directory> Save valid layouts as .nest plus detailed JSON reports"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --parallel <n> Solves to run at once (default 3; 1 = strictly sequential,"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" which gives the cleanest per-engine timings)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(" --help Show this message");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Options
|
||||||
|
{
|
||||||
|
public string InputPath;
|
||||||
|
public List<Size> SheetSizes = new();
|
||||||
|
public double? PartSpacing;
|
||||||
|
public List<string> EngineNames = new();
|
||||||
|
public string CsvPath;
|
||||||
|
public string OutputDirectory;
|
||||||
|
public double? SalvageRate;
|
||||||
|
public double? MinimumSalvageDimension;
|
||||||
|
public int Parallel = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Console + CSV reporting for benchmark results. Ranking rule per job:
|
||||||
|
/// valid beats invalid; placing every requested part beats not; then lower
|
||||||
|
/// JobResult.Cost wins - salvage-credited sheet area consumed plus a
|
||||||
|
/// largest-sheet penalty per unplaced part, so dropping awkward parts can
|
||||||
|
/// never buy a better score; then fewer plates. Ties beyond that are a
|
||||||
|
/// shared win. Across jobs, costs and areas are summed (not averaged), so
|
||||||
|
/// a big job weighs more than a three-part one.
|
||||||
|
/// </summary>
|
||||||
|
public static class Report
|
||||||
|
{
|
||||||
|
private const double Epsilon = 1e-6;
|
||||||
|
|
||||||
|
public static void PrintDetailed(List<JobResult> results)
|
||||||
|
{
|
||||||
|
foreach (var jobGroup in results.GroupBy(r => r.JobName))
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"=== {jobGroup.Key} ===");
|
||||||
|
|
||||||
|
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList();
|
||||||
|
var best = ranked.Count > 0 ? ranked[0] : null;
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"{"Engine", -16} {"Result", -9} {"Parts", -10} {"Util%", -7} {"Net%", -7} {"Cost", -12} {"Plates", -18} {"Time(ms)", -9} Notes"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var r in ranked)
|
||||||
|
{
|
||||||
|
var isWinner = best != null && Compare(r, best) == 0 && r.Valid;
|
||||||
|
var marker = isWinner ? "*" : " ";
|
||||||
|
var status =
|
||||||
|
r.Crashed ? "CRASH"
|
||||||
|
: r.Valid ? "ok"
|
||||||
|
: "INVALID";
|
||||||
|
var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}";
|
||||||
|
var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-";
|
||||||
|
var netCol = r.Valid ? $"{r.NetUtilization * 100:F1}" : "-";
|
||||||
|
var platesCol =
|
||||||
|
r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-";
|
||||||
|
var notes = r.Crashed ? r.Error : string.Join("; ", r.Violations.Take(2));
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"{marker}{r.EngineName, -15} {status, -9} {partsCol, -10} {utilCol, -7} {netCol, -7} {r.Cost, -12:F1} {platesCol, -18} {r.ElapsedMs, -9} {notes}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PrintSummary(List<JobResult> results)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("=== Summary ===");
|
||||||
|
|
||||||
|
var byEngine = results
|
||||||
|
.GroupBy(r => r.EngineName)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
Engine = g.Key,
|
||||||
|
Jobs = g.Count(),
|
||||||
|
Valid = g.Count(r => r.Valid),
|
||||||
|
Crashed = g.Count(r => r.Crashed),
|
||||||
|
FullyPlaced = g.Count(r => r.FullyPlaced),
|
||||||
|
Unplaced = g.Sum(r => r.PartsUnplaced),
|
||||||
|
PlacedArea = g.Where(r => r.Valid).Sum(r => r.PlacedArea),
|
||||||
|
PlateArea = g.Where(r => r.Valid).Sum(r => r.PlateArea),
|
||||||
|
NetSheetArea = g.Where(r => r.Valid).Sum(r => r.NetSheetArea),
|
||||||
|
TotalCost = g.Sum(r => r.Cost),
|
||||||
|
TotalPlates = g.Sum(r => r.PlatesUsed),
|
||||||
|
TotalTimeMs = g.Sum(r => r.ElapsedMs),
|
||||||
|
})
|
||||||
|
.OrderBy(e => e.TotalCost)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var wins = CountWins(results);
|
||||||
|
|
||||||
|
// Util% and Net% are area-weighted over valid runs (sum placed / sum
|
||||||
|
// sheet), not a mean of per-job percentages. TotalCost sums across
|
||||||
|
// jobs, so it is only meaningful when every job uses the same units.
|
||||||
|
Console.WriteLine(
|
||||||
|
$"{"Engine", -16} {"Jobs", -6} {"Valid", -7} {"Complete", -9} {"Unplaced", -9} {"Wins", -6} {"Util%", -7} {"Net%", -7} {"TotalCost", -14} {"Plates", -8} {"TotalTime(ms)", -14}"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var e in byEngine)
|
||||||
|
{
|
||||||
|
var util = e.PlateArea > 0 ? e.PlacedArea / e.PlateArea * 100 : 0;
|
||||||
|
var netUtil = e.NetSheetArea > 0 ? e.PlacedArea / e.NetSheetArea * 100 : 0;
|
||||||
|
var winCount = wins.TryGetValue(e.Engine, out var w) ? w : 0;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"{e.Engine, -16} {e.Jobs, -6} {e.Valid, -7} {e.FullyPlaced, -9} {e.Unplaced, -9} {winCount, -6} {util, -7:F1} {netUtil, -7:F1} {e.TotalCost, -14:F1} {e.TotalPlates, -8} {e.TotalTimeMs, -14}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteCsv(string path, List<JobResult> results)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(
|
||||||
|
"Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,NetUtilization,PlateArea,NetSheetArea,Cost,PlatesUsed,SizeBreakdown,ElapsedMs,Notes"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var r in results)
|
||||||
|
{
|
||||||
|
var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations);
|
||||||
|
sb.AppendLine(
|
||||||
|
string.Join(
|
||||||
|
",",
|
||||||
|
Csv(r.JobName),
|
||||||
|
Csv(r.EngineName),
|
||||||
|
r.Valid,
|
||||||
|
r.Crashed,
|
||||||
|
r.FullyPlaced,
|
||||||
|
r.PartsPlaced,
|
||||||
|
r.PartsRequested,
|
||||||
|
r.Utilization.ToString("F4", CultureInfo.InvariantCulture),
|
||||||
|
r.NetUtilization.ToString("F4", CultureInfo.InvariantCulture),
|
||||||
|
r.PlateArea.ToString("F2", CultureInfo.InvariantCulture),
|
||||||
|
r.NetSheetArea.ToString("F2", CultureInfo.InvariantCulture),
|
||||||
|
r.Cost.ToString("F2", CultureInfo.InvariantCulture),
|
||||||
|
r.PlatesUsed,
|
||||||
|
Csv(SizeSummary(r.SizeBreakdown)),
|
||||||
|
r.ElapsedMs,
|
||||||
|
Csv(notes)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(path, sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SizeSummary(Dictionary<string, int> breakdown)
|
||||||
|
{
|
||||||
|
if (breakdown == null || breakdown.Count == 0)
|
||||||
|
return "-";
|
||||||
|
|
||||||
|
return string.Join("; ", breakdown.Select(kv => $"{kv.Key}×{kv.Value}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Csv(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||||
|
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, int> CountWins(List<JobResult> results)
|
||||||
|
{
|
||||||
|
var wins = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
foreach (var jobGroup in results.GroupBy(r => r.JobName))
|
||||||
|
{
|
||||||
|
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList();
|
||||||
|
|
||||||
|
if (ranked.Count == 0 || !ranked[0].Valid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var r in ranked.TakeWhile(r => Compare(r, ranked[0]) == 0))
|
||||||
|
wins[r.EngineName] = wins.GetValueOrDefault(r.EngineName) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Lower sorts first (better). Valid beats invalid, complete beats
|
||||||
|
/// incomplete, then lower cost (relative tolerance, since costs are areas
|
||||||
|
/// in whatever units the job uses), then fewer plates.</summary>
|
||||||
|
public static int Compare(JobResult a, JobResult b)
|
||||||
|
{
|
||||||
|
if (a.Valid != b.Valid)
|
||||||
|
return a.Valid ? -1 : 1;
|
||||||
|
|
||||||
|
if (!a.Valid)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (a.FullyPlaced != b.FullyPlaced)
|
||||||
|
return a.FullyPlaced ? -1 : 1;
|
||||||
|
|
||||||
|
var costDiff = a.Cost - b.Cost;
|
||||||
|
var scale = System.Math.Max(1, System.Math.Max(a.Cost, b.Cost));
|
||||||
|
|
||||||
|
if (System.Math.Abs(costDiff) > Epsilon * scale)
|
||||||
|
return costDiff > 0 ? 1 : -1;
|
||||||
|
|
||||||
|
if (a.PlatesUsed != b.PlatesUsed)
|
||||||
|
return a.PlatesUsed > b.PlatesUsed ? 1 : -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RootNamespace>OpenNest.Console</RootNamespace>
|
<RootNamespace>OpenNest.Console</RootNamespace>
|
||||||
<AssemblyName>OpenNest.Console</AssemblyName>
|
<AssemblyName>OpenNest.Console</AssemblyName>
|
||||||
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
|
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
|
||||||
|
|||||||
+308
-60
@@ -1,13 +1,19 @@
|
|||||||
using OpenNest;
|
|
||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.IO;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using OpenNest;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
|
using OpenNest.IO.Bending;
|
||||||
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
|
using OpenNest.Engine.Jobs.Placement;
|
||||||
|
|
||||||
return NestConsole.Run(args);
|
return NestConsole.Run(args);
|
||||||
|
|
||||||
@@ -20,12 +26,58 @@ static class NestConsole
|
|||||||
if (options == null)
|
if (options == null)
|
||||||
return 0; // --help was requested
|
return 0; // --help was requested
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.RepairBendsMillimeters.HasValue
|
||||||
|
&& (
|
||||||
|
options.CadUnits == BendRepairUnits.Unspecified
|
||||||
|
|| !double.IsFinite(options.RepairBendsMillimeters.Value)
|
||||||
|
|| options.RepairBendsMillimeters <= 0.001
|
||||||
|
|| options.RepairBendsMillimeters > 3.175
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"Error: --repair-bends-mm requires a limit > 0.001 and <= 3.175 mm and --cad-units inches|mm."
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.ListPosts)
|
if (options.ListPosts)
|
||||||
{
|
{
|
||||||
ListPostProcessors(options);
|
ListPostProcessors(options);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate --engine up front: autonest names a jobs engine, plain fill names a
|
||||||
|
// single-plate placement strategy. Unknown names exit with the valid choices.
|
||||||
|
if (options.AutoNest)
|
||||||
|
{
|
||||||
|
var isJobsEngine = NestingEngineRegistry.AvailableEngines.Any(e =>
|
||||||
|
e.Name.Equals(options.Engine, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
if (!isJobsEngine)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"Error: unknown engine '{options.Engine}'. Jobs engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PlateFillService.ResolveStrategy(options.Engine);
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"Error: unknown engine '{options.Engine}'. Fill strategies: {string.Join(", ", PlateFillService.BuiltInStrategies)} (jobs engines such as StockLadder require --autonest)"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.InputFiles.Count == 0)
|
if (options.InputFiles.Count == 0)
|
||||||
{
|
{
|
||||||
PrintUsage();
|
PrintUsage();
|
||||||
@@ -82,6 +134,26 @@ static class NestConsole
|
|||||||
{
|
{
|
||||||
switch (args[i])
|
switch (args[i])
|
||||||
{
|
{
|
||||||
|
case "--repair-bends-mm":
|
||||||
|
o.RepairBendsMillimeters =
|
||||||
|
i + 1 < args.Length
|
||||||
|
&& double.TryParse(
|
||||||
|
args[++i],
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out var limit
|
||||||
|
)
|
||||||
|
? limit
|
||||||
|
: double.NaN;
|
||||||
|
break;
|
||||||
|
case "--cad-units" when i + 1 < args.Length:
|
||||||
|
o.CadUnits = args[++i] switch
|
||||||
|
{
|
||||||
|
"inches" => BendRepairUnits.Inches,
|
||||||
|
"mm" => BendRepairUnits.Millimeters,
|
||||||
|
_ => BendRepairUnits.Unspecified,
|
||||||
|
};
|
||||||
|
break;
|
||||||
case "--drawing" when i + 1 < args.Length:
|
case "--drawing" when i + 1 < args.Length:
|
||||||
o.DrawingName = args[++i];
|
o.DrawingName = args[++i];
|
||||||
break;
|
break;
|
||||||
@@ -117,7 +189,7 @@ static class NestConsole
|
|||||||
o.AutoNest = true;
|
o.AutoNest = true;
|
||||||
break;
|
break;
|
||||||
case "--engine" when i + 1 < args.Length:
|
case "--engine" when i + 1 < args.Length:
|
||||||
NestEngineRegistry.ActiveEngineName = args[++i];
|
o.Engine = args[++i];
|
||||||
break;
|
break;
|
||||||
case "--post" when i + 1 < args.Length:
|
case "--post" when i + 1 < args.Length:
|
||||||
o.PostName = args[++i];
|
o.PostName = args[++i];
|
||||||
@@ -149,10 +221,14 @@ static class NestConsole
|
|||||||
{
|
{
|
||||||
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
||||||
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||||
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)
|
||||||
var dxfFiles = options.InputFiles.Where(f =>
|
);
|
||||||
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) ||
|
var dxfFiles = options
|
||||||
f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)).ToList();
|
.InputFiles.Where(f =>
|
||||||
|
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// If we have a nest file, load it and optionally add DXFs.
|
// If we have a nest file, load it and optionally add DXFs.
|
||||||
if (nestFile != null)
|
if (nestFile != null)
|
||||||
@@ -167,13 +243,15 @@ static class NestConsole
|
|||||||
|
|
||||||
if (options.PlateIndex >= nest.Plates.Count)
|
if (options.PlateIndex >= nest.Plates.Count)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})");
|
Console.Error.WriteLine(
|
||||||
|
$"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})"
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var dxf in dxfFiles)
|
foreach (var dxf in dxfFiles)
|
||||||
{
|
{
|
||||||
var drawing = ImportDxf(dxf);
|
var drawing = ImportDxf(dxf, options);
|
||||||
|
|
||||||
if (drawing == null)
|
if (drawing == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -194,7 +272,9 @@ static class NestConsole
|
|||||||
|
|
||||||
if (!options.PlateSize.HasValue)
|
if (!options.PlateSize.HasValue)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Error: --size WxL is required when importing DXF files without a nest");
|
Console.Error.WriteLine(
|
||||||
|
"Error: --size WxL is required when importing DXF files without a nest"
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +284,7 @@ static class NestConsole
|
|||||||
|
|
||||||
foreach (var dxf in dxfFiles)
|
foreach (var dxf in dxfFiles)
|
||||||
{
|
{
|
||||||
var drawing = ImportDxf(dxf);
|
var drawing = ImportDxf(dxf, options);
|
||||||
|
|
||||||
if (drawing == null)
|
if (drawing == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -216,11 +296,28 @@ static class NestConsole
|
|||||||
return newNest;
|
return newNest;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Drawing ImportDxf(string path)
|
static Drawing ImportDxf(string path, Options options)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return CadImporter.ImportDrawing(path);
|
var result = CadImporter.Import(
|
||||||
|
path,
|
||||||
|
new CadImportOptions
|
||||||
|
{
|
||||||
|
BendRepair = options.RepairBendsMillimeters.HasValue
|
||||||
|
? new BendRepairOptions
|
||||||
|
{
|
||||||
|
DrawingUnits = options.CadUnits,
|
||||||
|
MaxEndpointMovementMillimeters = options.RepairBendsMillimeters.Value,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
foreach (var report in result.BendRepairReports)
|
||||||
|
Console.WriteLine(
|
||||||
|
$"Bend repair {Path.GetFileName(path)} #{report.BendIndex + 1}: {report.Status}: {report.Reason} ({report.OriginalStart} -> {report.Start}; {report.OriginalEnd} -> {report.End})"
|
||||||
|
);
|
||||||
|
return CadImporter.BuildDrawing(result, result.Entities, result.Bends, 1, null, null);
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
@@ -256,7 +353,8 @@ static class NestConsole
|
|||||||
// Only apply size override when it wasn't already used to create the plate.
|
// Only apply size override when it wasn't already used to create the plate.
|
||||||
var hasDxfOnly = !options.InputFiles.Any(f =>
|
var hasDxfOnly = !options.InputFiles.Any(f =>
|
||||||
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||||
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
|
||||||
if (options.PlateSize.HasValue && !hasDxfOnly)
|
if (options.PlateSize.HasValue && !hasDxfOnly)
|
||||||
plate.Size = options.PlateSize.Value;
|
plate.Size = options.PlateSize.Value;
|
||||||
@@ -264,33 +362,51 @@ static class NestConsole
|
|||||||
|
|
||||||
static Drawing ResolveDrawing(Nest nest, Options options)
|
static Drawing ResolveDrawing(Nest nest, Options options)
|
||||||
{
|
{
|
||||||
var drawing = options.DrawingName != null
|
var drawing =
|
||||||
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
|
options.DrawingName != null
|
||||||
: nest.Drawings.FirstOrDefault();
|
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
|
||||||
|
: nest.Drawings.FirstOrDefault();
|
||||||
|
|
||||||
if (drawing != null)
|
if (drawing != null)
|
||||||
return drawing;
|
return drawing;
|
||||||
|
|
||||||
Console.Error.WriteLine(options.DrawingName != null
|
Console.Error.WriteLine(
|
||||||
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
options.DrawingName != null
|
||||||
: "Error: nest file contains no drawings");
|
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
||||||
|
: "Error: nest file contains no drawings"
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options)
|
static void PrintHeader(
|
||||||
|
Nest nest,
|
||||||
|
Plate plate,
|
||||||
|
Drawing drawing,
|
||||||
|
int existingCount,
|
||||||
|
Options options
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Nest: {nest.Name}");
|
Console.WriteLine($"Nest: {nest.Name}");
|
||||||
var wa = plate.WorkArea();
|
var wa = plate.WorkArea();
|
||||||
Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}");
|
Console.WriteLine(
|
||||||
|
$"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}"
|
||||||
|
);
|
||||||
Console.WriteLine($"Drawing: {drawing.Name}");
|
Console.WriteLine($"Drawing: {drawing.Name}");
|
||||||
Console.WriteLine(options.KeepParts
|
Console.WriteLine(
|
||||||
? $"Keeping {existingCount} existing parts"
|
options.KeepParts
|
||||||
: $"Cleared {existingCount} existing parts");
|
? $"Keeping {existingCount} existing parts"
|
||||||
|
: $"Cleared {existingCount} existing parts"
|
||||||
|
);
|
||||||
Console.WriteLine("---");
|
Console.WriteLine("---");
|
||||||
}
|
}
|
||||||
|
|
||||||
static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options)
|
static (bool success, long elapsedMs) Fill(
|
||||||
|
Nest nest,
|
||||||
|
Plate plate,
|
||||||
|
Drawing drawing,
|
||||||
|
Options options
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
bool success;
|
bool success;
|
||||||
@@ -310,33 +426,113 @@ static class NestConsole
|
|||||||
nestItems.Add(new NestItem { Drawing = d, Quantity = qty });
|
nestItems.Add(new NestItem { Drawing = d, Quantity = qty });
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
Console.WriteLine(
|
||||||
|
$"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts"
|
||||||
|
);
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
success = AutoNestJob(plate, nestItems, options.Engine);
|
||||||
var nestParts = engine.Nest(nestItems, null, CancellationToken.None);
|
|
||||||
plate.Parts.AddRange(nestParts);
|
|
||||||
success = nestParts.Count > 0;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
// Single-plate fill: explicit placement strategy through the public service;
|
||||||
|
// the process-global engine registry is never consulted.
|
||||||
|
var strategy = ResolveFillStrategy(options.Engine);
|
||||||
var item = new NestItem { Drawing = drawing, Quantity = options.Quantity };
|
var item = new NestItem { Drawing = drawing, Quantity = options.Quantity };
|
||||||
success = engine.Fill(item);
|
var parts = PlateFillService.FillItem(
|
||||||
|
strategy,
|
||||||
|
plate,
|
||||||
|
item,
|
||||||
|
plate.WorkArea(),
|
||||||
|
null,
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parts.Count > 0)
|
||||||
|
plate.Parts.AddRange(parts);
|
||||||
|
success = parts.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
return (success, sw.ElapsedMilliseconds);
|
return (success, sw.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves the drawings as one whole job against this single plate using the named jobs
|
||||||
|
/// engine, then commits the returned placements onto the plate. Placements are mapped back
|
||||||
|
/// onto the caller's original drawings (same pose semantics as NestResultMaterializer), so
|
||||||
|
/// the saved nest keeps its existing drawing identities.
|
||||||
|
/// </summary>
|
||||||
|
static bool AutoNestJob(Plate plate, List<NestItem> nestItems, string engineName)
|
||||||
|
{
|
||||||
|
var engine = NestingEngineRegistry.Create(engineName);
|
||||||
|
|
||||||
|
var parts = new List<NestJobPart>(nestItems.Count);
|
||||||
|
var drawingsByPartId = new Dictionary<string, Drawing>(StringComparer.Ordinal);
|
||||||
|
for (var i = 0; i < nestItems.Count; i++)
|
||||||
|
{
|
||||||
|
var partId = $"part-{i}";
|
||||||
|
parts.Add(DrawingJobMapper.FromItem(partId, nestItems[i]));
|
||||||
|
drawingsByPartId[partId] = nestItems[i].Drawing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One physical sheet: this plate, this solve — the runner owns stock accounting.
|
||||||
|
var stock = DrawingJobMapper.FromPlate("plate-0", plate, 1);
|
||||||
|
var job = new NestJob(parts, [stock]);
|
||||||
|
|
||||||
|
var result = engine.Solve(job, null, CancellationToken.None);
|
||||||
|
|
||||||
|
var committed = 0;
|
||||||
|
foreach (var plateResult in result.Plates)
|
||||||
|
{
|
||||||
|
foreach (var pose in plateResult.Placements)
|
||||||
|
{
|
||||||
|
if (!drawingsByPartId.TryGetValue(pose.PartId, out var drawing))
|
||||||
|
continue;
|
||||||
|
var part = new Part(drawing);
|
||||||
|
part.Rotate(pose.Rotation);
|
||||||
|
part.Location = new Vector(pose.X, pose.Y);
|
||||||
|
part.UpdateBounds();
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
committed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Engine: {engineName} — committed {committed} placements");
|
||||||
|
return committed > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ResolveFillStrategy(string engineName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return PlateFillService.ResolveStrategy(engineName);
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
var isJobEngine = NestingEngineRegistry.AvailableEngines.Any(e =>
|
||||||
|
e.Name.Equals(engineName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
isJobEngine
|
||||||
|
? $"Error: engine '{engineName}' is a whole-job engine; single-plate fill supports: {string.Join(", ", PlateFillService.BuiltInStrategies)}. Use --autonest for whole-job engines."
|
||||||
|
: $"Error: unknown engine '{engineName}'. Engines: {string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))}"
|
||||||
|
);
|
||||||
|
Environment.Exit(1);
|
||||||
|
throw; // unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int CheckOverlaps(Plate plate, Options options)
|
static int CheckOverlaps(Plate plate, Options options)
|
||||||
{
|
{
|
||||||
if (!options.CheckOverlaps || plate.Parts.Count == 0)
|
if (!options.CheckOverlaps || plate.Parts.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
||||||
Console.WriteLine(hasOverlaps
|
Console.WriteLine(
|
||||||
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
hasOverlaps
|
||||||
: "Overlap check: PASS");
|
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
||||||
|
: "Overlap check: PASS"
|
||||||
|
);
|
||||||
|
|
||||||
return overlapPts.Count;
|
return overlapPts.Count;
|
||||||
}
|
}
|
||||||
@@ -355,9 +551,12 @@ static class NestConsole
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var firstInput = options.InputFiles[0];
|
var firstInput = options.InputFiles[0];
|
||||||
var outputFile = options.OutputFile ?? Path.Combine(
|
var outputFile =
|
||||||
Path.GetDirectoryName(firstInput),
|
options.OutputFile
|
||||||
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}");
|
?? Path.Combine(
|
||||||
|
Path.GetDirectoryName(firstInput),
|
||||||
|
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}"
|
||||||
|
);
|
||||||
|
|
||||||
new NestWriter(nest).Write(outputFile);
|
new NestWriter(nest).Write(outputFile);
|
||||||
Console.WriteLine($"Saved: {outputFile}");
|
Console.WriteLine($"Saved: {outputFile}");
|
||||||
@@ -368,8 +567,8 @@ static class NestConsole
|
|||||||
if (options.PostsDir != null)
|
if (options.PostsDir != null)
|
||||||
return options.PostsDir;
|
return options.PostsDir;
|
||||||
|
|
||||||
var exePath = Assembly.GetEntryAssembly()?.Location
|
var exePath =
|
||||||
?? typeof(NestConsole).Assembly.Location;
|
Assembly.GetEntryAssembly()?.Location ?? typeof(NestConsole).Assembly.Location;
|
||||||
return Path.Combine(Path.GetDirectoryName(exePath), "Posts");
|
return Path.Combine(Path.GetDirectoryName(exePath), "Posts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,7 +587,11 @@ static class NestConsole
|
|||||||
|
|
||||||
foreach (var type in assembly.GetTypes())
|
foreach (var type in assembly.GetTypes())
|
||||||
{
|
{
|
||||||
if (!typeof(IPostProcessor).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract)
|
if (
|
||||||
|
!typeof(IPostProcessor).IsAssignableFrom(type)
|
||||||
|
|| type.IsInterface
|
||||||
|
|| type.IsAbstract
|
||||||
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (Activator.CreateInstance(type) is IPostProcessor processor)
|
if (Activator.CreateInstance(type) is IPostProcessor processor)
|
||||||
@@ -397,7 +600,9 @@ static class NestConsole
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}");
|
Console.Error.WriteLine(
|
||||||
|
$"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +623,7 @@ static class NestConsole
|
|||||||
Console.WriteLine($"Post processors ({postsDir}):");
|
Console.WriteLine($"Post processors ({postsDir}):");
|
||||||
|
|
||||||
foreach (var p in processors)
|
foreach (var p in processors)
|
||||||
Console.WriteLine($" {p.Name,-30} {p.Description}");
|
Console.WriteLine($" {p.Name, -30} {p.Description}");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PostProcess(Nest nest, Options options)
|
static void PostProcess(Nest nest, Options options)
|
||||||
@@ -429,14 +634,17 @@ static class NestConsole
|
|||||||
var postsDir = ResolvePostsDir(options);
|
var postsDir = ResolvePostsDir(options);
|
||||||
var processors = LoadPostProcessors(postsDir);
|
var processors = LoadPostProcessors(postsDir);
|
||||||
var post = processors.FirstOrDefault(p =>
|
var post = processors.FirstOrDefault(p =>
|
||||||
p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase));
|
p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
|
||||||
if (post == null)
|
if (post == null)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found");
|
Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found");
|
||||||
|
|
||||||
if (processors.Count > 0)
|
if (processors.Count > 0)
|
||||||
Console.Error.WriteLine($"Available: {string.Join(", ", processors.Select(p => p.Name))}");
|
Console.Error.WriteLine(
|
||||||
|
$"Available: {string.Join(", ", processors.Select(p => p.Name))}"
|
||||||
|
);
|
||||||
else
|
else
|
||||||
Console.Error.WriteLine($"No post processors found in: {postsDir}");
|
Console.Error.WriteLine($"No post processors found in: {postsDir}");
|
||||||
|
|
||||||
@@ -450,7 +658,8 @@ static class NestConsole
|
|||||||
var firstInput = options.InputFiles[0];
|
var firstInput = options.InputFiles[0];
|
||||||
outputFile = Path.Combine(
|
outputFile = Path.Combine(
|
||||||
Path.GetDirectoryName(firstInput),
|
Path.GetDirectoryName(firstInput),
|
||||||
$"{Path.GetFileNameWithoutExtension(firstInput)}.cnc");
|
$"{Path.GetFileNameWithoutExtension(firstInput)}.cnc"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
post.Post(nest, outputFile);
|
post.Post(nest, outputFile);
|
||||||
@@ -462,28 +671,64 @@ static class NestConsole
|
|||||||
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Arguments:");
|
Console.Error.WriteLine("Arguments:");
|
||||||
Console.Error.WriteLine(" input-files One or more .nest nest files or .dxf/.dwg drawing files");
|
Console.Error.WriteLine(
|
||||||
|
" input-files One or more .nest nest files or .dxf/.dwg drawing files"
|
||||||
|
);
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Modes:");
|
Console.Error.WriteLine("Modes:");
|
||||||
Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)");
|
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(" <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(
|
||||||
|
" <nest.nest> <part.dxf> Load nest and add imported DXF drawings"
|
||||||
|
);
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Options:");
|
Console.Error.WriteLine("Options:");
|
||||||
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
Console.Error.WriteLine(
|
||||||
|
" --repair-bends-mm <n> Opt-in endpoint/tick repair, limit >0.001 to 3.175 physical mm"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --cad-units inches|mm Explicit source coordinate units required for bend repair"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --drawing <name> Drawing name to fill with (default: first drawing)"
|
||||||
|
);
|
||||||
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
||||||
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
Console.Error.WriteLine(
|
||||||
|
" --quantity <n> Max parts to place (default: 0 = unlimited)"
|
||||||
|
);
|
||||||
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
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(
|
||||||
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.nest)");
|
" --size <WxL> Override plate size (e.g. 60x120); required for DXF-only mode"
|
||||||
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(
|
||||||
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
" --output <path> Output nest file path (default: <input>-result.nest)"
|
||||||
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --autonest Whole-job nesting via the jobs engine (--engine) instead of single-plate fill"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --engine <name> With --autonest: jobs engine (default: Default; also StockLadder, Strip, ...)."
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" Without --autonest: fill strategy (Default, Strip, Vertical Remnant, Horizontal Remnant)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --keep-parts Don't clear existing parts before filling"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --check-overlaps Run overlap detection after fill (exit code 1 if found)"
|
||||||
|
);
|
||||||
Console.Error.WriteLine(" --no-save Skip saving output file");
|
Console.Error.WriteLine(" --no-save Skip saving output file");
|
||||||
Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
|
Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
|
||||||
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)");
|
Console.Error.WriteLine(
|
||||||
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)");
|
" --post-output <path> Output file for post processor (default: <input>.cnc)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)"
|
||||||
|
);
|
||||||
Console.Error.WriteLine(" --list-posts List available post processors and exit");
|
Console.Error.WriteLine(" --list-posts List available post processors and exit");
|
||||||
Console.Error.WriteLine(" -h, --help Show this help");
|
Console.Error.WriteLine(" -h, --help Show this help");
|
||||||
}
|
}
|
||||||
@@ -501,10 +746,13 @@ static class NestConsole
|
|||||||
public bool NoSave;
|
public bool NoSave;
|
||||||
public bool KeepParts;
|
public bool KeepParts;
|
||||||
public bool AutoNest;
|
public bool AutoNest;
|
||||||
|
public string Engine = "Default";
|
||||||
public string TemplateFile;
|
public string TemplateFile;
|
||||||
public string PostName;
|
public string PostName;
|
||||||
public string PostOutput;
|
public string PostOutput;
|
||||||
public string PostsDir;
|
public string PostsDir;
|
||||||
public bool ListPosts;
|
public bool ListPosts;
|
||||||
|
public double? RepairBendsMillimeters;
|
||||||
|
public BendRepairUnits CadUnits;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-12
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -7,7 +7,10 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
public static void Vertically(Entity fixedEntity, Entity movableEntity)
|
public static void Vertically(Entity fixedEntity, Entity movableEntity)
|
||||||
{
|
{
|
||||||
movableEntity.Offset(fixedEntity.BoundingBox.Center.X - movableEntity.BoundingBox.Center.X, 0);
|
movableEntity.Offset(
|
||||||
|
fixedEntity.BoundingBox.Center.X - movableEntity.BoundingBox.Center.X,
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Vertically(Entity fixedEntity, List<Entity> entities)
|
public static void Vertically(Entity fixedEntity, List<Entity> entities)
|
||||||
@@ -17,7 +20,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static void Vertically(Part fixedPart, Part movablePart)
|
public static void Vertically(Part fixedPart, Part movablePart)
|
||||||
{
|
{
|
||||||
movablePart.Offset(fixedPart.BoundingBox.Center.X - movablePart.BoundingBox.Center.X, 0);
|
movablePart.Offset(
|
||||||
|
fixedPart.BoundingBox.Center.X - movablePart.BoundingBox.Center.X,
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Vertically(Part fixedPart, List<Part> parts)
|
public static void Vertically(Part fixedPart, List<Part> parts)
|
||||||
@@ -27,7 +33,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static void Horizontally(Entity fixedEntity, Entity movableEntity)
|
public static void Horizontally(Entity fixedEntity, Entity movableEntity)
|
||||||
{
|
{
|
||||||
movableEntity.Offset(0, fixedEntity.BoundingBox.Center.Y - movableEntity.BoundingBox.Center.Y);
|
movableEntity.Offset(
|
||||||
|
0,
|
||||||
|
fixedEntity.BoundingBox.Center.Y - movableEntity.BoundingBox.Center.Y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Horizontally(Entity fixedEntity, List<Entity> entities)
|
public static void Horizontally(Entity fixedEntity, List<Entity> entities)
|
||||||
@@ -37,7 +46,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static void Horizontally(Part fixedPart, Part movablePart)
|
public static void Horizontally(Part fixedPart, Part movablePart)
|
||||||
{
|
{
|
||||||
movablePart.Offset(0, fixedPart.BoundingBox.Center.Y - movablePart.BoundingBox.Center.Y);
|
movablePart.Offset(
|
||||||
|
0,
|
||||||
|
fixedPart.BoundingBox.Center.Y - movablePart.BoundingBox.Center.Y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Horizontally(Part fixedPart, List<Part> parts)
|
public static void Horizontally(Part fixedPart, List<Part> parts)
|
||||||
@@ -67,7 +79,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static void Right(Entity fixedEntity, Entity movableEntity)
|
public static void Right(Entity fixedEntity, Entity movableEntity)
|
||||||
{
|
{
|
||||||
movableEntity.Offset(fixedEntity.BoundingBox.Right - movableEntity.BoundingBox.Right, 0);
|
movableEntity.Offset(
|
||||||
|
fixedEntity.BoundingBox.Right - movableEntity.BoundingBox.Right,
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Right(Entity fixedEntity, List<Entity> entities)
|
public static void Right(Entity fixedEntity, List<Entity> entities)
|
||||||
@@ -107,7 +122,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static void Bottom(Entity fixedEntity, Entity movableEntity)
|
public static void Bottom(Entity fixedEntity, Entity movableEntity)
|
||||||
{
|
{
|
||||||
movableEntity.Offset(0, fixedEntity.BoundingBox.Bottom - movableEntity.BoundingBox.Bottom);
|
movableEntity.Offset(
|
||||||
|
0,
|
||||||
|
fixedEntity.BoundingBox.Bottom - movableEntity.BoundingBox.Bottom
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Bottom(Entity fixedEntity, List<Entity> entities)
|
public static void Bottom(Entity fixedEntity, List<Entity> entities)
|
||||||
@@ -137,14 +155,19 @@ namespace OpenNest
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var list = new List<Part>(parts);
|
var list = new List<Part>(parts);
|
||||||
list.Sort((p1, p2) => horizontal
|
list.Sort(
|
||||||
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
|
(p1, p2) =>
|
||||||
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y));
|
horizontal
|
||||||
|
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
|
||||||
|
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y)
|
||||||
|
);
|
||||||
|
|
||||||
var lastIndex = list.Count - 1;
|
var lastIndex = list.Count - 1;
|
||||||
|
|
||||||
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
|
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
|
||||||
var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y;
|
var end = horizontal
|
||||||
|
? list[lastIndex].BoundingBox.Center.X
|
||||||
|
: list[lastIndex].BoundingBox.Center.Y;
|
||||||
|
|
||||||
var spacing = (end - start) / lastIndex;
|
var spacing = (end - start) / lastIndex;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
namespace OpenNest
|
||||||
namespace OpenNest
|
|
||||||
{
|
{
|
||||||
public enum AlignType
|
public enum AlignType
|
||||||
{
|
{
|
||||||
@@ -10,6 +9,6 @@ namespace OpenNest
|
|||||||
Horizontally,
|
Horizontally,
|
||||||
Vertically,
|
Vertically,
|
||||||
EvenlySpaceHorizontally,
|
EvenlySpaceHorizontally,
|
||||||
EvenlySpaceVertically
|
EvenlySpaceVertically,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Bending
|
namespace OpenNest.Bending
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,7 @@ namespace OpenNest.Bending
|
|||||||
public static readonly Layer EtchLayer = new Layer("ETCH")
|
public static readonly Layer EtchLayer = new Layer("ETCH")
|
||||||
{
|
{
|
||||||
Color = Color.Green,
|
Color = Color.Green,
|
||||||
IsVisible = true
|
IsVisible = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private const double DefaultEtchLength = 1.0;
|
private const double DefaultEtchLength = 1.0;
|
||||||
@@ -32,9 +32,8 @@ namespace OpenNest.Bending
|
|||||||
|
|
||||||
public double Length => StartPoint.DistanceTo(EndPoint);
|
public double Length => StartPoint.DistanceTo(EndPoint);
|
||||||
|
|
||||||
public double AngleRadians => Angle.HasValue
|
public double AngleRadians =>
|
||||||
? OpenNest.Math.Angle.ToRadians(Angle.Value)
|
Angle.HasValue ? OpenNest.Math.Angle.ToRadians(Angle.Value) : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
public Line ToLine() => new Line(StartPoint, EndPoint);
|
public Line ToLine() => new Line(StartPoint, EndPoint);
|
||||||
|
|
||||||
@@ -66,7 +65,9 @@ namespace OpenNest.Bending
|
|||||||
var dx = System.Math.Cos(angle) * etchLength;
|
var dx = System.Math.Cos(angle) * etchLength;
|
||||||
var dy = System.Math.Sin(angle) * etchLength;
|
var dy = System.Math.Sin(angle) * etchLength;
|
||||||
|
|
||||||
result.Add(CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy)));
|
result.Add(
|
||||||
|
CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy))
|
||||||
|
);
|
||||||
result.Add(CreateEtchLine(new Vector(EndPoint.X - dx, EndPoint.Y - dy), EndPoint));
|
result.Add(CreateEtchLine(new Vector(EndPoint.X - dx, EndPoint.Y - dy), EndPoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +80,8 @@ namespace OpenNest.Bending
|
|||||||
public static void UpdateEtchEntities(List<Entity> entities, List<Bend> bends)
|
public static void UpdateEtchEntities(List<Entity> entities, List<Bend> bends)
|
||||||
{
|
{
|
||||||
entities.RemoveAll(e => e.Tag == BendEtchTag);
|
entities.RemoveAll(e => e.Tag == BendEtchTag);
|
||||||
if (bends == null) return;
|
if (bends == null)
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (var bend in bends)
|
foreach (var bend in bends)
|
||||||
entities.AddRange(bend.GetEtchEntities());
|
entities.AddRange(bend.GetEtchEntities());
|
||||||
@@ -87,7 +89,12 @@ namespace OpenNest.Bending
|
|||||||
|
|
||||||
private static Line CreateEtchLine(Vector start, Vector end)
|
private static Line CreateEtchLine(Vector start, Vector end)
|
||||||
{
|
{
|
||||||
return new Line(start, end) { Layer = EtchLayer, Color = Color.Green, Tag = BendEtchTag };
|
return new Line(start, end)
|
||||||
|
{
|
||||||
|
Layer = EtchLayer,
|
||||||
|
Color = Color.Green,
|
||||||
|
Tag = BendEtchTag,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ namespace OpenNest.Bending
|
|||||||
{
|
{
|
||||||
Unknown,
|
Unknown,
|
||||||
Up,
|
Up,
|
||||||
Down
|
Down,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
public class ArcMove : Motion
|
public class ArcMove : Motion
|
||||||
{
|
{
|
||||||
public ArcMove()
|
public ArcMove() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArcMove(double x, double y, double i, double j, RotationType rotation = RotationType.CCW)
|
public ArcMove(
|
||||||
: this(new Vector(x, y), new Vector(i, j), rotation)
|
double x,
|
||||||
{
|
double y,
|
||||||
}
|
double i,
|
||||||
|
double j,
|
||||||
|
RotationType rotation = RotationType.CCW
|
||||||
|
)
|
||||||
|
: this(new Vector(x, y), new Vector(i, j), rotation) { }
|
||||||
|
|
||||||
public ArcMove(Vector endPoint, Vector centerPoint, RotationType rotation = RotationType.CCW)
|
public ArcMove(
|
||||||
|
Vector endPoint,
|
||||||
|
Vector centerPoint,
|
||||||
|
RotationType rotation = RotationType.CCW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
EndPoint = endPoint;
|
EndPoint = endPoint;
|
||||||
CenterPoint = centerPoint;
|
CenterPoint = centerPoint;
|
||||||
@@ -68,7 +74,8 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
Layer = Layer,
|
Layer = Layer,
|
||||||
Suppressed = Suppressed,
|
Suppressed = Suppressed,
|
||||||
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
VariableRefs =
|
||||||
|
VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +92,9 @@ namespace OpenNest.CNC
|
|||||||
var i = CenterPoint.X.ToString(dp);
|
var i = CenterPoint.X.ToString(dp);
|
||||||
var j = CenterPoint.Y.ToString(dp);
|
var j = CenterPoint.Y.ToString(dp);
|
||||||
|
|
||||||
return Rotation == RotationType.CW ?
|
return Rotation == RotationType.CW
|
||||||
string.Format("G02 X{0} Y{1} I{2} J{3}", x, y, i, j) :
|
? string.Format("G02 X{0} Y{1} I{2} J{3}", x, y, i, j)
|
||||||
string.Format("G03 X{0} Y{1} I{2} J{3}", x, y, i, j);
|
: string.Format("G03 X{0} Y{1} I{2} J{3}", x, y, i, j);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
namespace OpenNest.CNC
|
||||||
namespace OpenNest.CNC
|
|
||||||
{
|
{
|
||||||
public enum CodeType
|
public enum CodeType
|
||||||
{
|
{
|
||||||
@@ -9,6 +8,6 @@ namespace OpenNest.CNC
|
|||||||
RapidMove,
|
RapidMove,
|
||||||
SetFeedrate,
|
SetFeedrate,
|
||||||
SetKerf,
|
SetKerf,
|
||||||
SubProgramCall
|
SubProgramCall,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class Comment : ICode
|
public class Comment : ICode
|
||||||
{
|
{
|
||||||
public Comment()
|
public Comment() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Comment(string value)
|
public Comment(string value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using OpenNest.Math;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
for (var iter = 0; iter < 3; iter++)
|
for (var iter = 0; iter < 3; iter++)
|
||||||
{
|
{
|
||||||
var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point;
|
var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point;
|
||||||
perimeterSeed = FindPerimeterIntersection(profile.Perimeter, lastCutoutPt, nextPartStart, out _);
|
perimeterSeed = FindPerimeterIntersection(
|
||||||
|
profile.Perimeter,
|
||||||
|
lastCutoutPt,
|
||||||
|
nextPartStart,
|
||||||
|
out _
|
||||||
|
);
|
||||||
|
|
||||||
orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed);
|
orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed);
|
||||||
orderedCutouts.Reverse();
|
orderedCutouts.Reverse();
|
||||||
@@ -56,7 +61,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
}
|
}
|
||||||
|
|
||||||
var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point;
|
var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point;
|
||||||
perimeterPt = FindPerimeterIntersection(profile.Perimeter, finalLastCutout, nextPartStart, out perimeterEntity);
|
perimeterPt = FindPerimeterIntersection(
|
||||||
|
profile.Perimeter,
|
||||||
|
finalLastCutout,
|
||||||
|
nextPartStart,
|
||||||
|
out perimeterEntity
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -79,18 +89,25 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
if (!profile.Perimeter.IsClosed())
|
if (!profile.Perimeter.IsClosed())
|
||||||
EmitRawContour(result, profile.Perimeter);
|
EmitRawContour(result, profile.Perimeter);
|
||||||
else
|
else
|
||||||
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
EmitContour(
|
||||||
|
result,
|
||||||
|
profile.Perimeter,
|
||||||
|
perimeterPt,
|
||||||
|
perimeterEntity,
|
||||||
|
ContourType.External
|
||||||
|
);
|
||||||
|
|
||||||
result.Mode = Mode.Incremental;
|
result.Mode = Mode.Incremental;
|
||||||
|
|
||||||
return new CuttingResult
|
return new CuttingResult { Program = result, LastCutPoint = perimeterPt };
|
||||||
{
|
|
||||||
Program = result,
|
|
||||||
LastCutPoint = perimeterPt
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CuttingResult ApplySingle(Program partProgram, Vector point, Entity entity, ContourType contourType)
|
public CuttingResult ApplySingle(
|
||||||
|
Program partProgram,
|
||||||
|
Vector point,
|
||||||
|
Entity entity,
|
||||||
|
ContourType contourType
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
@@ -141,14 +158,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
result.Mode = Mode.Incremental;
|
result.Mode = Mode.Incremental;
|
||||||
|
|
||||||
return new CuttingResult
|
return new CuttingResult { Program = result, LastCutPoint = point };
|
||||||
{
|
|
||||||
Program = result,
|
|
||||||
LastCutPoint = point
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (Shape Shape, Entity Entity) FindTargetShape(ShapeProfile profile, Vector point, Entity clickedEntity)
|
private static (Shape Shape, Entity Entity) FindTargetShape(
|
||||||
|
ShapeProfile profile,
|
||||||
|
Vector point,
|
||||||
|
Entity clickedEntity
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var matched = FindMatchingEntity(profile.Perimeter, clickedEntity);
|
var matched = FindMatchingEntity(profile.Perimeter, clickedEntity);
|
||||||
if (matched != null)
|
if (matched != null)
|
||||||
@@ -190,20 +207,26 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
if (shapeEntity is Line sLine && clickedEntity is Line cLine)
|
if (shapeEntity is Line sLine && clickedEntity is Line cLine)
|
||||||
{
|
{
|
||||||
if (sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon
|
if (
|
||||||
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon)
|
sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon
|
||||||
|
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon
|
||||||
|
)
|
||||||
return shapeEntity;
|
return shapeEntity;
|
||||||
}
|
}
|
||||||
else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc)
|
else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc)
|
||||||
{
|
{
|
||||||
if (System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon
|
if (
|
||||||
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon)
|
System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon
|
||||||
|
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon
|
||||||
|
)
|
||||||
return shapeEntity;
|
return shapeEntity;
|
||||||
}
|
}
|
||||||
else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle)
|
else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle)
|
||||||
{
|
{
|
||||||
if (System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon
|
if (
|
||||||
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon)
|
System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon
|
||||||
|
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon
|
||||||
|
)
|
||||||
return shapeEntity;
|
return shapeEntity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +241,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint));
|
program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
private static List<ContourEntry> ResolveLeadInPoints(
|
||||||
|
List<Shape> cutouts,
|
||||||
|
Vector startPoint
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var entries = new ContourEntry[cutouts.Count];
|
var entries = new ContourEntry[cutouts.Count];
|
||||||
var currentPoint = startPoint;
|
var currentPoint = startPoint;
|
||||||
@@ -235,7 +261,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ContourEntry>(entries);
|
return new List<ContourEntry>(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector FindPerimeterIntersection(Shape perimeter, Vector lastCutout, Vector nextPartStart, out Entity entity)
|
private static Vector FindPerimeterIntersection(
|
||||||
|
Shape perimeter,
|
||||||
|
Vector lastCutout,
|
||||||
|
Vector nextPartStart,
|
||||||
|
out Entity entity
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var ray = new Line(lastCutout, nextPartStart);
|
var ray = new Line(lastCutout, nextPartStart);
|
||||||
|
|
||||||
@@ -269,7 +300,13 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return HashCode.Combine(r, a);
|
return HashCode.Combine(r, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
private void EmitContour(
|
||||||
|
Program program,
|
||||||
|
Shape shape,
|
||||||
|
Vector point,
|
||||||
|
Entity entity,
|
||||||
|
ContourType? forceType = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var contourType = forceType ?? DetectContourType(shape);
|
var contourType = forceType ?? DetectContourType(shape);
|
||||||
var winding = DetermineWinding(shape);
|
var winding = DetermineWinding(shape);
|
||||||
@@ -289,7 +326,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var outwardAngle = normal - System.Math.PI;
|
var outwardAngle = normal - System.Math.PI;
|
||||||
point = new Vector(
|
point = new Vector(
|
||||||
circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle),
|
circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle),
|
||||||
circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle));
|
circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
||||||
@@ -297,7 +335,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
// Build hole sub-program relative to (0,0)
|
// Build hole sub-program relative to (0,0)
|
||||||
var holeCenter = circle.Center;
|
var holeCenter = circle.Center;
|
||||||
var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y);
|
var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y);
|
||||||
var relativeCircle = new Circle(new Vector(0, 0), circle.Radius) { Rotation = circle.Rotation };
|
var relativeCircle = new Circle(new Vector(0, 0), circle.Radius)
|
||||||
|
{
|
||||||
|
Rotation = circle.Rotation,
|
||||||
|
};
|
||||||
var relativeShape = new Shape();
|
var relativeShape = new Shape();
|
||||||
relativeShape.Entities.Add(relativeCircle);
|
relativeShape.Entities.Add(relativeCircle);
|
||||||
|
|
||||||
@@ -314,12 +355,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
if (!program.SubPrograms.ContainsKey(key))
|
if (!program.SubPrograms.ContainsKey(key))
|
||||||
program.SubPrograms[key] = subPgm;
|
program.SubPrograms[key] = subPgm;
|
||||||
|
|
||||||
program.Codes.Add(new SubProgramCall
|
program.Codes.Add(
|
||||||
{
|
new SubProgramCall
|
||||||
Id = key,
|
{
|
||||||
Program = program.SubPrograms[key],
|
Id = key,
|
||||||
Offset = holeCenter
|
Program = program.SubPrograms[key],
|
||||||
});
|
Offset = holeCenter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -328,7 +371,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
var reindexedShape = shape.ReindexAt(point, entity);
|
var reindexedShape = shape.ReindexAt(point, entity);
|
||||||
|
|
||||||
if (Parameters.TabsEnabled && Parameters.TabConfig != null && contourType == ContourType.External)
|
if (
|
||||||
|
Parameters.TabsEnabled
|
||||||
|
&& Parameters.TabConfig != null
|
||||||
|
&& contourType == ContourType.External
|
||||||
|
)
|
||||||
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
||||||
@@ -337,7 +384,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
private void EmitScribeContours(Program program, List<Entity> scribeEntities)
|
private void EmitScribeContours(Program program, List<Entity> scribeEntities)
|
||||||
{
|
{
|
||||||
if (scribeEntities.Count == 0) return;
|
if (scribeEntities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
var shapes = ShapeBuilder.GetShapes(scribeEntities);
|
var shapes = ShapeBuilder.GetShapes(scribeEntities);
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
@@ -388,8 +436,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return ContourType.Internal;
|
return ContourType.Internal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType,
|
public static double ComputeNormal(
|
||||||
RotationType winding = RotationType.CW)
|
Vector point,
|
||||||
|
Entity entity,
|
||||||
|
ContourType contourType,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
double normal;
|
double normal;
|
||||||
|
|
||||||
@@ -442,7 +494,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return polygon.RotationDirection();
|
return polygon.RotationDirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
private LeadIn ClampLeadInForCircle(
|
||||||
|
LeadIn leadIn,
|
||||||
|
Circle circle,
|
||||||
|
Vector contourPoint,
|
||||||
|
double normalAngle
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
|
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
|
||||||
return leadIn;
|
return leadIn;
|
||||||
@@ -492,7 +549,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn,
|
ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn,
|
||||||
ContourType.Internal => Parameters.InternalLeadIn,
|
ContourType.Internal => Parameters.InternalLeadIn,
|
||||||
_ => Parameters.ExternalLeadIn
|
_ => Parameters.ExternalLeadIn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +559,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut,
|
ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut,
|
||||||
ContourType.Internal => Parameters.InternalLeadOut,
|
ContourType.Internal => Parameters.InternalLeadOut,
|
||||||
_ => Parameters.ExternalLeadOut
|
_ => Parameters.ExternalLeadOut,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,12 +622,18 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
private static Vector EntityStartPoint(Entity entity)
|
private static Vector EntityStartPoint(Entity entity)
|
||||||
{
|
{
|
||||||
if (entity is Line line) return line.StartPoint;
|
if (entity is Line line)
|
||||||
if (entity is Arc arc) return arc.StartPoint();
|
return line.StartPoint;
|
||||||
|
if (entity is Arc arc)
|
||||||
|
return arc.StartPoint();
|
||||||
return Vector.Zero;
|
return Vector.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display)
|
private List<ICode> ConvertShapeToMoves(
|
||||||
|
Shape shape,
|
||||||
|
Vector startPoint,
|
||||||
|
LayerType layer = LayerType.Display
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var moves = new List<ICode>();
|
var moves = new List<ICode>();
|
||||||
|
|
||||||
@@ -582,15 +645,28 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
}
|
}
|
||||||
else if (entity is Arc arc)
|
else if (entity is Arc arc)
|
||||||
{
|
{
|
||||||
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer });
|
moves.Add(
|
||||||
|
new ArcMove(
|
||||||
|
arc.EndPoint(),
|
||||||
|
arc.Center,
|
||||||
|
arc.IsReversed ? RotationType.CW : RotationType.CCW
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Layer = layer,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (entity is Circle circle)
|
else if (entity is Circle circle)
|
||||||
{
|
{
|
||||||
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer });
|
moves.Add(
|
||||||
|
new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}");
|
throw new System.InvalidOperationException(
|
||||||
|
$"Unsupported entity type: {entity.Type}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,9 +676,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
private static Vector GetShapeStartPoint(Shape shape)
|
private static Vector GetShapeStartPoint(Shape shape)
|
||||||
{
|
{
|
||||||
var first = shape.Entities[0];
|
var first = shape.Entities[0];
|
||||||
if (first is Line line) return line.StartPoint;
|
if (first is Line line)
|
||||||
if (first is Arc arc) return arc.StartPoint();
|
return line.StartPoint;
|
||||||
if (first is Circle circle) return new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
|
if (first is Arc arc)
|
||||||
|
return arc.StartPoint();
|
||||||
|
if (first is Circle circle)
|
||||||
|
return new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
|
||||||
return Vector.Zero;
|
return Vector.Zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
External,
|
External,
|
||||||
Internal,
|
Internal,
|
||||||
ArcCircle
|
ArcCircle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
|
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
|
||||||
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
|
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, ApproachAngle = 90 };
|
public LeadIn InternalLeadIn { get; set; } =
|
||||||
|
new LineLeadIn { Length = 0.125, ApproachAngle = 90 };
|
||||||
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
|
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -7,19 +7,23 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public double Radius { get; set; }
|
public double Radius { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
var arcCenter = new Vector(
|
var arcCenter = new Vector(
|
||||||
contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle),
|
contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle),
|
||||||
contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle));
|
contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle)
|
||||||
|
);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }
|
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +34,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
return new Vector(
|
return new Vector(
|
||||||
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
|
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
|
||||||
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
|
arcCenterY + Radius * System.Math.Sin(contourNormalAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LeadIn Scale(double factor) =>
|
public override LeadIn Scale(double factor) => new ArcLeadIn { Radius = Radius * factor };
|
||||||
new ArcLeadIn { Radius = Radius * factor };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double ArcRadius { get; set; }
|
public double ArcRadius { get; set; }
|
||||||
public double Kerf { get; set; }
|
public double Kerf { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
@@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
|
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
|
||||||
var arcStart = new Vector(
|
var arcStart = new Vector(
|
||||||
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
||||||
arcCenterY + ArcRadius * System.Math.Sin(lineAngle));
|
arcCenterY + ArcRadius * System.Math.Sin(lineAngle)
|
||||||
|
);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(arcStart) { Layer = LayerType.Leadin },
|
new LinearMove(arcStart) { Layer = LayerType.Leadin },
|
||||||
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }
|
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
return new Vector(
|
return new Vector(
|
||||||
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||||
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
arcStartY + LineLength * System.Math.Sin(lineAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LeadIn Scale(double factor) =>
|
public override LeadIn Scale(double factor) =>
|
||||||
new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf };
|
new CleanHoleLeadIn
|
||||||
|
{
|
||||||
|
LineLength = LineLength * factor,
|
||||||
|
ArcRadius = ArcRadius * factor,
|
||||||
|
Kerf = Kerf,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
public abstract class LeadIn
|
public abstract class LeadIn
|
||||||
{
|
{
|
||||||
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public abstract List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW);
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
);
|
||||||
|
|
||||||
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double ApproachAngle { get; set; } = 135.0;
|
public double ApproachAngle { get; set; } = 135.0;
|
||||||
public double ArcRadius { get; set; }
|
public double ArcRadius { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
@@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||||
var arcStart = new Vector(
|
var arcStart = new Vector(
|
||||||
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
||||||
arcCenterY + ArcRadius * System.Math.Sin(lineAngle));
|
arcCenterY + ArcRadius * System.Math.Sin(lineAngle)
|
||||||
|
);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(arcStart) { Layer = LayerType.Leadin },
|
new LinearMove(arcStart) { Layer = LayerType.Leadin },
|
||||||
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }
|
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
return new Vector(
|
return new Vector(
|
||||||
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||||
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
arcStartY + LineLength * System.Math.Sin(lineAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LeadIn Scale(double factor) =>
|
public override LeadIn Scale(double factor) =>
|
||||||
new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle };
|
new LineArcLeadIn
|
||||||
|
{
|
||||||
|
LineLength = LineLength * factor,
|
||||||
|
ArcRadius = ArcRadius * factor,
|
||||||
|
ApproachAngle = ApproachAngle,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -9,15 +9,18 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double Length { get; set; }
|
public double Length { get; set; }
|
||||||
public double ApproachAngle { get; set; } = 90.0;
|
public double ApproachAngle { get; set; } = 90.0;
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin }
|
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +29,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle);
|
var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle);
|
||||||
return new Vector(
|
return new Vector(
|
||||||
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
|
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
|
||||||
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
|
contourStartPoint.Y + Length * System.Math.Sin(approachAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LeadIn Scale(double factor) =>
|
public override LeadIn Scale(double factor) =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -11,21 +11,25 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double Length2 { get; set; }
|
public double Length2 { get; set; }
|
||||||
public double ApproachAngle2 { get; set; } = 90.0;
|
public double ApproachAngle2 { get; set; } = 90.0;
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
|
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
|
||||||
var midPoint = new Vector(
|
var midPoint = new Vector(
|
||||||
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
|
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
|
||||||
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle));
|
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle)
|
||||||
|
);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(midPoint) { Layer = LayerType.Leadin },
|
new LinearMove(midPoint) { Layer = LayerType.Leadin },
|
||||||
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin }
|
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,10 +42,17 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2);
|
var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2);
|
||||||
return new Vector(
|
return new Vector(
|
||||||
midX + Length1 * System.Math.Cos(firstAngle),
|
midX + Length1 * System.Math.Cos(firstAngle),
|
||||||
midY + Length1 * System.Math.Sin(firstAngle));
|
midY + Length1 * System.Math.Sin(firstAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LeadIn Scale(double factor) =>
|
public override LeadIn Scale(double factor) =>
|
||||||
new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 };
|
new LineLineLeadIn
|
||||||
|
{
|
||||||
|
Length1 = Length1 * factor,
|
||||||
|
ApproachAngle1 = ApproachAngle1,
|
||||||
|
Length2 = Length2 * factor,
|
||||||
|
ApproachAngle2 = ApproachAngle2,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
public class NoLeadIn : LeadIn
|
public class NoLeadIn : LeadIn
|
||||||
{
|
{
|
||||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourStartPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new List<ICode>
|
return new List<ICode> { new RapidMove(contourStartPoint) };
|
||||||
{
|
|
||||||
new RapidMove(contourStartPoint)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -7,8 +7,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public double Radius { get; set; }
|
public double Radius { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle);
|
var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle);
|
||||||
var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
|
var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
|
||||||
@@ -16,11 +19,12 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
var endPoint = new Vector(
|
var endPoint = new Vector(
|
||||||
arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2),
|
arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2),
|
||||||
arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2));
|
arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2)
|
||||||
|
);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout }
|
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
public abstract class LeadOut
|
public abstract class LeadOut
|
||||||
{
|
{
|
||||||
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
public abstract List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW);
|
Vector contourEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -9,18 +9,19 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double Length { get; set; }
|
public double Length { get; set; }
|
||||||
public double ApproachAngle { get; set; } = 90.0;
|
public double ApproachAngle { get; set; } = 90.0;
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle);
|
var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle);
|
||||||
var endPoint = new Vector(
|
var endPoint = new Vector(
|
||||||
contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
|
contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
|
||||||
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle));
|
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle)
|
||||||
|
);
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode> { new LinearMove(endPoint) { Layer = LayerType.Leadout } };
|
||||||
{
|
|
||||||
new LinearMove(endPoint) { Layer = LayerType.Leadout }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
public class NoLeadOut : LeadOut
|
public class NoLeadOut : LeadOut
|
||||||
{
|
{
|
||||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
public override List<ICode> Generate(
|
||||||
RotationType winding = RotationType.CW)
|
Vector contourEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new List<ICode>();
|
return new List<ICode>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
BottomSide = 4,
|
BottomSide = 4,
|
||||||
EdgeStart = 5,
|
EdgeStart = 5,
|
||||||
LeftSide = 7,
|
LeftSide = 7,
|
||||||
RightSideAlt = 8
|
RightSideAlt = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SequenceParameters
|
public class SequenceParameters
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double BreakerAngle { get; set; }
|
public double BreakerAngle { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(
|
public override List<ICode> Generate(
|
||||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
Vector tabStartPoint,
|
||||||
RotationType winding = RotationType.CW)
|
Vector tabEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var codes = new List<ICode>();
|
var codes = new List<ICode>();
|
||||||
|
|
||||||
@@ -21,7 +24,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
var scoreAngle = contourNormalAngle + System.Math.PI;
|
var scoreAngle = contourNormalAngle + System.Math.PI;
|
||||||
var scoreEnd = new Vector(
|
var scoreEnd = new Vector(
|
||||||
tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle),
|
tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle),
|
||||||
tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle));
|
tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle)
|
||||||
|
);
|
||||||
codes.Add(new LinearMove(scoreEnd));
|
codes.Add(new LinearMove(scoreEnd));
|
||||||
codes.Add(new RapidMove(tabEndPoint));
|
codes.Add(new RapidMove(tabEndPoint));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -8,13 +8,13 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public int MachineTabId { get; set; }
|
public int MachineTabId { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(
|
public override List<ICode> Generate(
|
||||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
Vector tabStartPoint,
|
||||||
RotationType winding = RotationType.CW)
|
Vector tabEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return new List<ICode>
|
return new List<ICode> { new RapidMove(tabEndPoint) };
|
||||||
{
|
|
||||||
new RapidMove(tabEndPoint)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -11,8 +11,11 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public double CutoutMaxHeight { get; set; }
|
public double CutoutMaxHeight { get; set; }
|
||||||
|
|
||||||
public override List<ICode> Generate(
|
public override List<ICode> Generate(
|
||||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
Vector tabStartPoint,
|
||||||
RotationType winding = RotationType.CW)
|
Vector tabEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var codes = new List<ICode>();
|
var codes = new List<ICode>();
|
||||||
|
|
||||||
@@ -29,8 +32,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
public bool AppliesToCutout(double cutoutWidth, double cutoutHeight)
|
public bool AppliesToCutout(double cutoutWidth, double cutoutHeight)
|
||||||
{
|
{
|
||||||
return cutoutWidth >= CutoutMinWidth && cutoutWidth <= CutoutMaxWidth
|
return cutoutWidth >= CutoutMinWidth
|
||||||
&& cutoutHeight >= CutoutMinHeight && cutoutHeight <= CutoutMaxHeight;
|
&& cutoutWidth <= CutoutMaxWidth
|
||||||
|
&& cutoutHeight >= CutoutMinHeight
|
||||||
|
&& cutoutHeight <= CutoutMaxHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,10 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public LeadOut TabLeadOut { get; set; }
|
public LeadOut TabLeadOut { get; set; }
|
||||||
|
|
||||||
public abstract List<ICode> Generate(
|
public abstract List<ICode> Generate(
|
||||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
Vector tabStartPoint,
|
||||||
RotationType winding = RotationType.CW);
|
Vector tabEndPoint,
|
||||||
|
double contourNormalAngle,
|
||||||
|
RotationType winding = RotationType.CW
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
|
|
||||||
public const int UseMax = -2;
|
public const int UseMax = -2;
|
||||||
|
|
||||||
public Feedrate()
|
public Feedrate() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Feedrate(double value)
|
public Feedrate(double value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
namespace OpenNest.CNC
|
||||||
namespace OpenNest.CNC
|
|
||||||
{
|
{
|
||||||
public enum KerfType
|
public enum KerfType
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
Left,
|
Left,
|
||||||
Right
|
Right,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
namespace OpenNest.CNC
|
||||||
namespace OpenNest.CNC
|
|
||||||
{
|
{
|
||||||
public enum LayerType
|
public enum LayerType
|
||||||
{
|
{
|
||||||
@@ -7,6 +6,6 @@ namespace OpenNest.CNC
|
|||||||
Scribe,
|
Scribe,
|
||||||
Cut,
|
Cut,
|
||||||
Leadin,
|
Leadin,
|
||||||
Leadout
|
Leadout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ namespace OpenNest.CNC
|
|||||||
public class LinearMove : Motion
|
public class LinearMove : Motion
|
||||||
{
|
{
|
||||||
public LinearMove()
|
public LinearMove()
|
||||||
: this(new Vector())
|
: this(new Vector()) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinearMove(double x, double y)
|
public LinearMove(double x, double y)
|
||||||
: this(new Vector(x, y))
|
: this(new Vector(x, y)) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinearMove(Vector endPoint)
|
public LinearMove(Vector endPoint)
|
||||||
{
|
{
|
||||||
@@ -34,7 +30,8 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
Layer = Layer,
|
Layer = Layer,
|
||||||
Suppressed = Suppressed,
|
Suppressed = Suppressed,
|
||||||
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
VariableRefs =
|
||||||
|
VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
namespace OpenNest.CNC
|
||||||
namespace OpenNest.CNC
|
|
||||||
{
|
{
|
||||||
public enum Mode
|
public enum Mode
|
||||||
{
|
{
|
||||||
Absolute,
|
Absolute,
|
||||||
Incremental
|
Incremental,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+206
-182
@@ -1,8 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,8 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
public List<ICode> Codes;
|
public List<ICode> Codes;
|
||||||
|
|
||||||
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, VariableDefinition> Variables { get; } =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public Dictionary<int, Program> SubPrograms { get; } = new();
|
public Dictionary<int, Program> SubPrograms { get; } = new();
|
||||||
|
|
||||||
@@ -66,9 +67,17 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
if (code is Motion m)
|
if (code is Motion m)
|
||||||
{
|
{
|
||||||
var cmd = m is RapidMove ? "G00" : (m is ArcMove am ? (am.Rotation == RotationType.CW ? "G02" : "G03") : "G01");
|
var cmd =
|
||||||
|
m is RapidMove
|
||||||
|
? "G00"
|
||||||
|
: (
|
||||||
|
m is ArcMove am
|
||||||
|
? (am.Rotation == RotationType.CW ? "G02" : "G03")
|
||||||
|
: "G01"
|
||||||
|
);
|
||||||
sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}");
|
sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}");
|
||||||
if (m is ArcMove arc) sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}");
|
if (m is ArcMove arc)
|
||||||
|
sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +106,8 @@ namespace OpenNest.CNC
|
|||||||
var dy = subpgm.Offset.Y - origin.Y;
|
var dy = subpgm.Offset.Y - origin.Y;
|
||||||
subpgm.Offset = new Geometry.Vector(
|
subpgm.Offset = new Geometry.Vector(
|
||||||
origin.X + dx * cos - dy * sin,
|
origin.X + dx * cos - dy * sin,
|
||||||
origin.Y + dx * sin + dy * cos);
|
origin.Y + dx * sin + dy * cos
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subpgm.Program != null)
|
if (subpgm.Program != null)
|
||||||
@@ -130,8 +140,7 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
if (code is SubProgramCall subpgm)
|
if (code is SubProgramCall subpgm)
|
||||||
{
|
{
|
||||||
subpgm.Offset = new Geometry.Vector(
|
subpgm.Offset = new Geometry.Vector(subpgm.Offset.X + x, subpgm.Offset.Y + y);
|
||||||
subpgm.Offset.X + x, subpgm.Offset.Y + y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
@@ -159,7 +168,9 @@ namespace OpenNest.CNC
|
|||||||
if (code is SubProgramCall subpgm)
|
if (code is SubProgramCall subpgm)
|
||||||
{
|
{
|
||||||
subpgm.Offset = new Geometry.Vector(
|
subpgm.Offset = new Geometry.Vector(
|
||||||
subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y);
|
subpgm.Offset.X + voffset.X,
|
||||||
|
subpgm.Offset.Y + voffset.Y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
@@ -258,56 +269,63 @@ namespace OpenNest.CNC
|
|||||||
switch (Mode)
|
switch (Mode)
|
||||||
{
|
{
|
||||||
case Mode.Absolute:
|
case Mode.Absolute:
|
||||||
|
{
|
||||||
|
for (int i = Codes.Count; i >= 0; --i)
|
||||||
{
|
{
|
||||||
for (int i = Codes.Count; i >= 0; --i)
|
var code = Codes[i];
|
||||||
{
|
var motion = code as Motion;
|
||||||
var code = Codes[i];
|
|
||||||
var motion = code as Motion;
|
|
||||||
|
|
||||||
if (motion == null) continue;
|
if (motion == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
return motion.EndPoint;
|
return motion.EndPoint;
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case Mode.Incremental:
|
case Mode.Incremental:
|
||||||
|
{
|
||||||
|
var pos = new Vector(0, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < Codes.Count; ++i)
|
||||||
{
|
{
|
||||||
var pos = new Vector(0, 0);
|
var code = Codes[i];
|
||||||
|
var motion = code as Motion;
|
||||||
|
|
||||||
for (int i = 0; i < Codes.Count; ++i)
|
if (motion == null)
|
||||||
{
|
continue;
|
||||||
var code = Codes[i];
|
|
||||||
var motion = code as Motion;
|
|
||||||
|
|
||||||
if (motion == null) continue;
|
pos += motion.EndPoint;
|
||||||
|
|
||||||
pos += motion.EndPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Vector(0, 0);
|
return new Vector(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bounding box of the geometry the program visits. The tool's starting position is not
|
||||||
|
/// part of the geometry, so the origin only contributes when the program reaches it.
|
||||||
|
/// An empty program returns a zero-size box at the origin.
|
||||||
|
/// </summary>
|
||||||
public Box BoundingBox()
|
public Box BoundingBox()
|
||||||
{
|
{
|
||||||
var origin = new Vector(0, 0);
|
var origin = new Vector(0, 0);
|
||||||
return BoundingBox(ref origin);
|
return BoundingBox(ref origin, out var box) ? box : new Box(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Box BoundingBox(ref Vector pos)
|
private bool BoundingBox(ref Vector pos, out Box result)
|
||||||
{
|
{
|
||||||
// Capture the frame origin at entry. Sub-program Offsets and
|
// Capture the frame origin at entry. Sub-program Offsets and
|
||||||
// absolute-mode endpoints are relative to this fixed origin.
|
// absolute-mode endpoints are relative to this fixed origin.
|
||||||
var frameOrigin = pos;
|
var frameOrigin = pos;
|
||||||
|
|
||||||
double minX = 0.0;
|
var minX = double.PositiveInfinity;
|
||||||
double minY = 0.0;
|
var minY = double.PositiveInfinity;
|
||||||
double maxX = 0.0;
|
var maxX = double.NegativeInfinity;
|
||||||
double maxY = 0.0;
|
var maxY = double.NegativeInfinity;
|
||||||
|
|
||||||
for (int i = 0; i < Codes.Count; ++i)
|
for (int i = 0; i < Codes.Count; ++i)
|
||||||
{
|
{
|
||||||
@@ -316,174 +334,180 @@ namespace OpenNest.CNC
|
|||||||
switch (code.Type)
|
switch (code.Type)
|
||||||
{
|
{
|
||||||
case CodeType.LinearMove:
|
case CodeType.LinearMove:
|
||||||
{
|
{
|
||||||
var line = (LinearMove)code;
|
var line = (LinearMove)code;
|
||||||
var pt = Mode == Mode.Absolute ?
|
var pt =
|
||||||
frameOrigin + line.EndPoint :
|
Mode == Mode.Absolute
|
||||||
line.EndPoint + pos;
|
|
||||||
|
|
||||||
if (pt.X > maxX)
|
|
||||||
maxX = pt.X;
|
|
||||||
else if (pt.X < minX)
|
|
||||||
minX = pt.X;
|
|
||||||
|
|
||||||
if (pt.Y > maxY)
|
|
||||||
maxY = pt.Y;
|
|
||||||
else if (pt.Y < minY)
|
|
||||||
minY = pt.Y;
|
|
||||||
|
|
||||||
pos = pt;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CodeType.RapidMove:
|
|
||||||
{
|
|
||||||
var line = (RapidMove)code;
|
|
||||||
var pt = Mode == Mode.Absolute
|
|
||||||
? frameOrigin + line.EndPoint
|
? frameOrigin + line.EndPoint
|
||||||
: line.EndPoint + pos;
|
: line.EndPoint + pos;
|
||||||
|
|
||||||
if (pt.X > maxX)
|
if (pt.X > maxX)
|
||||||
maxX = pt.X;
|
maxX = pt.X;
|
||||||
else if (pt.X < minX)
|
if (pt.X < minX)
|
||||||
minX = pt.X;
|
minX = pt.X;
|
||||||
|
|
||||||
if (pt.Y > maxY)
|
if (pt.Y > maxY)
|
||||||
maxY = pt.Y;
|
maxY = pt.Y;
|
||||||
else if (pt.Y < minY)
|
if (pt.Y < minY)
|
||||||
minY = pt.Y;
|
minY = pt.Y;
|
||||||
|
|
||||||
pos = pt;
|
pos = pt;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case CodeType.RapidMove:
|
||||||
|
{
|
||||||
|
var line = (RapidMove)code;
|
||||||
|
var pt =
|
||||||
|
Mode == Mode.Absolute
|
||||||
|
? frameOrigin + line.EndPoint
|
||||||
|
: line.EndPoint + pos;
|
||||||
|
|
||||||
|
if (pt.X > maxX)
|
||||||
|
maxX = pt.X;
|
||||||
|
if (pt.X < minX)
|
||||||
|
minX = pt.X;
|
||||||
|
|
||||||
|
if (pt.Y > maxY)
|
||||||
|
maxY = pt.Y;
|
||||||
|
if (pt.Y < minY)
|
||||||
|
minY = pt.Y;
|
||||||
|
|
||||||
|
pos = pt;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case CodeType.ArcMove:
|
case CodeType.ArcMove:
|
||||||
|
{
|
||||||
|
var arc = (ArcMove)code;
|
||||||
|
var radius = arc.CenterPoint.DistanceTo(arc.EndPoint);
|
||||||
|
|
||||||
|
Vector endpt;
|
||||||
|
Vector centerpt;
|
||||||
|
|
||||||
|
if (Mode == Mode.Incremental)
|
||||||
{
|
{
|
||||||
var arc = (ArcMove)code;
|
endpt = arc.EndPoint + pos;
|
||||||
var radius = arc.CenterPoint.DistanceTo(arc.EndPoint);
|
centerpt = arc.CenterPoint + pos;
|
||||||
|
|
||||||
Vector endpt;
|
|
||||||
Vector centerpt;
|
|
||||||
|
|
||||||
if (Mode == Mode.Incremental)
|
|
||||||
{
|
|
||||||
endpt = arc.EndPoint + pos;
|
|
||||||
centerpt = arc.CenterPoint + pos;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
endpt = frameOrigin + arc.EndPoint;
|
|
||||||
centerpt = frameOrigin + arc.CenterPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
double minX1;
|
|
||||||
double minY1;
|
|
||||||
double maxX1;
|
|
||||||
double maxY1;
|
|
||||||
|
|
||||||
if (pos.X < endpt.X)
|
|
||||||
{
|
|
||||||
minX1 = pos.X;
|
|
||||||
maxX1 = endpt.X;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
minX1 = endpt.X;
|
|
||||||
maxX1 = pos.X;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pos.Y < endpt.Y)
|
|
||||||
{
|
|
||||||
minY1 = pos.Y;
|
|
||||||
maxY1 = endpt.Y;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
minY1 = endpt.Y;
|
|
||||||
maxY1 = pos.Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startAngle = pos.AngleFrom(centerpt);
|
|
||||||
var endAngle = endpt.AngleFrom(centerpt);
|
|
||||||
|
|
||||||
// switch the angle to counter clockwise.
|
|
||||||
if (arc.Rotation == RotationType.CW)
|
|
||||||
Generic.Swap(ref startAngle, ref endAngle);
|
|
||||||
|
|
||||||
startAngle = Angle.NormalizeRad(startAngle);
|
|
||||||
endAngle = Angle.NormalizeRad(endAngle);
|
|
||||||
|
|
||||||
if (Angle.IsBetweenRad(Angle.HalfPI, startAngle, endAngle))
|
|
||||||
maxY1 = centerpt.Y + radius;
|
|
||||||
|
|
||||||
if (Angle.IsBetweenRad(System.Math.PI, startAngle, endAngle))
|
|
||||||
minX1 = centerpt.X - radius;
|
|
||||||
|
|
||||||
const double oneHalfPI = System.Math.PI * 1.5;
|
|
||||||
|
|
||||||
if (Angle.IsBetweenRad(oneHalfPI, startAngle, endAngle))
|
|
||||||
minY1 = centerpt.Y - radius;
|
|
||||||
|
|
||||||
if (Angle.IsBetweenRad(Angle.TwoPI, startAngle, endAngle))
|
|
||||||
maxX1 = centerpt.X + radius;
|
|
||||||
|
|
||||||
if (maxX1 > maxX)
|
|
||||||
maxX = maxX1;
|
|
||||||
|
|
||||||
if (minX1 < minX)
|
|
||||||
minX = minX1;
|
|
||||||
|
|
||||||
if (maxY1 > maxY)
|
|
||||||
maxY = maxY1;
|
|
||||||
|
|
||||||
if (minY1 < minY)
|
|
||||||
minY = minY1;
|
|
||||||
|
|
||||||
pos = endpt;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
endpt = frameOrigin + arc.EndPoint;
|
||||||
|
centerpt = frameOrigin + arc.CenterPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
double minX1;
|
||||||
|
double minY1;
|
||||||
|
double maxX1;
|
||||||
|
double maxY1;
|
||||||
|
|
||||||
|
if (pos.X < endpt.X)
|
||||||
|
{
|
||||||
|
minX1 = pos.X;
|
||||||
|
maxX1 = endpt.X;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
minX1 = endpt.X;
|
||||||
|
maxX1 = pos.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.Y < endpt.Y)
|
||||||
|
{
|
||||||
|
minY1 = pos.Y;
|
||||||
|
maxY1 = endpt.Y;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
minY1 = endpt.Y;
|
||||||
|
maxY1 = pos.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startAngle = pos.AngleFrom(centerpt);
|
||||||
|
var endAngle = endpt.AngleFrom(centerpt);
|
||||||
|
|
||||||
|
// switch the angle to counter clockwise.
|
||||||
|
if (arc.Rotation == RotationType.CW)
|
||||||
|
Generic.Swap(ref startAngle, ref endAngle);
|
||||||
|
|
||||||
|
startAngle = Angle.NormalizeRad(startAngle);
|
||||||
|
endAngle = Angle.NormalizeRad(endAngle);
|
||||||
|
|
||||||
|
if (Angle.IsBetweenRad(Angle.HalfPI, startAngle, endAngle))
|
||||||
|
maxY1 = centerpt.Y + radius;
|
||||||
|
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI, startAngle, endAngle))
|
||||||
|
minX1 = centerpt.X - radius;
|
||||||
|
|
||||||
|
const double oneHalfPI = System.Math.PI * 1.5;
|
||||||
|
|
||||||
|
if (Angle.IsBetweenRad(oneHalfPI, startAngle, endAngle))
|
||||||
|
minY1 = centerpt.Y - radius;
|
||||||
|
|
||||||
|
if (Angle.IsBetweenRad(Angle.TwoPI, startAngle, endAngle))
|
||||||
|
maxX1 = centerpt.X + radius;
|
||||||
|
|
||||||
|
if (maxX1 > maxX)
|
||||||
|
maxX = maxX1;
|
||||||
|
|
||||||
|
if (minX1 < minX)
|
||||||
|
minX = minX1;
|
||||||
|
|
||||||
|
if (maxY1 > maxY)
|
||||||
|
maxY = maxY1;
|
||||||
|
|
||||||
|
if (minY1 < minY)
|
||||||
|
minY = minY1;
|
||||||
|
|
||||||
|
pos = endpt;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
if (subpgm.Program == null)
|
if (subpgm.Program == null)
|
||||||
break;
|
|
||||||
|
|
||||||
// Sub-program frame origin in this program's frame
|
|
||||||
// is frameOrigin + Offset, regardless of current pos.
|
|
||||||
pos = frameOrigin + subpgm.Offset;
|
|
||||||
var box = subpgm.Program.BoundingBox(ref pos);
|
|
||||||
|
|
||||||
if (box.Left < minX)
|
|
||||||
minX = box.Left;
|
|
||||||
|
|
||||||
if (box.Right > maxX)
|
|
||||||
maxX = box.Right;
|
|
||||||
|
|
||||||
if (box.Bottom < minY)
|
|
||||||
minY = box.Bottom;
|
|
||||||
|
|
||||||
if (box.Top > maxY)
|
|
||||||
maxY = box.Top;
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
// Sub-program frame origin in this program's frame
|
||||||
|
// is frameOrigin + Offset, regardless of current pos.
|
||||||
|
pos = frameOrigin + subpgm.Offset;
|
||||||
|
if (!subpgm.Program.BoundingBox(ref pos, out var box))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (box.Left < minX)
|
||||||
|
minX = box.Left;
|
||||||
|
|
||||||
|
if (box.Right > maxX)
|
||||||
|
maxX = box.Right;
|
||||||
|
|
||||||
|
if (box.Bottom < minY)
|
||||||
|
minY = box.Bottom;
|
||||||
|
|
||||||
|
if (box.Top > maxY)
|
||||||
|
maxY = box.Top;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Box(minX, minY, maxX - minX, maxY - minY);
|
if (minX > maxX || minY > maxY)
|
||||||
|
{
|
||||||
|
result = new Box(0, 0, 0, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new Box(minX, minY, maxX - minX, maxY - minY);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object Clone()
|
public object Clone()
|
||||||
{
|
{
|
||||||
var pgm = new Program()
|
var pgm = new Program() { mode = this.mode, Rotation = this.Rotation };
|
||||||
{
|
|
||||||
mode = this.mode,
|
|
||||||
Rotation = this.Rotation
|
|
||||||
};
|
|
||||||
|
|
||||||
var codes = new ICode[Length];
|
var codes = new ICode[Length];
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
public List<string> EmitDeclarations()
|
public List<string> EmitDeclarations()
|
||||||
{
|
{
|
||||||
return _variables.Values
|
return _variables
|
||||||
.Where(v => v.Expression != null)
|
.Values.Where(v => v.Expression != null)
|
||||||
.OrderBy(v => v.Number)
|
.OrderBy(v => v.Number)
|
||||||
.Select(v => $"{v.Reference}={v.Expression} ({FormatComment(v.Name)})")
|
.Select(v => $"{v.Reference}={v.Expression} ({FormatComment(v.Name)})")
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -36,7 +36,13 @@ namespace OpenNest.CNC
|
|||||||
return basePos;
|
return basePos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Walk(Program pgm, Vector basePos, ref Vector pos, bool skipFirst, List<Segment> results)
|
private static void Walk(
|
||||||
|
Program pgm,
|
||||||
|
Vector basePos,
|
||||||
|
ref Vector pos,
|
||||||
|
bool skipFirst,
|
||||||
|
List<Segment> results
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var skipped = !skipFirst;
|
var skipped = !skipFirst;
|
||||||
|
|
||||||
@@ -60,9 +66,10 @@ namespace OpenNest.CNC
|
|||||||
}
|
}
|
||||||
else if (code is Motion motion)
|
else if (code is Motion motion)
|
||||||
{
|
{
|
||||||
var endpt = pgm.Mode == Mode.Incremental
|
var endpt =
|
||||||
? motion.EndPoint + pos
|
pgm.Mode == Mode.Incremental
|
||||||
: motion.EndPoint + basePos;
|
? motion.EndPoint + pos
|
||||||
|
: motion.EndPoint + basePos;
|
||||||
|
|
||||||
if (code.Type == CodeType.RapidMove)
|
if (code.Type == CodeType.RapidMove)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ namespace OpenNest.CNC
|
|||||||
return new RapidMove(EndPoint)
|
return new RapidMove(EndPoint)
|
||||||
{
|
{
|
||||||
Suppressed = Suppressed,
|
Suppressed = Suppressed,
|
||||||
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
VariableRefs =
|
||||||
|
VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ namespace OpenNest.CNC
|
|||||||
private double rotation;
|
private double rotation;
|
||||||
private Program program;
|
private Program program;
|
||||||
|
|
||||||
public SubProgramCall()
|
public SubProgramCall() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public SubProgramCall(Program program, double rotation)
|
public SubProgramCall(Program program, double rotation)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ namespace OpenNest.CNC
|
|||||||
public bool Inline { get; }
|
public bool Inline { get; }
|
||||||
public bool Global { get; }
|
public bool Global { get; }
|
||||||
|
|
||||||
public VariableDefinition(string name, string expression, double value,
|
public VariableDefinition(
|
||||||
bool inline = false, bool global = false)
|
string name,
|
||||||
|
string expression,
|
||||||
|
double value,
|
||||||
|
bool inline = false,
|
||||||
|
bool global = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Expression = expression;
|
Expression = expression;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Linq;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -14,6 +14,12 @@ namespace OpenNest
|
|||||||
/// <summary>Angles with |v| below this (radians) are snapped to 0.</summary>
|
/// <summary>Angles with |v| below this (radians) are snapped to 0.</summary>
|
||||||
public const double SnapToZero = 0.001;
|
public const double SnapToZero = 0.001;
|
||||||
|
|
||||||
|
/// <summary>Centroid offsets below this fraction of the MBR extent count as symmetric.</summary>
|
||||||
|
private const double SymmetryTolerance = 1e-6;
|
||||||
|
|
||||||
|
/// <summary>Angular margin (radians) keeping axis-aligned centroid offsets off the edge of the preferred quadrant.</summary>
|
||||||
|
private const double PreferenceMargin = 0.001;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Derives the canonical angle from a pre-computed MBR. Used both by Compute (which
|
/// Derives the canonical angle from a pre-computed MBR. Used both by Compute (which
|
||||||
/// computes the MBR itself) and by PartClassifier (which already has one). Single formula
|
/// computes the MBR itself) and by PartClassifier (which already has one). Single formula
|
||||||
@@ -44,8 +50,9 @@ namespace OpenNest
|
|||||||
if (drawing?.Program == null)
|
if (drawing?.Program == null)
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
||||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
var entities = ConvertProgram
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.ToGeometry(drawing.Program)
|
||||||
|
.Where(e => SpecialLayers.IsMaterial(e.Layer));
|
||||||
|
|
||||||
var shapes = ShapeBuilder.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
@@ -72,7 +79,89 @@ namespace OpenNest
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
||||||
var mbr = RotatingCalipers.MinimumBoundingRectangle(hull);
|
var mbr = RotatingCalipers.MinimumBoundingRectangle(hull);
|
||||||
return FromMbr(mbr);
|
var angle = FromMbr(mbr);
|
||||||
|
if (mbr.Area <= OpenNest.Math.Tolerance.Epsilon)
|
||||||
|
return angle;
|
||||||
|
|
||||||
|
var quarterTurns = PreferredQuarterTurns(polygon, hull, angle);
|
||||||
|
if (quarterTurns == 0)
|
||||||
|
return angle;
|
||||||
|
|
||||||
|
return NormalizeSigned(angle + quarterTurns * System.Math.PI / 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The MBR only fixes the frame modulo 90°, leaving four equivalent orientations. Nest
|
||||||
|
/// results are not 90°-symmetric, so pick one deterministically: the quarter-turn count
|
||||||
|
/// that puts the perimeter's centroid toward the lower-left of its MBR. Shapes with no
|
||||||
|
/// centroid offset (rectangles, circles) are symmetric and keep the MBR orientation.
|
||||||
|
/// </summary>
|
||||||
|
private static int PreferredQuarterTurns(Polygon polygon, Polygon hull, double angle)
|
||||||
|
{
|
||||||
|
var minX = double.MaxValue;
|
||||||
|
var minY = double.MaxValue;
|
||||||
|
var maxX = double.MinValue;
|
||||||
|
var maxY = double.MinValue;
|
||||||
|
foreach (var vertex in hull.Vertices)
|
||||||
|
{
|
||||||
|
var rotated = vertex.Rotate(angle);
|
||||||
|
minX = System.Math.Min(minX, rotated.X);
|
||||||
|
minY = System.Math.Min(minY, rotated.Y);
|
||||||
|
maxX = System.Math.Max(maxX, rotated.X);
|
||||||
|
maxY = System.Math.Max(maxY, rotated.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
var centroid = Centroid(polygon).Rotate(angle);
|
||||||
|
var dx = centroid.X - (minX + maxX) / 2.0;
|
||||||
|
var dy = centroid.Y - (minY + maxY) / 2.0;
|
||||||
|
|
||||||
|
var extent = System.Math.Max(maxX - minX, maxY - minY);
|
||||||
|
if (System.Math.Sqrt(dx * dx + dy * dy) <= SymmetryTolerance * extent)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Choose k so the offset direction lands in [PI - margin, 3PI/2 - margin). The margin
|
||||||
|
// keeps offsets lying exactly on an axis (mirror-symmetric parts) away from the
|
||||||
|
// interval edge so floating-point noise cannot flip the choice.
|
||||||
|
var halfPi = System.Math.PI / 2.0;
|
||||||
|
var direction = System.Math.Atan2(dy, dx);
|
||||||
|
for (var turns = 0; turns < 4; turns++)
|
||||||
|
{
|
||||||
|
var relative = direction + turns * halfPi - (System.Math.PI - PreferenceMargin);
|
||||||
|
relative -= 2.0 * System.Math.PI * System.Math.Floor(relative / (2.0 * System.Math.PI));
|
||||||
|
if (relative < halfPi)
|
||||||
|
return turns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector Centroid(Polygon polygon)
|
||||||
|
{
|
||||||
|
var vertices = polygon.Vertices;
|
||||||
|
var doubleArea = 0.0;
|
||||||
|
var cx = 0.0;
|
||||||
|
var cy = 0.0;
|
||||||
|
for (var i = 0; i < vertices.Count; i++)
|
||||||
|
{
|
||||||
|
var p = vertices[i];
|
||||||
|
var q = vertices[(i + 1) % vertices.Count];
|
||||||
|
var cross = p.X * q.Y - q.X * p.Y;
|
||||||
|
doubleArea += cross;
|
||||||
|
cx += (p.X + q.X) * cross;
|
||||||
|
cy += (p.Y + q.Y) * cross;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.Math.Abs(doubleArea) <= OpenNest.Math.Tolerance.Epsilon)
|
||||||
|
return new Vector(vertices.Average(v => v.X), vertices.Average(v => v.Y));
|
||||||
|
|
||||||
|
return new Vector(cx / (3.0 * doubleArea), cy / (3.0 * doubleArea));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double NormalizeSigned(double angle)
|
||||||
|
{
|
||||||
|
var twoPi = 2.0 * System.Math.PI;
|
||||||
|
angle -= twoPi * System.Math.Floor((angle + System.Math.PI) / twoPi);
|
||||||
|
return angle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,5 @@
|
|||||||
|
|
||||||
namespace OpenNest.Collections
|
namespace OpenNest.Collections
|
||||||
{
|
{
|
||||||
public class DrawingCollection : HashSet<Drawing>
|
public class DrawingCollection : HashSet<Drawing> { }
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.Geometry;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,7 @@ namespace OpenNest.Converters
|
|||||||
Perimeter,
|
Perimeter,
|
||||||
Hole,
|
Hole,
|
||||||
Etch,
|
Etch,
|
||||||
Open
|
Open,
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ContourInfo
|
public sealed class ContourInfo
|
||||||
@@ -91,7 +91,8 @@ namespace OpenNest.Converters
|
|||||||
// Non-perimeter shapes first (matches CNC cut order: holes before perimeter)
|
// Non-perimeter shapes first (matches CNC cut order: holes before perimeter)
|
||||||
for (var i = 0; i < shapes.Count; i++)
|
for (var i = 0; i < shapes.Count; i++)
|
||||||
{
|
{
|
||||||
if (i == perimeterIndex) continue;
|
if (i == perimeterIndex)
|
||||||
|
continue;
|
||||||
var shape = shapes[i];
|
var shape = shapes[i];
|
||||||
var type = ClassifyShape(shape);
|
var type = ClassifyShape(shape);
|
||||||
|
|
||||||
@@ -116,7 +117,13 @@ namespace OpenNest.Converters
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perimeter last
|
// Perimeter last
|
||||||
result.Add(new ContourInfo(shapes[perimeterIndex], ContourClassification.Perimeter, "Perimeter"));
|
result.Add(
|
||||||
|
new ContourInfo(
|
||||||
|
shapes[perimeterIndex],
|
||||||
|
ContourClassification.Perimeter,
|
||||||
|
"Perimeter"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -124,8 +131,12 @@ namespace OpenNest.Converters
|
|||||||
private static ContourClassification ClassifyShape(Shape shape)
|
private static ContourClassification ClassifyShape(Shape shape)
|
||||||
{
|
{
|
||||||
// Check etch layer — all entities must be on ETCH layer
|
// Check etch layer — all entities must be on ETCH layer
|
||||||
if (shape.Entities.Count > 0 &&
|
if (
|
||||||
shape.Entities.All(e => string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)))
|
shape.Entities.Count > 0
|
||||||
|
&& shape.Entities.All(e =>
|
||||||
|
string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
return ContourClassification.Etch;
|
return ContourClassification.Etch;
|
||||||
|
|
||||||
if (shape.IsClosed())
|
if (shape.IsClosed())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.CNC;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
{
|
{
|
||||||
@@ -87,14 +87,24 @@ namespace OpenNest.Converters
|
|||||||
|
|
||||||
lastpt = endpt;
|
lastpt = endpt;
|
||||||
|
|
||||||
|
var layer = ClassifyLayer(arc);
|
||||||
var sweep = System.Math.Abs(arc.SweepAngle());
|
var sweep = System.Math.Abs(arc.SweepAngle());
|
||||||
if (sweep < Tolerance.Epsilon || sweep.IsEqualTo(Angle.TwoPI))
|
if (sweep < Tolerance.Epsilon || sweep.IsEqualTo(Angle.TwoPI))
|
||||||
{
|
{
|
||||||
pgm.LineTo(endpt);
|
pgm.Codes.Add(new LinearMove(endpt) { Layer = layer });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
pgm.ArcTo(endpt, arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW);
|
pgm.Codes.Add(
|
||||||
|
new ArcMove(
|
||||||
|
endpt,
|
||||||
|
arc.Center,
|
||||||
|
arc.IsReversed ? RotationType.CW : RotationType.CCW
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Layer = layer,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastpt;
|
return lastpt;
|
||||||
@@ -107,7 +117,12 @@ namespace OpenNest.Converters
|
|||||||
if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
||||||
pgm.MoveTo(startpt);
|
pgm.MoveTo(startpt);
|
||||||
|
|
||||||
pgm.ArcTo(startpt, circle.Center, circle.Rotation);
|
pgm.Codes.Add(
|
||||||
|
new ArcMove(startpt, circle.Center, circle.Rotation)
|
||||||
|
{
|
||||||
|
Layer = ClassifyLayer(circle),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
lastpt = startpt;
|
lastpt = startpt;
|
||||||
return lastpt;
|
return lastpt;
|
||||||
@@ -118,13 +133,27 @@ namespace OpenNest.Converters
|
|||||||
if (line.StartPoint.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
if (line.StartPoint.DistanceTo(lastpt) > Tolerance.ChainTolerance)
|
||||||
pgm.MoveTo(line.StartPoint);
|
pgm.MoveTo(line.StartPoint);
|
||||||
|
|
||||||
var move = new LinearMove(line.EndPoint);
|
pgm.Codes.Add(new LinearMove(line.EndPoint) { Layer = ClassifyLayer(line) });
|
||||||
if (string.Equals(line.Layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase))
|
|
||||||
move.Layer = LayerType.Scribe;
|
|
||||||
pgm.Codes.Add(move);
|
|
||||||
|
|
||||||
lastpt = line.EndPoint;
|
lastpt = line.EndPoint;
|
||||||
return lastpt;
|
return lastpt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Engrave/etch/scribe geometry maps to Scribe so the post processor can treat it as a
|
||||||
|
// separate tool pass; everything else keeps the move's default Cut layer. SCRIBE is the
|
||||||
|
// name marks carry once saved (SpecialLayers.Scribe), so drawings rebuilt from stored
|
||||||
|
// entities must map it too or their marks silently become cut moves.
|
||||||
|
private static LayerType ClassifyLayer(Entity geo)
|
||||||
|
{
|
||||||
|
var name = geo.Layer?.Name;
|
||||||
|
if (
|
||||||
|
string.Equals(name, "ENGRAVE", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(name, "ETCH", System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(name, SpecialLayers.Scribe.Name, System.StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
return LayerType.Scribe;
|
||||||
|
|
||||||
|
return LayerType.Cut;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.CNC;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,12 @@ namespace OpenNest.Converters
|
|||||||
return geometry;
|
return geometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
private static void AddProgram(
|
||||||
|
Program program,
|
||||||
|
ref Mode mode,
|
||||||
|
ref Vector curpos,
|
||||||
|
ref List<Entity> geometry
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Capture the frame origin at entry. Sub-program Offsets are relative
|
// Capture the frame origin at entry. Sub-program Offsets are relative
|
||||||
// to this fixed origin, not to the current tool position.
|
// to this fixed origin, not to the current tool position.
|
||||||
@@ -49,7 +54,10 @@ namespace OpenNest.Converters
|
|||||||
|
|
||||||
// The sub-program's frame origin in this program's frame is
|
// The sub-program's frame origin in this program's frame is
|
||||||
// frameOrigin + Offset — independent of current tool position.
|
// frameOrigin + Offset — independent of current tool position.
|
||||||
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
|
curpos = new Vector(
|
||||||
|
frameOrigin.X + subpgm.Offset.X,
|
||||||
|
frameOrigin.Y + subpgm.Offset.Y
|
||||||
|
);
|
||||||
|
|
||||||
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
|
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
|
||||||
mode = savedMode;
|
mode = savedMode;
|
||||||
@@ -58,7 +66,12 @@ namespace OpenNest.Converters
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddLinearMove(LinearMove linearMove, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
private static void AddLinearMove(
|
||||||
|
LinearMove linearMove,
|
||||||
|
ref Mode mode,
|
||||||
|
ref Vector curpos,
|
||||||
|
ref List<Entity> geometry
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var pt = linearMove.EndPoint;
|
var pt = linearMove.EndPoint;
|
||||||
|
|
||||||
@@ -66,16 +79,17 @@ namespace OpenNest.Converters
|
|||||||
pt += curpos;
|
pt += curpos;
|
||||||
|
|
||||||
var layer = ConvertLayer(linearMove.Layer);
|
var layer = ConvertLayer(linearMove.Layer);
|
||||||
var line = new Line(curpos, pt)
|
var line = new Line(curpos, pt) { Layer = layer, Color = layer.Color };
|
||||||
{
|
|
||||||
Layer = layer,
|
|
||||||
Color = layer.Color
|
|
||||||
};
|
|
||||||
geometry.Add(line);
|
geometry.Add(line);
|
||||||
curpos = pt;
|
curpos = pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddRapidMove(RapidMove rapidMove, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
private static void AddRapidMove(
|
||||||
|
RapidMove rapidMove,
|
||||||
|
ref Mode mode,
|
||||||
|
ref Vector curpos,
|
||||||
|
ref List<Entity> geometry
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var pt = rapidMove.EndPoint;
|
var pt = rapidMove.EndPoint;
|
||||||
|
|
||||||
@@ -85,13 +99,18 @@ namespace OpenNest.Converters
|
|||||||
var line = new Line(curpos, pt)
|
var line = new Line(curpos, pt)
|
||||||
{
|
{
|
||||||
Layer = SpecialLayers.Rapid,
|
Layer = SpecialLayers.Rapid,
|
||||||
Color = SpecialLayers.Rapid.Color
|
Color = SpecialLayers.Rapid.Color,
|
||||||
};
|
};
|
||||||
geometry.Add(line);
|
geometry.Add(line);
|
||||||
curpos = pt;
|
curpos = pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddArcMove(ArcMove arcMove, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
private static void AddArcMove(
|
||||||
|
ArcMove arcMove,
|
||||||
|
ref Mode mode,
|
||||||
|
ref Vector curpos,
|
||||||
|
ref List<Entity> geometry
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var center = arcMove.CenterPoint;
|
var center = arcMove.CenterPoint;
|
||||||
var endpt = arcMove.EndPoint;
|
var endpt = arcMove.EndPoint;
|
||||||
@@ -102,6 +121,8 @@ namespace OpenNest.Converters
|
|||||||
center += curpos;
|
center += curpos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
center = FitCenterToEndpoints(center, curpos, endpt);
|
||||||
|
|
||||||
var startAngle = center.AngleTo(curpos);
|
var startAngle = center.AngleTo(curpos);
|
||||||
var endAngle = center.AngleTo(endpt);
|
var endAngle = center.AngleTo(endpt);
|
||||||
|
|
||||||
@@ -112,13 +133,61 @@ namespace OpenNest.Converters
|
|||||||
var layer = ConvertLayer(arcMove.Layer);
|
var layer = ConvertLayer(arcMove.Layer);
|
||||||
|
|
||||||
if (startAngle.IsEqualTo(endAngle))
|
if (startAngle.IsEqualTo(endAngle))
|
||||||
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color, Rotation = arcMove.Rotation });
|
geometry.Add(
|
||||||
|
new Circle(center, radius)
|
||||||
|
{
|
||||||
|
Layer = layer,
|
||||||
|
Color = layer.Color,
|
||||||
|
Rotation = arcMove.Rotation,
|
||||||
|
}
|
||||||
|
);
|
||||||
else
|
else
|
||||||
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
|
geometry.Add(
|
||||||
|
new Arc(
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
startAngle,
|
||||||
|
endAngle,
|
||||||
|
arcMove.Rotation == RotationType.CW
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Layer = layer,
|
||||||
|
Color = layer.Color,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
curpos = endpt;
|
curpos = endpt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Programs can carry arc centers that are not quite equidistant from the
|
||||||
|
/// start and end points (e.g. I0.03 on a 0.0598 chord). Building the arc from
|
||||||
|
/// the end radius alone then leaves its start point off the previous move's
|
||||||
|
/// end, which breaks contour chaining. Project the center onto the chord's
|
||||||
|
/// perpendicular bisector so the arc passes through both endpoints exactly.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector FitCenterToEndpoints(Vector center, Vector start, Vector end)
|
||||||
|
{
|
||||||
|
var startRadius = center.DistanceTo(start);
|
||||||
|
var endRadius = center.DistanceTo(end);
|
||||||
|
|
||||||
|
if (startRadius.IsEqualTo(endRadius))
|
||||||
|
return center;
|
||||||
|
|
||||||
|
var chord = end - start;
|
||||||
|
var chordLengthSq = chord.X * chord.X + chord.Y * chord.Y;
|
||||||
|
|
||||||
|
// Full circle (start == end): no chord to fit against.
|
||||||
|
if (chordLengthSq < Tolerance.Epsilon * Tolerance.Epsilon)
|
||||||
|
return center;
|
||||||
|
|
||||||
|
var mid = new Vector((start.X + end.X) * 0.5, (start.Y + end.Y) * 0.5);
|
||||||
|
var normal = new Vector(-chord.Y, chord.X);
|
||||||
|
var t = ((center.X - mid.X) * normal.X + (center.Y - mid.Y) * normal.Y) / chordLengthSq;
|
||||||
|
|
||||||
|
return new Vector(mid.X + normal.X * t, mid.Y + normal.Y * t);
|
||||||
|
}
|
||||||
|
|
||||||
private static Layer ConvertLayer(LayerType layer)
|
private static Layer ConvertLayer(LayerType layer)
|
||||||
{
|
{
|
||||||
switch (layer)
|
switch (layer)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the Scribe layer on program moves that were saved as cuts. Programs built from
|
||||||
|
/// stored entities before ConvertGeometry recognized the SCRIBE layer name turned etch marks
|
||||||
|
/// into Cut moves, which nesting then treated as open cut geometry. The drawing's source
|
||||||
|
/// entities still carry the mark layer, so matching moves are reclassified from them.
|
||||||
|
/// </summary>
|
||||||
|
public static class ScribeLayerRepair
|
||||||
|
{
|
||||||
|
private const double MatchTolerance = 0.001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reclassifies Cut moves in <paramref name="program"/> that lie on a mark entity in
|
||||||
|
/// <paramref name="sourceEntities"/>. Program coordinates are source coordinates shifted
|
||||||
|
/// by -<paramref name="sourceOffset"/>. Returns the number of moves reclassified.
|
||||||
|
/// </summary>
|
||||||
|
public static int Apply(Program program, IEnumerable<Entity> sourceEntities, Vector sourceOffset)
|
||||||
|
{
|
||||||
|
if (program == null || sourceEntities == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var marks = sourceEntities.Where(e => IsMarkLayer(e.Layer)).ToList();
|
||||||
|
if (marks.Count == 0 || program.Codes.Any(c => c is SubProgramCall))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// ToGeometry emits exactly one entity per rapid/linear/arc move of a flat program.
|
||||||
|
var motions = program.Codes.Where(c => c is RapidMove or LinearMove or ArcMove).ToList();
|
||||||
|
var geometry = ConvertProgram.ToGeometry(program);
|
||||||
|
if (geometry.Count != motions.Count)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var repaired = 0;
|
||||||
|
for (var i = 0; i < motions.Count; i++)
|
||||||
|
{
|
||||||
|
var source = Translate(geometry[i], sourceOffset);
|
||||||
|
switch (motions[i])
|
||||||
|
{
|
||||||
|
case LinearMove line when line.Layer == LayerType.Cut && IsOnMark(source, marks):
|
||||||
|
line.Layer = LayerType.Scribe;
|
||||||
|
repaired++;
|
||||||
|
break;
|
||||||
|
case ArcMove arc when arc.Layer == LayerType.Cut && IsOnMark(source, marks):
|
||||||
|
arc.Layer = LayerType.Scribe;
|
||||||
|
repaired++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repaired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsMarkLayer(Layer layer) =>
|
||||||
|
layer != null
|
||||||
|
&& (
|
||||||
|
layer == SpecialLayers.Scribe
|
||||||
|
|| string.Equals(layer.Name, SpecialLayers.Scribe.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(layer.Name, "ETCH", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(layer.Name, "ENGRAVE", StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static List<Vector> Translate(Entity entity, Vector offset)
|
||||||
|
{
|
||||||
|
var points = entity switch
|
||||||
|
{
|
||||||
|
Line l => new List<Vector>
|
||||||
|
{
|
||||||
|
l.StartPoint,
|
||||||
|
l.EndPoint,
|
||||||
|
new Vector((l.StartPoint.X + l.EndPoint.X) / 2, (l.StartPoint.Y + l.EndPoint.Y) / 2),
|
||||||
|
},
|
||||||
|
Arc a => new List<Vector> { a.StartPoint(), a.EndPoint(), a.MidPoint() },
|
||||||
|
Circle c => new List<Vector>
|
||||||
|
{
|
||||||
|
new Vector(c.Center.X + c.Radius, c.Center.Y),
|
||||||
|
new Vector(c.Center.X - c.Radius, c.Center.Y),
|
||||||
|
new Vector(c.Center.X, c.Center.Y + c.Radius),
|
||||||
|
},
|
||||||
|
_ => new List<Vector>(),
|
||||||
|
};
|
||||||
|
return points.ConvertAll(p => new Vector(p.X + offset.X, p.Y + offset.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A move is a mark when every sample point lies on one single mark entity.
|
||||||
|
private static bool IsOnMark(List<Vector> points, List<Entity> marks) =>
|
||||||
|
points.Count > 0
|
||||||
|
&& marks.Any(m => points.All(p => m.ClosestPointTo(p).DistanceTo(p) <= MatchTolerance));
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
-26
@@ -1,18 +1,20 @@
|
|||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Geometry;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
public enum CutOffAxis
|
public enum CutOffAxis
|
||||||
{
|
{
|
||||||
Horizontal,
|
Horizontal,
|
||||||
Vertical
|
Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CutOff
|
public class CutOff
|
||||||
{
|
{
|
||||||
|
private const double OffsetTolerance = 0.001;
|
||||||
|
|
||||||
public Vector Position { get; set; }
|
public Vector Position { get; set; }
|
||||||
public CutOffAxis Axis { get; set; }
|
public CutOffAxis Axis { get; set; }
|
||||||
public double? StartLimit { get; set; }
|
public double? StartLimit { get; set; }
|
||||||
@@ -26,7 +28,11 @@ namespace OpenNest
|
|||||||
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Regenerate(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache = null)
|
public void Regenerate(
|
||||||
|
Plate plate,
|
||||||
|
CutOffSettings settings,
|
||||||
|
Dictionary<Part, Entity> cache = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var segments = ComputeSegments(plate, settings, cache);
|
var segments = ComputeSegments(plate, settings, cache);
|
||||||
var program = BuildProgram(segments, settings);
|
var program = BuildProgram(segments, settings);
|
||||||
@@ -40,11 +46,17 @@ namespace OpenNest
|
|||||||
return $"CutOff-{axisChar}-{coord:F2}";
|
return $"CutOff-{axisChar}-{coord:F2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache)
|
private List<(double Start, double End)> ComputeSegments(
|
||||||
|
Plate plate,
|
||||||
|
CutOffSettings settings,
|
||||||
|
Dictionary<Part, Entity> cache
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var bounds = plate.BoundingBox(includeParts: false);
|
var bounds = plate.BoundingBox(includeParts: false);
|
||||||
|
|
||||||
double lineStart, lineEnd, cutPosition;
|
double lineStart,
|
||||||
|
lineEnd,
|
||||||
|
cutPosition;
|
||||||
|
|
||||||
if (Axis == CutOffAxis.Vertical)
|
if (Axis == CutOffAxis.Vertical)
|
||||||
{
|
{
|
||||||
@@ -68,7 +80,14 @@ namespace OpenNest
|
|||||||
|
|
||||||
Entity perimeter = null;
|
Entity perimeter = null;
|
||||||
cache?.TryGetValue(part, out perimeter);
|
cache?.TryGetValue(part, out perimeter);
|
||||||
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
|
var partExclusions = GetPartExclusions(
|
||||||
|
part,
|
||||||
|
perimeter,
|
||||||
|
cutPosition,
|
||||||
|
lineStart,
|
||||||
|
lineEnd,
|
||||||
|
settings.PartClearance
|
||||||
|
);
|
||||||
exclusions.AddRange(partExclusions);
|
exclusions.AddRange(partExclusions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +126,13 @@ namespace OpenNest
|
|||||||
private static readonly List<(double Start, double End)> EmptyExclusions = new();
|
private static readonly List<(double Start, double End)> EmptyExclusions = new();
|
||||||
|
|
||||||
private List<(double Start, double End)> GetPartExclusions(
|
private List<(double Start, double End)> GetPartExclusions(
|
||||||
Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
Part part,
|
||||||
|
Entity perimeter,
|
||||||
|
double cutPosition,
|
||||||
|
double lineStart,
|
||||||
|
double lineEnd,
|
||||||
|
double clearance
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var bb = part.BoundingBox;
|
var bb = part.BoundingBox;
|
||||||
var (partMin, partMax) = AxisBounds(bb, clearance);
|
var (partMin, partMax) = AxisBounds(bb, clearance);
|
||||||
@@ -118,7 +143,13 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (perimeter != null)
|
if (perimeter != null)
|
||||||
{
|
{
|
||||||
var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance);
|
var perimeterExclusions = IntersectPerimeter(
|
||||||
|
perimeter,
|
||||||
|
cutPosition,
|
||||||
|
lineStart,
|
||||||
|
lineEnd,
|
||||||
|
clearance
|
||||||
|
);
|
||||||
if (perimeterExclusions != null)
|
if (perimeterExclusions != null)
|
||||||
return perimeterExclusions;
|
return perimeterExclusions;
|
||||||
}
|
}
|
||||||
@@ -127,17 +158,33 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<(double Start, double End)> IntersectPerimeter(
|
private List<(double Start, double End)> IntersectPerimeter(
|
||||||
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
Entity perimeter,
|
||||||
|
double cutPosition,
|
||||||
|
double lineStart,
|
||||||
|
double lineEnd,
|
||||||
|
double clearance
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
|
var offset = OffsetOutward(perimeter, clearance);
|
||||||
var usedOffset = target != perimeter;
|
var usedOffset = offset != null;
|
||||||
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
var targets = offset ?? new List<Entity> { perimeter };
|
||||||
|
var cutLine = new Line(
|
||||||
|
MakePoint(cutPosition, lineStart),
|
||||||
|
MakePoint(cutPosition, lineEnd)
|
||||||
|
);
|
||||||
|
|
||||||
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
var pts = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var target in targets)
|
||||||
|
{
|
||||||
|
if (target.Intersects(cutLine, out var targetPts))
|
||||||
|
pts.AddRange(targetPts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pts.Count < 2)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var coords = pts
|
var coords = pts.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
|
||||||
.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
|
|
||||||
.OrderBy(c => c)
|
.OrderBy(c => c)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -152,21 +199,37 @@ namespace OpenNest
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Entity OffsetOutward(Entity perimeter, double clearance)
|
/// <summary>
|
||||||
|
/// Grows the perimeter by the clearance as one Clipper region offset, so slots
|
||||||
|
/// narrower than twice the clearance close up instead of leaving a gap the cut
|
||||||
|
/// could run into. Holes appear only where the perimeter curls back on itself.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Entity> OffsetOutward(Entity perimeter, double clearance)
|
||||||
{
|
{
|
||||||
if (clearance <= 0)
|
if (clearance <= 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
try
|
var offset = perimeter switch
|
||||||
{
|
|
||||||
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
|
|
||||||
offset?.UpdateBounds();
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
|
Shape shape => ClipperBridge.OffsetPerimeter(
|
||||||
|
shape,
|
||||||
|
clearance,
|
||||||
|
OffsetTolerance,
|
||||||
|
circumscribe: true
|
||||||
|
),
|
||||||
|
Polygon polygon => ClipperBridge.OffsetPerimeter(
|
||||||
|
polygon,
|
||||||
|
clearance,
|
||||||
|
OffsetTolerance,
|
||||||
|
circumscribe: true
|
||||||
|
),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (offset == null || offset.Outers.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
return offset.Outers.Concat(offset.Holes).Cast<Entity>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
||||||
@@ -184,7 +247,10 @@ namespace OpenNest
|
|||||||
? (bb.Y - clearance, bb.Y + bb.Width + clearance)
|
? (bb.Y - clearance, bb.Y + bb.Width + clearance)
|
||||||
: (bb.X - clearance, bb.X + bb.Length + clearance);
|
: (bb.X - clearance, bb.X + bb.Length + clearance);
|
||||||
|
|
||||||
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
|
private Program BuildProgram(
|
||||||
|
List<(double Start, double End)> segments,
|
||||||
|
CutOffSettings settings
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var program = new Program();
|
var program = new Program();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ namespace OpenNest
|
|||||||
public enum CutDirection
|
public enum CutDirection
|
||||||
{
|
{
|
||||||
TowardOrigin,
|
TowardOrigin,
|
||||||
AwayFromOrigin
|
AwayFromOrigin,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CutOffSettings
|
public class CutOffSettings
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ public class CutParameters
|
|||||||
public string PostProcessor { get; set; }
|
public string PostProcessor { get; set; }
|
||||||
public Units Units { get; set; }
|
public Units Units { get; set; }
|
||||||
|
|
||||||
public static CutParameters Default => new()
|
public static CutParameters Default =>
|
||||||
{
|
new()
|
||||||
Feedrate = 100,
|
{
|
||||||
RapidTravelRate = 300,
|
Feedrate = 100,
|
||||||
PierceTime = TimeSpan.FromSeconds(0.5),
|
RapidTravelRate = 300,
|
||||||
Units = OpenNest.Units.Inches
|
PierceTime = TimeSpan.FromSeconds(0.5),
|
||||||
};
|
Units = OpenNest.Units.Inches,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-24
@@ -1,12 +1,12 @@
|
|||||||
using OpenNest.Bending;
|
using System;
|
||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Geometry;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using OpenNest.Bending;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -18,18 +18,18 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static Color[] PartColors = new Color[]
|
public static Color[] PartColors = new Color[]
|
||||||
{
|
{
|
||||||
Color.FromArgb(205, 92, 92), // Indian Red
|
Color.FromArgb(205, 92, 92), // Indian Red
|
||||||
Color.FromArgb(148, 103, 189), // Medium Purple
|
Color.FromArgb(148, 103, 189), // Medium Purple
|
||||||
Color.FromArgb(75, 180, 175), // Teal
|
Color.FromArgb(75, 180, 175), // Teal
|
||||||
Color.FromArgb(210, 190, 75), // Goldenrod
|
Color.FromArgb(210, 190, 75), // Goldenrod
|
||||||
Color.FromArgb(190, 85, 175), // Orchid
|
Color.FromArgb(190, 85, 175), // Orchid
|
||||||
Color.FromArgb(185, 115, 85), // Sienna
|
Color.FromArgb(185, 115, 85), // Sienna
|
||||||
Color.FromArgb(120, 100, 190), // Slate Blue
|
Color.FromArgb(120, 100, 190), // Slate Blue
|
||||||
Color.FromArgb(200, 100, 140), // Rose
|
Color.FromArgb(200, 100, 140), // Rose
|
||||||
Color.FromArgb(80, 175, 155), // Sea Green
|
Color.FromArgb(80, 175, 155), // Sea Green
|
||||||
Color.FromArgb(195, 160, 85), // Dark Khaki
|
Color.FromArgb(195, 160, 85), // Dark Khaki
|
||||||
Color.FromArgb(175, 95, 160), // Plum
|
Color.FromArgb(175, 95, 160), // Plum
|
||||||
Color.FromArgb(215, 130, 130), // Light Coral
|
Color.FromArgb(215, 130, 130), // Light Coral
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Color GetNextColor()
|
public static Color GetNextColor()
|
||||||
@@ -40,14 +40,10 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Drawing()
|
public Drawing()
|
||||||
: this(string.Empty, new Program())
|
: this(string.Empty, new Program()) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Drawing(string name)
|
public Drawing(string name)
|
||||||
: this(name, new Program())
|
: this(name, new Program()) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Drawing(string name, Program pgm)
|
public Drawing(string name, Program pgm)
|
||||||
{
|
{
|
||||||
@@ -127,7 +123,9 @@ namespace OpenNest
|
|||||||
|
|
||||||
public void UpdateArea()
|
public void UpdateArea()
|
||||||
{
|
{
|
||||||
var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid);
|
var geometry = ConvertProgram
|
||||||
|
.ToGeometry(Program)
|
||||||
|
.Where(entity => SpecialLayers.IsMaterial(entity.Layer));
|
||||||
var shapes = ShapeBuilder.GetShapes(geometry);
|
var shapes = ShapeBuilder.GetShapes(geometry);
|
||||||
|
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -12,16 +12,18 @@ namespace OpenNest.Geometry
|
|||||||
private Vector center;
|
private Vector center;
|
||||||
private bool reversed;
|
private bool reversed;
|
||||||
|
|
||||||
public Arc()
|
public Arc() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Arc(double x, double y, double r, double a1, double a2, bool reversed = false)
|
public Arc(double x, double y, double r, double a1, double a2, bool reversed = false)
|
||||||
: this(new Vector(x, y), r, a1, a2, reversed)
|
: this(new Vector(x, y), r, a1, a2, reversed) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Arc(Vector center, double radius, double startAngle, double endAngle, bool reversed = false)
|
public Arc(
|
||||||
|
Vector center,
|
||||||
|
double radius,
|
||||||
|
double startAngle,
|
||||||
|
double endAngle,
|
||||||
|
bool reversed = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
this.center = center;
|
this.center = center;
|
||||||
this.radius = radius;
|
this.radius = radius;
|
||||||
@@ -93,8 +95,7 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsFullCircle() =>
|
public bool IsFullCircle() => SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
|
||||||
SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Angle in radians between start and end angles.
|
/// Angle in radians between start and end angles.
|
||||||
@@ -130,10 +131,7 @@ namespace OpenNest.Geometry
|
|||||||
public RotationType Rotation
|
public RotationType Rotation
|
||||||
{
|
{
|
||||||
get { return IsReversed ? RotationType.CW : RotationType.CCW; }
|
get { return IsReversed ? RotationType.CW : RotationType.CCW; }
|
||||||
set
|
set { IsReversed = (value == RotationType.CW); }
|
||||||
{
|
|
||||||
IsReversed = (value == RotationType.CW);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -144,7 +142,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
return new Vector(
|
return new Vector(
|
||||||
Center.X + Radius * System.Math.Cos(StartAngle),
|
Center.X + Radius * System.Math.Cos(StartAngle),
|
||||||
Center.Y + Radius * System.Math.Sin(StartAngle));
|
Center.Y + Radius * System.Math.Sin(StartAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -155,7 +154,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
return new Vector(
|
return new Vector(
|
||||||
Center.X + Radius * System.Math.Cos(EndAngle),
|
Center.X + Radius * System.Math.Cos(EndAngle),
|
||||||
Center.Y + Radius * System.Math.Sin(EndAngle));
|
Center.Y + Radius * System.Math.Sin(EndAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -166,7 +166,8 @@ namespace OpenNest.Geometry
|
|||||||
var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
|
var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
|
||||||
return new Vector(
|
return new Vector(
|
||||||
Center.X + Radius * System.Math.Cos(midAngle),
|
Center.X + Radius * System.Math.Cos(midAngle),
|
||||||
Center.Y + Radius * System.Math.Sin(midAngle));
|
Center.Y + Radius * System.Math.Sin(midAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -231,7 +232,10 @@ namespace OpenNest.Geometry
|
|||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius);
|
var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius);
|
||||||
return System.Math.Max(1, (int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle));
|
return System.Math.Max(
|
||||||
|
1,
|
||||||
|
(int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -242,21 +246,23 @@ namespace OpenNest.Geometry
|
|||||||
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
|
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
|
||||||
{
|
{
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
var stepAngle = reversed
|
var stepAngle = reversed ? -SweepAngle() / segments : SweepAngle() / segments;
|
||||||
? -SweepAngle() / segments
|
|
||||||
: SweepAngle() / segments;
|
|
||||||
|
|
||||||
var r = circumscribe && segments > 0
|
var r =
|
||||||
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
|
circumscribe && segments > 0
|
||||||
: Radius;
|
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
|
||||||
|
: Radius;
|
||||||
|
|
||||||
for (int i = 0; i <= segments; ++i)
|
for (int i = 0; i <= segments; ++i)
|
||||||
{
|
{
|
||||||
var angle = stepAngle * i + StartAngle;
|
var angle = stepAngle * i + StartAngle;
|
||||||
|
|
||||||
points.Add(new Vector(
|
points.Add(
|
||||||
System.Math.Cos(angle) * r + Center.X,
|
new Vector(
|
||||||
System.Math.Sin(angle) * r + Center.Y));
|
System.Math.Cos(angle) * r + Center.X,
|
||||||
|
System.Math.Sin(angle) * r + Center.Y
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
@@ -437,19 +443,23 @@ namespace OpenNest.Geometry
|
|||||||
boundingBox.Width = maxY - minY;
|
boundingBox.Width = maxY - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets the arc to the given side of its travel direction. The center lies to
|
||||||
|
/// the left of a CCW arc and to the right of a CW (reversed) one, so the arc grows
|
||||||
|
/// on the other side and shrinks toward its center. Returns null when it shrinks
|
||||||
|
/// to nothing.
|
||||||
|
/// </summary>
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
if (side == OffsetSide.Left && reversed)
|
var grows = (side == OffsetSide.Left) == reversed;
|
||||||
{
|
|
||||||
return new Arc(center, radius + distance, startAngle, endAngle, reversed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (distance >= radius)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new Arc(center, radius - distance, startAngle, endAngle, reversed);
|
if (grows)
|
||||||
}
|
return new Arc(center, radius + distance, startAngle, endAngle, reversed);
|
||||||
|
|
||||||
|
if (distance >= radius)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new Arc(center, radius - distance, startAngle, endAngle, reversed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, Vector pt)
|
public override Entity OffsetEntity(double distance, Vector pt)
|
||||||
@@ -470,7 +480,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
return new Vector(
|
return new Vector(
|
||||||
System.Math.Cos(angle) * Radius + Center.X,
|
System.Math.Cos(angle) * Radius + Center.X,
|
||||||
System.Math.Sin(angle) * Radius + Center.Y);
|
System.Math.Sin(angle) * Radius + Center.Y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -500,7 +511,8 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Intersect.Intersects(this, arc, out pts); ;
|
return Intersect.Intersects(this, arc, out pts);
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ namespace OpenNest.Geometry
|
|||||||
/// the arc passes through both endpoints and departs P1 in the given direction.
|
/// the arc passes through both endpoints and departs P1 in the given direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static (Vector center, double radius, double deviation) FitWithStartTangent(
|
internal static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||||
List<Vector> points, Vector tangent)
|
List<Vector> points,
|
||||||
|
Vector tangent
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (points.Count < 3)
|
if (points.Count < 3)
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
@@ -57,14 +59,22 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fits a circular arc constrained to be tangent to the given directions at both
|
/// Fits a circular arc that passes exactly through both the first and last points
|
||||||
/// the first and last points. The center lies at the intersection of the normals
|
/// while matching the given endpoint tangents as closely as possible. For any
|
||||||
/// at P1 and Pn, guaranteeing the arc departs P1 in the start direction and arrives
|
/// circle through two points, the tangents at those points make equal mirrored
|
||||||
/// at Pn in the end direction. Uses the radius from P1 (exact start tangent);
|
/// angles with the chord, so the achievable inscribed angle is the average of the
|
||||||
/// deviation includes any endpoint gap at Pn.
|
/// two requested ones — when the requested tangents are consistent with a single
|
||||||
|
/// circular arc, both are matched exactly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static (Vector center, double radius, double deviation) FitWithDualTangent(
|
internal static (
|
||||||
List<Vector> points, Vector startTangent, Vector endTangent)
|
Vector center,
|
||||||
|
double radius,
|
||||||
|
double deviation
|
||||||
|
) FitThroughEndpointsWithTangents(
|
||||||
|
List<Vector> points,
|
||||||
|
Vector startTangent,
|
||||||
|
Vector endTangent
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (points.Count < 3)
|
if (points.Count < 3)
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
@@ -72,48 +82,51 @@ namespace OpenNest.Geometry
|
|||||||
var p1 = points[0];
|
var p1 = points[0];
|
||||||
var pn = points[^1];
|
var pn = points[^1];
|
||||||
|
|
||||||
var stLen = System.Math.Sqrt(startTangent.X * startTangent.X + startTangent.Y * startTangent.Y);
|
|
||||||
var etLen = System.Math.Sqrt(endTangent.X * endTangent.X + endTangent.Y * endTangent.Y);
|
|
||||||
if (stLen < 1e-10 || etLen < 1e-10)
|
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
|
||||||
|
|
||||||
// Normal to start tangent at P1 (perpendicular)
|
|
||||||
var n1x = -startTangent.Y / stLen;
|
|
||||||
var n1y = startTangent.X / stLen;
|
|
||||||
|
|
||||||
// Normal to end tangent at Pn
|
|
||||||
var n2x = -endTangent.Y / etLen;
|
|
||||||
var n2y = endTangent.X / etLen;
|
|
||||||
|
|
||||||
// Solve: P1 + t1*N1 = Pn + t2*N2
|
|
||||||
var det = n1x * (-n2y) - (-n2x) * n1y;
|
|
||||||
if (System.Math.Abs(det) < 1e-10)
|
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
|
||||||
|
|
||||||
var dx = pn.X - p1.X;
|
var dx = pn.X - p1.X;
|
||||||
var dy = pn.Y - p1.Y;
|
var dy = pn.Y - p1.Y;
|
||||||
var t1 = (dx * (-n2y) - (-n2x) * dy) / det;
|
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
if (chordLen < 1e-10)
|
||||||
var cx = p1.X + t1 * n1x;
|
|
||||||
var cy = p1.Y + t1 * n1y;
|
|
||||||
|
|
||||||
// Use radius from P1 (guarantees exact start tangent and passes through P1)
|
|
||||||
var r1 = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
|
|
||||||
if (r1 < 1e-10)
|
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
// Measure endpoint gap at Pn
|
var ux = dx / chordLen;
|
||||||
var r2 = System.Math.Sqrt((cx - pn.X) * (cx - pn.X) + (cy - pn.Y) * (cy - pn.Y));
|
var uy = dy / chordLen;
|
||||||
var endpointDev = System.Math.Abs(r2 - r1);
|
|
||||||
|
|
||||||
var interiorDev = MaxRadialDeviation(points, cx, cy, r1);
|
// Inscribed angle between chord and tangent at each endpoint (mirrored at Pn)
|
||||||
return (new Vector(cx, cy), r1, System.Math.Max(endpointDev, interiorDev));
|
var theta1 = SignedAngle(ux, uy, startTangent);
|
||||||
|
var theta2 = -SignedAngle(ux, uy, endTangent);
|
||||||
|
var theta = (theta1 + theta2) / 2;
|
||||||
|
|
||||||
|
// Nearly straight or degenerate (sweep would exceed ~356 degrees)
|
||||||
|
if (System.Math.Abs(theta) < 1e-3 || System.Math.Abs(theta) > System.Math.PI * 0.99)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var halfChord = chordLen / 2;
|
||||||
|
var radius = halfChord / System.Math.Abs(System.Math.Sin(theta));
|
||||||
|
var d = -halfChord / System.Math.Tan(theta);
|
||||||
|
|
||||||
|
var cx = (p1.X + pn.X) / 2 + d * -uy;
|
||||||
|
var cy = (p1.Y + pn.Y) / 2 + d * ux;
|
||||||
|
|
||||||
|
return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double SignedAngle(double ux, double uy, Vector to)
|
||||||
|
{
|
||||||
|
var len = System.Math.Sqrt(to.X * to.X + to.Y * to.Y);
|
||||||
|
if (len < 1e-10)
|
||||||
|
return 0;
|
||||||
|
return System.Math.Atan2(ux * to.Y - uy * to.X, ux * to.X + uy * to.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the maximum radial deviation of interior points from a circle.
|
/// Computes the maximum radial deviation of interior points from a circle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius)
|
internal static double MaxRadialDeviation(
|
||||||
|
List<Vector> points,
|
||||||
|
double cx,
|
||||||
|
double cy,
|
||||||
|
double radius
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var maxDev = 0.0;
|
var maxDev = 0.0;
|
||||||
for (var i = 1; i < points.Count - 1; i++)
|
for (var i = 1; i < points.Count - 1; i++)
|
||||||
@@ -122,7 +135,8 @@ namespace OpenNest.Geometry
|
|||||||
var py = points[i].Y - cy;
|
var py = points[i].Y - cy;
|
||||||
var dist = System.Math.Sqrt(px * px + py * py);
|
var dist = System.Math.Sqrt(px * px + py * py);
|
||||||
var dev = System.Math.Abs(dist - radius);
|
var dev = System.Math.Abs(dist - radius);
|
||||||
if (dev > maxDev) maxDev = dev;
|
if (dev > maxDev)
|
||||||
|
maxDev = dev;
|
||||||
}
|
}
|
||||||
return maxDev;
|
return maxDev;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
foreach (var box in boxes)
|
foreach (var box in boxes)
|
||||||
{
|
{
|
||||||
if (box.Left < minX) minX = box.Left;
|
if (box.Left < minX)
|
||||||
if (box.Right > maxX) maxX = box.Right;
|
minX = box.Left;
|
||||||
if (box.Bottom < minY) minY = box.Bottom;
|
if (box.Right > maxX)
|
||||||
if (box.Top > maxY) maxY = box.Top;
|
maxX = box.Right;
|
||||||
|
if (box.Bottom < minY)
|
||||||
|
minY = box.Bottom;
|
||||||
|
if (box.Top > maxY)
|
||||||
|
maxY = box.Top;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Box(minX, minY, maxX - minX, maxY - minY);
|
return new Box(minX, minY, maxX - minX, maxY - minY);
|
||||||
@@ -41,11 +45,15 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var vertex = pts[i];
|
var vertex = pts[i];
|
||||||
|
|
||||||
if (vertex.X < minX) minX = vertex.X;
|
if (vertex.X < minX)
|
||||||
else if (vertex.X > maxX) maxX = vertex.X;
|
minX = vertex.X;
|
||||||
|
else if (vertex.X > maxX)
|
||||||
|
maxX = vertex.X;
|
||||||
|
|
||||||
if (vertex.Y < minY) minY = vertex.Y;
|
if (vertex.Y < minY)
|
||||||
else if (vertex.Y > maxY) maxY = vertex.Y;
|
minY = vertex.Y;
|
||||||
|
else if (vertex.Y > maxY)
|
||||||
|
maxY = vertex.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Box(minX, minY, maxX - minX, maxY - minY);
|
return new Box(minX, minY, maxX - minX, maxY - minY);
|
||||||
@@ -65,10 +73,14 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
foreach (var box in items)
|
foreach (var box in items)
|
||||||
{
|
{
|
||||||
if (box.Left < left) left = box.Left;
|
if (box.Left < left)
|
||||||
if (box.Right > right) right = box.Right;
|
left = box.Left;
|
||||||
if (box.Bottom < bottom) bottom = box.Bottom;
|
if (box.Right > right)
|
||||||
if (box.Top > top) top = box.Top;
|
right = box.Right;
|
||||||
|
if (box.Bottom < bottom)
|
||||||
|
bottom = box.Bottom;
|
||||||
|
if (box.Top > top)
|
||||||
|
top = box.Top;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Box(left, bottom, right - left, top - bottom);
|
return new Box(left, bottom, right - left, top - bottom);
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ namespace OpenNest.Geometry
|
|||||||
public static readonly Box Empty = new Box();
|
public static readonly Box Empty = new Box();
|
||||||
|
|
||||||
public Box()
|
public Box()
|
||||||
: this(0, 0, 0, 0)
|
: this(0, 0, 0, 0) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Box(double x, double y, double w, double h)
|
public Box(double x, double y, double w, double h)
|
||||||
{
|
{
|
||||||
@@ -117,10 +115,14 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public bool Intersects(Box box)
|
public bool Intersects(Box box)
|
||||||
{
|
{
|
||||||
if (Left >= box.Right) return false;
|
if (Left >= box.Right)
|
||||||
if (Right <= box.Left) return false;
|
return false;
|
||||||
if (Top <= box.Bottom) return false;
|
if (Right <= box.Left)
|
||||||
if (Bottom >= box.Top) return false;
|
return false;
|
||||||
|
if (Top <= box.Bottom)
|
||||||
|
return false;
|
||||||
|
if (Bottom >= box.Top)
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -146,18 +148,24 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public bool Contains(Box box)
|
public bool Contains(Box box)
|
||||||
{
|
{
|
||||||
if (box.Top > Top) return false;
|
if (box.Top > Top)
|
||||||
if (box.Left < Left) return false;
|
return false;
|
||||||
if (box.Right > Right) return false;
|
if (box.Left < Left)
|
||||||
if (box.Bottom < Bottom) return false;
|
return false;
|
||||||
|
if (box.Right > Right)
|
||||||
|
return false;
|
||||||
|
if (box.Bottom < Bottom)
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Contains(Vector pt)
|
public bool Contains(Vector pt)
|
||||||
{
|
{
|
||||||
return pt.X >= Left - Tolerance.Epsilon && pt.X <= Right + Tolerance.Epsilon
|
return pt.X >= Left - Tolerance.Epsilon
|
||||||
&& pt.Y >= Bottom - Tolerance.Epsilon && pt.Y <= Top + Tolerance.Epsilon;
|
&& pt.X <= Right + Tolerance.Epsilon
|
||||||
|
&& pt.Y >= Bottom - Tolerance.Epsilon
|
||||||
|
&& pt.Y <= Top + Tolerance.Epsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsHorizontalTo(Box box)
|
public bool IsHorizontalTo(Box box)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Math;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -8,14 +8,10 @@ namespace OpenNest.Geometry
|
|||||||
private Vector center;
|
private Vector center;
|
||||||
private double radius;
|
private double radius;
|
||||||
|
|
||||||
public Circle()
|
public Circle() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Circle(double x, double y, double radius)
|
public Circle(double x, double y, double radius)
|
||||||
: this(new Vector(x, y), radius)
|
: this(new Vector(x, y), radius) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Circle(Vector center, double radius)
|
public Circle(Vector center, double radius)
|
||||||
{
|
{
|
||||||
@@ -137,21 +133,22 @@ namespace OpenNest.Geometry
|
|||||||
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
|
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
|
||||||
{
|
{
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
var stepAngle = Rotation == RotationType.CW
|
var stepAngle =
|
||||||
? -Angle.TwoPI / segments
|
Rotation == RotationType.CW ? -Angle.TwoPI / segments : Angle.TwoPI / segments;
|
||||||
: Angle.TwoPI / segments;
|
|
||||||
|
|
||||||
var r = circumscribe && segments > 0
|
var r =
|
||||||
? Radius / System.Math.Cos(stepAngle / 2.0)
|
circumscribe && segments > 0 ? Radius / System.Math.Cos(stepAngle / 2.0) : Radius;
|
||||||
: Radius;
|
|
||||||
|
|
||||||
for (int i = 0; i <= segments; ++i)
|
for (int i = 0; i <= segments; ++i)
|
||||||
{
|
{
|
||||||
var angle = stepAngle * i;
|
var angle = stepAngle * i;
|
||||||
|
|
||||||
points.Add(new Vector(
|
points.Add(
|
||||||
System.Math.Cos(angle) * r + Center.X,
|
new Vector(
|
||||||
System.Math.Sin(angle) * r + Center.Y));
|
System.Math.Cos(angle) * r + Center.X,
|
||||||
|
System.Math.Sin(angle) * r + Center.Y
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
@@ -276,17 +273,18 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
if (side == OffsetSide.Left && Rotation == RotationType.CCW)
|
// The center lies to the left of a CCW circle and to the right of a CW one.
|
||||||
|
var shrinks = (side == OffsetSide.Left) == (Rotation == RotationType.CCW);
|
||||||
|
|
||||||
|
if (shrinks)
|
||||||
{
|
{
|
||||||
return Radius <= distance ? null : new Circle(center, Radius - distance)
|
return Radius <= distance
|
||||||
{
|
? null
|
||||||
Layer = Layer,
|
: new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation };
|
||||||
Rotation = Rotation
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new Circle(center, Radius + distance) { Layer = Layer };
|
return new Circle(center, Radius + distance) { Layer = Layer, Rotation = Rotation };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,11 +292,9 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
if (ContainsPoint(pt))
|
if (ContainsPoint(pt))
|
||||||
{
|
{
|
||||||
return Radius <= distance ? null : new Circle(center, Radius - distance)
|
return Radius <= distance
|
||||||
{
|
? null
|
||||||
Layer = Layer,
|
: new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation };
|
||||||
Rotation = Rotation
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -317,7 +313,8 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
return new Vector(
|
return new Vector(
|
||||||
System.Math.Cos(angle) * Radius + Center.X,
|
System.Math.Cos(angle) * Radius + Center.X,
|
||||||
System.Math.Sin(angle) * Radius + Center.Y);
|
System.Math.Sin(angle) * Radius + Center.Y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -350,7 +347,9 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
var dist = Center.DistanceTo(circle.Center);
|
var dist = Center.DistanceTo(circle.Center);
|
||||||
return (dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius));
|
return (
|
||||||
|
dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Clipper2Lib;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Region offsetting through Clipper2, for CPU-side preparation only: work done
|
||||||
|
/// once per drawing, rotation or spacing whose output is cached and fed to hot
|
||||||
|
/// loops. Per-pair tests (<see cref="Collision"/>) stay hand-rolled so they can
|
||||||
|
/// be ported to a GPU kernel.
|
||||||
|
/// </summary>
|
||||||
|
public static class ClipperBridge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Decimal places Clipper keeps (1e-4 in either inches or mm).
|
||||||
|
/// </summary>
|
||||||
|
public const int Precision = 4;
|
||||||
|
|
||||||
|
private const double MiterLimit = 2.0;
|
||||||
|
|
||||||
|
private const double ConservativeJoinFactor = 0.25;
|
||||||
|
|
||||||
|
private const double ValidationJoinFactor = 0.1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a polygon to a Clipper path, dropping the closing vertex and
|
||||||
|
/// orienting it positive (CCW) or negative (CW).
|
||||||
|
/// </summary>
|
||||||
|
public static PathD ToPath(Polygon polygon, bool positive)
|
||||||
|
{
|
||||||
|
var path = ToPath(polygon, new Vector());
|
||||||
|
|
||||||
|
if (path.Count >= 3 && Clipper.IsPositive(path) != positive)
|
||||||
|
path.Reverse();
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a polygon to a Clipper path with an optional offset, dropping the
|
||||||
|
/// closing vertex and keeping the polygon's own winding.
|
||||||
|
/// </summary>
|
||||||
|
public static PathD ToPath(Polygon polygon, Vector offset)
|
||||||
|
{
|
||||||
|
var verts = polygon.Vertices;
|
||||||
|
var n = verts.Count;
|
||||||
|
|
||||||
|
if (n > 1 && verts[0].X == verts[n - 1].X && verts[0].Y == verts[n - 1].Y)
|
||||||
|
n--;
|
||||||
|
|
||||||
|
var path = new PathD(n);
|
||||||
|
|
||||||
|
for (var i = 0; i < n; i++)
|
||||||
|
path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y));
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a Clipper path to a closed polygon with updated bounds.
|
||||||
|
/// </summary>
|
||||||
|
public static Polygon ToPolygon(PathD path)
|
||||||
|
{
|
||||||
|
var polygon = new Polygon();
|
||||||
|
|
||||||
|
foreach (var pt in path)
|
||||||
|
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
||||||
|
|
||||||
|
polygon.Close();
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flattens a profile into a Clipper region: perimeter positive, cutouts negative.
|
||||||
|
/// </summary>
|
||||||
|
public static PathsD ToRegion(ShapeProfile profile, double tolerance, bool circumscribe)
|
||||||
|
{
|
||||||
|
var region = new PathsD(profile.Cutouts.Count + 1);
|
||||||
|
AddShape(region, profile.Perimeter, tolerance, circumscribe, positive: true);
|
||||||
|
|
||||||
|
// A cutout is flattened the opposite way: circumscribing it would shrink the
|
||||||
|
// material around it, so inscribe instead to keep the region conservative.
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
AddShape(region, cutout, tolerance, !circumscribe, positive: false);
|
||||||
|
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets a part region outward by <paramref name="distance"/>: the perimeter
|
||||||
|
/// grows and the cutouts shrink. Features narrower than twice the distance
|
||||||
|
/// collapse, and cutouts that close up disappear. Joins are round, with chords
|
||||||
|
/// no more than <paramref name="tolerance"/> from the true arc.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="circumscribe">
|
||||||
|
/// When true, the result never under-estimates the offset: perimeter arcs are
|
||||||
|
/// flattened outside the true curve, cutout arcs inside it, and the inflation is
|
||||||
|
/// padded by the round-join chord error and Clipper's rounding.
|
||||||
|
/// </param>
|
||||||
|
public static OffsetRegion Offset(
|
||||||
|
ShapeProfile profile,
|
||||||
|
double distance,
|
||||||
|
double tolerance,
|
||||||
|
bool circumscribe = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var region = ToRegion(profile, tolerance, circumscribe);
|
||||||
|
return Offset(region, distance, tolerance, circumscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets a single closed shape outward, ignoring any cutouts. A perimeter that
|
||||||
|
/// curls back on itself (a C shape with a narrow mouth) can gain holes.
|
||||||
|
/// </summary>
|
||||||
|
public static OffsetRegion OffsetPerimeter(
|
||||||
|
Shape perimeter,
|
||||||
|
double distance,
|
||||||
|
double tolerance,
|
||||||
|
bool circumscribe = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var polygon = Flatten(perimeter, tolerance, circumscribe);
|
||||||
|
return OffsetPerimeter(polygon, distance, tolerance, circumscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets a closed polygon outward, whatever its winding.
|
||||||
|
/// </summary>
|
||||||
|
public static OffsetRegion OffsetPerimeter(
|
||||||
|
Polygon perimeter,
|
||||||
|
double distance,
|
||||||
|
double tolerance,
|
||||||
|
bool circumscribe = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var region = new PathsD(1);
|
||||||
|
AddPolygon(region, perimeter, positive: true);
|
||||||
|
return Offset(region, distance, tolerance, circumscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets an already-flattened region (outers positive, holes negative).
|
||||||
|
/// A distance of zero only unions the region, with no conservative padding.
|
||||||
|
/// </summary>
|
||||||
|
public static OffsetRegion Offset(
|
||||||
|
PathsD region,
|
||||||
|
double distance,
|
||||||
|
double tolerance,
|
||||||
|
bool circumscribe = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Round joins put their vertices on the true arc, so each chord sits inside
|
||||||
|
// it by up to the join tolerance. In conservative mode, joins use a finer
|
||||||
|
// tolerance and the inflation is padded by it (plus Clipper's rounding).
|
||||||
|
var delta = distance;
|
||||||
|
var joinTolerance = tolerance;
|
||||||
|
|
||||||
|
if (circumscribe && distance > 0)
|
||||||
|
{
|
||||||
|
joinTolerance = tolerance * ConservativeJoinFactor;
|
||||||
|
delta += joinTolerance + 0.5 * System.Math.Pow(10, -Precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inflate(region, delta, joinTolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset for checking a finished layout against its spacing. Arcs are flattened
|
||||||
|
/// as in conservative mode (perimeter arcs circumscribed, cutout arcs inscribed),
|
||||||
|
/// but round joins use a tenth of the tolerance and nothing is padded, so a layout
|
||||||
|
/// exactly at the spacing passes. The only under-estimate is the join chord error
|
||||||
|
/// at convex corners, at most a tenth of <paramref name="tolerance"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static OffsetRegion OffsetForValidation(
|
||||||
|
ShapeProfile profile,
|
||||||
|
double distance,
|
||||||
|
double tolerance
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var region = ToRegion(profile, tolerance, circumscribe: true);
|
||||||
|
return Inflate(region, distance, tolerance * ValidationJoinFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OffsetRegion Inflate(PathsD region, double delta, double joinTolerance)
|
||||||
|
{
|
||||||
|
var inflated =
|
||||||
|
delta <= 0
|
||||||
|
? Union(region)
|
||||||
|
: Clipper.InflatePaths(
|
||||||
|
region,
|
||||||
|
delta,
|
||||||
|
JoinType.Round,
|
||||||
|
EndType.Polygon,
|
||||||
|
MiterLimit,
|
||||||
|
Precision,
|
||||||
|
joinTolerance
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = new OffsetRegion(new List<Polygon>(), new List<Polygon>());
|
||||||
|
|
||||||
|
foreach (var path in inflated)
|
||||||
|
{
|
||||||
|
if (path.Count < 3)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (Clipper.IsPositive(path))
|
||||||
|
result.Outers.Add(ToPolygon(path));
|
||||||
|
else
|
||||||
|
result.Holes.Add(ToPolygon(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Miter-offsets a closed polygon by <paramref name="delta"/> (positive grows it,
|
||||||
|
/// negative shrinks it). Returns the largest resulting polygon (CCW), or null
|
||||||
|
/// when the polygon collapses.
|
||||||
|
/// </summary>
|
||||||
|
public static Polygon OffsetMiter(Polygon polygon, double delta)
|
||||||
|
{
|
||||||
|
var path = ToPath(polygon, positive: true);
|
||||||
|
|
||||||
|
if (path.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var inflated = Clipper.InflatePaths(
|
||||||
|
new PathsD { path },
|
||||||
|
delta,
|
||||||
|
JoinType.Miter,
|
||||||
|
EndType.Polygon,
|
||||||
|
MiterLimit,
|
||||||
|
Precision
|
||||||
|
);
|
||||||
|
|
||||||
|
PathD largest = null;
|
||||||
|
var largestArea = 0.0;
|
||||||
|
|
||||||
|
foreach (var candidate in inflated)
|
||||||
|
{
|
||||||
|
var area = Clipper.Area(candidate);
|
||||||
|
|
||||||
|
if (area > largestArea)
|
||||||
|
{
|
||||||
|
largest = candidate;
|
||||||
|
largestArea = area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return largest == null ? null : ToPolygon(largest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flattens a closed shape to a polygon whose chords stay within
|
||||||
|
/// <paramref name="tolerance"/> of every arc. Inscribed, the vertices lie on the
|
||||||
|
/// arcs. Circumscribed, arc endpoints stay on the arc and the interior vertices sit
|
||||||
|
/// on tangent intersections, so the polygon never falls inside the curve and never
|
||||||
|
/// pokes past the straight edges an arc meets.
|
||||||
|
/// </summary>
|
||||||
|
public static Polygon Flatten(Shape shape, double tolerance, bool circumscribe)
|
||||||
|
{
|
||||||
|
var polygon = new Polygon();
|
||||||
|
|
||||||
|
foreach (var entity in shape.Entities)
|
||||||
|
{
|
||||||
|
switch (entity)
|
||||||
|
{
|
||||||
|
case Line line:
|
||||||
|
polygon.Vertices.Add(line.StartPoint);
|
||||||
|
polygon.Vertices.Add(line.EndPoint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Arc arc:
|
||||||
|
AddArc(polygon.Vertices, arc, tolerance, circumscribe);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Circle circle:
|
||||||
|
AddCircle(polygon.Vertices, circle, tolerance, circumscribe);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
polygon.Close();
|
||||||
|
polygon.Cleanup();
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddArc(List<Vector> points, Arc arc, double tolerance, bool circumscribe)
|
||||||
|
{
|
||||||
|
if (!circumscribe)
|
||||||
|
{
|
||||||
|
points.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sweep = arc.SweepAngle();
|
||||||
|
var segments = CircumscribedSegments(arc.Radius, sweep, tolerance);
|
||||||
|
var step = (arc.IsReversed ? -sweep : sweep) / segments;
|
||||||
|
var r = arc.Radius / System.Math.Cos(System.Math.Abs(step) / 2);
|
||||||
|
|
||||||
|
points.Add(arc.StartPoint());
|
||||||
|
|
||||||
|
for (var i = 0; i < segments; i++)
|
||||||
|
{
|
||||||
|
var angle = arc.StartAngle + step * (i + 0.5);
|
||||||
|
points.Add(
|
||||||
|
new Vector(
|
||||||
|
arc.Center.X + r * System.Math.Cos(angle),
|
||||||
|
arc.Center.Y + r * System.Math.Sin(angle)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
points.Add(arc.EndPoint());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddCircle(
|
||||||
|
List<Vector> points,
|
||||||
|
Circle circle,
|
||||||
|
double tolerance,
|
||||||
|
bool circumscribe
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!circumscribe)
|
||||||
|
{
|
||||||
|
points.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments = CircumscribedSegments(circle.Radius, Angle.TwoPI, tolerance);
|
||||||
|
var step = Angle.TwoPI / segments;
|
||||||
|
var r = circle.Radius / System.Math.Cos(step / 2);
|
||||||
|
|
||||||
|
for (var i = 0; i < segments; i++)
|
||||||
|
{
|
||||||
|
points.Add(
|
||||||
|
new Vector(
|
||||||
|
circle.Center.X + r * System.Math.Cos(step * i),
|
||||||
|
circle.Center.Y + r * System.Math.Sin(step * i)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Segments for a circumscribed arc: a tangent-intersection vertex sits
|
||||||
|
/// radius / cos(step / 2) from the center, so keep that within the tolerance, and
|
||||||
|
/// keep each step at 90 degrees or less so the tangents meet close to the arc.
|
||||||
|
/// </summary>
|
||||||
|
private static int CircumscribedSegments(double radius, double sweep, double tolerance)
|
||||||
|
{
|
||||||
|
var maxHalfStep = System.Math.Acos(radius / (radius + tolerance));
|
||||||
|
var segments = (int)System.Math.Ceiling(System.Math.Abs(sweep) / (2 * maxHalfStep));
|
||||||
|
var quarters = (int)System.Math.Ceiling(System.Math.Abs(sweep) / Angle.HalfPI);
|
||||||
|
|
||||||
|
return System.Math.Max(1, System.Math.Max(segments, quarters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PathsD Union(PathsD region)
|
||||||
|
{
|
||||||
|
var clipper = new ClipperD(Precision);
|
||||||
|
clipper.AddSubject(region);
|
||||||
|
|
||||||
|
var solution = new PathsD();
|
||||||
|
clipper.Execute(ClipType.Union, FillRule.NonZero, solution);
|
||||||
|
return solution;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddShape(
|
||||||
|
PathsD region,
|
||||||
|
Shape shape,
|
||||||
|
double tolerance,
|
||||||
|
bool circumscribe,
|
||||||
|
bool positive
|
||||||
|
)
|
||||||
|
{
|
||||||
|
AddPolygon(region, Flatten(shape, tolerance, circumscribe), positive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPolygon(PathsD region, Polygon polygon, bool positive)
|
||||||
|
{
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var path = ToPath(polygon, positive);
|
||||||
|
|
||||||
|
if (path.Count >= 3)
|
||||||
|
region.Add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of <see cref="ClipperBridge.Offset(ShapeProfile, double, double, bool)"/>:
|
||||||
|
/// outer boundaries (CCW) and holes (CW), as closed polygons.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record OffsetRegion(List<Polygon> Outers, List<Polygon> Holes)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The outer boundary with the largest area, or null when the region is empty.
|
||||||
|
/// </summary>
|
||||||
|
public Polygon LargestOuter()
|
||||||
|
{
|
||||||
|
Polygon best = null;
|
||||||
|
var bestArea = 0.0;
|
||||||
|
|
||||||
|
foreach (var outer in Outers)
|
||||||
|
{
|
||||||
|
var area = outer.Area();
|
||||||
|
|
||||||
|
if (best == null || area > bestArea)
|
||||||
|
{
|
||||||
|
best = outer;
|
||||||
|
bestArea = area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
-114
@@ -1,12 +1,32 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Polygon overlap test with hole subtraction. This is the reference implementation
|
||||||
|
/// for a future GPU kernel, so it deliberately stays hand-rolled instead of using
|
||||||
|
/// Clipper (which is CPU-only and allocation-heavy; see <see cref="ClipperBridge"/>
|
||||||
|
/// for the CPU preparation that feeds it).
|
||||||
|
/// <para>
|
||||||
|
/// GPU-port contract. Per-polygon preparation, done once per drawing and rotation,
|
||||||
|
/// then cached and uploaded: the spacing offset (<see cref="ClipperBridge"/>),
|
||||||
|
/// triangulation (<see cref="ConvexDecomposition.Triangulate"/>) of the outline and
|
||||||
|
/// each hole, and the bounding box of every polygon and triangle. Per-pair work,
|
||||||
|
/// kernel-shaped (fixed-size, loop-only, no recursion): the bounding-box rejects,
|
||||||
|
/// Sutherland-Hodgman clipping of convex triangle pairs (<c>ClipConvex</c>), and
|
||||||
|
/// subtraction of hole triangles from the clipped regions (<c>SubtractTriangles</c>).
|
||||||
|
/// Inputs are closed, lines-only polygons; winding is normalized by triangulation.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
public static class Collision
|
public static class Collision
|
||||||
{
|
{
|
||||||
public static CollisionResult Check(Polygon a, Polygon b,
|
public static CollisionResult Check(
|
||||||
List<Polygon> holesA = null, List<Polygon> holesB = null)
|
Polygon a,
|
||||||
|
Polygon b,
|
||||||
|
List<Polygon> holesA = null,
|
||||||
|
List<Polygon> holesB = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Step 1: Bounding box pre-filter
|
// Step 1: Bounding box pre-filter
|
||||||
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
||||||
@@ -46,8 +66,12 @@ namespace OpenNest.Geometry
|
|||||||
return new CollisionResult(true, regions, intersectionPoints);
|
return new CollisionResult(true, regions, intersectionPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasOverlap(Polygon a, Polygon b,
|
public static bool HasOverlap(
|
||||||
List<Polygon> holesA = null, List<Polygon> holesB = null)
|
Polygon a,
|
||||||
|
Polygon b,
|
||||||
|
List<Polygon> holesA = null,
|
||||||
|
List<Polygon> holesB = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
||||||
return false;
|
return false;
|
||||||
@@ -57,8 +81,10 @@ namespace OpenNest.Geometry
|
|||||||
return Check(a, b, holesA, holesB).Overlaps;
|
return Check(a, b, holesA, holesB).Overlaps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<CollisionResult> CheckAll(List<Polygon> polygons,
|
public static List<CollisionResult> CheckAll(
|
||||||
List<List<Polygon>> holes = null)
|
List<Polygon> polygons,
|
||||||
|
List<List<Polygon>> holes = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var results = new List<CollisionResult>();
|
var results = new List<CollisionResult>();
|
||||||
|
|
||||||
@@ -78,8 +104,7 @@ namespace OpenNest.Geometry
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasAnyOverlap(List<Polygon> polygons,
|
public static bool HasAnyOverlap(List<Polygon> polygons, List<List<Polygon>> holes = null)
|
||||||
List<List<Polygon>> holes = null)
|
|
||||||
{
|
{
|
||||||
for (var i = 0; i < polygons.Count; i++)
|
for (var i = 0; i < polygons.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -98,10 +123,8 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
private static bool BoundingBoxesOverlap(Box a, Box b)
|
private static bool BoundingBoxesOverlap(Box a, Box b)
|
||||||
{
|
{
|
||||||
var overlapX = System.Math.Min(a.Right, b.Right)
|
var overlapX = System.Math.Min(a.Right, b.Right) - System.Math.Max(a.Left, b.Left);
|
||||||
- System.Math.Max(a.Left, b.Left);
|
var overlapY = System.Math.Min(a.Top, b.Top) - System.Math.Max(a.Bottom, b.Bottom);
|
||||||
var overlapY = System.Math.Min(a.Top, b.Top)
|
|
||||||
- System.Math.Max(a.Bottom, b.Bottom);
|
|
||||||
|
|
||||||
return overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon;
|
return overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon;
|
||||||
}
|
}
|
||||||
@@ -161,67 +184,14 @@ namespace OpenNest.Geometry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static Polygon ClipConvex(Polygon subject, Polygon clip)
|
private static Polygon ClipConvex(Polygon subject, Polygon clip)
|
||||||
{
|
{
|
||||||
var output = new List<Vector>(subject.Vertices);
|
var output = OpenVertices(subject);
|
||||||
|
var clipVerts = OpenVertices(clip);
|
||||||
// Remove closing vertex if present
|
for (var i = 0; i < clipVerts.Count && output.Count >= 3; i++)
|
||||||
if (output.Count > 1 && output[0].X == output[output.Count - 1].X
|
|
||||||
&& output[0].Y == output[output.Count - 1].Y)
|
|
||||||
output.RemoveAt(output.Count - 1);
|
|
||||||
|
|
||||||
var clipVerts = new List<Vector>(clip.Vertices);
|
|
||||||
if (clipVerts.Count > 1 && clipVerts[0].X == clipVerts[clipVerts.Count - 1].X
|
|
||||||
&& clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y)
|
|
||||||
clipVerts.RemoveAt(clipVerts.Count - 1);
|
|
||||||
|
|
||||||
for (var i = 0; i < clipVerts.Count; i++)
|
|
||||||
{
|
{
|
||||||
if (output.Count == 0)
|
output = ClipHalfSpace(output, clipVerts[i], clipVerts[(i + 1) % clipVerts.Count], true);
|
||||||
return null;
|
|
||||||
|
|
||||||
var edgeStart = clipVerts[i];
|
|
||||||
var edgeEnd = clipVerts[(i + 1) % clipVerts.Count];
|
|
||||||
var input = output;
|
|
||||||
output = new List<Vector>();
|
|
||||||
|
|
||||||
for (var j = 0; j < input.Count; j++)
|
|
||||||
{
|
|
||||||
var current = input[j];
|
|
||||||
var next = input[(j + 1) % input.Count];
|
|
||||||
var currentInside = Cross(edgeStart, edgeEnd, current) >= -Tolerance.Epsilon;
|
|
||||||
var nextInside = Cross(edgeStart, edgeEnd, next) >= -Tolerance.Epsilon;
|
|
||||||
|
|
||||||
if (currentInside)
|
|
||||||
{
|
|
||||||
output.Add(current);
|
|
||||||
if (!nextInside)
|
|
||||||
{
|
|
||||||
var ix = LineIntersection(edgeStart, edgeEnd, current, next);
|
|
||||||
if (ix.IsValid())
|
|
||||||
output.Add(ix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (nextInside)
|
|
||||||
{
|
|
||||||
var ix = LineIntersection(edgeStart, edgeEnd, current, next);
|
|
||||||
if (ix.IsValid())
|
|
||||||
output.Add(ix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.Count < 3)
|
return PositiveAreaPolygon(output);
|
||||||
return null;
|
|
||||||
|
|
||||||
var result = new Polygon();
|
|
||||||
result.Vertices.AddRange(output);
|
|
||||||
result.Close();
|
|
||||||
result.UpdateBounds();
|
|
||||||
|
|
||||||
// Reject degenerate slivers
|
|
||||||
if (result.Area() < Tolerance.Epsilon)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -231,36 +201,23 @@ namespace OpenNest.Geometry
|
|||||||
private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point)
|
private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point)
|
||||||
{
|
{
|
||||||
return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y)
|
return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y)
|
||||||
- (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
|
- (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Intersection of lines (a1->a2) and (b1->b2). Returns Vector.Invalid if parallel.
|
|
||||||
/// </summary>
|
|
||||||
private static Vector LineIntersection(Vector a1, Vector a2, Vector b1, Vector b2)
|
|
||||||
{
|
|
||||||
var d1x = a2.X - a1.X;
|
|
||||||
var d1y = a2.Y - a1.Y;
|
|
||||||
var d2x = b2.X - b1.X;
|
|
||||||
var d2y = b2.Y - b1.Y;
|
|
||||||
var cross = d1x * d2y - d1y * d2x;
|
|
||||||
|
|
||||||
if (System.Math.Abs(cross) < Tolerance.Epsilon)
|
|
||||||
return Vector.Invalid;
|
|
||||||
|
|
||||||
var t = ((b1.X - a1.X) * d2y - (b1.Y - a1.Y) * d2x) / cross;
|
|
||||||
return new Vector(a1.X + t * d1x, a1.Y + t * d1y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subtracts holes from overlap regions.
|
/// Subtracts holes from overlap regions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<Polygon> SubtractHoles(List<Polygon> regions,
|
private static List<Polygon> SubtractHoles(
|
||||||
List<Polygon> holesA, List<Polygon> holesB)
|
List<Polygon> regions,
|
||||||
|
List<Polygon> holesA,
|
||||||
|
List<Polygon> holesB
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var allHoles = new List<Polygon>();
|
var allHoles = new List<Polygon>();
|
||||||
if (holesA != null) allHoles.AddRange(holesA);
|
if (holesA != null)
|
||||||
if (holesB != null) allHoles.AddRange(holesB);
|
allHoles.AddRange(holesA);
|
||||||
|
if (holesB != null)
|
||||||
|
allHoles.AddRange(holesB);
|
||||||
|
|
||||||
if (allHoles.Count == 0)
|
if (allHoles.Count == 0)
|
||||||
return regions;
|
return regions;
|
||||||
@@ -286,8 +243,9 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subtracts hole triangles from a region. Conservative: partial overlaps
|
/// Subtracts hole triangles from a convex region. At each edge, emit the outside
|
||||||
/// keep the full piece triangle (acceptable for visual shading).
|
/// portion and carry only the inside remainder to the next edge. The emitted
|
||||||
|
/// pieces are disjoint and convex, so no repeated triangulation is needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
|
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
|
||||||
{
|
{
|
||||||
@@ -295,36 +253,117 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
foreach (var holeTri in holeTris)
|
foreach (var holeTri in holeTris)
|
||||||
{
|
{
|
||||||
if (!BoundingBoxesOverlap(region.BoundingBox, holeTri.BoundingBox))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var next = new List<Polygon>();
|
var next = new List<Polygon>();
|
||||||
|
|
||||||
foreach (var piece in current)
|
foreach (var piece in current)
|
||||||
{
|
{
|
||||||
var pieceTris = TriangulateWithBounds(piece);
|
// Subtraction must also remove thin fragments created by clipping.
|
||||||
|
// The pair-level length tolerance would skip some of these even
|
||||||
foreach (var pieceTri in pieceTris)
|
// when their area is large enough to count as an overlap.
|
||||||
|
var a = piece.BoundingBox;
|
||||||
|
var b = holeTri.BoundingBox;
|
||||||
|
if (a.Right <= b.Left || b.Right <= a.Left || a.Top <= b.Bottom || b.Top <= a.Bottom)
|
||||||
{
|
{
|
||||||
var inside = ClipConvex(pieceTri, holeTri);
|
next.Add(piece);
|
||||||
if (inside == null)
|
continue;
|
||||||
{
|
}
|
||||||
// No overlap with hole - keep
|
|
||||||
next.Add(pieceTri);
|
var remainder = OpenVertices(piece);
|
||||||
}
|
var holeVerts = OpenVertices(holeTri);
|
||||||
else if (inside.Area() < pieceTri.Area() - Tolerance.Epsilon)
|
for (var i = 0; i < holeVerts.Count && remainder.Count >= 3; i++)
|
||||||
{
|
{
|
||||||
// Partial overlap - keep the piece (conservative)
|
var start = holeVerts[i];
|
||||||
next.Add(pieceTri);
|
var end = holeVerts[(i + 1) % holeVerts.Count];
|
||||||
}
|
var outside = PositiveAreaPolygon(ClipHalfSpace(remainder, start, end, false));
|
||||||
// else: fully inside hole - discard
|
if (outside != null)
|
||||||
|
next.Add(outside);
|
||||||
|
remainder = ClipHalfSpace(remainder, start, end, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current = next;
|
current = next;
|
||||||
|
if (current.Count == 0)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clips an open vertex list against one half-space. Classification and
|
||||||
|
/// interpolation use the same signed cross products: intersections always
|
||||||
|
/// lie on the input segment. An epsilon-shifted inside test combined with
|
||||||
|
/// intersections on the unshifted line can extrapolate and create material.
|
||||||
|
/// Apply the area tolerance only to the resulting polygons, not to edge signs.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Vector> ClipHalfSpace(
|
||||||
|
List<Vector> vertices,
|
||||||
|
Vector edgeStart,
|
||||||
|
Vector edgeEnd,
|
||||||
|
bool inside
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var kept = new List<Vector>();
|
||||||
|
for (var i = 0; i < vertices.Count; i++)
|
||||||
|
{
|
||||||
|
var current = vertices[i];
|
||||||
|
var next = vertices[(i + 1) % vertices.Count];
|
||||||
|
var currentDistance = Cross(edgeStart, edgeEnd, current);
|
||||||
|
var nextDistance = Cross(edgeStart, edgeEnd, next);
|
||||||
|
if (inside ? currentDistance >= 0 : currentDistance <= 0)
|
||||||
|
AddDistinct(kept, current);
|
||||||
|
|
||||||
|
// Only strict opposite signs cross the line. Boundary endpoints
|
||||||
|
// are already kept, and near-parallel crossings need no cutoff.
|
||||||
|
if ((currentDistance < 0 && nextDistance > 0) || (currentDistance > 0 && nextDistance < 0))
|
||||||
|
{
|
||||||
|
var t = currentDistance / (currentDistance - nextDistance);
|
||||||
|
AddDistinct(kept, new Vector(
|
||||||
|
current.X + t * (next.X - current.X),
|
||||||
|
current.Y + t * (next.Y - current.Y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kept.Count > 1 && SamePoint(kept[0], kept[kept.Count - 1]))
|
||||||
|
kept.RemoveAt(kept.Count - 1);
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SamePoint(Vector a, Vector b) => a.X == b.X && a.Y == b.Y;
|
||||||
|
|
||||||
|
private static void AddDistinct(List<Vector> vertices, Vector point)
|
||||||
|
{
|
||||||
|
if (vertices.Count == 0 || !SamePoint(vertices[vertices.Count - 1], point))
|
||||||
|
vertices.Add(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Vector> OpenVertices(Polygon polygon)
|
||||||
|
{
|
||||||
|
var vertices = new List<Vector>(polygon.Vertices);
|
||||||
|
if (vertices.Count > 1 && SamePoint(vertices[0], vertices[vertices.Count - 1]))
|
||||||
|
vertices.RemoveAt(vertices.Count - 1);
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Polygon PositiveAreaPolygon(List<Vector> vertices)
|
||||||
|
{
|
||||||
|
if (vertices.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Measure relative to a vertex to avoid cancellation of world-coordinate
|
||||||
|
// products when a small clipped fragment is far from the origin.
|
||||||
|
var twiceArea = 0.0;
|
||||||
|
for (var i = 1; i + 1 < vertices.Count; i++)
|
||||||
|
twiceArea += Cross(vertices[0], vertices[i], vertices[i + 1]);
|
||||||
|
if (System.Math.Abs(twiceArea) <= 2 * Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var polygon = new Polygon();
|
||||||
|
polygon.Vertices.AddRange(vertices);
|
||||||
|
// Polygon.Close uses fuzzy Vector equality; clipping needs an exact
|
||||||
|
// closing vertex even when the last edge is shorter than Epsilon.
|
||||||
|
polygon.Vertices.Add(vertices[0]);
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,17 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
public class CollisionResult
|
public class CollisionResult
|
||||||
{
|
{
|
||||||
public static readonly CollisionResult None = new(false, new List<Polygon>(), new List<Vector>());
|
public static readonly CollisionResult None = new(
|
||||||
|
false,
|
||||||
|
new List<Polygon>(),
|
||||||
|
new List<Vector>()
|
||||||
|
);
|
||||||
|
|
||||||
public CollisionResult(bool overlaps, List<Polygon> overlapRegions, List<Vector> intersectionPoints)
|
public CollisionResult(
|
||||||
|
bool overlaps,
|
||||||
|
List<Polygon> overlapRegions,
|
||||||
|
List<Vector> intersectionPoints
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Overlaps = overlaps;
|
Overlaps = overlaps;
|
||||||
OverlapRegions = overlapRegions;
|
OverlapRegions = overlapRegions;
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ namespace OpenNest.Geometry
|
|||||||
var verts = new List<Vector>(polygon.Vertices);
|
var verts = new List<Vector>(polygon.Vertices);
|
||||||
|
|
||||||
// Remove closing vertex if polygon is closed.
|
// Remove closing vertex if polygon is closed.
|
||||||
if (verts.Count > 1 && verts[0].X == verts[verts.Count - 1].X
|
if (
|
||||||
&& verts[0].Y == verts[verts.Count - 1].Y)
|
verts.Count > 1
|
||||||
|
&& verts[0].X == verts[verts.Count - 1].X
|
||||||
|
&& verts[0].Y == verts[verts.Count - 1].Y
|
||||||
|
)
|
||||||
verts.RemoveAt(verts.Count - 1);
|
verts.RemoveAt(verts.Count - 1);
|
||||||
|
|
||||||
if (verts.Count < 3)
|
if (verts.Count < 3)
|
||||||
@@ -84,8 +87,14 @@ namespace OpenNest.Geometry
|
|||||||
/// Tests whether the vertex at curr forms an ear (a convex vertex whose
|
/// Tests whether the vertex at curr forms an ear (a convex vertex whose
|
||||||
/// triangle contains no other polygon vertices).
|
/// triangle contains no other polygon vertices).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool IsEar(Vector prev, Vector curr, Vector next,
|
private static bool IsEar(
|
||||||
List<Vector> verts, List<int> indices, int n)
|
Vector prev,
|
||||||
|
Vector curr,
|
||||||
|
Vector next,
|
||||||
|
List<Vector> verts,
|
||||||
|
List<int> indices,
|
||||||
|
int n
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Must be convex (CCW turn).
|
// Must be convex (CCW turn).
|
||||||
if (Cross(prev, curr, next) <= 0)
|
if (Cross(prev, curr, next) <= 0)
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
foreach (var p in sorted)
|
foreach (var p in sorted)
|
||||||
{
|
{
|
||||||
while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0)
|
while (
|
||||||
|
lower.Count >= 2
|
||||||
|
&& Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0
|
||||||
|
)
|
||||||
lower.RemoveAt(lower.Count - 1);
|
lower.RemoveAt(lower.Count - 1);
|
||||||
|
|
||||||
lower.Add(p);
|
lower.Add(p);
|
||||||
@@ -32,7 +35,10 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var p = sorted[i];
|
var p = sorted[i];
|
||||||
|
|
||||||
while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0)
|
while (
|
||||||
|
upper.Count >= 2
|
||||||
|
&& Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0
|
||||||
|
)
|
||||||
upper.RemoveAt(upper.Count - 1);
|
upper.RemoveAt(upper.Count - 1);
|
||||||
|
|
||||||
upper.Add(p);
|
upper.Add(p);
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable flat-array polygon with a uniform edge grid, used as an outer-shell
|
||||||
|
/// clearance prefilter. Two closed polygons share positive area only when an edge pair
|
||||||
|
/// crosses/touches or one polygon's vertex lies strictly inside the other; neither
|
||||||
|
/// happening certifies the two closed regions (hence any materials inside them) are
|
||||||
|
/// clear. <see cref="Relate"/> returns Clear only in that certified case and Unknown
|
||||||
|
/// for uncertain contacts, so it can only ever skip the exact <see cref="Collision"/>
|
||||||
|
/// gate when the exact gate would also find no overlap - the exact gate triangulates
|
||||||
|
/// both polygons per call and dominates runtime on finely flattened arc geometry.
|
||||||
|
/// <para>
|
||||||
|
/// A <see cref="EdgeGridPolygonTemplate"/> holds the shared geometry; <see cref="Translated"/>
|
||||||
|
/// produces a placement in world coordinates in O(1) - translation leaves the grid and
|
||||||
|
/// all cell indices unchanged, only the predicate coordinates shift.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EdgeGridPolygon
|
||||||
|
{
|
||||||
|
/// <summary>Vertex-on-segment / collinearity tolerance for conservative touches.</summary>
|
||||||
|
private const double TouchEps = 1e-9;
|
||||||
|
|
||||||
|
private readonly EdgeGridPolygonTemplate _template;
|
||||||
|
|
||||||
|
/// <summary>Translation applied to the shared template geometry.</summary>
|
||||||
|
private readonly double Dx;
|
||||||
|
|
||||||
|
private readonly double Dy;
|
||||||
|
|
||||||
|
private EdgeGridPolygon(EdgeGridPolygonTemplate template, double dx, double dy)
|
||||||
|
{
|
||||||
|
_template = template;
|
||||||
|
Dx = dx;
|
||||||
|
Dy = dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double MinX => _template.MinX + Dx;
|
||||||
|
private double MinY => _template.MinY + Dy;
|
||||||
|
private double MaxX => _template.MaxX + Dx;
|
||||||
|
private double MaxY => _template.MaxY + Dy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds from a closed <see cref="Polygon"/> (last vertex may repeat the first).
|
||||||
|
/// Returns null when the polygon has no usable ring - callers treat that as
|
||||||
|
/// "no information" and fall through to the exact gate.
|
||||||
|
/// </summary>
|
||||||
|
public static EdgeGridPolygon? From(Polygon polygon)
|
||||||
|
{
|
||||||
|
var template = EdgeGridPolygonTemplate.Build(polygon);
|
||||||
|
return template == null ? null : new EdgeGridPolygon(template, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a placement sharing immutable geometry, with an added translation.</summary>
|
||||||
|
public EdgeGridPolygon Translated(double dx, double dy) => new(_template, Dx + dx, Dy + dy);
|
||||||
|
|
||||||
|
private double X(int i) => _template.X[i] + Dx;
|
||||||
|
private double Y(int i) => _template.Y[i] + Dy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Certifies disjoint filled perimeters. Any crossing, containment or uncertain
|
||||||
|
/// boundary contact returns Unknown and must defer to the exact collision test.
|
||||||
|
/// Holes need not be supplied: removing material cannot invalidate Clear.
|
||||||
|
/// </summary>
|
||||||
|
public static ShellRelation Relate(EdgeGridPolygon a, EdgeGridPolygon b)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
a.MaxX <= b.MinX
|
||||||
|
|| b.MaxX <= a.MinX
|
||||||
|
|| a.MaxY <= b.MinY
|
||||||
|
|| b.MaxY <= a.MinY
|
||||||
|
)
|
||||||
|
return ShellRelation.Clear; // disjoint bounding boxes
|
||||||
|
|
||||||
|
// One walk per direction reports the strongest edge relation: a transversal
|
||||||
|
// crossing shares a positive-area wedge (overlap); a mere touch shares zero
|
||||||
|
// area but may hide a crossing in near-degenerate coordinates (unknown).
|
||||||
|
var edge = EdgeRelation(a, b);
|
||||||
|
if (edge < 2)
|
||||||
|
{
|
||||||
|
var back = EdgeRelation(b, a);
|
||||||
|
if (back > edge)
|
||||||
|
edge = back;
|
||||||
|
}
|
||||||
|
if (edge == 2)
|
||||||
|
return ShellRelation.Unknown;
|
||||||
|
|
||||||
|
// Only fully disjoint boundaries can certify clearance. Point touches and
|
||||||
|
// collinear/near-degenerate contacts always defer to the reference test.
|
||||||
|
switch (edge)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
if (ContainsPointStrictly(a, b.X(0), b.Y(0)))
|
||||||
|
return ShellRelation.Unknown;
|
||||||
|
if (ContainsPointStrictly(b, a.X(0), a.Y(0)))
|
||||||
|
return ShellRelation.Unknown;
|
||||||
|
return ShellRelation.Clear;
|
||||||
|
default:
|
||||||
|
return ShellRelation.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Classifies whether any edge of <paramref name="q"/> crosses or touches the boundary of
|
||||||
|
/// <paramref name="p"/>. Walks p's grid using each query edge's own bbox cells.
|
||||||
|
/// p's grid lives in p's LOCAL frame (the template's own coordinates), so the
|
||||||
|
/// query edge is converted by subtracting p's translation first.
|
||||||
|
/// </summary>
|
||||||
|
private static int EdgeRelation(EdgeGridPolygon p, EdgeGridPolygon q)
|
||||||
|
{
|
||||||
|
var t = p._template;
|
||||||
|
var n = t.Count;
|
||||||
|
Span<int> seen = n <= 1024 ? stackalloc int[n] : new int[n];
|
||||||
|
seen.Clear();
|
||||||
|
var head = t.Head;
|
||||||
|
var nodeEdge = t.NodeEdge;
|
||||||
|
var nodeNext = t.NodeNext;
|
||||||
|
var no = q._template.Count;
|
||||||
|
var strongest = 0;
|
||||||
|
|
||||||
|
for (var e = 0; e < no; e++)
|
||||||
|
{
|
||||||
|
// Stamp per QUERY edge: a grid edge may need testing against every query
|
||||||
|
// edge; the dedupe only collapses cells an individual query edge crosses
|
||||||
|
// more than once.
|
||||||
|
var stamp = e + 1;
|
||||||
|
var i2 = (e + 1) % no;
|
||||||
|
var p0x = q.X(e) - p.Dx;
|
||||||
|
var p0y = q.Y(e) - p.Dy;
|
||||||
|
var p1x = q.X(i2) - p.Dx;
|
||||||
|
var p1y = q.Y(i2) - p.Dy;
|
||||||
|
|
||||||
|
var c0 = ColLow(t, p0x, p1x);
|
||||||
|
if (c0 > ColHigh(t, p0x, p1x))
|
||||||
|
continue;
|
||||||
|
var c1 = ColHigh(t, p0x, p1x);
|
||||||
|
var r0 = RowLow(t, p0y, p1y);
|
||||||
|
if (r0 > RowHigh(t, p0y, p1y))
|
||||||
|
continue;
|
||||||
|
var r1 = RowHigh(t, p0y, p1y);
|
||||||
|
|
||||||
|
for (var r = r0; r <= r1; r++)
|
||||||
|
for (var c = c0; c <= c1; c++)
|
||||||
|
for (var nIdx = head[r * t.Cols + c]; nIdx >= 0; nIdx = nodeNext[nIdx])
|
||||||
|
{
|
||||||
|
var ea = nodeEdge[nIdx];
|
||||||
|
if (seen[ea] == stamp)
|
||||||
|
continue;
|
||||||
|
seen[ea] = stamp;
|
||||||
|
var a2 = (ea + 1) % n;
|
||||||
|
var relation = SegmentRelation(
|
||||||
|
t.X[ea], t.Y[ea], t.X[a2], t.Y[a2], p0x, p0y, p1x, p1y
|
||||||
|
);
|
||||||
|
if (relation == 2)
|
||||||
|
return 2; // transversal crossing
|
||||||
|
if (relation > strongest)
|
||||||
|
strongest = relation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strongest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Segment-pair relation: 2 = transversal crossing (strict sign flips on both
|
||||||
|
/// orientations - the regions share a positive-area wedge); 1 = a clean endpoint
|
||||||
|
/// touch (zero shared area by itself; callers decide via interior-vertex tests);
|
||||||
|
/// 3 = collinear or near-degenerate contact (a shared boundary segment can hide
|
||||||
|
/// either a same-side positive overlap or an opposite-side tangency, so it must
|
||||||
|
/// defer to the exact gate); 0 = disjoint.
|
||||||
|
/// </summary>
|
||||||
|
private static int SegmentRelation(
|
||||||
|
double ax, double ay, double bx, double by, double cx, double cy, double dx, double dy
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var rx = bx - ax;
|
||||||
|
var ry = by - ay;
|
||||||
|
var sx = dx - cx;
|
||||||
|
var sy = dy - cy;
|
||||||
|
var d1 = rx * (cy - ay) - ry * (cx - ax);
|
||||||
|
var d2 = rx * (dy - ay) - ry * (dx - ax);
|
||||||
|
var d3 = sx * (ay - cy) - sy * (ax - cx);
|
||||||
|
var d4 = sx * (by - cy) - sy * (bx - cx);
|
||||||
|
|
||||||
|
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
|
||||||
|
return 2; // proper crossing
|
||||||
|
|
||||||
|
// A near-zero orientation means the configuration is collinear or too close to
|
||||||
|
// classify; only exact-zero orientations get the clean point-touch verdict.
|
||||||
|
var scale = System.Math.Max(
|
||||||
|
1e-30,
|
||||||
|
System.Math.Max(System.Math.Abs(rx) + System.Math.Abs(ry), System.Math.Abs(sx) + System.Math.Abs(sy))
|
||||||
|
);
|
||||||
|
var eps = TouchEps * scale;
|
||||||
|
var nearDegenerate =
|
||||||
|
(System.Math.Abs(d1) <= eps && d1 != 0)
|
||||||
|
|| (System.Math.Abs(d2) <= eps && d2 != 0)
|
||||||
|
|| (System.Math.Abs(d3) <= eps && d3 != 0)
|
||||||
|
|| (System.Math.Abs(d4) <= eps && d4 != 0);
|
||||||
|
var exactDegenerate = d1 == 0 || d2 == 0 || d3 == 0 || d4 == 0;
|
||||||
|
|
||||||
|
var touch =
|
||||||
|
(d1 == 0 && PointOnSegment(cx, cy, ax, ay, bx, by))
|
||||||
|
|| (d2 == 0 && PointOnSegment(dx, dy, ax, ay, bx, by))
|
||||||
|
|| (d3 == 0 && PointOnSegment(ax, ay, cx, cy, dx, dy))
|
||||||
|
|| (d4 == 0 && PointOnSegment(bx, by, cx, cy, dx, dy));
|
||||||
|
|
||||||
|
if (nearDegenerate)
|
||||||
|
return 3;
|
||||||
|
if (exactDegenerate)
|
||||||
|
// Collinear: contact along a segment (or too close to tell) must defer to
|
||||||
|
// the exact gate; collinear but disjoint edges simply do not touch.
|
||||||
|
return touch ? 3 : 0;
|
||||||
|
if (touch)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PointOnSegment(
|
||||||
|
double px, double py, double ax, double ay, double bx, double by
|
||||||
|
) =>
|
||||||
|
System.Math.Min(ax, bx) - TouchEps <= px
|
||||||
|
&& px <= System.Math.Max(ax, bx) + TouchEps
|
||||||
|
&& System.Math.Min(ay, by) - TouchEps <= py
|
||||||
|
&& py <= System.Math.Max(ay, by) + TouchEps;
|
||||||
|
|
||||||
|
/// <summary>Strict ray-cast containment (boundary touches are excluded upstream).</summary>
|
||||||
|
private static bool ContainsPointStrictly(EdgeGridPolygon poly, double px, double py)
|
||||||
|
{
|
||||||
|
var t = poly._template;
|
||||||
|
var inside = false;
|
||||||
|
var n = t.Count;
|
||||||
|
for (var i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
var j = (i + 1) % n;
|
||||||
|
var yi = poly.Y(i);
|
||||||
|
var yj = poly.Y(j);
|
||||||
|
if ((yi > py) != (yj > py))
|
||||||
|
{
|
||||||
|
var xAt = poly.X(i) + (py - yi) / (yj - yi) * (poly.X(j) - poly.X(i));
|
||||||
|
if (px < xAt)
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ColLow(EdgeGridPolygonTemplate t, double a, double b) =>
|
||||||
|
System.Math.Clamp((int)System.Math.Floor((System.Math.Min(a, b) - t.MinX) / t.CellSize), 0, t.Cols);
|
||||||
|
|
||||||
|
private static int ColHigh(EdgeGridPolygonTemplate t, double a, double b) =>
|
||||||
|
System.Math.Clamp((int)System.Math.Floor((System.Math.Max(a, b) - t.MinX) / t.CellSize), -1, t.Cols - 1);
|
||||||
|
|
||||||
|
private static int RowLow(EdgeGridPolygonTemplate t, double a, double b) =>
|
||||||
|
System.Math.Clamp((int)System.Math.Floor((System.Math.Min(a, b) - t.MinY) / t.CellSize), 0, t.Rows);
|
||||||
|
|
||||||
|
private static int RowHigh(EdgeGridPolygonTemplate t, double a, double b) =>
|
||||||
|
System.Math.Clamp((int)System.Math.Floor((System.Math.Max(a, b) - t.MinY) / t.CellSize), -1, t.Rows - 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared, immutable grid geometry for <see cref="EdgeGridPolygon"/>; the grid is defined
|
||||||
|
/// relative to the shape's own local coordinates, so translated instances reuse it.
|
||||||
|
/// Per-query deduplication scratch is local, so placements may be queried concurrently.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class EdgeGridPolygonTemplate
|
||||||
|
{
|
||||||
|
internal readonly double[] X;
|
||||||
|
internal readonly double[] Y;
|
||||||
|
internal readonly int Count;
|
||||||
|
internal readonly double MinX;
|
||||||
|
internal readonly double MinY;
|
||||||
|
internal readonly double MaxX;
|
||||||
|
internal readonly double MaxY;
|
||||||
|
|
||||||
|
internal readonly double CellSize;
|
||||||
|
|
||||||
|
internal readonly int Cols;
|
||||||
|
internal readonly int Rows;
|
||||||
|
internal readonly int[] Head;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grid nodes as parallel (edge, next) arrays: an edge spanning several cells gets
|
||||||
|
/// one node PER cell - a single next-per-edge chain would corrupt the other cells'
|
||||||
|
/// chains and silently drop edges from the walk.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly int[] NodeEdge;
|
||||||
|
|
||||||
|
internal readonly int[] NodeNext;
|
||||||
|
|
||||||
|
|
||||||
|
private EdgeGridPolygonTemplate(
|
||||||
|
double[] x,
|
||||||
|
double[] y,
|
||||||
|
int count,
|
||||||
|
double minX,
|
||||||
|
double minY,
|
||||||
|
double maxX,
|
||||||
|
double maxY
|
||||||
|
)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
Count = count;
|
||||||
|
MinX = minX;
|
||||||
|
MinY = minY;
|
||||||
|
MaxX = maxX;
|
||||||
|
MaxY = maxY;
|
||||||
|
|
||||||
|
var extentX = System.Math.Max(maxX - minX, 1e-9);
|
||||||
|
var extentY = System.Math.Max(maxY - minY, 1e-9);
|
||||||
|
CellSize = System.Math.Max(System.Math.Max(extentX, extentY) / 16.0, 1e-9);
|
||||||
|
Cols = System.Math.Clamp((int)System.Math.Ceiling(extentX / CellSize) + 1, 1, 48);
|
||||||
|
Rows = System.Math.Clamp((int)System.Math.Ceiling(extentY / CellSize) + 1, 1, 48);
|
||||||
|
Head = new int[Cols * Rows];
|
||||||
|
Array.Fill(Head, -1);
|
||||||
|
|
||||||
|
// Pass 1: count nodes; pass 2: fill (edge, next) node arrays.
|
||||||
|
var cellsPerEdge = new int[count];
|
||||||
|
var total = 0;
|
||||||
|
for (var e = 0; e < count; e++)
|
||||||
|
{
|
||||||
|
var i2 = (e + 1) % count;
|
||||||
|
var c0 = ClampCol(System.Math.Min(x[e], x[i2]) - minX);
|
||||||
|
var c1 = ClampCol(System.Math.Max(x[e], x[i2]) - minX);
|
||||||
|
var r0 = ClampRow(System.Math.Min(y[e], y[i2]) - minY);
|
||||||
|
var r1 = ClampRow(System.Math.Max(y[e], y[i2]) - minY);
|
||||||
|
cellsPerEdge[e] = (c1 - c0 + 1) * (r1 - r0 + 1);
|
||||||
|
total += cellsPerEdge[e];
|
||||||
|
}
|
||||||
|
NodeEdge = new int[total];
|
||||||
|
NodeNext = new int[total];
|
||||||
|
var node = 0;
|
||||||
|
for (var e = 0; e < count; e++)
|
||||||
|
{
|
||||||
|
var i2 = (e + 1) % count;
|
||||||
|
var c0 = ClampCol(System.Math.Min(x[e], x[i2]) - minX);
|
||||||
|
var c1 = ClampCol(System.Math.Max(x[e], x[i2]) - minX);
|
||||||
|
var r0 = ClampRow(System.Math.Min(y[e], y[i2]) - minY);
|
||||||
|
var r1 = ClampRow(System.Math.Max(y[e], y[i2]) - minY);
|
||||||
|
for (var r = r0; r <= r1; r++)
|
||||||
|
for (var c = c0; c <= c1; c++)
|
||||||
|
{
|
||||||
|
var cell = r * Cols + c;
|
||||||
|
NodeEdge[node] = e;
|
||||||
|
NodeNext[node] = Head[cell];
|
||||||
|
Head[cell] = node;
|
||||||
|
node++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ClampCol(double dx) =>
|
||||||
|
System.Math.Clamp((int)System.Math.Floor(dx / CellSize), 0, Cols - 1);
|
||||||
|
|
||||||
|
private int ClampRow(double dy) =>
|
||||||
|
System.Math.Clamp((int)System.Math.Floor(dy / CellSize), 0, Rows - 1);
|
||||||
|
|
||||||
|
internal static EdgeGridPolygonTemplate? Build(Polygon polygon)
|
||||||
|
{
|
||||||
|
var vertices = polygon.Vertices;
|
||||||
|
var n = vertices.Count;
|
||||||
|
if (n >= 2 && vertices[0].X == vertices[n - 1].X && vertices[0].Y == vertices[n - 1].Y)
|
||||||
|
n--;
|
||||||
|
if (n < 3)
|
||||||
|
return null;
|
||||||
|
var xs = new double[n];
|
||||||
|
var ys = new double[n];
|
||||||
|
var minX = double.MaxValue;
|
||||||
|
var minY = double.MaxValue;
|
||||||
|
var maxX = double.MinValue;
|
||||||
|
var maxY = double.MinValue;
|
||||||
|
for (var i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
var vx = vertices[i].X;
|
||||||
|
var vy = vertices[i].Y;
|
||||||
|
xs[i] = vx;
|
||||||
|
ys[i] = vy;
|
||||||
|
if (vx < minX)
|
||||||
|
minX = vx;
|
||||||
|
if (vx > maxX)
|
||||||
|
maxX = vx;
|
||||||
|
if (vy < minY)
|
||||||
|
minY = vy;
|
||||||
|
if (vy > maxY)
|
||||||
|
maxY = vy;
|
||||||
|
}
|
||||||
|
return new EdgeGridPolygonTemplate(xs, ys, n, minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Conservative result of an outer-perimeter prefilter.</summary>
|
||||||
|
public enum ShellRelation
|
||||||
|
{
|
||||||
|
/// <summary>Filled perimeters, and therefore their material, are disjoint.</summary>
|
||||||
|
Clear,
|
||||||
|
/// <summary>Run an exact collision test; the prefilter cannot certify clearance.</summary>
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -9,7 +9,13 @@ namespace OpenNest.Geometry
|
|||||||
private const int MaxSubdivisionDepth = 12;
|
private const int MaxSubdivisionDepth = 12;
|
||||||
private const int DeviationSamples = 20;
|
private const int DeviationSamples = 20;
|
||||||
|
|
||||||
internal static Vector EvaluatePoint(double semiMajor, double semiMinor, double rotation, Vector center, double t)
|
internal static Vector EvaluatePoint(
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
Vector center,
|
||||||
|
double t
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var x = semiMajor * System.Math.Cos(t);
|
var x = semiMajor * System.Math.Cos(t);
|
||||||
var y = semiMinor * System.Math.Sin(t);
|
var y = semiMinor * System.Math.Sin(t);
|
||||||
@@ -17,12 +23,15 @@ namespace OpenNest.Geometry
|
|||||||
var cos = System.Math.Cos(rotation);
|
var cos = System.Math.Cos(rotation);
|
||||||
var sin = System.Math.Sin(rotation);
|
var sin = System.Math.Sin(rotation);
|
||||||
|
|
||||||
return new Vector(
|
return new Vector(center.X + x * cos - y * sin, center.Y + x * sin + y * cos);
|
||||||
center.X + x * cos - y * sin,
|
|
||||||
center.Y + x * sin + y * cos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Vector EvaluateTangent(double semiMajor, double semiMinor, double rotation, double t)
|
internal static Vector EvaluateTangent(
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
double t
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var tx = -semiMajor * System.Math.Sin(t);
|
var tx = -semiMajor * System.Math.Sin(t);
|
||||||
var ty = semiMinor * System.Math.Cos(t);
|
var ty = semiMinor * System.Math.Cos(t);
|
||||||
@@ -30,12 +39,15 @@ namespace OpenNest.Geometry
|
|||||||
var cos = System.Math.Cos(rotation);
|
var cos = System.Math.Cos(rotation);
|
||||||
var sin = System.Math.Sin(rotation);
|
var sin = System.Math.Sin(rotation);
|
||||||
|
|
||||||
return new Vector(
|
return new Vector(tx * cos - ty * sin, tx * sin + ty * cos);
|
||||||
tx * cos - ty * sin,
|
|
||||||
tx * sin + ty * cos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Vector EvaluateNormal(double semiMajor, double semiMinor, double rotation, double t)
|
internal static Vector EvaluateNormal(
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
double t
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Inward normal: perpendicular to tangent, pointing toward center of curvature.
|
// Inward normal: perpendicular to tangent, pointing toward center of curvature.
|
||||||
// In local coords: N(t) = (-b*cos(t), -a*sin(t))
|
// In local coords: N(t) = (-b*cos(t), -a*sin(t))
|
||||||
@@ -45,9 +57,7 @@ namespace OpenNest.Geometry
|
|||||||
var cos = System.Math.Cos(rotation);
|
var cos = System.Math.Cos(rotation);
|
||||||
var sin = System.Math.Sin(rotation);
|
var sin = System.Math.Sin(rotation);
|
||||||
|
|
||||||
return new Vector(
|
return new Vector(nx * cos - ny * sin, nx * sin + ny * cos);
|
||||||
nx * cos - ny * sin,
|
|
||||||
nx * sin + ny * cos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Vector IntersectNormals(Vector p1, Vector n1, Vector p2, Vector n2)
|
internal static Vector IntersectNormals(Vector p1, Vector n1, Vector p2, Vector n2)
|
||||||
@@ -83,11 +93,21 @@ namespace OpenNest.Geometry
|
|||||||
return new Vector(ux + c.X, uy + c.Y);
|
return new Vector(ux + c.X, uy + c.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Entity> Convert(Vector center, double semiMajor, double semiMinor,
|
public static List<Entity> Convert(
|
||||||
double rotation, double startParam, double endParam, double tolerance = 0.001)
|
Vector center,
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
double startParam,
|
||||||
|
double endParam,
|
||||||
|
double tolerance = 0.001
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (tolerance <= 0)
|
if (tolerance <= 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(tolerance), "Tolerance must be positive.");
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(tolerance),
|
||||||
|
"Tolerance must be positive."
|
||||||
|
);
|
||||||
if (semiMajor <= 0 || semiMinor <= 0)
|
if (semiMajor <= 0 || semiMinor <= 0)
|
||||||
throw new ArgumentOutOfRangeException("Semi-axis lengths must be positive.");
|
throw new ArgumentOutOfRangeException("Semi-axis lengths must be positive.");
|
||||||
|
|
||||||
@@ -102,14 +122,28 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
var entities = new List<Entity>();
|
var entities = new List<Entity>();
|
||||||
for (var i = 0; i < splits.Count - 1; i++)
|
for (var i = 0; i < splits.Count - 1; i++)
|
||||||
FitSegment(center, semiMajor, semiMinor, rotation,
|
FitSegment(
|
||||||
splits[i], splits[i + 1], tolerance, entities, 0);
|
center,
|
||||||
|
semiMajor,
|
||||||
|
semiMinor,
|
||||||
|
rotation,
|
||||||
|
splits[i],
|
||||||
|
splits[i + 1],
|
||||||
|
tolerance,
|
||||||
|
entities,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Entity> ConvertCircle(Vector center, double radius,
|
private static List<Entity> ConvertCircle(
|
||||||
double rotation, double startParam, double endParam)
|
Vector center,
|
||||||
|
double radius,
|
||||||
|
double rotation,
|
||||||
|
double startParam,
|
||||||
|
double endParam
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var sweep = endParam - startParam;
|
var sweep = endParam - startParam;
|
||||||
var isFull = System.Math.Abs(sweep - Angle.TwoPI) < 0.01;
|
var isFull = System.Math.Abs(sweep - Angle.TwoPI) < 0.01;
|
||||||
@@ -123,7 +157,7 @@ namespace OpenNest.Geometry
|
|||||||
return new List<Entity>
|
return new List<Entity>
|
||||||
{
|
{
|
||||||
new Arc(center, radius, startAngle1, midAngle, false),
|
new Arc(center, radius, startAngle1, midAngle, false),
|
||||||
new Arc(center, radius, midAngle, endAngle2, false)
|
new Arc(center, radius, midAngle, endAngle2, false),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +170,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var splits = new List<double> { startParam };
|
var splits = new List<double> { startParam };
|
||||||
|
|
||||||
var firstQuadrant = System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2);
|
var firstQuadrant =
|
||||||
|
System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2);
|
||||||
for (var q = firstQuadrant; q < endParam; q += System.Math.PI / 2)
|
for (var q = firstQuadrant; q < endParam; q += System.Math.PI / 2)
|
||||||
{
|
{
|
||||||
if (q > startParam + 1e-10 && q < endParam - 1e-10)
|
if (q > startParam + 1e-10 && q < endParam - 1e-10)
|
||||||
@@ -147,8 +182,17 @@ namespace OpenNest.Geometry
|
|||||||
return splits;
|
return splits;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void FitSegment(Vector center, double semiMajor, double semiMinor,
|
private static void FitSegment(
|
||||||
double rotation, double t0, double t1, double tolerance, List<Entity> results, int depth)
|
Vector center,
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
double t0,
|
||||||
|
double t1,
|
||||||
|
double tolerance,
|
||||||
|
List<Entity> results,
|
||||||
|
int depth
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t0);
|
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t0);
|
||||||
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t1);
|
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t1);
|
||||||
@@ -168,12 +212,29 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
var radius = p0.DistanceTo(arcCenter);
|
var radius = p0.DistanceTo(arcCenter);
|
||||||
var maxDev = MeasureDeviation(center, semiMajor, semiMinor, rotation,
|
var maxDev = MeasureDeviation(
|
||||||
t0, t1, arcCenter, radius);
|
center,
|
||||||
|
semiMajor,
|
||||||
|
semiMinor,
|
||||||
|
rotation,
|
||||||
|
t0,
|
||||||
|
t1,
|
||||||
|
arcCenter,
|
||||||
|
radius
|
||||||
|
);
|
||||||
|
|
||||||
if (maxDev <= tolerance)
|
if (maxDev <= tolerance)
|
||||||
{
|
{
|
||||||
var arc = CreateArc(arcCenter, radius, center, semiMajor, semiMinor, rotation, t0, t1);
|
var arc = CreateArc(
|
||||||
|
arcCenter,
|
||||||
|
radius,
|
||||||
|
center,
|
||||||
|
semiMajor,
|
||||||
|
semiMinor,
|
||||||
|
rotation,
|
||||||
|
t0,
|
||||||
|
t1
|
||||||
|
);
|
||||||
if (arc.SweepAngle() < Tolerance.Epsilon)
|
if (arc.SweepAngle() < Tolerance.Epsilon)
|
||||||
results.Add(new Line(p0, p1));
|
results.Add(new Line(p0, p1));
|
||||||
else
|
else
|
||||||
@@ -182,13 +243,41 @@ namespace OpenNest.Geometry
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tMid = (t0 + t1) / 2.0;
|
var tMid = (t0 + t1) / 2.0;
|
||||||
FitSegment(center, semiMajor, semiMinor, rotation, t0, tMid, tolerance, results, depth + 1);
|
FitSegment(
|
||||||
FitSegment(center, semiMajor, semiMinor, rotation, tMid, t1, tolerance, results, depth + 1);
|
center,
|
||||||
|
semiMajor,
|
||||||
|
semiMinor,
|
||||||
|
rotation,
|
||||||
|
t0,
|
||||||
|
tMid,
|
||||||
|
tolerance,
|
||||||
|
results,
|
||||||
|
depth + 1
|
||||||
|
);
|
||||||
|
FitSegment(
|
||||||
|
center,
|
||||||
|
semiMajor,
|
||||||
|
semiMinor,
|
||||||
|
rotation,
|
||||||
|
tMid,
|
||||||
|
t1,
|
||||||
|
tolerance,
|
||||||
|
results,
|
||||||
|
depth + 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double MeasureDeviation(Vector center, double semiMajor, double semiMinor,
|
private static double MeasureDeviation(
|
||||||
double rotation, double t0, double t1, Vector arcCenter, double radius)
|
Vector center,
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
double t0,
|
||||||
|
double t1,
|
||||||
|
Vector arcCenter,
|
||||||
|
double radius
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var maxDev = 0.0;
|
var maxDev = 0.0;
|
||||||
for (var i = 1; i <= DeviationSamples; i++)
|
for (var i = 1; i <= DeviationSamples; i++)
|
||||||
@@ -197,14 +286,22 @@ namespace OpenNest.Geometry
|
|||||||
var p = EvaluatePoint(semiMajor, semiMinor, rotation, center, t);
|
var p = EvaluatePoint(semiMajor, semiMinor, rotation, center, t);
|
||||||
var dist = p.DistanceTo(arcCenter);
|
var dist = p.DistanceTo(arcCenter);
|
||||||
var dev = System.Math.Abs(dist - radius);
|
var dev = System.Math.Abs(dist - radius);
|
||||||
if (dev > maxDev) maxDev = dev;
|
if (dev > maxDev)
|
||||||
|
maxDev = dev;
|
||||||
}
|
}
|
||||||
return maxDev;
|
return maxDev;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Arc CreateArc(Vector arcCenter, double radius,
|
private static Arc CreateArc(
|
||||||
Vector ellipseCenter, double semiMajor, double semiMinor, double rotation,
|
Vector arcCenter,
|
||||||
double t0, double t1)
|
double radius,
|
||||||
|
Vector ellipseCenter,
|
||||||
|
double semiMajor,
|
||||||
|
double semiMinor,
|
||||||
|
double rotation,
|
||||||
|
double t0,
|
||||||
|
double t1
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0);
|
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0);
|
||||||
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1);
|
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1);
|
||||||
@@ -225,8 +322,10 @@ namespace OpenNest.Geometry
|
|||||||
var points = new List<Vector> { p0, pMid, p1 };
|
var points = new List<Vector> { p0, pMid, p1 };
|
||||||
var isReversed = SumSignedAngles(arcCenter, points) < 0;
|
var isReversed = SumSignedAngles(arcCenter, points) < 0;
|
||||||
|
|
||||||
if (startAngle < 0) startAngle += Angle.TwoPI;
|
if (startAngle < 0)
|
||||||
if (endAngle < 0) endAngle += Angle.TwoPI;
|
startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0)
|
||||||
|
endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
return new Arc(arcCenter, radius, startAngle, endAngle, isReversed);
|
return new Arc(arcCenter, radius, startAngle, endAngle, isReversed);
|
||||||
}
|
}
|
||||||
@@ -239,8 +338,10 @@ namespace OpenNest.Geometry
|
|||||||
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
||||||
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
||||||
var da = a2 - a1;
|
var da = a2 - a1;
|
||||||
while (da > System.Math.PI) da -= Angle.TwoPI;
|
while (da > System.Math.PI)
|
||||||
while (da < -System.Math.PI) da += Angle.TwoPI;
|
da -= Angle.TwoPI;
|
||||||
|
while (da < -System.Math.PI)
|
||||||
|
da += Angle.TwoPI;
|
||||||
total += da;
|
total += da;
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -331,7 +331,11 @@ namespace OpenNest.Geometry
|
|||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
|
public static BoundingRectangleResult FindBestRotation(
|
||||||
|
this List<Entity> entities,
|
||||||
|
double startAngle = 0,
|
||||||
|
double endAngle = Angle.TwoPI
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Check for Shape entity first (recursive case returns early)
|
// Check for Shape entity first (recursive case returns early)
|
||||||
foreach (var entity in entities)
|
foreach (var entity in entities)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
namespace OpenNest.Geometry
|
||||||
namespace OpenNest.Geometry
|
|
||||||
{
|
{
|
||||||
public enum EntityType
|
public enum EntityType
|
||||||
{
|
{
|
||||||
@@ -7,6 +6,6 @@ namespace OpenNest.Geometry
|
|||||||
Circle,
|
Circle,
|
||||||
Line,
|
Line,
|
||||||
Shape,
|
Shape,
|
||||||
Polygon
|
Polygon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public static class GeometryOptimizer
|
public static class GeometryOptimizer
|
||||||
{
|
{
|
||||||
public static void Optimize(IList<Arc> arcs) =>
|
public static void Optimize(IList<Arc> arcs) =>
|
||||||
MergePass(arcs,
|
MergePass(
|
||||||
|
arcs,
|
||||||
(list, item, i) => list.GetCoradialArs(item, i),
|
(list, item, i) => list.GetCoradialArs(item, i),
|
||||||
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined));
|
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined)
|
||||||
|
);
|
||||||
|
|
||||||
public static void Optimize(IList<Line> lines) =>
|
public static void Optimize(IList<Line> lines) =>
|
||||||
MergePass(lines,
|
MergePass(
|
||||||
|
lines,
|
||||||
(list, item, i) => list.GetCollinearLines(item, i),
|
(list, item, i) => list.GetCollinearLines(item, i),
|
||||||
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined));
|
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined)
|
||||||
|
);
|
||||||
|
|
||||||
public static void Deduplicate(IList<Circle> circles)
|
public static void Deduplicate(IList<Circle> circles)
|
||||||
{
|
{
|
||||||
@@ -23,8 +27,10 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
for (var j = i - 1; j >= 0; j--)
|
for (var j = i - 1; j >= 0; j--)
|
||||||
{
|
{
|
||||||
if (circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon
|
if (
|
||||||
&& circles[i].Radius.IsEqualTo(circles[j].Radius))
|
circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon
|
||||||
|
&& circles[i].Radius.IsEqualTo(circles[j].Radius)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
circles.RemoveAt(i);
|
circles.RemoveAt(i);
|
||||||
break;
|
break;
|
||||||
@@ -39,9 +45,11 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
for (var j = arcs.Count - 1; j >= 0; j--)
|
for (var j = arcs.Count - 1; j >= 0; j--)
|
||||||
{
|
{
|
||||||
if (arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon
|
if (
|
||||||
|
arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon
|
||||||
&& arcs[j].Radius.IsEqualTo(circles[i].Radius)
|
&& arcs[j].Radius.IsEqualTo(circles[i].Radius)
|
||||||
&& arcs[j].IsFullCircle())
|
&& arcs[j].IsFullCircle()
|
||||||
|
)
|
||||||
{
|
{
|
||||||
arcs.RemoveAt(j);
|
arcs.RemoveAt(j);
|
||||||
}
|
}
|
||||||
@@ -51,9 +59,12 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
private delegate bool TryJoin<T>(T a, T b, out T joined);
|
private delegate bool TryJoin<T>(T a, T b, out T joined);
|
||||||
|
|
||||||
private static void MergePass<T>(IList<T> items,
|
private static void MergePass<T>(
|
||||||
|
IList<T> items,
|
||||||
Func<IList<T>, T, int, List<T>> findCandidates,
|
Func<IList<T>, T, int, List<T>> findCandidates,
|
||||||
TryJoin<T> tryJoin) where T : class
|
TryJoin<T> tryJoin
|
||||||
|
)
|
||||||
|
where T : class
|
||||||
{
|
{
|
||||||
for (var i = 0; i < items.Count; ++i)
|
for (var i = 0; i < items.Count; ++i)
|
||||||
{
|
{
|
||||||
@@ -117,10 +128,14 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
if (!onPoint)
|
if (!onPoint)
|
||||||
{
|
{
|
||||||
if (t1 < b2 - Tolerance.Epsilon) return false;
|
if (t1 < b2 - Tolerance.Epsilon)
|
||||||
if (b1 > t2 + Tolerance.Epsilon) return false;
|
return false;
|
||||||
if (l1 > r2 + Tolerance.Epsilon) return false;
|
if (b1 > t2 + Tolerance.Epsilon)
|
||||||
if (r1 < l2 - Tolerance.Epsilon) return false;
|
return false;
|
||||||
|
if (l1 > r2 + Tolerance.Epsilon)
|
||||||
|
return false;
|
||||||
|
if (r1 < l2 - Tolerance.Epsilon)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var l = l1 < l2 ? l1 : l2;
|
var l = l1 < l2 ? l1 : l2;
|
||||||
@@ -129,9 +144,17 @@ namespace OpenNest.Geometry
|
|||||||
var b = b1 < b2 ? b1 : b2;
|
var b = b1 < b2 ? b1 : b2;
|
||||||
|
|
||||||
if (!line1.IsVertical() && line1.Slope() < 0)
|
if (!line1.IsVertical() && line1.Slope() < 0)
|
||||||
lineOut = new Line(new Vector(l, t), new Vector(r, b)) { Layer = line1.Layer, Color = line1.Color };
|
lineOut = new Line(new Vector(l, t), new Vector(r, b))
|
||||||
|
{
|
||||||
|
Layer = line1.Layer,
|
||||||
|
Color = line1.Color,
|
||||||
|
};
|
||||||
else
|
else
|
||||||
lineOut = new Line(new Vector(l, b), new Vector(r, t)) { Layer = line1.Layer, Color = line1.Color };
|
lineOut = new Line(new Vector(l, b), new Vector(r, t))
|
||||||
|
{
|
||||||
|
Layer = line1.Layer,
|
||||||
|
Color = line1.Color,
|
||||||
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -177,33 +200,47 @@ namespace OpenNest.Geometry
|
|||||||
if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
|
if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (startAngle < 0) startAngle += Angle.TwoPI;
|
if (startAngle < 0)
|
||||||
if (endAngle < 0) endAngle += Angle.TwoPI;
|
startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0)
|
||||||
|
endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color };
|
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle)
|
||||||
|
{
|
||||||
|
Layer = arc1.Layer,
|
||||||
|
Color = arc1.Color,
|
||||||
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex)
|
private static List<Line> GetCollinearLines(
|
||||||
|
this IList<Line> lines,
|
||||||
|
Line line,
|
||||||
|
int startIndex
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var collinearLines = new List<Line>();
|
var collinearLines = new List<Line>();
|
||||||
|
|
||||||
Parallel.For(startIndex, lines.Count, index =>
|
Parallel.For(
|
||||||
{
|
startIndex,
|
||||||
var compareLine = lines[index];
|
lines.Count,
|
||||||
|
index =>
|
||||||
if (Object.ReferenceEquals(line, compareLine))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!line.IsCollinearTo(compareLine))
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (collinearLines)
|
|
||||||
{
|
{
|
||||||
collinearLines.Add(compareLine);
|
var compareLine = lines[index];
|
||||||
|
|
||||||
|
if (Object.ReferenceEquals(line, compareLine))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!line.IsCollinearTo(compareLine))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (collinearLines)
|
||||||
|
{
|
||||||
|
collinearLines.Add(compareLine);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return collinearLines;
|
return collinearLines;
|
||||||
}
|
}
|
||||||
@@ -212,21 +249,25 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var coradialArcs = new List<Arc>();
|
var coradialArcs = new List<Arc>();
|
||||||
|
|
||||||
Parallel.For(startIndex, arcs.Count, index =>
|
Parallel.For(
|
||||||
{
|
startIndex,
|
||||||
var compareArc = arcs[index];
|
arcs.Count,
|
||||||
|
index =>
|
||||||
if (Object.ReferenceEquals(arc, compareArc))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!arc.IsCoradialTo(compareArc))
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (coradialArcs)
|
|
||||||
{
|
{
|
||||||
coradialArcs.Add(compareArc);
|
var compareArc = arcs[index];
|
||||||
|
|
||||||
|
if (Object.ReferenceEquals(arc, compareArc))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!arc.IsCoradialTo(compareArc))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (coradialArcs)
|
||||||
|
{
|
||||||
|
coradialArcs.Add(compareArc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return coradialArcs;
|
return coradialArcs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ public class ArcCandidate
|
|||||||
public double MaxDeviation { get; set; }
|
public double MaxDeviation { get; set; }
|
||||||
public Box BoundingBox { get; set; }
|
public Box BoundingBox { get; set; }
|
||||||
public bool IsSelected { get; set; } = true;
|
public bool IsSelected { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>First point of the original line segments this candidate covers.</summary>
|
/// <summary>First point of the original line segments this candidate covers.</summary>
|
||||||
public Vector FirstPoint { get; set; }
|
public Vector FirstPoint { get; set; }
|
||||||
|
|
||||||
/// <summary>Last point of the original line segments this candidate covers.</summary>
|
/// <summary>Last point of the original line segments this candidate covers.</summary>
|
||||||
public Vector LastPoint { get; set; }
|
public Vector LastPoint { get; set; }
|
||||||
}
|
}
|
||||||
@@ -46,9 +48,7 @@ public class MirrorAxisResult
|
|||||||
var dx = p.X - Point.X;
|
var dx = p.X - Point.X;
|
||||||
var dy = p.Y - Point.Y;
|
var dy = p.Y - Point.Y;
|
||||||
var dot = dx * Direction.X + dy * Direction.Y;
|
var dot = dx * Direction.X + dy * Direction.Y;
|
||||||
return new Vector(
|
return new Vector(p.X - 2 * (dx - dot * Direction.X), p.Y - 2 * (dy - dot * Direction.Y));
|
||||||
p.X - 2 * (dx - dot * Direction.X),
|
|
||||||
p.Y - 2 * (dy - dot * Direction.Y));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +74,14 @@ public class GeometrySimplifier
|
|||||||
var runStart = i;
|
var runStart = i;
|
||||||
var layerName = entities[i].Layer?.Name;
|
var layerName = entities[i].Layer?.Name;
|
||||||
var lineCount = 0;
|
var lineCount = 0;
|
||||||
while (i < entities.Count && (entities[i] is Line || entities[i] is Arc) && entities[i].Layer?.Name == layerName)
|
while (
|
||||||
|
i < entities.Count
|
||||||
|
&& (entities[i] is Line || entities[i] is Arc)
|
||||||
|
&& entities[i].Layer?.Name == layerName
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (entities[i] is Line) lineCount++;
|
if (entities[i] is Line)
|
||||||
|
lineCount++;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
var runEnd = i - 1;
|
var runEnd = i - 1;
|
||||||
@@ -90,10 +95,7 @@ public class GeometrySimplifier
|
|||||||
|
|
||||||
public Shape Apply(Shape shape, List<ArcCandidate> candidates)
|
public Shape Apply(Shape shape, List<ArcCandidate> candidates)
|
||||||
{
|
{
|
||||||
var selected = candidates
|
var selected = candidates.Where(c => c.IsSelected).OrderBy(c => c.StartIndex).ToList();
|
||||||
.Where(c => c.IsSelected)
|
|
||||||
.OrderBy(c => c.StartIndex)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var newEntities = new List<Entity>();
|
var newEntities = new List<Entity>();
|
||||||
var i = 0;
|
var i = 0;
|
||||||
@@ -132,11 +134,10 @@ public class GeometrySimplifier
|
|||||||
foreach (var e in shape.Entities)
|
foreach (var e in shape.Entities)
|
||||||
midpoints.Add(e.BoundingBox.Center);
|
midpoints.Add(e.BoundingBox.Center);
|
||||||
|
|
||||||
if (midpoints.Count < 4) return MirrorAxisResult.None;
|
if (midpoints.Count < 4)
|
||||||
|
return MirrorAxisResult.None;
|
||||||
|
|
||||||
var centroid = new Vector(
|
var centroid = new Vector(midpoints.Average(p => p.X), midpoints.Average(p => p.Y));
|
||||||
midpoints.Average(p => p.X),
|
|
||||||
midpoints.Average(p => p.Y));
|
|
||||||
var cx = centroid.X;
|
var cx = centroid.X;
|
||||||
var cy = centroid.Y;
|
var cy = centroid.Y;
|
||||||
|
|
||||||
@@ -190,8 +191,7 @@ public class GeometrySimplifier
|
|||||||
return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None;
|
return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double NormalizeAngle(double angle) =>
|
private static double NormalizeAngle(double angle) => angle < 0 ? angle + Angle.TwoPI : angle;
|
||||||
angle < 0 ? angle + Angle.TwoPI : angle;
|
|
||||||
|
|
||||||
private static Vector Normalize(Vector v)
|
private static Vector Normalize(Vector v)
|
||||||
{
|
{
|
||||||
@@ -231,7 +231,8 @@ public class GeometrySimplifier
|
|||||||
|
|
||||||
for (var j = 0; j < points.Count; j++)
|
for (var j = 0; j < points.Count; j++)
|
||||||
{
|
{
|
||||||
if (i == j) continue;
|
if (i == j)
|
||||||
|
continue;
|
||||||
var d = reflected.DistanceTo(points[j]);
|
var d = reflected.DistanceTo(points[j]);
|
||||||
if (d < matchTol)
|
if (d < matchTol)
|
||||||
{
|
{
|
||||||
@@ -251,17 +252,20 @@ public class GeometrySimplifier
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Symmetrize(List<ArcCandidate> candidates, MirrorAxisResult axis)
|
public void Symmetrize(List<ArcCandidate> candidates, MirrorAxisResult axis)
|
||||||
{
|
{
|
||||||
if (!axis.IsValid || candidates.Count < 2) return;
|
if (!axis.IsValid || candidates.Count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
var paired = new HashSet<int>();
|
var paired = new HashSet<int>();
|
||||||
|
|
||||||
for (var i = 0; i < candidates.Count; i++)
|
for (var i = 0; i < candidates.Count; i++)
|
||||||
{
|
{
|
||||||
if (paired.Contains(i)) continue;
|
if (paired.Contains(i))
|
||||||
|
continue;
|
||||||
|
|
||||||
var ci = candidates[i];
|
var ci = candidates[i];
|
||||||
var ciCenter = ci.BoundingBox.Center;
|
var ciCenter = ci.BoundingBox.Center;
|
||||||
if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) continue; // on the axis
|
if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1)
|
||||||
|
continue; // on the axis
|
||||||
|
|
||||||
var mirrorCenter = axis.Reflect(ciCenter);
|
var mirrorCenter = axis.Reflect(ciCenter);
|
||||||
|
|
||||||
@@ -269,7 +273,8 @@ public class GeometrySimplifier
|
|||||||
var bestDist = double.MaxValue;
|
var bestDist = double.MaxValue;
|
||||||
for (var j = i + 1; j < candidates.Count; j++)
|
for (var j = i + 1; j < candidates.Count; j++)
|
||||||
{
|
{
|
||||||
if (paired.Contains(j)) continue;
|
if (paired.Contains(j))
|
||||||
|
continue;
|
||||||
var d = mirrorCenter.DistanceTo(candidates[j].BoundingBox.Center);
|
var d = mirrorCenter.DistanceTo(candidates[j].BoundingBox.Center);
|
||||||
if (d < bestDist)
|
if (d < bestDist)
|
||||||
{
|
{
|
||||||
@@ -279,7 +284,8 @@ public class GeometrySimplifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
var matchTol = System.Math.Max(ci.BoundingBox.Width, ci.BoundingBox.Length) * 0.5;
|
var matchTol = System.Math.Max(ci.BoundingBox.Width, ci.BoundingBox.Length) * 0.5;
|
||||||
if (bestJ < 0 || bestDist > matchTol) continue;
|
if (bestJ < 0 || bestDist > matchTol)
|
||||||
|
continue;
|
||||||
|
|
||||||
paired.Add(i);
|
paired.Add(i);
|
||||||
paired.Add(bestJ);
|
paired.Add(bestJ);
|
||||||
@@ -287,7 +293,10 @@ public class GeometrySimplifier
|
|||||||
var cj = candidates[bestJ];
|
var cj = candidates[bestJ];
|
||||||
var sourceIdx = i;
|
var sourceIdx = i;
|
||||||
var targetIdx = bestJ;
|
var targetIdx = bestJ;
|
||||||
if (cj.LineCount > ci.LineCount || (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation))
|
if (
|
||||||
|
cj.LineCount > ci.LineCount
|
||||||
|
|| (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
sourceIdx = bestJ;
|
sourceIdx = bestJ;
|
||||||
targetIdx = i;
|
targetIdx = i;
|
||||||
@@ -323,8 +332,12 @@ public class GeometrySimplifier
|
|||||||
var mirrorEp = axis.Reflect(ep);
|
var mirrorEp = axis.Reflect(ep);
|
||||||
|
|
||||||
// Mirroring reverses winding — swap start/end to preserve arc direction
|
// Mirroring reverses winding — swap start/end to preserve arc direction
|
||||||
var mirrorStart = NormalizeAngle(System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X));
|
var mirrorStart = NormalizeAngle(
|
||||||
var mirrorEnd = NormalizeAngle(System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X));
|
System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X)
|
||||||
|
);
|
||||||
|
var mirrorEnd = NormalizeAngle(
|
||||||
|
System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X)
|
||||||
|
);
|
||||||
|
|
||||||
var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed);
|
var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed);
|
||||||
result.Layer = arc.Layer;
|
result.Layer = arc.Layer;
|
||||||
@@ -332,7 +345,12 @@ public class GeometrySimplifier
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FindCandidatesInRun(List<Entity> entities, int runStart, int runEnd, List<ArcCandidate> candidates)
|
private void FindCandidatesInRun(
|
||||||
|
List<Entity> entities,
|
||||||
|
int runStart,
|
||||||
|
int runEnd,
|
||||||
|
List<ArcCandidate> candidates
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var j = runStart;
|
var j = runStart;
|
||||||
var chainedTangent = Vector.Invalid;
|
var chainedTangent = Vector.Invalid;
|
||||||
@@ -349,46 +367,63 @@ public class GeometrySimplifier
|
|||||||
|
|
||||||
chainedTangent = ComputeEndTangent(result.Center, result.Points);
|
chainedTangent = ComputeEndTangent(result.Center, result.Points);
|
||||||
var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]);
|
var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]);
|
||||||
candidates.Add(new ArcCandidate
|
candidates.Add(
|
||||||
{
|
new ArcCandidate
|
||||||
StartIndex = j,
|
{
|
||||||
EndIndex = result.EndIndex,
|
StartIndex = j,
|
||||||
FittedArc = arc,
|
EndIndex = result.EndIndex,
|
||||||
MaxDeviation = result.Deviation,
|
FittedArc = arc,
|
||||||
BoundingBox = result.Points.GetBoundingBox(),
|
MaxDeviation = result.Deviation,
|
||||||
FirstPoint = arc.StartPoint(),
|
BoundingBox = result.Points.GetBoundingBox(),
|
||||||
LastPoint = arc.EndPoint(),
|
FirstPoint = arc.StartPoint(),
|
||||||
});
|
LastPoint = arc.EndPoint(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
j = result.EndIndex + 1;
|
j = result.EndIndex + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ArcFitResult(Vector Center, double Radius, double Deviation, List<Vector> Points, int EndIndex);
|
private record ArcFitResult(
|
||||||
|
Vector Center,
|
||||||
|
double Radius,
|
||||||
|
double Deviation,
|
||||||
|
List<Vector> Points,
|
||||||
|
int EndIndex
|
||||||
|
);
|
||||||
|
|
||||||
private ArcFitResult TryFitArcAt(List<Entity> entities, int start, int runEnd, Vector chainedTangent)
|
private ArcFitResult TryFitArcAt(
|
||||||
|
List<Entity> entities,
|
||||||
|
int start,
|
||||||
|
int runEnd,
|
||||||
|
Vector chainedTangent
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var k = start + MinLines - 1;
|
var k = start + MinLines - 1;
|
||||||
if (k > runEnd) return null;
|
if (k > runEnd)
|
||||||
|
return null;
|
||||||
|
|
||||||
var points = CollectPoints(entities, start, k);
|
var points = CollectPoints(entities, start, k);
|
||||||
if (points.Count < 3) return null;
|
if (points.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
var startTangent = chainedTangent.IsValid()
|
var startTangent = EstimateStartTangent(entities, start, points, chainedTangent);
|
||||||
? chainedTangent
|
var endTangent = EstimateEndTangent(entities, k, points);
|
||||||
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
|
||||||
|
|
||||||
var endTangent = GetExitDirection(entities[k]);
|
|
||||||
var (center, radius, dev) = TryFit(points, startTangent, endTangent);
|
var (center, radius, dev) = TryFit(points, startTangent, endTangent);
|
||||||
if (!center.IsValid()) return null;
|
if (!center.IsValid())
|
||||||
|
return null;
|
||||||
|
|
||||||
// Extend the arc as far as possible
|
// Extend the arc as far as possible
|
||||||
while (k + 1 <= runEnd)
|
while (k + 1 <= runEnd)
|
||||||
{
|
{
|
||||||
var extPoints = CollectPoints(entities, start, k + 1);
|
var extPoints = CollectPoints(entities, start, k + 1);
|
||||||
var extEndTangent = GetExitDirection(entities[k + 1]);
|
if (extPoints.Count < 3)
|
||||||
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent, extEndTangent) : (Vector.Invalid, 0, 0d);
|
break;
|
||||||
if (!nc.IsValid()) break;
|
|
||||||
|
var extEndTangent = EstimateEndTangent(entities, k + 1, extPoints);
|
||||||
|
var (nc, nr, nd) = TryFit(extPoints, startTangent, extEndTangent);
|
||||||
|
if (!nc.IsValid())
|
||||||
|
break;
|
||||||
|
|
||||||
k++;
|
k++;
|
||||||
center = nc;
|
center = nc;
|
||||||
@@ -407,37 +442,228 @@ public class GeometrySimplifier
|
|||||||
return new ArcFitResult(center, radius, dev, points, k);
|
return new ArcFitResult(center, radius, dev, points, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent, Vector endTangent)
|
private (Vector center, double radius, double deviation) TryFit(
|
||||||
|
List<Vector> points,
|
||||||
|
TangentEstimate start,
|
||||||
|
TangentEstimate end
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Try dual-tangent fit first (matches direction at both endpoints)
|
foreach (var (center, radius, dev) in FitAttempts(points, start, end))
|
||||||
if (endTangent.IsValid())
|
|
||||||
{
|
{
|
||||||
var (dc, dr, dd) = ArcFit.FitWithDualTangent(points, startTangent, endTangent);
|
if (!center.IsValid() || dev > Tolerance)
|
||||||
if (dc.IsValid() && dd <= Tolerance)
|
continue;
|
||||||
|
|
||||||
|
// Check that the arc doesn't bulge away from the original line segments
|
||||||
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
|
||||||
|
if (arcDev > Tolerance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return (center, radius, System.Math.Max(dev, arcDev));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Vector.Invalid, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Yields fit attempts in preference order. A trusted tangent (chained from the
|
||||||
|
/// previous arc, an adjacent original arc, or a long straight edge) is enforced
|
||||||
|
/// exactly on its side; otherwise the tangency error is balanced between both
|
||||||
|
/// endpoints. The unconstrained mirror-axis fit is the last resort. Every attempt
|
||||||
|
/// passes exactly through both endpoints, so no gaps are introduced.
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerable<(Vector center, double radius, double deviation)> FitAttempts(
|
||||||
|
List<Vector> points,
|
||||||
|
TangentEstimate start,
|
||||||
|
TangentEstimate end
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (start.Trusted && !end.Trusted)
|
||||||
|
{
|
||||||
|
yield return ArcFit.FitWithStartTangent(points, start.Direction);
|
||||||
|
yield return ArcFit.FitThroughEndpointsWithTangents(
|
||||||
|
points,
|
||||||
|
start.Direction,
|
||||||
|
end.Direction
|
||||||
|
);
|
||||||
|
yield return FitWithEndTangent(points, end.Direction);
|
||||||
|
}
|
||||||
|
else if (end.Trusted && !start.Trusted)
|
||||||
|
{
|
||||||
|
yield return FitWithEndTangent(points, end.Direction);
|
||||||
|
yield return ArcFit.FitThroughEndpointsWithTangents(
|
||||||
|
points,
|
||||||
|
start.Direction,
|
||||||
|
end.Direction
|
||||||
|
);
|
||||||
|
yield return ArcFit.FitWithStartTangent(points, start.Direction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
yield return ArcFit.FitThroughEndpointsWithTangents(
|
||||||
|
points,
|
||||||
|
start.Direction,
|
||||||
|
end.Direction
|
||||||
|
);
|
||||||
|
yield return ArcFit.FitWithStartTangent(points, start.Direction);
|
||||||
|
yield return FitWithEndTangent(points, end.Direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return FitMirrorAxis(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fits an arc through both endpoints with an exact tangent at the last point,
|
||||||
|
/// by running the start-tangent fit on the reversed point sequence.
|
||||||
|
/// </summary>
|
||||||
|
private static (Vector center, double radius, double deviation) FitWithEndTangent(
|
||||||
|
List<Vector> points,
|
||||||
|
Vector endTangent
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var reversed = new List<Vector>(points);
|
||||||
|
reversed.Reverse();
|
||||||
|
return ArcFit.FitWithStartTangent(reversed, new Vector(-endTangent.X, -endTangent.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An estimated tangent direction at a fit endpoint. Trusted estimates come from
|
||||||
|
/// exact geometry (a chained arc, an adjacent original arc, or a long straight
|
||||||
|
/// edge) and are enforced exactly; untrusted ones are derived from the polyline
|
||||||
|
/// vertices and only guide the fit.
|
||||||
|
/// </summary>
|
||||||
|
private readonly record struct TangentEstimate(Vector Direction, bool Trusted);
|
||||||
|
|
||||||
|
/// <summary>Segment-length ratio above which a neighboring line counts as a true
|
||||||
|
/// straight edge (rather than another chord of the tessellated curve).</summary>
|
||||||
|
private const double NeighborEdgeFactor = 3.0;
|
||||||
|
|
||||||
|
private static TangentEstimate EstimateStartTangent(
|
||||||
|
List<Entity> entities,
|
||||||
|
int start,
|
||||||
|
List<Vector> points,
|
||||||
|
Vector chainedTangent
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (chainedTangent.IsValid())
|
||||||
|
return new TangentEstimate(chainedTangent, true);
|
||||||
|
|
||||||
|
if (entities[start] is Arc startArc)
|
||||||
|
return new TangentEstimate(GetEntryDirection(startArc), true);
|
||||||
|
|
||||||
|
var firstChordLen = points[0].DistanceTo(points[1]);
|
||||||
|
if (start > 0)
|
||||||
|
{
|
||||||
|
var prev = entities[start - 1];
|
||||||
|
var prevEnd = prev switch
|
||||||
{
|
{
|
||||||
var isRev = SumSignedAngles(dc, points) < 0;
|
Line l => l.EndPoint,
|
||||||
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev);
|
Arc a => a.EndPoint(),
|
||||||
if (aDev <= Tolerance)
|
_ => Vector.Invalid,
|
||||||
return (dc, dr, System.Math.Max(dd, aDev));
|
};
|
||||||
|
if (prevEnd.IsValid() && prevEnd.DistanceTo(points[0]) < 1e-6)
|
||||||
|
{
|
||||||
|
if (prev is Arc)
|
||||||
|
return new TangentEstimate(GetExitDirection(prev), true);
|
||||||
|
if (
|
||||||
|
prev is Line prevLine
|
||||||
|
&& prevLine.StartPoint.DistanceTo(prevLine.EndPoint)
|
||||||
|
>= NeighborEdgeFactor * firstChordLen
|
||||||
|
)
|
||||||
|
return new TangentEstimate(GetExitDirection(prevLine), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to start-tangent-only, then mirror axis
|
var chord = new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
||||||
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent);
|
if (points.Count >= 3)
|
||||||
if (!center.IsValid() || dev > Tolerance)
|
return new TangentEstimate(
|
||||||
(center, radius, dev) = FitMirrorAxis(points);
|
EstimateVertexTangent(points[0], points[1], points[2], chord),
|
||||||
if (!center.IsValid() || dev > Tolerance)
|
false
|
||||||
return (Vector.Invalid, 0, 0);
|
);
|
||||||
|
return new TangentEstimate(chord, false);
|
||||||
// Check that the arc doesn't bulge away from the original line segments
|
|
||||||
var isReversed = SumSignedAngles(center, points) < 0;
|
|
||||||
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
|
|
||||||
if (arcDev > Tolerance)
|
|
||||||
return (Vector.Invalid, 0, 0);
|
|
||||||
|
|
||||||
return (center, radius, System.Math.Max(dev, arcDev));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TangentEstimate EstimateEndTangent(
|
||||||
|
List<Entity> entities,
|
||||||
|
int k,
|
||||||
|
List<Vector> points
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (entities[k] is Arc endArc)
|
||||||
|
return new TangentEstimate(GetExitDirection(endArc), true);
|
||||||
|
|
||||||
|
var lastChordLen = points[^1].DistanceTo(points[^2]);
|
||||||
|
if (k + 1 < entities.Count)
|
||||||
|
{
|
||||||
|
var next = entities[k + 1];
|
||||||
|
var nextStart = next switch
|
||||||
|
{
|
||||||
|
Line l => l.StartPoint,
|
||||||
|
Arc a => a.StartPoint(),
|
||||||
|
_ => Vector.Invalid,
|
||||||
|
};
|
||||||
|
if (nextStart.IsValid() && nextStart.DistanceTo(points[^1]) < 1e-6)
|
||||||
|
{
|
||||||
|
if (next is Arc nextArc)
|
||||||
|
return new TangentEstimate(GetEntryDirection(nextArc), true);
|
||||||
|
if (
|
||||||
|
next is Line nextLine
|
||||||
|
&& nextLine.StartPoint.DistanceTo(nextLine.EndPoint)
|
||||||
|
>= NeighborEdgeFactor * lastChordLen
|
||||||
|
)
|
||||||
|
return new TangentEstimate(GetExitDirection(nextLine), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chord = new Vector(points[^1].X - points[^2].X, points[^1].Y - points[^2].Y);
|
||||||
|
if (points.Count >= 3)
|
||||||
|
return new TangentEstimate(
|
||||||
|
EstimateVertexTangent(points[^1], points[^2], points[^3], chord),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return new TangentEstimate(chord, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates the curve tangent at a polyline vertex from the circle through it and
|
||||||
|
/// its two nearest neighbors. A raw chord direction is off from the true tangent by
|
||||||
|
/// half the chord's subtended angle; the circumcircle estimate removes that bias.
|
||||||
|
/// Falls back to the travel direction when the three points are collinear.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector EstimateVertexTangent(Vector at, Vector b, Vector c, Vector travel)
|
||||||
|
{
|
||||||
|
var d = 2 * (at.X * (b.Y - c.Y) + b.X * (c.Y - at.Y) + c.X * (at.Y - b.Y));
|
||||||
|
if (System.Math.Abs(d) < 1e-14)
|
||||||
|
return travel;
|
||||||
|
|
||||||
|
var sqA = at.X * at.X + at.Y * at.Y;
|
||||||
|
var sqB = b.X * b.X + b.Y * b.Y;
|
||||||
|
var sqC = c.X * c.X + c.Y * c.Y;
|
||||||
|
var cx = (sqA * (b.Y - c.Y) + sqB * (c.Y - at.Y) + sqC * (at.Y - b.Y)) / d;
|
||||||
|
var cy = (sqA * (c.X - b.X) + sqB * (at.X - c.X) + sqC * (b.X - at.X)) / d;
|
||||||
|
|
||||||
|
var tangent = new Vector(-(at.Y - cy), at.X - cx);
|
||||||
|
if (tangent.X * travel.X + tangent.Y * travel.Y < 0)
|
||||||
|
tangent = new Vector(-tangent.X, -tangent.Y);
|
||||||
|
return tangent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the entry direction (tangent at start point) of an entity.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector GetEntryDirection(Entity entity) =>
|
||||||
|
entity switch
|
||||||
|
{
|
||||||
|
Line line => new Vector(
|
||||||
|
line.EndPoint.X - line.StartPoint.X,
|
||||||
|
line.EndPoint.Y - line.StartPoint.Y
|
||||||
|
),
|
||||||
|
Arc arc => arc.IsReversed
|
||||||
|
? new Vector(System.Math.Sin(arc.StartAngle), -System.Math.Cos(arc.StartAngle))
|
||||||
|
: new Vector(-System.Math.Sin(arc.StartAngle), System.Math.Cos(arc.StartAngle)),
|
||||||
|
_ => Vector.Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the tangent direction at the last point of a fitted arc,
|
/// Computes the tangent direction at the last point of a fitted arc,
|
||||||
/// used to chain tangent continuity to the next arc.
|
/// used to chain tangent continuity to the next arc.
|
||||||
@@ -488,9 +714,17 @@ public class GeometrySimplifier
|
|||||||
var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta);
|
var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta);
|
||||||
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
||||||
|
|
||||||
var dOpt = GoldenSectionMin(dInit - range, dInit + range,
|
var dOpt = GoldenSectionMin(
|
||||||
d => ArcFit.MaxRadialDeviation(points, mx + d * nx, my + d * ny,
|
dInit - range,
|
||||||
System.Math.Sqrt(halfChord * halfChord + d * d)));
|
dInit + range,
|
||||||
|
d =>
|
||||||
|
ArcFit.MaxRadialDeviation(
|
||||||
|
points,
|
||||||
|
mx + d * nx,
|
||||||
|
my + d * ny,
|
||||||
|
System.Math.Sqrt(halfChord * halfChord + d * d)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
|
var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
|
||||||
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
|
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
|
||||||
@@ -542,13 +776,22 @@ public class GeometrySimplifier
|
|||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Arc CreateArc(Vector center, double radius, List<Vector> points, Entity sourceEntity)
|
private static Arc CreateArc(
|
||||||
|
Vector center,
|
||||||
|
double radius,
|
||||||
|
List<Vector> points,
|
||||||
|
Entity sourceEntity
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var firstPoint = points[0];
|
var firstPoint = points[0];
|
||||||
var lastPoint = points[^1];
|
var lastPoint = points[^1];
|
||||||
|
|
||||||
var startAngle = NormalizeAngle(System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X));
|
var startAngle = NormalizeAngle(
|
||||||
var endAngle = NormalizeAngle(System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X));
|
System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X)
|
||||||
|
);
|
||||||
|
var endAngle = NormalizeAngle(
|
||||||
|
System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X)
|
||||||
|
);
|
||||||
var isReversed = SumSignedAngles(center, points) < 0;
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
|
||||||
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
|
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
|
||||||
@@ -560,14 +803,18 @@ public class GeometrySimplifier
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the exit direction (tangent at endpoint) of an entity.
|
/// Returns the exit direction (tangent at endpoint) of an entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Vector GetExitDirection(Entity entity) => entity switch
|
private static Vector GetExitDirection(Entity entity) =>
|
||||||
{
|
entity switch
|
||||||
Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y),
|
{
|
||||||
Arc arc => arc.IsReversed
|
Line line => new Vector(
|
||||||
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle))
|
line.EndPoint.X - line.StartPoint.X,
|
||||||
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
|
line.EndPoint.Y - line.StartPoint.Y
|
||||||
_ => Vector.Invalid,
|
),
|
||||||
};
|
Arc arc => arc.IsReversed
|
||||||
|
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle))
|
||||||
|
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
|
||||||
|
_ => Vector.Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sums signed angular change traversing consecutive points around a center.
|
/// Sums signed angular change traversing consecutive points around a center.
|
||||||
@@ -581,8 +828,10 @@ public class GeometrySimplifier
|
|||||||
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
||||||
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
||||||
var da = a2 - a1;
|
var da = a2 - a1;
|
||||||
while (da > System.Math.PI) da -= Angle.TwoPI;
|
while (da > System.Math.PI)
|
||||||
while (da < -System.Math.PI) da += Angle.TwoPI;
|
da -= Angle.TwoPI;
|
||||||
|
while (da < -System.Math.PI)
|
||||||
|
da += Angle.TwoPI;
|
||||||
total += da;
|
total += da;
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
@@ -593,7 +842,12 @@ public class GeometrySimplifier
|
|||||||
/// back to the original line segments. This catches cases where points lie
|
/// back to the original line segments. This catches cases where points lie
|
||||||
/// on a large circle but the arc bulges far from the original straight geometry.
|
/// on a large circle but the arc bulges far from the original straight geometry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static double MaxArcToSegmentDeviation(List<Vector> points, Vector center, double radius, bool isReversed)
|
private static double MaxArcToSegmentDeviation(
|
||||||
|
List<Vector> points,
|
||||||
|
Vector center,
|
||||||
|
double radius,
|
||||||
|
bool isReversed
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X);
|
var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X);
|
||||||
var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X);
|
var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X);
|
||||||
@@ -601,11 +855,13 @@ public class GeometrySimplifier
|
|||||||
var sweep = endAngle - startAngle;
|
var sweep = endAngle - startAngle;
|
||||||
if (isReversed)
|
if (isReversed)
|
||||||
{
|
{
|
||||||
if (sweep > 0) sweep -= Angle.TwoPI;
|
if (sweep > 0)
|
||||||
|
sweep -= Angle.TwoPI;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (sweep < 0) sweep += Angle.TwoPI;
|
if (sweep < 0)
|
||||||
|
sweep += Angle.TwoPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10));
|
var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10));
|
||||||
@@ -624,9 +880,11 @@ public class GeometrySimplifier
|
|||||||
for (var j = 0; j < points.Count - 1; j++)
|
for (var j = 0; j < points.Count - 1; j++)
|
||||||
{
|
{
|
||||||
var dist = DistanceToSegment(arcPt, points[j], points[j + 1]);
|
var dist = DistanceToSegment(arcPt, points[j], points[j + 1]);
|
||||||
if (dist < minDist) minDist = dist;
|
if (dist < minDist)
|
||||||
|
minDist = dist;
|
||||||
}
|
}
|
||||||
if (minDist > maxDev) maxDev = minDist;
|
if (minDist > maxDev)
|
||||||
|
maxDev = minDist;
|
||||||
}
|
}
|
||||||
return maxDev;
|
return maxDev;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
namespace OpenNest.Geometry
|
||||||
namespace OpenNest.Geometry
|
|
||||||
{
|
{
|
||||||
public interface IBoundable
|
public interface IBoundable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
using Clipper2Lib;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the Inner-Fit Polygon (IFP) — the feasible region where a part's
|
|
||||||
/// reference point can be placed so the part stays entirely within the plate boundary.
|
|
||||||
/// For a rectangular plate, the IFP is the plate shrunk by the part's bounding dimensions.
|
|
||||||
/// </summary>
|
|
||||||
public static class InnerFitPolygon
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the IFP for placing a part polygon inside a rectangular work area.
|
|
||||||
/// The result is a polygon representing all valid reference point positions.
|
|
||||||
/// </summary>
|
|
||||||
public static Polygon Compute(Box workArea, Polygon partPolygon)
|
|
||||||
{
|
|
||||||
// Get the part's bounding box relative to its reference point (origin).
|
|
||||||
var verts = partPolygon.Vertices;
|
|
||||||
|
|
||||||
if (verts.Count < 3)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
var minX = verts[0].X;
|
|
||||||
var maxX = verts[0].X;
|
|
||||||
var minY = verts[0].Y;
|
|
||||||
var maxY = verts[0].Y;
|
|
||||||
|
|
||||||
for (var i = 1; i < verts.Count; i++)
|
|
||||||
{
|
|
||||||
if (verts[i].X < minX) minX = verts[i].X;
|
|
||||||
if (verts[i].X > maxX) maxX = verts[i].X;
|
|
||||||
if (verts[i].Y < minY) minY = verts[i].Y;
|
|
||||||
if (verts[i].Y > maxY) maxY = verts[i].Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The IFP is the work area shrunk inward by the part's extent in each direction.
|
|
||||||
// The reference point can range from (workArea.Left - minX) to (workArea.Right - maxX)
|
|
||||||
// and (workArea.Bottom - minY) to (workArea.Top - maxY).
|
|
||||||
var ifpLeft = workArea.X - minX;
|
|
||||||
var ifpRight = workArea.Right - maxX;
|
|
||||||
var ifpBottom = workArea.Y - minY;
|
|
||||||
var ifpTop = workArea.Top - maxY;
|
|
||||||
|
|
||||||
// If the part doesn't fit, return an empty polygon.
|
|
||||||
if (ifpRight < ifpLeft || ifpTop < ifpBottom)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
var result = new Polygon();
|
|
||||||
result.Vertices.Add(new Vector(ifpLeft, ifpBottom));
|
|
||||||
result.Vertices.Add(new Vector(ifpRight, ifpBottom));
|
|
||||||
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
|
||||||
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
|
||||||
result.Close();
|
|
||||||
result.UpdateBounds();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the feasible region for placing a part given already-placed parts.
|
|
||||||
/// FeasibleRegion = IFP(plate, part) - union(NFP(placed_i, part))
|
|
||||||
/// Returns the polygon representing valid placement positions, or an empty
|
|
||||||
/// polygon if no valid position exists.
|
|
||||||
/// </summary>
|
|
||||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
|
|
||||||
{
|
|
||||||
if (ifp.Vertices.Count < 3)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
if (nfpPaths == null || nfpPaths.Count == 0)
|
|
||||||
return ifp;
|
|
||||||
|
|
||||||
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
|
||||||
var ifpPaths = new PathsD { ifpPath };
|
|
||||||
|
|
||||||
// Subtract the NFPs from the IFP.
|
|
||||||
// Clipper2 handles the implicit union of the clip paths.
|
|
||||||
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
|
|
||||||
|
|
||||||
if (feasible.Count == 0)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
// Find the polygon with the bottom-left-most point.
|
|
||||||
// This ensures we pick the correct region for placement.
|
|
||||||
PathD bestPath = null;
|
|
||||||
var bestY = double.MaxValue;
|
|
||||||
var bestX = double.MaxValue;
|
|
||||||
|
|
||||||
foreach (var path in feasible)
|
|
||||||
{
|
|
||||||
foreach (var pt in path)
|
|
||||||
{
|
|
||||||
if (pt.y < bestY || (pt.y == bestY && pt.x < bestX))
|
|
||||||
{
|
|
||||||
bestY = pt.y;
|
|
||||||
bestX = pt.x;
|
|
||||||
bestPath = path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.
|
|
||||||
/// Returns Vector.Invalid if the polygon has no vertices.
|
|
||||||
/// </summary>
|
|
||||||
public static Vector FindBottomLeftPoint(Polygon polygon)
|
|
||||||
{
|
|
||||||
if (polygon.Vertices.Count == 0)
|
|
||||||
return Vector.Invalid;
|
|
||||||
|
|
||||||
var best = polygon.Vertices[0];
|
|
||||||
|
|
||||||
for (var i = 1; i < polygon.Vertices.Count; i++)
|
|
||||||
{
|
|
||||||
var v = polygon.Vertices[i];
|
|
||||||
|
|
||||||
if (v.Y < best.Y || (v.Y == best.Y && v.X < best.X))
|
|
||||||
best = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -18,8 +18,19 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
pts = pts.Where(pt =>
|
pts = pts.Where(pt =>
|
||||||
Angle.IsBetweenRad(arc1.Center.AngleTo(pt), arc1.StartAngle, arc1.EndAngle, arc1.IsReversed) &&
|
Angle.IsBetweenRad(
|
||||||
Angle.IsBetweenRad(arc2.Center.AngleTo(pt), arc2.StartAngle, arc2.EndAngle, arc2.IsReversed))
|
arc1.Center.AngleTo(pt),
|
||||||
|
arc1.StartAngle,
|
||||||
|
arc1.EndAngle,
|
||||||
|
arc1.IsReversed
|
||||||
|
)
|
||||||
|
&& Angle.IsBetweenRad(
|
||||||
|
arc2.Center.AngleTo(pt),
|
||||||
|
arc2.StartAngle,
|
||||||
|
arc2.EndAngle,
|
||||||
|
arc2.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return pts.Count > 0;
|
return pts.Count > 0;
|
||||||
@@ -35,11 +46,15 @@ namespace OpenNest.Geometry
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pts = pts.Where(pt => Angle.IsBetweenRad(
|
pts = pts.Where(pt =>
|
||||||
arc.Center.AngleTo(pt),
|
Angle.IsBetweenRad(
|
||||||
arc.StartAngle,
|
arc.Center.AngleTo(pt),
|
||||||
arc.EndAngle,
|
arc.StartAngle,
|
||||||
arc.IsReversed)).ToList();
|
arc.EndAngle,
|
||||||
|
arc.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return pts.Count > 0;
|
return pts.Count > 0;
|
||||||
}
|
}
|
||||||
@@ -54,11 +69,15 @@ namespace OpenNest.Geometry
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pts = pts.Where(pt => Angle.IsBetweenRad(
|
pts = pts.Where(pt =>
|
||||||
arc.Center.AngleTo(pt),
|
Angle.IsBetweenRad(
|
||||||
arc.StartAngle,
|
arc.Center.AngleTo(pt),
|
||||||
arc.EndAngle,
|
arc.StartAngle,
|
||||||
arc.IsReversed)).ToList();
|
arc.EndAngle,
|
||||||
|
arc.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return pts.Count > 0;
|
return pts.Count > 0;
|
||||||
}
|
}
|
||||||
@@ -74,11 +93,15 @@ namespace OpenNest.Geometry
|
|||||||
pts2.AddRange(pts3);
|
pts2.AddRange(pts3);
|
||||||
}
|
}
|
||||||
|
|
||||||
pts = pts2.Where(pt => Angle.IsBetweenRad(
|
pts = pts2.Where(pt =>
|
||||||
arc.Center.AngleTo(pt),
|
Angle.IsBetweenRad(
|
||||||
arc.StartAngle,
|
arc.Center.AngleTo(pt),
|
||||||
arc.EndAngle,
|
arc.StartAngle,
|
||||||
arc.IsReversed)).ToList();
|
arc.EndAngle,
|
||||||
|
arc.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return pts.Count > 0;
|
return pts.Count > 0;
|
||||||
}
|
}
|
||||||
@@ -95,11 +118,15 @@ namespace OpenNest.Geometry
|
|||||||
pts2.AddRange(pts3);
|
pts2.AddRange(pts3);
|
||||||
}
|
}
|
||||||
|
|
||||||
pts = pts2.Where(pt => Angle.IsBetweenRad(
|
pts = pts2.Where(pt =>
|
||||||
arc.Center.AngleTo(pt),
|
Angle.IsBetweenRad(
|
||||||
arc.StartAngle,
|
arc.Center.AngleTo(pt),
|
||||||
arc.EndAngle,
|
arc.StartAngle,
|
||||||
arc.IsReversed)).ToList();
|
arc.EndAngle,
|
||||||
|
arc.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return pts.Count > 0;
|
return pts.Count > 0;
|
||||||
}
|
}
|
||||||
@@ -123,20 +150,22 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
var d = circle2.Center - circle1.Center;
|
var d = circle2.Center - circle1.Center;
|
||||||
var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance);
|
var a =
|
||||||
|
(
|
||||||
|
circle1.Radius * circle1.Radius
|
||||||
|
- circle2.Radius * circle2.Radius
|
||||||
|
+ distance * distance
|
||||||
|
) / (2.0 * distance);
|
||||||
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
|
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
|
||||||
|
|
||||||
var pt = new Vector(
|
var pt = new Vector(
|
||||||
circle1.Center.X + (a * d.X) / distance,
|
circle1.Center.X + (a * d.X) / distance,
|
||||||
circle1.Center.Y + (a * d.Y) / distance);
|
circle1.Center.Y + (a * d.Y) / distance
|
||||||
|
);
|
||||||
|
|
||||||
var i1 = new Vector(
|
var i1 = new Vector(pt.X + (h * d.Y) / distance, pt.Y - (h * d.X) / distance);
|
||||||
pt.X + (h * d.Y) / distance,
|
|
||||||
pt.Y - (h * d.X) / distance);
|
|
||||||
|
|
||||||
var i2 = new Vector(
|
var i2 = new Vector(pt.X - (h * d.Y) / distance, pt.Y + (h * d.X) / distance);
|
||||||
pt.X - (h * d.Y) / distance,
|
|
||||||
pt.Y + (h * d.X) / distance);
|
|
||||||
|
|
||||||
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
|
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace OpenNest.Geometry
|
|||||||
public static readonly Layer Default = new Layer("0")
|
public static readonly Layer Default = new Layer("0")
|
||||||
{
|
{
|
||||||
Color = Color.White,
|
Color = Color.White,
|
||||||
IsVisible = true
|
IsVisible = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public Layer(string name)
|
public Layer(string name)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -9,14 +9,10 @@ namespace OpenNest.Geometry
|
|||||||
internal Vector pt1;
|
internal Vector pt1;
|
||||||
internal Vector pt2;
|
internal Vector pt2;
|
||||||
|
|
||||||
public Line()
|
public Line() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Line(double x1, double y1, double x2, double y2)
|
public Line(double x1, double y1, double x2, double y2)
|
||||||
: this(new Vector(x1, y1), new Vector(x2, y2))
|
: this(new Vector(x1, y1), new Vector(x2, y2)) { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Line(Vector startPoint, Vector endPoint)
|
public Line(Vector startPoint, Vector endPoint)
|
||||||
{
|
{
|
||||||
@@ -83,9 +79,7 @@ namespace OpenNest.Geometry
|
|||||||
return EndPoint;
|
return EndPoint;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new Vector(
|
return new Vector(StartPoint.X + param * diff2.X, StartPoint.Y + param * diff2.Y);
|
||||||
StartPoint.X + param * diff2.X,
|
|
||||||
StartPoint.Y + param * diff2.Y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +366,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the bounding box.
|
/// Updates the bounding box.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override sealed void UpdateBounds()
|
public sealed override void UpdateBounds()
|
||||||
{
|
{
|
||||||
if (StartPoint.X < EndPoint.X)
|
if (StartPoint.X < EndPoint.X)
|
||||||
{
|
{
|
||||||
@@ -404,11 +398,9 @@ namespace OpenNest.Geometry
|
|||||||
var x = System.Math.Cos(angle) * distance;
|
var x = System.Math.Cos(angle) * distance;
|
||||||
var y = System.Math.Sin(angle) * distance;
|
var y = System.Math.Sin(angle) * distance;
|
||||||
|
|
||||||
var pt = new Vector(x, y);
|
var pt = side == OffsetSide.Left ? new Vector(x, y) : new Vector(-x, -y);
|
||||||
|
|
||||||
return side == OffsetSide.Left
|
return new Line(StartPoint + pt, EndPoint + pt);
|
||||||
? new Line(StartPoint + pt, EndPoint + pt)
|
|
||||||
: new Line(EndPoint + pt, StartPoint + pt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, Vector pt)
|
public override Entity OffsetEntity(double distance, Vector pt)
|
||||||
@@ -429,13 +421,13 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns>A tuple of (first, second) sub-lines.</returns>
|
/// <returns>A tuple of (first, second) sub-lines.</returns>
|
||||||
public (Line first, Line second) SplitAt(Vector point)
|
public (Line first, Line second) SplitAt(Vector point)
|
||||||
{
|
{
|
||||||
var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon
|
var first =
|
||||||
? null
|
point.DistanceTo(StartPoint) < Tolerance.Epsilon
|
||||||
: new Line(StartPoint, point);
|
? null
|
||||||
|
: new Line(StartPoint, point);
|
||||||
|
|
||||||
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon
|
var second =
|
||||||
? null
|
point.DistanceTo(EndPoint) < Tolerance.Epsilon ? null : new Line(point, EndPoint);
|
||||||
: new Line(point, EndPoint);
|
|
||||||
|
|
||||||
return (first, second);
|
return (first, second);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -11,21 +11,71 @@ namespace OpenNest.Geometry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NoFitPolygon
|
public static class NoFitPolygon
|
||||||
{
|
{
|
||||||
private const double ClipperScale = 1000.0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the NFP between a stationary polygon A and an orbiting polygon B.
|
/// Computes forbidden translations of moving around stationary. Interior means
|
||||||
/// NFP(A, B) = Minkowski sum of A and -B (B reflected through its reference point).
|
/// overlap and boundary means touch, subject to Clipper rounding at precision.
|
||||||
|
/// Inputs are simple filled perimeters, with either winding and optional closing
|
||||||
|
/// vertices. Cutouts are not supported: use Collision for hole-aware decisions.
|
||||||
|
/// The moving reference point is the origin, not its first vertex. Cache this
|
||||||
|
/// CPU preparation result. Rings with fewer than three vertices produce no region.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Polygon Compute(Polygon stationary, Polygon orbiting)
|
public static PathsD Compute(PathD stationary, PathD moving, int precision = ClipperBridge.Precision)
|
||||||
{
|
{
|
||||||
var reflected = Reflect(orbiting);
|
var a = Normalize(stationary);
|
||||||
return MinkowskiSum(stationary, reflected);
|
var b = Normalize(moving);
|
||||||
|
if (a.Count < 3 || b.Count < 3)
|
||||||
|
return new PathsD();
|
||||||
|
|
||||||
|
if (IsConvex(a) && IsConvex(b))
|
||||||
|
return new PathsD { ClipperBridge.ToPath(ComputeConvex(
|
||||||
|
ClipperBridge.ToPolygon(a), ClipperBridge.ToPolygon(b)), true) };
|
||||||
|
|
||||||
|
var negB = new PathD(b.Count);
|
||||||
|
foreach (var point in b)
|
||||||
|
negB.Add(new PointD(-point.x, -point.y));
|
||||||
|
// The boundary sweep alone misses both kinds of containment.
|
||||||
|
var sweep = Minkowski.Sum(negB, a, true, precision);
|
||||||
|
sweep.Add(Clipper.TranslatePath(a, negB[0].x, negB[0].y));
|
||||||
|
sweep.Add(Clipper.TranslatePath(negB, a[0].x, a[0].y));
|
||||||
|
return Clipper.Union(sweep, new PathsD(), FillRule.NonZero, precision);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized version of Compute for polygons known to be convex.
|
/// Computes forbidden origin translations for two filled, lines-only perimeters.
|
||||||
/// Bypasses expensive triangulation and Clipper unions.
|
/// Cutouts are not supported; use Collision for hole-aware decisions.
|
||||||
|
/// </summary>
|
||||||
|
public static PathsD Compute(Polygon stationary, Polygon moving) =>
|
||||||
|
Compute(ClipperBridge.ToPath(stationary, true), ClipperBridge.ToPath(moving, true));
|
||||||
|
|
||||||
|
private static PathD Normalize(PathD source)
|
||||||
|
{
|
||||||
|
var path = new PathD();
|
||||||
|
foreach (var point in source)
|
||||||
|
if (path.Count == 0 || path[path.Count - 1].x != point.x || path[path.Count - 1].y != point.y)
|
||||||
|
path.Add(point);
|
||||||
|
if (path.Count > 1 && path[0].x == path[path.Count - 1].x && path[0].y == path[path.Count - 1].y)
|
||||||
|
path.RemoveAt(path.Count - 1);
|
||||||
|
if (!Clipper.IsPositive(path))
|
||||||
|
path.Reverse();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsConvex(PathD path)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < path.Count; i++)
|
||||||
|
{
|
||||||
|
var a = path[i];
|
||||||
|
var b = path[(i + 1) % path.Count];
|
||||||
|
var c = path[(i + 2) % path.Count];
|
||||||
|
if ((b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x) < 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the NFP between a convex stationary polygon A and a convex orbiting
|
||||||
|
/// polygon B: the Minkowski sum of A and -B (B reflected through its reference point).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Polygon ComputeConvex(Polygon stationary, Polygon orbiting)
|
public static Polygon ComputeConvex(Polygon stationary, Polygon orbiting)
|
||||||
{
|
{
|
||||||
@@ -48,42 +98,6 @@ namespace OpenNest.Geometry
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the Minkowski sum of two polygons using convex decomposition.
|
|
||||||
/// For convex polygons, uses the direct O(n+m) merge-sort of edge vectors.
|
|
||||||
/// For concave polygons, decomposes into triangles, computes pairwise
|
|
||||||
/// convex Minkowski sums, and unions the results with Clipper2.
|
|
||||||
/// </summary>
|
|
||||||
private static Polygon MinkowskiSum(Polygon a, Polygon b)
|
|
||||||
{
|
|
||||||
var trisA = ConvexDecomposition.Triangulate(a);
|
|
||||||
var trisB = ConvexDecomposition.Triangulate(b);
|
|
||||||
|
|
||||||
if (trisA.Count == 0 || trisB.Count == 0)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
var partialSums = new List<Polygon>();
|
|
||||||
|
|
||||||
foreach (var ta in trisA)
|
|
||||||
{
|
|
||||||
foreach (var tb in trisB)
|
|
||||||
{
|
|
||||||
var sum = ConvexMinkowskiSum(ta, tb);
|
|
||||||
|
|
||||||
if (sum.Vertices.Count >= 3)
|
|
||||||
partialSums.Add(sum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (partialSums.Count == 0)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
if (partialSums.Count == 1)
|
|
||||||
return partialSums[0];
|
|
||||||
|
|
||||||
return UnionPolygons(partialSums);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the Minkowski sum of two convex polygons by merging their
|
/// Computes the Minkowski sum of two convex polygons by merging their
|
||||||
/// edge vectors sorted by angle. O(n+m) where n and m are vertex counts.
|
/// edge vectors sorted by angle. O(n+m) where n and m are vertex counts.
|
||||||
@@ -99,14 +113,15 @@ namespace OpenNest.Geometry
|
|||||||
var startB = FindBottomLeft(b);
|
var startB = FindBottomLeft(b);
|
||||||
|
|
||||||
var result = new Polygon();
|
var result = new Polygon();
|
||||||
|
|
||||||
// The starting point of the Minkowski sum A + B is the sum of the
|
// 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
|
// starting points of A and B. For NFP = A + (-B), this is
|
||||||
// startA + startReflectedB.
|
// startA + startReflectedB.
|
||||||
var current = new Vector(
|
var current = new Vector(
|
||||||
a.Vertices[startA].X + b.Vertices[startB].X,
|
a.Vertices[startA].X + b.Vertices[startB].X,
|
||||||
a.Vertices[startA].Y + b.Vertices[startB].Y);
|
a.Vertices[startA].Y + b.Vertices[startB].Y
|
||||||
|
);
|
||||||
|
|
||||||
result.Vertices.Add(current);
|
result.Vertices.Add(current);
|
||||||
|
|
||||||
var ia = 0;
|
var ia = 0;
|
||||||
@@ -132,10 +147,12 @@ namespace OpenNest.Geometry
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
|
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
|
||||||
if (angleA < 0) angleA += Angle.TwoPI;
|
if (angleA < 0)
|
||||||
|
angleA += Angle.TwoPI;
|
||||||
|
|
||||||
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
|
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
|
||||||
if (angleB < 0) angleB += Angle.TwoPI;
|
if (angleB < 0)
|
||||||
|
angleB += Angle.TwoPI;
|
||||||
|
|
||||||
if (angleA < angleB)
|
if (angleA < angleB)
|
||||||
{
|
{
|
||||||
@@ -149,7 +166,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
edge = new Vector(
|
edge = new Vector(
|
||||||
orderedA[ia].X + orderedB[ib].X,
|
orderedA[ia].X + orderedB[ib].X,
|
||||||
orderedA[ia].Y + orderedB[ib].Y);
|
orderedA[ia].Y + orderedB[ib].Y
|
||||||
|
);
|
||||||
ia++;
|
ia++;
|
||||||
ib++;
|
ib++;
|
||||||
}
|
}
|
||||||
@@ -203,8 +221,10 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
for (var i = 1; i < n; i++)
|
for (var i = 1; i < n; i++)
|
||||||
{
|
{
|
||||||
if (verts[i].Y < verts[best].Y ||
|
if (
|
||||||
(verts[i].Y == verts[best].Y && verts[i].X < verts[best].X))
|
verts[i].Y < verts[best].Y
|
||||||
|
|| (verts[i].Y == verts[best].Y && verts[i].X < verts[best].X)
|
||||||
|
)
|
||||||
best = i;
|
best = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,81 +244,5 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unions multiple polygons using Clipper2.
|
|
||||||
/// Returns the outer boundary of the union as a single polygon.
|
|
||||||
/// </summary>
|
|
||||||
internal static Polygon UnionPolygons(List<Polygon> polygons)
|
|
||||||
{
|
|
||||||
var paths = new PathsD();
|
|
||||||
|
|
||||||
foreach (var poly in polygons)
|
|
||||||
{
|
|
||||||
var path = ToClipperPath(poly);
|
|
||||||
|
|
||||||
if (path.Count >= 3)
|
|
||||||
paths.Add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paths.Count == 0)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
var result = Clipper.Union(paths, FillRule.NonZero);
|
|
||||||
|
|
||||||
if (result.Count == 0)
|
|
||||||
return new Polygon();
|
|
||||||
|
|
||||||
// Find the largest polygon (by area) as the outer boundary.
|
|
||||||
var largest = result[0];
|
|
||||||
var largestArea = System.Math.Abs(Clipper.Area(largest));
|
|
||||||
|
|
||||||
for (var i = 1; i < result.Count; i++)
|
|
||||||
{
|
|
||||||
var area = System.Math.Abs(Clipper.Area(result[i]));
|
|
||||||
|
|
||||||
if (area > largestArea)
|
|
||||||
{
|
|
||||||
largest = result[i];
|
|
||||||
largestArea = area;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return FromClipperPath(largest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset.
|
|
||||||
/// </summary>
|
|
||||||
public static PathD ToClipperPath(Polygon polygon, Vector offset = default)
|
|
||||||
{
|
|
||||||
var path = new PathD();
|
|
||||||
var verts = polygon.Vertices;
|
|
||||||
var n = verts.Count;
|
|
||||||
|
|
||||||
// Skip closing vertex if present.
|
|
||||||
if (n > 1 && verts[0].X == verts[n - 1].X && verts[0].Y == verts[n - 1].Y)
|
|
||||||
n--;
|
|
||||||
|
|
||||||
for (var i = 0; i < n; i++)
|
|
||||||
path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y));
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
|
||||||
/// </summary>
|
|
||||||
public static Polygon FromClipperPath(PathD path)
|
|
||||||
{
|
|
||||||
var polygon = new Polygon();
|
|
||||||
|
|
||||||
foreach (var pt in path)
|
|
||||||
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
|
||||||
|
|
||||||
polygon.Close();
|
|
||||||
polygon.UpdateBounds();
|
|
||||||
return polygon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
public static class PolyLabel
|
public static class PolyLabel
|
||||||
{
|
{
|
||||||
public static Vector Find(Polygon outer, IList<Polygon> holes = null, double precision = 0.5)
|
public static Vector Find(
|
||||||
|
Polygon outer,
|
||||||
|
IList<Polygon> holes = null,
|
||||||
|
double precision = 0.5
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (outer.Vertices.Count < 3)
|
if (outer.Vertices.Count < 3)
|
||||||
return outer.Vertices.Count > 0
|
return outer.Vertices.Count > 0 ? outer.Vertices[0] : new Vector();
|
||||||
? outer.Vertices[0]
|
|
||||||
: new Vector();
|
|
||||||
|
|
||||||
var minX = double.MaxValue;
|
var minX = double.MaxValue;
|
||||||
var minY = double.MaxValue;
|
var minY = double.MaxValue;
|
||||||
@@ -19,10 +21,14 @@ namespace OpenNest.Geometry
|
|||||||
for (var i = 0; i < outer.Vertices.Count; i++)
|
for (var i = 0; i < outer.Vertices.Count; i++)
|
||||||
{
|
{
|
||||||
var v = outer.Vertices[i];
|
var v = outer.Vertices[i];
|
||||||
if (v.X < minX) minX = v.X;
|
if (v.X < minX)
|
||||||
if (v.Y < minY) minY = v.Y;
|
minX = v.X;
|
||||||
if (v.X > maxX) maxX = v.X;
|
if (v.Y < minY)
|
||||||
if (v.Y > maxY) maxY = v.Y;
|
minY = v.Y;
|
||||||
|
if (v.X > maxX)
|
||||||
|
maxX = v.X;
|
||||||
|
if (v.Y > maxY)
|
||||||
|
maxY = v.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = maxX - minX;
|
var width = maxX - minX;
|
||||||
@@ -37,8 +43,8 @@ namespace OpenNest.Geometry
|
|||||||
var queue = new List<Cell>();
|
var queue = new List<Cell>();
|
||||||
|
|
||||||
for (var x = minX; x < maxX; x += cellSize)
|
for (var x = minX; x < maxX; x += cellSize)
|
||||||
for (var y = minY; y < maxY; y += cellSize)
|
for (var y = minY; y < maxY; y += cellSize)
|
||||||
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes));
|
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes));
|
||||||
|
|
||||||
queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist));
|
queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist));
|
||||||
|
|
||||||
@@ -194,7 +200,12 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double PointToAllEdgesDist(double x, double y, Polygon outer, IList<Polygon> holes)
|
private static double PointToAllEdgesDist(
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
Polygon outer,
|
||||||
|
IList<Polygon> holes
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minDist = PointToPolygonDist(x, y, outer);
|
var minDist = PointToPolygonDist(x, y, outer);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -107,7 +107,9 @@ namespace OpenNest.Geometry
|
|||||||
public RotationType RotationDirection()
|
public RotationType RotationDirection()
|
||||||
{
|
{
|
||||||
if (Vertices.Count < 3)
|
if (Vertices.Count < 3)
|
||||||
throw new Exception("Not enough points to determine direction. Must have at least 3 points.");
|
throw new Exception(
|
||||||
|
"Not enough points to determine direction. Must have at least 3 points."
|
||||||
|
);
|
||||||
|
|
||||||
return CalculateArea() > 0 ? RotationType.CCW : RotationType.CW;
|
return CalculateArea() > 0 ? RotationType.CCW : RotationType.CW;
|
||||||
}
|
}
|
||||||
@@ -309,11 +311,15 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var vertex = Vertices[i];
|
var vertex = Vertices[i];
|
||||||
|
|
||||||
if (vertex.X < minX) minX = vertex.X;
|
if (vertex.X < minX)
|
||||||
else if (vertex.X > maxX) maxX = vertex.X;
|
minX = vertex.X;
|
||||||
|
else if (vertex.X > maxX)
|
||||||
|
maxX = vertex.X;
|
||||||
|
|
||||||
if (vertex.Y < minY) minY = vertex.Y;
|
if (vertex.Y < minY)
|
||||||
else if (vertex.Y > maxY) maxY = vertex.Y;
|
minY = vertex.Y;
|
||||||
|
else if (vertex.Y > maxY)
|
||||||
|
maxY = vertex.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
boundingBox.X = minX;
|
boundingBox.X = minX;
|
||||||
@@ -322,55 +328,29 @@ namespace OpenNest.Geometry
|
|||||||
boundingBox.Width = maxY - minY;
|
boundingBox.Width = maxY - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Miter-offsets the closed polygon to the given side, keeping its winding.
|
||||||
|
/// Corners sharper than the miter limit are squared off, and features that
|
||||||
|
/// collapse under the offset are dropped. When the offset splits the polygon,
|
||||||
|
/// the largest piece is returned.
|
||||||
|
/// </summary>
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
if (Vertices.Count < 3)
|
if (Vertices.Count < 3)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var isClosed = IsClosed();
|
|
||||||
var count = isClosed ? Vertices.Count - 1 : Vertices.Count;
|
|
||||||
if (count < 3)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var ccw = CalculateArea() > 0;
|
var ccw = CalculateArea() > 0;
|
||||||
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
|
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
|
||||||
var sign = side == outward ? 1.0 : -1.0;
|
var delta = side == outward ? distance : -distance;
|
||||||
var d = distance * sign;
|
|
||||||
|
|
||||||
var normals = new Vector[count];
|
var result = ClipperBridge.OffsetMiter(this, delta);
|
||||||
for (var i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var next = (i + 1) % count;
|
|
||||||
var dx = Vertices[next].X - Vertices[i].X;
|
|
||||||
var dy = Vertices[next].Y - Vertices[i].Y;
|
|
||||||
var len = System.Math.Sqrt(dx * dx + dy * dy);
|
|
||||||
if (len < Tolerance.Epsilon)
|
|
||||||
return null;
|
|
||||||
normals[i] = new Vector(-dy / len * d, dx / len * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new Polygon();
|
if (result == null)
|
||||||
for (var i = 0; i < count; i++)
|
return null;
|
||||||
{
|
|
||||||
var prev = (i - 1 + count) % count;
|
|
||||||
|
|
||||||
var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
|
if (!ccw)
|
||||||
var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
|
result.Reverse();
|
||||||
var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
|
|
||||||
var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);
|
|
||||||
|
|
||||||
var edgeA = new Line(a1, a2);
|
|
||||||
var edgeB = new Line(b1, b2);
|
|
||||||
|
|
||||||
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
|
|
||||||
result.Vertices.Add(pt);
|
|
||||||
else
|
|
||||||
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Close();
|
|
||||||
result.RemoveSelfIntersections();
|
|
||||||
result.UpdateBounds();
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,8 +359,10 @@ namespace OpenNest.Geometry
|
|||||||
var left = OffsetEntity(distance, OffsetSide.Left);
|
var left = OffsetEntity(distance, OffsetSide.Left);
|
||||||
var right = OffsetEntity(distance, OffsetSide.Right);
|
var right = OffsetEntity(distance, OffsetSide.Right);
|
||||||
|
|
||||||
if (left == null) return right;
|
if (left == null)
|
||||||
if (right == null) return left;
|
return right;
|
||||||
|
if (right == null)
|
||||||
|
return left;
|
||||||
|
|
||||||
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
|
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
|
||||||
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
|
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
|
||||||
@@ -537,133 +519,6 @@ namespace OpenNest.Geometry
|
|||||||
get { return EntityType.Polygon; }
|
get { return EntityType.Polygon; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes self-intersecting loops from the polygon by finding non-adjacent
|
|
||||||
/// edge crossings and keeping the larger contour at each crossing.
|
|
||||||
/// </summary>
|
|
||||||
public void RemoveSelfIntersections()
|
|
||||||
{
|
|
||||||
if (!IsClosed() || Vertices.Count < 5)
|
|
||||||
return;
|
|
||||||
|
|
||||||
while (FindCrossing(out var edgeI, out var edgeJ, out var pt))
|
|
||||||
{
|
|
||||||
Vertices = SplitAtCrossing(edgeI, edgeJ, pt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool FindCrossing(out int edgeI, out int edgeJ, out Vector pt)
|
|
||||||
{
|
|
||||||
var n = Vertices.Count - 1;
|
|
||||||
|
|
||||||
// Pre-calculate edge bounding boxes to speed up intersection checks.
|
|
||||||
var edgeBounds = new (double minX, double maxX, double minY, double maxY)[n];
|
|
||||||
for (var i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
var v1 = Vertices[i];
|
|
||||||
var v2 = Vertices[i + 1];
|
|
||||||
edgeBounds[i] = (
|
|
||||||
System.Math.Min(v1.X, v2.X) - Tolerance.Epsilon,
|
|
||||||
System.Math.Max(v1.X, v2.X) + Tolerance.Epsilon,
|
|
||||||
System.Math.Min(v1.Y, v2.Y) - Tolerance.Epsilon,
|
|
||||||
System.Math.Max(v1.Y, v2.Y) + Tolerance.Epsilon
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
var bi = edgeBounds[i];
|
|
||||||
for (var j = i + 2; j < n; j++)
|
|
||||||
{
|
|
||||||
if (i == 0 && j == n - 1)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var bj = edgeBounds[j];
|
|
||||||
|
|
||||||
// Prune with bounding box check.
|
|
||||||
if (bi.maxX < bj.minX || bj.maxX < bi.minX ||
|
|
||||||
bi.maxY < bj.minY || bj.maxY < bi.minY)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
|
|
||||||
{
|
|
||||||
edgeI = i;
|
|
||||||
edgeJ = j;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
edgeI = edgeJ = -1;
|
|
||||||
pt = Vector.Zero;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Vector> SplitAtCrossing(int edgeI, int edgeJ, Vector pt)
|
|
||||||
{
|
|
||||||
var n = Vertices.Count - 1;
|
|
||||||
|
|
||||||
var loopA = Vertices.GetRange(0, edgeI + 1);
|
|
||||||
loopA.Add(pt);
|
|
||||||
loopA.AddRange(Vertices.GetRange(edgeJ + 1, n - edgeJ - 1));
|
|
||||||
loopA.Add(loopA[0]);
|
|
||||||
|
|
||||||
var loopB = new List<Vector> { pt };
|
|
||||||
loopB.AddRange(Vertices.GetRange(edgeI + 1, edgeJ - edgeI));
|
|
||||||
loopB.Add(pt);
|
|
||||||
|
|
||||||
var areaA = System.Math.Abs(CalculateArea(loopA));
|
|
||||||
var areaB = System.Math.Abs(CalculateArea(loopB));
|
|
||||||
|
|
||||||
return areaA >= areaB ? loopA : loopB;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt)
|
|
||||||
{
|
|
||||||
var da = a2 - a1;
|
|
||||||
var db = b2 - b1;
|
|
||||||
var cross = da.X * db.Y - da.Y * db.X;
|
|
||||||
|
|
||||||
if (cross.IsEqualTo(0.0))
|
|
||||||
{
|
|
||||||
pt = Vector.Zero;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dc = b1 - a1;
|
|
||||||
var t = (dc.X * db.Y - dc.Y * db.X) / cross;
|
|
||||||
var u = (dc.X * da.Y - dc.Y * da.X) / cross;
|
|
||||||
|
|
||||||
if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon &&
|
|
||||||
u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon)
|
|
||||||
{
|
|
||||||
pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pt = Vector.Zero;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double CalculateArea(List<Vector> vertices)
|
|
||||||
{
|
|
||||||
double xsum = 0;
|
|
||||||
double ysum = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < vertices.Count - 1; i++)
|
|
||||||
{
|
|
||||||
var current = vertices[i];
|
|
||||||
var next = vertices[i + 1];
|
|
||||||
|
|
||||||
xsum += current.X * next.Y;
|
|
||||||
ysum += current.Y * next.X;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (xsum - ysum) * 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Cleanup()
|
internal void Cleanup()
|
||||||
{
|
{
|
||||||
for (int i = Vertices.Count - 1; i > 0; i--)
|
for (int i = Vertices.Count - 1; i > 0; i--)
|
||||||
@@ -701,8 +556,10 @@ namespace OpenNest.Geometry
|
|||||||
var vi = Vertices[i];
|
var vi = Vertices[i];
|
||||||
var vj = Vertices[j];
|
var vj = Vertices[j];
|
||||||
|
|
||||||
if ((vi.Y > pt.Y) != (vj.Y > pt.Y) &&
|
if (
|
||||||
pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X)
|
(vi.Y > pt.Y) != (vj.Y > pt.Y)
|
||||||
|
&& pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X
|
||||||
|
)
|
||||||
{
|
{
|
||||||
inside = !inside;
|
inside = !inside;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -71,18 +71,24 @@ namespace OpenNest.Geometry
|
|||||||
var vy = ux;
|
var vy = ux;
|
||||||
|
|
||||||
// Project all hull vertices onto edge direction (u) and perpendicular (v)
|
// Project all hull vertices onto edge direction (u) and perpendicular (v)
|
||||||
double minU = double.MaxValue, maxU = double.MinValue;
|
double minU = double.MaxValue,
|
||||||
double minV = double.MaxValue, maxV = double.MinValue;
|
maxU = double.MinValue;
|
||||||
|
double minV = double.MaxValue,
|
||||||
|
maxV = double.MinValue;
|
||||||
|
|
||||||
for (int j = 0; j < n; j++)
|
for (int j = 0; j < n; j++)
|
||||||
{
|
{
|
||||||
var projU = vertices[j].X * ux + vertices[j].Y * uy;
|
var projU = vertices[j].X * ux + vertices[j].Y * uy;
|
||||||
var projV = vertices[j].X * vx + vertices[j].Y * vy;
|
var projV = vertices[j].X * vx + vertices[j].Y * vy;
|
||||||
|
|
||||||
if (projU < minU) minU = projU;
|
if (projU < minU)
|
||||||
if (projU > maxU) maxU = projU;
|
minU = projU;
|
||||||
if (projV < minV) minV = projV;
|
if (projU > maxU)
|
||||||
if (projV > maxV) maxV = projV;
|
maxU = projU;
|
||||||
|
if (projV < minV)
|
||||||
|
minV = projV;
|
||||||
|
if (projV > maxV)
|
||||||
|
maxV = projV;
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = maxU - minU;
|
var width = maxU - minU;
|
||||||
@@ -99,7 +105,11 @@ namespace OpenNest.Geometry
|
|||||||
return best ?? new BoundingRectangleResult(0, 0, 0);
|
return best ?? new BoundingRectangleResult(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BoundingRectangleResult MinimumBoundingRectangle(Polygon hull, double startAngle, double endAngle)
|
public static BoundingRectangleResult MinimumBoundingRectangle(
|
||||||
|
Polygon hull,
|
||||||
|
double startAngle,
|
||||||
|
double endAngle
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var vertices = hull.Vertices;
|
var vertices = hull.Vertices;
|
||||||
int n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
|
int n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
|
||||||
@@ -153,23 +163,33 @@ namespace OpenNest.Geometry
|
|||||||
return best ?? new BoundingRectangleResult(startAngle, 0, 0);
|
return best ?? new BoundingRectangleResult(startAngle, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BoundingRectangleResult EvaluateAtAngle(IList<Vector> vertices, int n, double angle)
|
private static BoundingRectangleResult EvaluateAtAngle(
|
||||||
|
IList<Vector> vertices,
|
||||||
|
int n,
|
||||||
|
double angle
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var cos = System.Math.Cos(angle);
|
var cos = System.Math.Cos(angle);
|
||||||
var sin = System.Math.Sin(angle);
|
var sin = System.Math.Sin(angle);
|
||||||
|
|
||||||
double minU = double.MaxValue, maxU = double.MinValue;
|
double minU = double.MaxValue,
|
||||||
double minV = double.MaxValue, maxV = double.MinValue;
|
maxU = double.MinValue;
|
||||||
|
double minV = double.MaxValue,
|
||||||
|
maxV = double.MinValue;
|
||||||
|
|
||||||
for (int j = 0; j < n; j++)
|
for (int j = 0; j < n; j++)
|
||||||
{
|
{
|
||||||
var projU = vertices[j].X * cos + vertices[j].Y * sin;
|
var projU = vertices[j].X * cos + vertices[j].Y * sin;
|
||||||
var projV = -vertices[j].X * sin + vertices[j].Y * cos;
|
var projV = -vertices[j].X * sin + vertices[j].Y * cos;
|
||||||
|
|
||||||
if (projU < minU) minU = projU;
|
if (projU < minU)
|
||||||
if (projU > maxU) maxU = projU;
|
minU = projU;
|
||||||
if (projV < minV) minV = projV;
|
if (projU > maxU)
|
||||||
if (projV > maxV) maxV = projV;
|
maxU = projU;
|
||||||
|
if (projV < minV)
|
||||||
|
minV = projV;
|
||||||
|
if (projV > maxV)
|
||||||
|
maxV = projV;
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = maxU - minU;
|
var width = maxU - minU;
|
||||||
|
|||||||
+250
-78
@@ -282,11 +282,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
case EntityType.Line:
|
case EntityType.Line:
|
||||||
var line = (Line)entity;
|
var line = (Line)entity;
|
||||||
polygon.Vertices.AddRange(new[]
|
polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint });
|
||||||
{
|
|
||||||
line.StartPoint,
|
|
||||||
line.EndPoint
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EntityType.Circle:
|
case EntityType.Circle:
|
||||||
@@ -302,6 +298,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
polygon.Close();
|
polygon.Close();
|
||||||
polygon.Cleanup();
|
polygon.Cleanup();
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
|
||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
@@ -320,21 +317,21 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
case EntityType.Arc:
|
case EntityType.Arc:
|
||||||
var arc = (Arc)entity;
|
var arc = (Arc)entity;
|
||||||
polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe));
|
polygon.Vertices.AddRange(
|
||||||
|
arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EntityType.Line:
|
case EntityType.Line:
|
||||||
var line = (Line)entity;
|
var line = (Line)entity;
|
||||||
polygon.Vertices.AddRange(new[]
|
polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint });
|
||||||
{
|
|
||||||
line.StartPoint,
|
|
||||||
line.EndPoint
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EntityType.Circle:
|
case EntityType.Circle:
|
||||||
var circle = (Circle)entity;
|
var circle = (Circle)entity;
|
||||||
polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe));
|
polygon.Vertices.AddRange(
|
||||||
|
circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -345,6 +342,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
polygon.Close();
|
polygon.Close();
|
||||||
polygon.Cleanup();
|
polygon.Cleanup();
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
|
||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
@@ -462,84 +460,227 @@ namespace OpenNest.Geometry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public override void UpdateBounds()
|
public override void UpdateBounds()
|
||||||
{
|
{
|
||||||
boundingBox = Entities.Select(geo => geo.BoundingBox)
|
boundingBox = Entities.Select(geo => geo.BoundingBox).ToList().GetBoundingBox();
|
||||||
.ToList()
|
|
||||||
.GetBoundingBox();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets each perimeter entity to the given side and joins the pieces into a
|
||||||
|
/// closed chain: line-line corners get a round join (convex) or a miter (concave),
|
||||||
|
/// other convex corners get a round join, and any remaining gap (a concave corner
|
||||||
|
/// involving an arc, or an entity that collapsed under the offset) is bridged
|
||||||
|
/// with a line. Cutouts are offset the same way.
|
||||||
|
/// <para>
|
||||||
|
/// Where a feature is narrower than twice the distance, the result keeps zero-area
|
||||||
|
/// spikes and inverted loops. They lie inside the true offset envelope, so they are
|
||||||
|
/// harmless to directional-distance queries, which only need a closed boundary
|
||||||
|
/// that never falls inside the envelope. Use <see cref="ClipperBridge"/> when a
|
||||||
|
/// clean region is needed.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
var offsetShape = new Shape();
|
var offsetShape = new Shape();
|
||||||
var definedShape = new ShapeProfile(this);
|
var definedShape = new ShapeProfile(this);
|
||||||
|
|
||||||
Entity firstEntity = null;
|
var pieces = new List<OffsetPiece>();
|
||||||
Entity firstOffsetEntity = null;
|
var collapsed = false;
|
||||||
Entity lastEntity = null;
|
|
||||||
Entity lastOffsetEntity = null;
|
|
||||||
|
|
||||||
foreach (var entity in definedShape.Perimeter.Entities)
|
foreach (var entity in definedShape.Perimeter.Entities)
|
||||||
{
|
{
|
||||||
var offsetEntity = entity.OffsetEntity(distance, side);
|
var offsetEntity = entity.OffsetEntity(distance, side);
|
||||||
|
|
||||||
if (offsetEntity == null)
|
if (offsetEntity == null)
|
||||||
|
{
|
||||||
|
collapsed = true;
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (firstEntity == null)
|
|
||||||
{
|
|
||||||
firstEntity = entity;
|
|
||||||
firstOffsetEntity = offsetEntity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (entity.Type)
|
pieces.Add(new OffsetPiece(entity, offsetEntity, collapsed));
|
||||||
{
|
collapsed = false;
|
||||||
case EntityType.Line:
|
|
||||||
{
|
|
||||||
var line = (Line)entity;
|
|
||||||
var offsetLine = (Line)offsetEntity;
|
|
||||||
|
|
||||||
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
|
|
||||||
{
|
|
||||||
JoinOffsetLines(
|
|
||||||
(Line)lastEntity, (Line)lastOffsetEntity,
|
|
||||||
line, offsetLine,
|
|
||||||
distance, side, offsetShape);
|
|
||||||
}
|
|
||||||
|
|
||||||
offsetShape.Entities.Add(offsetLine);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
offsetShape.Entities.Add(offsetEntity);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOffsetEntity = offsetEntity;
|
|
||||||
lastEntity = entity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the shape: join last offset entity back to first
|
// Entities that collapsed at the end of the loop sit before the first piece.
|
||||||
if (lastOffsetEntity != null && firstOffsetEntity != null
|
if (collapsed && pieces.Count > 0)
|
||||||
&& lastOffsetEntity != firstOffsetEntity
|
pieces[0] = pieces[0] with { CollapsedBefore = true };
|
||||||
&& lastOffsetEntity.Type == EntityType.Line
|
|
||||||
&& firstOffsetEntity.Type == EntityType.Line)
|
for (var i = 0; i < pieces.Count; i++)
|
||||||
{
|
{
|
||||||
JoinOffsetLines(
|
offsetShape.Entities.Add(pieces[i].Offset);
|
||||||
(Line)lastEntity, (Line)lastOffsetEntity,
|
|
||||||
(Line)firstEntity, (Line)firstOffsetEntity,
|
if (pieces.Count > 1)
|
||||||
distance, side, offsetShape);
|
{
|
||||||
|
JoinOffsetPieces(
|
||||||
|
pieces[i],
|
||||||
|
pieces[(i + 1) % pieces.Count],
|
||||||
|
distance,
|
||||||
|
side,
|
||||||
|
offsetShape
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var cutout in definedShape.Cutouts)
|
foreach (var cutout in definedShape.Cutouts)
|
||||||
offsetShape.Entities.AddRange(((Shape)cutout.OffsetEntity(distance, side)).Entities);
|
offsetShape.Entities.AddRange(
|
||||||
|
((Shape)cutout.OffsetEntity(distance, side)).Entities
|
||||||
|
);
|
||||||
|
|
||||||
return offsetShape;
|
return offsetShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct OffsetPiece(
|
||||||
|
Entity Source,
|
||||||
|
Entity Offset,
|
||||||
|
bool CollapsedBefore
|
||||||
|
);
|
||||||
|
|
||||||
|
private static void JoinOffsetPieces(
|
||||||
|
OffsetPiece last,
|
||||||
|
OffsetPiece next,
|
||||||
|
double distance,
|
||||||
|
OffsetSide side,
|
||||||
|
Shape offsetShape
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Lines meeting across a collapsed fillet are concave, so a miter trims both at
|
||||||
|
// their intersection. Parallel ones (a round-bottomed slot) fall through to
|
||||||
|
// the bridge below.
|
||||||
|
if (
|
||||||
|
next.CollapsedBefore
|
||||||
|
&& last.Offset is Line lastOffsetLine
|
||||||
|
&& next.Offset is Line nextOffsetLine
|
||||||
|
&& Intersect.IntersectsUnbounded(nextOffsetLine, lastOffsetLine, out var miter)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
lastOffsetLine.EndPoint = miter;
|
||||||
|
nextOffsetLine.StartPoint = miter;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!next.CollapsedBefore && last.Source is Line lastLine && next.Source is Line nextLine)
|
||||||
|
{
|
||||||
|
JoinOffsetLines(
|
||||||
|
lastLine,
|
||||||
|
(Line)last.Offset,
|
||||||
|
nextLine,
|
||||||
|
(Line)next.Offset,
|
||||||
|
distance,
|
||||||
|
side,
|
||||||
|
offsetShape
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!TryGetEnds(last.Offset, out _, out var gapStart)
|
||||||
|
|| !TryGetEnds(next.Offset, out var gapEnd, out _)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (gapStart.DistanceTo(gapEnd) <= OpenNest.Math.Tolerance.Epsilon)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!next.CollapsedBefore
|
||||||
|
&& IsConvexCorner(last.Source, next.Source, side, out var corner)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
offsetShape.Entities.Add(
|
||||||
|
new Arc(
|
||||||
|
corner,
|
||||||
|
distance,
|
||||||
|
corner.AngleTo(gapStart),
|
||||||
|
corner.AngleTo(gapEnd),
|
||||||
|
side == OffsetSide.Left
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concave corner or collapsed entity: the neighbors' offsets overlap, so a
|
||||||
|
// straight bridge stays inside the offset envelope and closes the chain.
|
||||||
|
offsetShape.Entities.Add(new Line(gapStart, gapEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsConvexCorner(
|
||||||
|
Entity last,
|
||||||
|
Entity next,
|
||||||
|
OffsetSide side,
|
||||||
|
out Vector corner
|
||||||
|
)
|
||||||
|
{
|
||||||
|
corner = default;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!TryGetEnds(last, out _, out corner)
|
||||||
|
|| !TryGetTangents(last, out _, out var d1)
|
||||||
|
|| !TryGetTangents(next, out var d2, out _)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cross = d1.X * d2.Y - d1.Y * d2.X;
|
||||||
|
|
||||||
|
return (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon)
|
||||||
|
|| (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetEnds(Entity entity, out Vector start, out Vector end)
|
||||||
|
{
|
||||||
|
switch (entity)
|
||||||
|
{
|
||||||
|
case Line line:
|
||||||
|
start = line.StartPoint;
|
||||||
|
end = line.EndPoint;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Arc arc:
|
||||||
|
start = arc.StartPoint();
|
||||||
|
end = arc.EndPoint();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
start = end = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Direction of travel at the start and end of a line or arc.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryGetTangents(Entity entity, out Vector start, out Vector end)
|
||||||
|
{
|
||||||
|
switch (entity)
|
||||||
|
{
|
||||||
|
case Line line:
|
||||||
|
start = end = line.EndPoint - line.StartPoint;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Arc arc:
|
||||||
|
start = ArcTangent(arc, arc.StartAngle);
|
||||||
|
end = ArcTangent(arc, arc.EndAngle);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
start = end = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector ArcTangent(Arc arc, double angle)
|
||||||
|
{
|
||||||
|
var sin = System.Math.Sin(angle);
|
||||||
|
var cos = System.Math.Cos(angle);
|
||||||
|
return arc.IsReversed ? new Vector(sin, -cos) : new Vector(-sin, cos);
|
||||||
|
}
|
||||||
|
|
||||||
private static void JoinOffsetLines(
|
private static void JoinOffsetLines(
|
||||||
Line lastLine, Line lastOffsetLine,
|
Line lastLine,
|
||||||
Line line, Line offsetLine,
|
Line lastOffsetLine,
|
||||||
double distance, OffsetSide side, Shape offsetShape)
|
Line line,
|
||||||
|
Line offsetLine,
|
||||||
|
double distance,
|
||||||
|
OffsetSide side,
|
||||||
|
Shape offsetShape
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Determine if this is a convex corner using the cross product of
|
// Determine if this is a convex corner using the cross product of
|
||||||
// the original line directions. Convex corners need an arc; concave
|
// the original line directions. Convex corners need an arc; concave
|
||||||
@@ -548,8 +689,9 @@ namespace OpenNest.Geometry
|
|||||||
var d2 = line.EndPoint - line.StartPoint;
|
var d2 = line.EndPoint - line.StartPoint;
|
||||||
var cross = d1.X * d2.Y - d1.Y * d2.X;
|
var cross = d1.X * d2.Y - d1.Y * d2.X;
|
||||||
|
|
||||||
var isConvex = (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) ||
|
var isConvex =
|
||||||
(side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
|
(side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon)
|
||||||
|
|| (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
|
||||||
|
|
||||||
if (isConvex)
|
if (isConvex)
|
||||||
{
|
{
|
||||||
@@ -559,11 +701,13 @@ namespace OpenNest.Geometry
|
|||||||
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
|
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
|
||||||
line.StartPoint.AngleTo(offsetLine.StartPoint),
|
line.StartPoint.AngleTo(offsetLine.StartPoint),
|
||||||
side == OffsetSide.Left
|
side == OffsetSide.Left
|
||||||
);
|
);
|
||||||
|
|
||||||
offsetShape.Entities.Add(arc);
|
offsetShape.Entities.Add(arc);
|
||||||
}
|
}
|
||||||
else if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection))
|
else if (
|
||||||
|
Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
offsetLine.StartPoint = intersection;
|
offsetLine.StartPoint = intersection;
|
||||||
lastOffsetLine.EndPoint = intersection;
|
lastOffsetLine.EndPoint = intersection;
|
||||||
@@ -576,7 +720,7 @@ namespace OpenNest.Geometry
|
|||||||
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
|
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
|
||||||
line.StartPoint.AngleTo(offsetLine.StartPoint),
|
line.StartPoint.AngleTo(offsetLine.StartPoint),
|
||||||
side == OffsetSide.Left
|
side == OffsetSide.Left
|
||||||
);
|
);
|
||||||
|
|
||||||
offsetShape.Entities.Add(arc);
|
offsetShape.Entities.Add(arc);
|
||||||
}
|
}
|
||||||
@@ -592,12 +736,15 @@ namespace OpenNest.Geometry
|
|||||||
/// Normalizes to CW winding before offsetting Left (which is outward for CW),
|
/// Normalizes to CW winding before offsetting Left (which is outward for CW),
|
||||||
/// making the method independent of the original contour winding direction.
|
/// making the method independent of the original contour winding direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Shape OffsetOutward(double distance)
|
internal Shape OffsetOutward(double distance)
|
||||||
{
|
{
|
||||||
var poly = ToPolygon();
|
var poly = ToPolygon();
|
||||||
|
|
||||||
if (poly == null || poly.Vertices.Count < 3
|
if (
|
||||||
|| poly.RotationDirection() == RotationType.CW)
|
poly == null
|
||||||
|
|| poly.Vertices.Count < 3
|
||||||
|
|| poly.RotationDirection() == RotationType.CW
|
||||||
|
)
|
||||||
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
// Shape is CCW — reverse to CW so Left offset goes outward.
|
// Shape is CCW — reverse to CW so Left offset goes outward.
|
||||||
@@ -611,10 +758,21 @@ namespace OpenNest.Geometry
|
|||||||
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
||||||
break;
|
break;
|
||||||
case Arc a:
|
case Arc a:
|
||||||
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
copy.Entities.Add(
|
||||||
|
new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed)
|
||||||
|
{
|
||||||
|
Layer = a.Layer,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case Circle c:
|
case Circle c:
|
||||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW });
|
copy.Entities.Add(
|
||||||
|
new Circle(c.Center, c.Radius)
|
||||||
|
{
|
||||||
|
Layer = c.Layer,
|
||||||
|
Rotation = RotationType.CW,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -627,12 +785,15 @@ namespace OpenNest.Geometry
|
|||||||
/// Normalizes to CCW winding before offsetting Left (which is inward for CCW),
|
/// Normalizes to CCW winding before offsetting Left (which is inward for CCW),
|
||||||
/// making the method independent of the original contour winding direction.
|
/// making the method independent of the original contour winding direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Shape OffsetInward(double distance)
|
internal Shape OffsetInward(double distance)
|
||||||
{
|
{
|
||||||
var poly = ToPolygon();
|
var poly = ToPolygon();
|
||||||
|
|
||||||
if (poly == null || poly.Vertices.Count < 3
|
if (
|
||||||
|| poly.RotationDirection() == RotationType.CCW)
|
poly == null
|
||||||
|
|| poly.Vertices.Count < 3
|
||||||
|
|| poly.RotationDirection() == RotationType.CCW
|
||||||
|
)
|
||||||
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
// Create a reversed copy to avoid mutating shared entity objects.
|
// Create a reversed copy to avoid mutating shared entity objects.
|
||||||
@@ -646,10 +807,21 @@ namespace OpenNest.Geometry
|
|||||||
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
||||||
break;
|
break;
|
||||||
case Arc a:
|
case Arc a:
|
||||||
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
copy.Entities.Add(
|
||||||
|
new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed)
|
||||||
|
{
|
||||||
|
Layer = a.Layer,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case Circle c:
|
case Circle c:
|
||||||
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW });
|
copy.Entities.Add(
|
||||||
|
new Circle(c.Center, c.Radius)
|
||||||
|
{
|
||||||
|
Layer = c.Layer,
|
||||||
|
Rotation = RotationType.CCW,
|
||||||
|
}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public static class ShapeBuilder
|
public static class ShapeBuilder
|
||||||
{
|
{
|
||||||
public static List<Shape> GetShapes(IEnumerable<Entity> entities, double? weldTolerance = null)
|
public static List<Shape> GetShapes(
|
||||||
|
IEnumerable<Entity> entities,
|
||||||
|
double? weldTolerance = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var lines = new List<Line>();
|
var lines = new List<Line>();
|
||||||
var arcs = new List<Arc>();
|
var arcs = new List<Arc>();
|
||||||
@@ -141,7 +144,11 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
private static void AddToGroup(
|
private static void AddToGroup(
|
||||||
List<List<(Entity entity, bool isStart, Vector point)>> groups,
|
List<List<(Entity entity, bool isStart, Vector point)>> groups,
|
||||||
Entity entity, bool isStart, Vector point, double tolerance)
|
Entity entity,
|
||||||
|
bool isStart,
|
||||||
|
Vector point,
|
||||||
|
double tolerance
|
||||||
|
)
|
||||||
{
|
{
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var poly = shape.ToPolygon();
|
var poly = shape.ToPolygon();
|
||||||
|
|
||||||
if (poly != null && poly.Vertices.Count >= 3
|
if (poly != null && poly.Vertices.Count >= 3 && poly.RotationDirection() != desired)
|
||||||
&& poly.RotationDirection() != desired)
|
|
||||||
{
|
{
|
||||||
shape.Reverse();
|
shape.Reverse();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public override string ToString() => $"{Width} x {Length}";
|
public override string ToString() => $"{Width} x {Length}";
|
||||||
|
|
||||||
public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
|
public string ToString(int decimalPlaces) =>
|
||||||
|
$"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -13,57 +13,72 @@ namespace OpenNest.Geometry
|
|||||||
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
|
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
|
||||||
{
|
{
|
||||||
return RayEdgeDistance(
|
return RayEdgeDistance(
|
||||||
vertex.X, vertex.Y,
|
vertex.X,
|
||||||
edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y,
|
vertex.Y,
|
||||||
direction);
|
edge.pt1.X,
|
||||||
|
edge.pt1.Y,
|
||||||
|
edge.pt2.X,
|
||||||
|
edge.pt2.Y,
|
||||||
|
direction
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.Runtime.CompilerServices.MethodImpl(
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
||||||
|
)]
|
||||||
private static double RayEdgeDistance(
|
private static double RayEdgeDistance(
|
||||||
double vx, double vy,
|
double vx,
|
||||||
double p1x, double p1y, double p2x, double p2y,
|
double vy,
|
||||||
PushDirection direction)
|
double p1x,
|
||||||
|
double p1y,
|
||||||
|
double p2x,
|
||||||
|
double p2y,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
{
|
{
|
||||||
case PushDirection.Left:
|
case PushDirection.Left:
|
||||||
case PushDirection.Right:
|
case PushDirection.Right:
|
||||||
{
|
{
|
||||||
var dy = p2y - p1y;
|
var dy = p2y - p1y;
|
||||||
if (System.Math.Abs(dy) < Tolerance.Epsilon)
|
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;
|
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.Down:
|
||||||
case PushDirection.Up:
|
case PushDirection.Up:
|
||||||
{
|
{
|
||||||
var dx = p2x - p1x;
|
var dx = p2x - p1x;
|
||||||
if (System.Math.Abs(dx) < Tolerance.Epsilon)
|
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;
|
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:
|
default:
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
@@ -75,11 +90,18 @@ namespace OpenNest.Geometry
|
|||||||
/// Returns double.MaxValue if the ray does not hit the segment.
|
/// Returns double.MaxValue if the ray does not hit the segment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Runtime.CompilerServices.MethodImpl(
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
||||||
|
)]
|
||||||
public static double RayEdgeDistance(
|
public static double RayEdgeDistance(
|
||||||
double vx, double vy,
|
double vx,
|
||||||
double p1x, double p1y, double p2x, double p2y,
|
double vy,
|
||||||
double dirX, double dirY)
|
double p1x,
|
||||||
|
double p1y,
|
||||||
|
double p2x,
|
||||||
|
double p2y,
|
||||||
|
double dirX,
|
||||||
|
double dirY
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var ex = p2x - p1x;
|
var ex = p2x - p1x;
|
||||||
var ey = p2y - p1y;
|
var ey = p2y - p1y;
|
||||||
@@ -99,8 +121,10 @@ namespace OpenNest.Geometry
|
|||||||
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
|
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
|
|
||||||
if (t > Tolerance.Epsilon) return t;
|
if (t > Tolerance.Epsilon)
|
||||||
if (t >= -Tolerance.Epsilon) return 0;
|
return t;
|
||||||
|
if (t >= -Tolerance.Epsilon)
|
||||||
|
return 0;
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +133,19 @@ namespace OpenNest.Geometry
|
|||||||
/// Returns false if no real intersection exists.
|
/// Returns false if no real intersection exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Runtime.CompilerServices.MethodImpl(
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
||||||
|
)]
|
||||||
private static bool SolveRayCircle(
|
private static bool SolveRayCircle(
|
||||||
double vx, double vy,
|
double vx,
|
||||||
double cx, double cy, double r,
|
double vy,
|
||||||
double dirX, double dirY,
|
double cx,
|
||||||
out double t1, out double t2)
|
double cy,
|
||||||
|
double r,
|
||||||
|
double dirX,
|
||||||
|
double dirY,
|
||||||
|
out double t1,
|
||||||
|
out double t2
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var ox = vx - cx;
|
var ox = vx - cx;
|
||||||
var oy = vy - cy;
|
var oy = vy - cy;
|
||||||
@@ -143,12 +174,20 @@ namespace OpenNest.Geometry
|
|||||||
/// angular span. Returns double.MaxValue if no hit.
|
/// angular span. Returns double.MaxValue if no hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Runtime.CompilerServices.MethodImpl(
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
||||||
|
)]
|
||||||
public static double RayArcDistance(
|
public static double RayArcDistance(
|
||||||
double vx, double vy,
|
double vx,
|
||||||
double cx, double cy, double r,
|
double vy,
|
||||||
double startAngle, double endAngle, bool reversed,
|
double cx,
|
||||||
double dirX, double dirY)
|
double cy,
|
||||||
|
double r,
|
||||||
|
double startAngle,
|
||||||
|
double endAngle,
|
||||||
|
bool reversed,
|
||||||
|
double dirX,
|
||||||
|
double dirY
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
@@ -157,16 +196,18 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
if (t1 > -Tolerance.Epsilon)
|
if (t1 > -Tolerance.Epsilon)
|
||||||
{
|
{
|
||||||
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
|
var hitAngle = Angle.NormalizeRad(
|
||||||
vy + t1 * dirY - cy, vx + t1 * dirX - cx));
|
System.Math.Atan2(vy + t1 * dirY - cy, vx + t1 * dirX - cx)
|
||||||
|
);
|
||||||
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
||||||
best = t1 > Tolerance.Epsilon ? t1 : 0;
|
best = t1 > Tolerance.Epsilon ? t1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t2 > -Tolerance.Epsilon && t2 < best)
|
if (t2 > -Tolerance.Epsilon && t2 < best)
|
||||||
{
|
{
|
||||||
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
|
var hitAngle = Angle.NormalizeRad(
|
||||||
vy + t2 * dirY - cy, vx + t2 * dirX - cx));
|
System.Math.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx)
|
||||||
|
);
|
||||||
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
||||||
best = t2 > Tolerance.Epsilon ? t2 : 0;
|
best = t2 > Tolerance.Epsilon ? t2 : 0;
|
||||||
}
|
}
|
||||||
@@ -179,19 +220,29 @@ namespace OpenNest.Geometry
|
|||||||
/// Returns double.MaxValue if no hit.
|
/// Returns double.MaxValue if no hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Runtime.CompilerServices.MethodImpl(
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
||||||
|
)]
|
||||||
public static double RayCircleDistance(
|
public static double RayCircleDistance(
|
||||||
double vx, double vy,
|
double vx,
|
||||||
double cx, double cy, double r,
|
double vy,
|
||||||
double dirX, double dirY)
|
double cx,
|
||||||
|
double cy,
|
||||||
|
double r,
|
||||||
|
double dirX,
|
||||||
|
double dirY
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
|
|
||||||
if (t1 > Tolerance.Epsilon) return t1;
|
if (t1 > Tolerance.Epsilon)
|
||||||
if (t1 >= -Tolerance.Epsilon) return 0;
|
return t1;
|
||||||
if (t2 > Tolerance.Epsilon) return t2;
|
if (t1 >= -Tolerance.Epsilon)
|
||||||
if (t2 >= -Tolerance.Epsilon) return 0;
|
return 0;
|
||||||
|
if (t2 > Tolerance.Epsilon)
|
||||||
|
return t2;
|
||||||
|
if (t2 >= -Tolerance.Epsilon)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
@@ -201,7 +252,11 @@ namespace OpenNest.Geometry
|
|||||||
/// any edge of movingLines contacts any edge of stationaryLines.
|
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||||
/// Returns double.MaxValue if no collision path exists.
|
/// Returns double.MaxValue if no collision path exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
public static double DirectionalDistance(
|
||||||
|
List<Line> movingLines,
|
||||||
|
List<Line> stationaryLines,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
||||||
}
|
}
|
||||||
@@ -211,8 +266,12 @@ namespace OpenNest.Geometry
|
|||||||
/// by (movingDx, movingDy) without creating new Line objects.
|
/// by (movingDx, movingDy) without creating new Line objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(
|
public static double DirectionalDistance(
|
||||||
List<Line> movingLines, double movingDx, double movingDy,
|
List<Line> movingLines,
|
||||||
List<Line> stationaryLines, PushDirection direction)
|
double movingDx,
|
||||||
|
double movingDy,
|
||||||
|
List<Line> stationaryLines,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
var movingOffset = new Vector(movingDx, movingDy);
|
var movingOffset = new Vector(movingDx, movingDy);
|
||||||
@@ -226,7 +285,8 @@ namespace OpenNest.Geometry
|
|||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||||
if (d < minDist) minDist = d;
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
@@ -239,7 +299,8 @@ namespace OpenNest.Geometry
|
|||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
||||||
if (d < minDist) minDist = d;
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
return minDist;
|
return minDist;
|
||||||
@@ -267,9 +328,12 @@ namespace OpenNest.Geometry
|
|||||||
/// to avoid all intermediate object allocations.
|
/// to avoid all intermediate object allocations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(
|
public static double DirectionalDistance(
|
||||||
(Vector start, Vector end)[] movingEdges, Vector movingOffset,
|
(Vector start, Vector end)[] movingEdges,
|
||||||
(Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset,
|
Vector movingOffset,
|
||||||
PushDirection direction)
|
(Vector start, Vector end)[] stationaryEdges,
|
||||||
|
Vector stationaryOffset,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
@@ -281,7 +345,8 @@ namespace OpenNest.Geometry
|
|||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
||||||
if (d < minDist) minDist = d;
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
@@ -293,15 +358,19 @@ namespace OpenNest.Geometry
|
|||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
||||||
if (d < minDist) minDist = d;
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
return minDist;
|
return minDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double OneWayDistance(
|
public static double OneWayDistance(
|
||||||
Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset,
|
Vector vertex,
|
||||||
PushDirection direction)
|
(Vector start, Vector end)[] edges,
|
||||||
|
Vector edgeOffset,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
var vx = vertex.X;
|
var vx = vertex.X;
|
||||||
@@ -315,7 +384,9 @@ namespace OpenNest.Geometry
|
|||||||
var e1 = edges[i].start + edgeOffset;
|
var e1 = edges[i].start + edgeOffset;
|
||||||
var e2 = edges[i].end + edgeOffset;
|
var e2 = edges[i].end + edgeOffset;
|
||||||
|
|
||||||
double perpValue, edgeMin, edgeMax;
|
double perpValue,
|
||||||
|
edgeMin,
|
||||||
|
edgeMax;
|
||||||
if (horizontal)
|
if (horizontal)
|
||||||
{
|
{
|
||||||
perpValue = vy;
|
perpValue = vy;
|
||||||
@@ -337,7 +408,8 @@ namespace OpenNest.Geometry
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
||||||
if (d < minDist) minDist = d;
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
return minDist;
|
return minDist;
|
||||||
@@ -347,11 +419,16 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
{
|
{
|
||||||
case PushDirection.Left: return PushDirection.Right;
|
case PushDirection.Left:
|
||||||
case PushDirection.Right: return PushDirection.Left;
|
return PushDirection.Right;
|
||||||
case PushDirection.Up: return PushDirection.Down;
|
case PushDirection.Right:
|
||||||
case PushDirection.Down: return PushDirection.Up;
|
return PushDirection.Left;
|
||||||
default: return direction;
|
case PushDirection.Up:
|
||||||
|
return PushDirection.Down;
|
||||||
|
case PushDirection.Down:
|
||||||
|
return PushDirection.Up;
|
||||||
|
default:
|
||||||
|
return direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,11 +441,16 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
{
|
{
|
||||||
case PushDirection.Left: return box.Left - boundary.Left;
|
case PushDirection.Left:
|
||||||
case PushDirection.Right: return boundary.Right - box.Right;
|
return box.Left - boundary.Left;
|
||||||
case PushDirection.Up: return boundary.Top - box.Top;
|
case PushDirection.Right:
|
||||||
case PushDirection.Down: return box.Bottom - boundary.Bottom;
|
return boundary.Right - box.Right;
|
||||||
default: return double.MaxValue;
|
case PushDirection.Up:
|
||||||
|
return boundary.Top - box.Top;
|
||||||
|
case PushDirection.Down:
|
||||||
|
return box.Bottom - boundary.Bottom;
|
||||||
|
default:
|
||||||
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,11 +458,16 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
{
|
{
|
||||||
case PushDirection.Left: return new Vector(-distance, 0);
|
case PushDirection.Left:
|
||||||
case PushDirection.Right: return new Vector(distance, 0);
|
return new Vector(-distance, 0);
|
||||||
case PushDirection.Up: return new Vector(0, distance);
|
case PushDirection.Right:
|
||||||
case PushDirection.Down: return new Vector(0, -distance);
|
return new Vector(distance, 0);
|
||||||
default: return new Vector();
|
case PushDirection.Up:
|
||||||
|
return new Vector(0, distance);
|
||||||
|
case PushDirection.Down:
|
||||||
|
return new Vector(0, -distance);
|
||||||
|
default:
|
||||||
|
return new Vector();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,11 +475,16 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
{
|
{
|
||||||
case PushDirection.Left: return from.Left - to.Right;
|
case PushDirection.Left:
|
||||||
case PushDirection.Right: return to.Left - from.Right;
|
return from.Left - to.Right;
|
||||||
case PushDirection.Up: return to.Bottom - from.Top;
|
case PushDirection.Right:
|
||||||
case PushDirection.Down: return from.Bottom - to.Top;
|
return to.Left - from.Right;
|
||||||
default: return double.MaxValue;
|
case PushDirection.Up:
|
||||||
|
return to.Bottom - from.Top;
|
||||||
|
case PushDirection.Down:
|
||||||
|
return from.Bottom - to.Top;
|
||||||
|
default:
|
||||||
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,23 +501,27 @@ namespace OpenNest.Geometry
|
|||||||
if (direction.X < -Tolerance.Epsilon)
|
if (direction.X < -Tolerance.Epsilon)
|
||||||
{
|
{
|
||||||
var d = (box.Left - boundary.Left) / -direction.X;
|
var d = (box.Left - boundary.Left) / -direction.X;
|
||||||
if (d < dist) dist = d;
|
if (d < dist)
|
||||||
|
dist = d;
|
||||||
}
|
}
|
||||||
else if (direction.X > Tolerance.Epsilon)
|
else if (direction.X > Tolerance.Epsilon)
|
||||||
{
|
{
|
||||||
var d = (boundary.Right - box.Right) / direction.X;
|
var d = (boundary.Right - box.Right) / direction.X;
|
||||||
if (d < dist) dist = d;
|
if (d < dist)
|
||||||
|
dist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (direction.Y < -Tolerance.Epsilon)
|
if (direction.Y < -Tolerance.Epsilon)
|
||||||
{
|
{
|
||||||
var d = (box.Bottom - boundary.Bottom) / -direction.Y;
|
var d = (box.Bottom - boundary.Bottom) / -direction.Y;
|
||||||
if (d < dist) dist = d;
|
if (d < dist)
|
||||||
|
dist = d;
|
||||||
}
|
}
|
||||||
else if (direction.Y > Tolerance.Epsilon)
|
else if (direction.Y > Tolerance.Epsilon)
|
||||||
{
|
{
|
||||||
var d = (boundary.Top - box.Top) / direction.Y;
|
var d = (boundary.Top - box.Top) / direction.Y;
|
||||||
if (d < dist) dist = d;
|
if (d < dist)
|
||||||
|
dist = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dist < 0 ? 0 : dist;
|
return dist < 0 ? 0 : dist;
|
||||||
@@ -463,7 +559,11 @@ namespace OpenNest.Geometry
|
|||||||
/// Computes the minimum translation distance along an arbitrary unit direction
|
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||||
/// before any edge of movingLines contacts any edge of stationaryLines.
|
/// before any edge of movingLines contacts any edge of stationaryLines.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction)
|
public static double DirectionalDistance(
|
||||||
|
List<Line> movingLines,
|
||||||
|
List<Line> stationaryLines,
|
||||||
|
Vector direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
var dirX = direction.X;
|
var dirX = direction.X;
|
||||||
@@ -476,8 +576,18 @@ namespace OpenNest.Geometry
|
|||||||
for (var i = 0; i < stationaryLines.Count; i++)
|
for (var i = 0; i < stationaryLines.Count; i++)
|
||||||
{
|
{
|
||||||
var e = stationaryLines[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);
|
var d = RayEdgeDistance(
|
||||||
if (d < minDist) minDist = d;
|
mv.X,
|
||||||
|
mv.Y,
|
||||||
|
e.pt1.X,
|
||||||
|
e.pt1.Y,
|
||||||
|
e.pt2.X,
|
||||||
|
e.pt2.Y,
|
||||||
|
dirX,
|
||||||
|
dirY
|
||||||
|
);
|
||||||
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,8 +601,18 @@ namespace OpenNest.Geometry
|
|||||||
for (var i = 0; i < movingLines.Count; i++)
|
for (var i = 0; i < movingLines.Count; i++)
|
||||||
{
|
{
|
||||||
var e = movingLines[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);
|
var d = RayEdgeDistance(
|
||||||
if (d < minDist) minDist = d;
|
sv.X,
|
||||||
|
sv.Y,
|
||||||
|
e.pt1.X,
|
||||||
|
e.pt1.Y,
|
||||||
|
e.pt2.X,
|
||||||
|
e.pt2.Y,
|
||||||
|
oppX,
|
||||||
|
oppY
|
||||||
|
);
|
||||||
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,9 +625,16 @@ namespace OpenNest.Geometry
|
|||||||
/// stationaryEntities. Delegates to the Vector-based overload.
|
/// stationaryEntities. Delegates to the Vector-based overload.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(
|
public static double DirectionalDistance(
|
||||||
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
|
List<Entity> movingEntities,
|
||||||
|
List<Entity> stationaryEntities,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
|
return DirectionalDistance(
|
||||||
|
movingEntities,
|
||||||
|
stationaryEntities,
|
||||||
|
DirectionToOffset(direction, 1.0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -517,7 +644,10 @@ namespace OpenNest.Geometry
|
|||||||
/// without tessellation.
|
/// without tessellation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(
|
public static double DirectionalDistance(
|
||||||
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
|
List<Entity> movingEntities,
|
||||||
|
List<Entity> stationaryEntities,
|
||||||
|
Vector direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
var dirX = direction.X;
|
var dirX = direction.X;
|
||||||
@@ -536,7 +666,8 @@ namespace OpenNest.Geometry
|
|||||||
if (d < minDist)
|
if (d < minDist)
|
||||||
{
|
{
|
||||||
minDist = d;
|
minDist = d;
|
||||||
if (d <= 0) return 0;
|
if (d <= 0)
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +688,8 @@ namespace OpenNest.Geometry
|
|||||||
if (d < minDist)
|
if (d < minDist)
|
||||||
{
|
{
|
||||||
minDist = d;
|
minDist = d;
|
||||||
if (d <= 0) return 0;
|
if (d <= 0)
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,10 +698,24 @@ namespace OpenNest.Geometry
|
|||||||
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
|
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
|
||||||
// closest point on a small corner arc to a straight edge may lie between
|
// closest point on a small corner arc to a straight edge may lie between
|
||||||
// those samples. Use ClosestPointTo to find it and fire a ray from there.
|
// those samples. Use ClosestPointTo to find it and fire a ray from there.
|
||||||
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist);
|
minDist = ArcToLineClosestDistance(
|
||||||
if (minDist <= 0) return 0;
|
movingEntities,
|
||||||
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist);
|
stationaryEntities,
|
||||||
if (minDist <= 0) return 0;
|
dirX,
|
||||||
|
dirY,
|
||||||
|
minDist
|
||||||
|
);
|
||||||
|
if (minDist <= 0)
|
||||||
|
return 0;
|
||||||
|
minDist = ArcToLineClosestDistance(
|
||||||
|
stationaryEntities,
|
||||||
|
movingEntities,
|
||||||
|
oppX,
|
||||||
|
oppY,
|
||||||
|
minDist
|
||||||
|
);
|
||||||
|
if (minDist <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
// Phase 4: Curve-to-curve direct distance.
|
// Phase 4: Curve-to-curve direct distance.
|
||||||
// The vertex-to-entity approach misses the closest contact between two
|
// The vertex-to-entity approach misses the closest contact between two
|
||||||
@@ -605,20 +751,35 @@ namespace OpenNest.Geometry
|
|||||||
if (me is Arc mArc)
|
if (me is Arc mArc)
|
||||||
{
|
{
|
||||||
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||||
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
if (
|
||||||
|
!Angle.IsBetweenRad(
|
||||||
|
angle,
|
||||||
|
mArc.StartAngle,
|
||||||
|
mArc.EndAngle,
|
||||||
|
mArc.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (se is Arc sArc)
|
if (se is Arc sArc)
|
||||||
{
|
{
|
||||||
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||||
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
if (
|
||||||
|
!Angle.IsBetweenRad(
|
||||||
|
angle,
|
||||||
|
sArc.StartAngle,
|
||||||
|
sArc.EndAngle,
|
||||||
|
sArc.IsReversed
|
||||||
|
)
|
||||||
|
)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
minDist = d;
|
minDist = d;
|
||||||
if (d <= 0) return 0;
|
if (d <= 0)
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,8 +787,12 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static double ArcToLineClosestDistance(
|
private static double ArcToLineClosestDistance(
|
||||||
List<Entity> arcEntities, List<Entity> lineEntities,
|
List<Entity> arcEntities,
|
||||||
double dirX, double dirY, double minDist)
|
List<Entity> lineEntities,
|
||||||
|
double dirX,
|
||||||
|
double dirY,
|
||||||
|
double minDist
|
||||||
|
)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < arcEntities.Count; i++)
|
for (var i = 0; i < arcEntities.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -662,15 +827,30 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var theta = k == 0 ? theta1 : theta2;
|
var theta = k == 0 ? theta1 : theta2;
|
||||||
|
|
||||||
if (!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed))
|
if (
|
||||||
|
!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)
|
||||||
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var qx = cx + r * System.Math.Cos(theta);
|
var qx = cx + r * System.Math.Cos(theta);
|
||||||
var qy = cy + r * System.Math.Sin(theta);
|
var qy = cy + r * System.Math.Sin(theta);
|
||||||
|
|
||||||
var d = RayEdgeDistance(qx, qy, p1x, p1y, line.pt2.X, line.pt2.Y,
|
var d = RayEdgeDistance(
|
||||||
dirX, dirY);
|
qx,
|
||||||
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
|
qy,
|
||||||
|
p1x,
|
||||||
|
p1y,
|
||||||
|
line.pt2.X,
|
||||||
|
line.pt2.Y,
|
||||||
|
dirX,
|
||||||
|
dirY
|
||||||
|
);
|
||||||
|
if (d < minDist)
|
||||||
|
{
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,28 +858,54 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static double RayEntityDistance(
|
private static double RayEntityDistance(
|
||||||
double vx, double vy, Entity entity, double dirX, double dirY)
|
double vx,
|
||||||
|
double vy,
|
||||||
|
Entity entity,
|
||||||
|
double dirX,
|
||||||
|
double dirY
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (entity is Line line)
|
if (entity is Line line)
|
||||||
{
|
{
|
||||||
return RayEdgeDistance(vx, vy,
|
return RayEdgeDistance(
|
||||||
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
vx,
|
||||||
dirX, dirY);
|
vy,
|
||||||
|
line.pt1.X,
|
||||||
|
line.pt1.Y,
|
||||||
|
line.pt2.X,
|
||||||
|
line.pt2.Y,
|
||||||
|
dirX,
|
||||||
|
dirY
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity is Arc arc)
|
if (entity is Arc arc)
|
||||||
{
|
{
|
||||||
return RayArcDistance(vx, vy,
|
return RayArcDistance(
|
||||||
arc.Center.X, arc.Center.Y, arc.Radius,
|
vx,
|
||||||
arc.StartAngle, arc.EndAngle, arc.IsReversed,
|
vy,
|
||||||
dirX, dirY);
|
arc.Center.X,
|
||||||
|
arc.Center.Y,
|
||||||
|
arc.Radius,
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed,
|
||||||
|
dirX,
|
||||||
|
dirY
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity is Circle circle)
|
if (entity is Circle circle)
|
||||||
{
|
{
|
||||||
return RayCircleDistance(vx, vy,
|
return RayCircleDistance(
|
||||||
circle.Center.X, circle.Center.Y, circle.Radius,
|
vx,
|
||||||
dirX, dirY);
|
vy,
|
||||||
|
circle.Center.X,
|
||||||
|
circle.Center.Y,
|
||||||
|
circle.Radius,
|
||||||
|
dirX,
|
||||||
|
dirY
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
@@ -759,7 +965,10 @@ namespace OpenNest.Geometry
|
|||||||
return CollectVertices(ToEdgeArray(lines), offset);
|
return CollectVertices(ToEdgeArray(lines), offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
private static HashSet<Vector> CollectVertices(
|
||||||
|
(Vector start, Vector end)[] edges,
|
||||||
|
Vector offset
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var vertices = new HashSet<Vector>();
|
var vertices = new HashSet<Vector>();
|
||||||
for (var i = 0; i < edges.Length; i++)
|
for (var i = 0; i < edges.Length; i++)
|
||||||
@@ -778,26 +987,48 @@ namespace OpenNest.Geometry
|
|||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
|
private static void SortEdgesForPruning(
|
||||||
|
(Vector start, Vector end)[] edges,
|
||||||
|
PushDirection direction
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
System.Array.Sort(edges, (a, b) =>
|
System.Array.Sort(
|
||||||
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
|
edges,
|
||||||
|
(a, b) =>
|
||||||
|
System
|
||||||
|
.Math.Min(a.start.Y, a.end.Y)
|
||||||
|
.CompareTo(System.Math.Min(b.start.Y, b.end.Y))
|
||||||
|
);
|
||||||
else
|
else
|
||||||
System.Array.Sort(edges, (a, b) =>
|
System.Array.Sort(
|
||||||
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
|
edges,
|
||||||
|
(a, b) =>
|
||||||
|
System
|
||||||
|
.Math.Min(a.start.X, a.end.X)
|
||||||
|
.CompareTo(System.Math.Min(b.start.X, b.end.X))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
|
private static bool TryGetCurveParams(
|
||||||
|
Entity entity,
|
||||||
|
out double cx,
|
||||||
|
out double cy,
|
||||||
|
out double r
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (entity is Circle circle)
|
if (entity is Circle circle)
|
||||||
{
|
{
|
||||||
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
|
cx = circle.Center.X;
|
||||||
|
cy = circle.Center.Y;
|
||||||
|
r = circle.Radius;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (entity is Arc arc)
|
if (entity is Arc arc)
|
||||||
{
|
{
|
||||||
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
|
cx = arc.Center.X;
|
||||||
|
cy = arc.Center.Y;
|
||||||
|
r = arc.Radius;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
cx = cy = r = 0;
|
cx = cy = r = 0;
|
||||||
@@ -850,7 +1081,13 @@ namespace OpenNest.Geometry
|
|||||||
return new Box(lft, btm, rgt - lft, top - btm);
|
return new Box(lft, btm, rgt - lft, top - btm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool FindVerticalLimits(Vector pt, Box bounds, List<Box> boxes, out double top, out double btm)
|
private static bool FindVerticalLimits(
|
||||||
|
Vector pt,
|
||||||
|
Box bounds,
|
||||||
|
List<Box> boxes,
|
||||||
|
out double top,
|
||||||
|
out double btm
|
||||||
|
)
|
||||||
{
|
{
|
||||||
top = double.MaxValue;
|
top = double.MaxValue;
|
||||||
btm = double.MinValue;
|
btm = double.MinValue;
|
||||||
@@ -868,20 +1105,30 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
if (top == double.MaxValue)
|
if (top == double.MaxValue)
|
||||||
{
|
{
|
||||||
if (bounds.Top > pt.Y) top = bounds.Top;
|
if (bounds.Top > pt.Y)
|
||||||
else return false;
|
top = bounds.Top;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btm == double.MinValue)
|
if (btm == double.MinValue)
|
||||||
{
|
{
|
||||||
if (bounds.Bottom < pt.Y) btm = bounds.Bottom;
|
if (bounds.Bottom < pt.Y)
|
||||||
else return false;
|
btm = bounds.Bottom;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool FindHorizontalLimits(Vector pt, Box bounds, List<Box> boxes, out double lft, out double rgt)
|
private static bool FindHorizontalLimits(
|
||||||
|
Vector pt,
|
||||||
|
Box bounds,
|
||||||
|
List<Box> boxes,
|
||||||
|
out double lft,
|
||||||
|
out double rgt
|
||||||
|
)
|
||||||
{
|
{
|
||||||
lft = double.MinValue;
|
lft = double.MinValue;
|
||||||
rgt = double.MaxValue;
|
rgt = double.MaxValue;
|
||||||
@@ -899,14 +1146,18 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
if (rgt == double.MaxValue)
|
if (rgt == double.MaxValue)
|
||||||
{
|
{
|
||||||
if (bounds.Right > pt.X) rgt = bounds.Right;
|
if (bounds.Right > pt.X)
|
||||||
else return false;
|
rgt = bounds.Right;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lft == double.MinValue)
|
if (lft == double.MinValue)
|
||||||
{
|
{
|
||||||
if (bounds.Left < pt.X) lft = bounds.Left;
|
if (bounds.Left < pt.X)
|
||||||
else return false;
|
lft = bounds.Left;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -8,7 +8,11 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
private const int MinPointsForArc = 3;
|
private const int MinPointsForArc = 3;
|
||||||
|
|
||||||
public static List<Entity> Convert(List<Vector> points, bool isClosed, double tolerance = 0.001)
|
public static List<Entity> Convert(
|
||||||
|
List<Vector> points,
|
||||||
|
bool isClosed,
|
||||||
|
double tolerance = 0.001
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (points == null || points.Count < 2)
|
if (points == null || points.Count < 2)
|
||||||
return new List<Entity>();
|
return new List<Entity>();
|
||||||
@@ -37,8 +41,12 @@ namespace OpenNest.Geometry
|
|||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ArcFitResult TryFitArc(List<Vector> points, int start,
|
private static ArcFitResult TryFitArc(
|
||||||
Vector chainedTangent, double tolerance)
|
List<Vector> points,
|
||||||
|
int start,
|
||||||
|
Vector chainedTangent,
|
||||||
|
double tolerance
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var minEnd = start + MinPointsForArc - 1;
|
var minEnd = start + MinPointsForArc - 1;
|
||||||
if (minEnd >= points.Count)
|
if (minEnd >= points.Count)
|
||||||
@@ -83,7 +91,8 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static (Vector center, double radius, double deviation) FitCircumscribed(
|
private static (Vector center, double radius, double deviation) FitCircumscribed(
|
||||||
List<Vector> points)
|
List<Vector> points
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (points.Count < 3)
|
if (points.Count < 3)
|
||||||
return (Vector.Invalid, 0, double.MaxValue);
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
@@ -131,11 +140,16 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||||
List<Vector> points, Vector tangent) =>
|
List<Vector> points,
|
||||||
ArcFit.FitWithStartTangent(points, tangent);
|
Vector tangent
|
||||||
|
) => ArcFit.FitWithStartTangent(points, tangent);
|
||||||
|
|
||||||
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
|
private static double MaxRadialDeviation(
|
||||||
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
|
List<Vector> points,
|
||||||
|
double cx,
|
||||||
|
double cy,
|
||||||
|
double radius
|
||||||
|
) => ArcFit.MaxRadialDeviation(points, cx, cy, radius);
|
||||||
|
|
||||||
private static double SumSignedAngles(Vector center, List<Vector> points)
|
private static double SumSignedAngles(Vector center, List<Vector> points)
|
||||||
{
|
{
|
||||||
@@ -145,8 +159,10 @@ namespace OpenNest.Geometry
|
|||||||
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
||||||
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
||||||
var da = a2 - a1;
|
var da = a2 - a1;
|
||||||
while (da > System.Math.PI) da -= Angle.TwoPI;
|
while (da > System.Math.PI)
|
||||||
while (da < -System.Math.PI) da += Angle.TwoPI;
|
da -= Angle.TwoPI;
|
||||||
|
while (da < -System.Math.PI)
|
||||||
|
da += Angle.TwoPI;
|
||||||
total += da;
|
total += da;
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
@@ -160,9 +176,7 @@ namespace OpenNest.Geometry
|
|||||||
var rx = lastPt.X - center.X;
|
var rx = lastPt.X - center.X;
|
||||||
var ry = lastPt.Y - center.Y;
|
var ry = lastPt.Y - center.Y;
|
||||||
|
|
||||||
return totalAngle >= 0
|
return totalAngle >= 0 ? new Vector(-ry, rx) : new Vector(ry, -rx);
|
||||||
? new Vector(-ry, rx)
|
|
||||||
: new Vector(ry, -rx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Arc CreateArc(Vector center, double radius, List<Vector> points)
|
private static Arc CreateArc(Vector center, double radius, List<Vector> points)
|
||||||
@@ -174,8 +188,10 @@ namespace OpenNest.Geometry
|
|||||||
var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X);
|
var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X);
|
||||||
var isReversed = SumSignedAngles(center, points) < 0;
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
|
||||||
if (startAngle < 0) startAngle += Angle.TwoPI;
|
if (startAngle < 0)
|
||||||
if (endAngle < 0) endAngle += Angle.TwoPI;
|
startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0)
|
||||||
|
endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
return new Arc(center, radius, startAngle, endAngle, isReversed);
|
return new Arc(center, radius, startAngle, endAngle, isReversed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,541 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable triangulation of a simple, closed, lines-only perimeter and its holes.
|
||||||
|
/// Cached triangles use the reference Collision clipping and hole-subtraction rules.
|
||||||
|
/// Translation is a parameter; preparation never retains mutable input polygons.
|
||||||
|
/// Scratch arrays and hole-piece lists are allocated per query, with a bounded
|
||||||
|
/// thread-local buffer pool. Null means the caller must use Collision.HasOverlap.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TriangulatedRegion
|
||||||
|
{
|
||||||
|
// Flat vertex pool (local frame) and triangle index triples (CCW).
|
||||||
|
private readonly double[] X;
|
||||||
|
private readonly double[] Y;
|
||||||
|
|
||||||
|
private readonly int[] _ia;
|
||||||
|
private readonly int[] _ib;
|
||||||
|
private readonly int[] _ic;
|
||||||
|
private readonly double[] _tMinX;
|
||||||
|
private readonly double[] _tMinY;
|
||||||
|
private readonly double[] _tMaxX;
|
||||||
|
private readonly double[] _tMaxY;
|
||||||
|
|
||||||
|
private double MinX { get; }
|
||||||
|
private double MinY { get; }
|
||||||
|
private double MaxX { get; }
|
||||||
|
private double MaxY { get; }
|
||||||
|
|
||||||
|
/// <summary>Triangulated holes in the same local frame (null when none).</summary>
|
||||||
|
private readonly TriangulatedRegion?[]? Holes;
|
||||||
|
|
||||||
|
// Scratch bound: clipped convex pieces stay small; anything larger bails.
|
||||||
|
private const int MaxClipVertices = 48;
|
||||||
|
private const int MaxPieces = 2048;
|
||||||
|
|
||||||
|
private TriangulatedRegion(
|
||||||
|
double[] x,
|
||||||
|
double[] y,
|
||||||
|
int[] ia,
|
||||||
|
int[] ib,
|
||||||
|
int[] ic,
|
||||||
|
double[] tMinX,
|
||||||
|
double[] tMinY,
|
||||||
|
double[] tMaxX,
|
||||||
|
double[] tMaxY,
|
||||||
|
TriangulatedRegion?[]? holes
|
||||||
|
)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
_ia = ia;
|
||||||
|
_ib = ib;
|
||||||
|
_ic = ic;
|
||||||
|
_tMinX = tMinX;
|
||||||
|
_tMinY = tMinY;
|
||||||
|
_tMaxX = tMaxX;
|
||||||
|
_tMaxY = tMaxY;
|
||||||
|
Holes = holes;
|
||||||
|
|
||||||
|
var minX = double.MaxValue;
|
||||||
|
var minY = double.MaxValue;
|
||||||
|
var maxX = double.MinValue;
|
||||||
|
var maxY = double.MinValue;
|
||||||
|
for (var i = 0; i < x.Length; i++)
|
||||||
|
{
|
||||||
|
if (x[i] < minX)
|
||||||
|
minX = x[i];
|
||||||
|
if (x[i] > maxX)
|
||||||
|
maxX = x[i];
|
||||||
|
if (y[i] < minY)
|
||||||
|
minY = y[i];
|
||||||
|
if (y[i] > maxY)
|
||||||
|
maxY = y[i];
|
||||||
|
}
|
||||||
|
MinX = minX;
|
||||||
|
MinY = minY;
|
||||||
|
MaxX = maxX;
|
||||||
|
MaxY = maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ear-clips a polygon ring into cached triangles. Returns null when
|
||||||
|
/// triangulation yields nothing usable - the caller falls back to Polygon gates.
|
||||||
|
/// </summary>
|
||||||
|
public static TriangulatedRegion? Build(Polygon perimeter, IReadOnlyList<Polygon>? holes = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tris = ConvexDecomposition.Triangulate(perimeter);
|
||||||
|
var count = tris.Count;
|
||||||
|
if (count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var xs = new double[count * 3];
|
||||||
|
var ys = new double[count * 3];
|
||||||
|
var ia = new int[count];
|
||||||
|
var ib = new int[count];
|
||||||
|
var ic = new int[count];
|
||||||
|
var minXA = new double[count];
|
||||||
|
var minYA = new double[count];
|
||||||
|
var maxXA = new double[count];
|
||||||
|
var maxYA = new double[count];
|
||||||
|
|
||||||
|
var k = 0;
|
||||||
|
for (var t = 0; t < count; t++)
|
||||||
|
{
|
||||||
|
var v = tris[t].Vertices; // closed: prev, curr, next, prev
|
||||||
|
ia[t] = k;
|
||||||
|
xs[k] = v[0].X;
|
||||||
|
ys[k] = v[0].Y;
|
||||||
|
k++;
|
||||||
|
ib[t] = k;
|
||||||
|
xs[k] = v[1].X;
|
||||||
|
ys[k] = v[1].Y;
|
||||||
|
k++;
|
||||||
|
ic[t] = k;
|
||||||
|
xs[k] = v[2].X;
|
||||||
|
ys[k] = v[2].Y;
|
||||||
|
k++;
|
||||||
|
minXA[t] = System.Math.Min(v[0].X, System.Math.Min(v[1].X, v[2].X));
|
||||||
|
minYA[t] = System.Math.Min(v[0].Y, System.Math.Min(v[1].Y, v[2].Y));
|
||||||
|
maxXA[t] = System.Math.Max(v[0].X, System.Math.Max(v[1].X, v[2].X));
|
||||||
|
maxYA[t] = System.Math.Max(v[0].Y, System.Math.Max(v[1].Y, v[2].Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
TriangulatedRegion[]? holeSets = null;
|
||||||
|
if (holes != null && holes.Count > 0)
|
||||||
|
{
|
||||||
|
holeSets = new TriangulatedRegion[holes.Count];
|
||||||
|
for (var h = 0; h < holes.Count; h++)
|
||||||
|
{
|
||||||
|
var holeTris = ConvexDecomposition.Triangulate(holes[h]);
|
||||||
|
if (holeTris.Count == 0)
|
||||||
|
continue;
|
||||||
|
var hx = new double[holeTris.Count * 3];
|
||||||
|
var hy = new double[holeTris.Count * 3];
|
||||||
|
var hia = new int[holeTris.Count];
|
||||||
|
var hib = new int[holeTris.Count];
|
||||||
|
var hic = new int[holeTris.Count];
|
||||||
|
var hminX = new double[holeTris.Count];
|
||||||
|
var hminY = new double[holeTris.Count];
|
||||||
|
var hmaxX = new double[holeTris.Count];
|
||||||
|
var hmaxY = new double[holeTris.Count];
|
||||||
|
var hk = 0;
|
||||||
|
for (var t = 0; t < holeTris.Count; t++)
|
||||||
|
{
|
||||||
|
var v = holeTris[t].Vertices;
|
||||||
|
hia[t] = hk;
|
||||||
|
hx[hk] = v[0].X;
|
||||||
|
hy[hk] = v[0].Y;
|
||||||
|
hk++;
|
||||||
|
hib[t] = hk;
|
||||||
|
hx[hk] = v[1].X;
|
||||||
|
hy[hk] = v[1].Y;
|
||||||
|
hk++;
|
||||||
|
hic[t] = hk;
|
||||||
|
hx[hk] = v[2].X;
|
||||||
|
hy[hk] = v[2].Y;
|
||||||
|
hk++;
|
||||||
|
hminX[t] = System.Math.Min(v[0].X, System.Math.Min(v[1].X, v[2].X));
|
||||||
|
hminY[t] = System.Math.Min(v[0].Y, System.Math.Min(v[1].Y, v[2].Y));
|
||||||
|
hmaxX[t] = System.Math.Max(v[0].X, System.Math.Max(v[1].X, v[2].X));
|
||||||
|
hmaxY[t] = System.Math.Max(v[0].Y, System.Math.Max(v[1].Y, v[2].Y));
|
||||||
|
}
|
||||||
|
holeSets[h] = new TriangulatedRegion(hx, hy, hia, hib, hic, hminX, hminY, hmaxX, hmaxY, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TriangulatedRegion(xs, ys, ia, ib, ic, minXA, minYA, maxXA, maxYA, holeSets);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Positive shared area (surviving both polygons' hole sets) between this
|
||||||
|
/// translated by (adx, ady) and other translated by (bdx, bdy). Returns null
|
||||||
|
/// when the scratch bounds are exceeded and the question cannot be decided.
|
||||||
|
/// Inputs and translations must have finite coordinates.
|
||||||
|
/// </summary>
|
||||||
|
public bool? Overlaps(TriangulatedRegion other, double adx, double ady, double bdx, double bdy)
|
||||||
|
{
|
||||||
|
// Same bbox rule as Collision.BoundingBoxesOverlap: overlap must exceed
|
||||||
|
// Tolerance.Epsilon on both axes, so a hairline box overlap never reaches the
|
||||||
|
// clip stage.
|
||||||
|
var eps = OpenNest.Math.Tolerance.Epsilon;
|
||||||
|
var overlapX =
|
||||||
|
System.Math.Min(MaxX + adx, other.MaxX + bdx) - System.Math.Max(MinX + adx, other.MinX + bdx);
|
||||||
|
var overlapY =
|
||||||
|
System.Math.Min(MaxY + ady, other.MaxY + bdy) - System.Math.Max(MinY + ady, other.MinY + bdy);
|
||||||
|
if (overlapX <= eps || overlapY <= eps)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var areaFloor = 2 * OpenNest.Math.Tolerance.Epsilon;
|
||||||
|
var clipA = new double[MaxClipVertices * 2];
|
||||||
|
var clipB = new double[MaxClipVertices * 2];
|
||||||
|
var piece = new double[MaxClipVertices * 2];
|
||||||
|
|
||||||
|
for (var ta = 0; ta < _ia.Length; ta++)
|
||||||
|
{
|
||||||
|
var aMinX = _tMinX[ta] + adx;
|
||||||
|
var aMaxX = _tMaxX[ta] + adx;
|
||||||
|
var aMinY = _tMinY[ta] + ady;
|
||||||
|
var aMaxY = _tMaxY[ta] + ady;
|
||||||
|
for (var tb = 0; tb < other._ia.Length; tb++)
|
||||||
|
{
|
||||||
|
var bMinX = other._tMinX[tb] + bdx;
|
||||||
|
var bMaxX = other._tMaxX[tb] + bdx;
|
||||||
|
var bMinY = other._tMinY[tb] + bdy;
|
||||||
|
var bMaxY = other._tMaxY[tb] + bdy;
|
||||||
|
if (
|
||||||
|
System.Math.Min(aMaxX, bMaxX) - System.Math.Max(aMinX, bMinX) <= eps
|
||||||
|
|| System.Math.Min(aMaxY, bMaxY) - System.Math.Max(aMinY, bMinY) <= eps
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var count = ClipTriangle(
|
||||||
|
ta, adx, ady, other, tb, bdx, bdy, clipA, clipB, piece
|
||||||
|
);
|
||||||
|
if (count >= MaxClipVertices)
|
||||||
|
return null;
|
||||||
|
if (count < 3)
|
||||||
|
continue;
|
||||||
|
if (TwiceArea(piece, count) <= areaFloor)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (hasHoles, undecided, survived) = SubtractAllHoles(
|
||||||
|
other, adx, ady, bdx, bdy, piece, count, areaFloor
|
||||||
|
);
|
||||||
|
if (undecided)
|
||||||
|
return null;
|
||||||
|
if (hasHoles)
|
||||||
|
{
|
||||||
|
if (survived)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true; // no holes on either side: the clipped region is overlap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subtracts both polygons' hole triangles from one clipped region, mirroring
|
||||||
|
/// Collision.SubtractHoles: for every hole triangle, every surviving piece is
|
||||||
|
/// split per edge into outside pieces (survivors) and the inside remainder
|
||||||
|
/// (consumed). True means a positive-area piece survived ALL holes.
|
||||||
|
/// </summary>
|
||||||
|
[ThreadStatic]
|
||||||
|
private static List<double[]>? s_pool;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static double[]? s_tmpA;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static double[]? s_tmpB;
|
||||||
|
|
||||||
|
private static double[] AcquireBuffer()
|
||||||
|
{
|
||||||
|
var pool = s_pool ??= new List<double[]>();
|
||||||
|
var n = pool.Count;
|
||||||
|
if (n == 0)
|
||||||
|
return new double[MaxClipVertices * 2];
|
||||||
|
var buf = pool[n - 1];
|
||||||
|
pool.RemoveAt(n - 1);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReleaseBuffer(double[] buf)
|
||||||
|
{
|
||||||
|
var pool = s_pool ??= new List<double[]>();
|
||||||
|
if (pool.Count < 64)
|
||||||
|
pool.Add(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (double[] Tmp, double[] Inside) ScratchPair()
|
||||||
|
{
|
||||||
|
s_tmpA ??= new double[MaxClipVertices * 2];
|
||||||
|
s_tmpB ??= new double[MaxClipVertices * 2];
|
||||||
|
return (s_tmpA, s_tmpB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool hasHoles, bool undecided, bool survived) SubtractAllHoles(
|
||||||
|
TriangulatedRegion other,
|
||||||
|
double adx,
|
||||||
|
double ady,
|
||||||
|
double bdx,
|
||||||
|
double bdy,
|
||||||
|
double[] piece,
|
||||||
|
int count,
|
||||||
|
double areaFloor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var allHoles = 0;
|
||||||
|
if (Holes != null)
|
||||||
|
allHoles += Holes.Length;
|
||||||
|
if (other.Holes != null)
|
||||||
|
allHoles += other.Holes.Length;
|
||||||
|
if (allHoles == 0)
|
||||||
|
return (false, false, false);
|
||||||
|
|
||||||
|
// pieces[0] is the caller's own buffer - never release it back to the pool.
|
||||||
|
var pieces = new List<(double[] Buf, int Count)> { (piece, count) };
|
||||||
|
var owned = new HashSet<double[]>();
|
||||||
|
|
||||||
|
bool SubtractOwner(TriangulatedRegion owner, double odx, double ody)
|
||||||
|
{
|
||||||
|
if (owner.Holes == null)
|
||||||
|
return true;
|
||||||
|
for (var h = 0; h < owner.Holes.Length && pieces.Count > 0; h++)
|
||||||
|
{
|
||||||
|
var hole = owner.Holes[h];
|
||||||
|
if (hole == null)
|
||||||
|
continue; // untriangulatable hole: nothing to subtract
|
||||||
|
for (var t = 0; t < hole._ia.Length && pieces.Count > 0; t++)
|
||||||
|
{
|
||||||
|
var hMinX = hole._tMinX[t] + odx;
|
||||||
|
var hMaxX = hole._tMaxX[t] + odx;
|
||||||
|
var hMinY = hole._tMinY[t] + ody;
|
||||||
|
var hMaxY = hole._tMaxY[t] + ody;
|
||||||
|
|
||||||
|
var next = new List<(double[], int)>();
|
||||||
|
for (var p = 0; p < pieces.Count; p++)
|
||||||
|
{
|
||||||
|
var (buf, pc) = pieces[p];
|
||||||
|
|
||||||
|
// Piece bbox (built-in uses <=: touching skips subtraction).
|
||||||
|
var pMinX = double.MaxValue;
|
||||||
|
var pMinY = double.MaxValue;
|
||||||
|
var pMaxX = double.MinValue;
|
||||||
|
var pMaxY = double.MinValue;
|
||||||
|
for (var v = 0; v < pc; v++)
|
||||||
|
{
|
||||||
|
var px = buf[v * 2];
|
||||||
|
var py = buf[v * 2 + 1];
|
||||||
|
if (px < pMinX)
|
||||||
|
pMinX = px;
|
||||||
|
if (px > pMaxX)
|
||||||
|
pMaxX = px;
|
||||||
|
if (py < pMinY)
|
||||||
|
pMinY = py;
|
||||||
|
if (py > pMaxY)
|
||||||
|
pMaxY = py;
|
||||||
|
}
|
||||||
|
if (pMaxX <= hMinX || hMaxX <= pMinX || pMaxY <= hMinY || hMaxY <= pMinY)
|
||||||
|
{
|
||||||
|
if (next.Count >= MaxPieces)
|
||||||
|
return false;
|
||||||
|
next.Add((buf, pc));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip the piece against the hole triangle's three edges: the
|
||||||
|
// outside of each edge survives as its own piece; the inside
|
||||||
|
// remainder continues into the next edge. The remainder inside
|
||||||
|
// all three edges is consumed (the hole ate it).
|
||||||
|
var rem = AcquireBuffer();
|
||||||
|
owned.Add(rem);
|
||||||
|
Array.Copy(buf, rem, pc * 2);
|
||||||
|
var remCount = pc;
|
||||||
|
var (tmp, insideBuf) = ScratchPair();
|
||||||
|
for (var e = 0; e < 3 && remCount >= 3; e++)
|
||||||
|
{
|
||||||
|
var ei = e == 0 ? hole._ia[t] : e == 1 ? hole._ib[t] : hole._ic[t];
|
||||||
|
var ej = e == 0 ? hole._ib[t] : e == 1 ? hole._ic[t] : hole._ia[t];
|
||||||
|
var sx = hole.X[ei] + odx;
|
||||||
|
var sy = hole.Y[ei] + ody;
|
||||||
|
var ex = hole.X[ej] + odx;
|
||||||
|
var ey = hole.Y[ej] + ody;
|
||||||
|
|
||||||
|
var outCount =
|
||||||
|
ClipHalfSpace(rem, remCount, sx, sy, ex, ey, false, tmp);
|
||||||
|
if (outCount >= MaxClipVertices)
|
||||||
|
return false;
|
||||||
|
if (outCount >= 3 && TwiceArea(tmp, outCount) > areaFloor)
|
||||||
|
{
|
||||||
|
if (next.Count >= MaxPieces)
|
||||||
|
return false; // undecided
|
||||||
|
var keep = AcquireBuffer();
|
||||||
|
owned.Add(keep);
|
||||||
|
Array.Copy(tmp, keep, outCount * 2);
|
||||||
|
next.Add((keep, outCount));
|
||||||
|
}
|
||||||
|
remCount =
|
||||||
|
ClipHalfSpace(rem, remCount, sx, sy, ex, ey, true, insideBuf);
|
||||||
|
if (remCount >= MaxClipVertices)
|
||||||
|
return false; // undecided
|
||||||
|
Array.Copy(insideBuf, rem, remCount * 2);
|
||||||
|
}
|
||||||
|
// The inside-all-edges remainder is consumed by the hole: drop it.
|
||||||
|
owned.Remove(rem);
|
||||||
|
ReleaseBuffer(rem);
|
||||||
|
if (owned.Remove(buf))
|
||||||
|
ReleaseBuffer(buf);
|
||||||
|
}
|
||||||
|
pieces = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!SubtractOwner(this, adx, ady) || !SubtractOwner(other, bdx, bdy))
|
||||||
|
return (true, true, false);
|
||||||
|
|
||||||
|
foreach (var (buf, pc) in pieces)
|
||||||
|
if (pc >= 3 && TwiceArea(buf, pc) > areaFloor)
|
||||||
|
return (true, false, true);
|
||||||
|
return (true, false, false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var buffer in owned)
|
||||||
|
ReleaseBuffer(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Clip this' triangle against other's triangle; returns count into piece.</summary>
|
||||||
|
private int ClipTriangle(
|
||||||
|
int ta,
|
||||||
|
double adx,
|
||||||
|
double ady,
|
||||||
|
TriangulatedRegion other,
|
||||||
|
int tb,
|
||||||
|
double bdx,
|
||||||
|
double bdy,
|
||||||
|
double[] bufA,
|
||||||
|
double[] bufB,
|
||||||
|
double[] piece
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var ia = _ia[ta];
|
||||||
|
var ib = _ib[ta];
|
||||||
|
var ic = _ic[ta];
|
||||||
|
bufA[0] = X[ia] + adx;
|
||||||
|
bufA[1] = Y[ia] + ady;
|
||||||
|
bufA[2] = X[ib] + adx;
|
||||||
|
bufA[3] = Y[ib] + ady;
|
||||||
|
bufA[4] = X[ic] + adx;
|
||||||
|
bufA[5] = Y[ic] + ady;
|
||||||
|
var count = 3;
|
||||||
|
|
||||||
|
for (var e = 0; e < 3 && count >= 3; e++)
|
||||||
|
{
|
||||||
|
var ei = e == 0 ? other._ia[tb] : e == 1 ? other._ib[tb] : other._ic[tb];
|
||||||
|
var ej = e == 0 ? other._ib[tb] : e == 1 ? other._ic[tb] : other._ia[tb];
|
||||||
|
var sx = other.X[ei] + bdx;
|
||||||
|
var sy = other.Y[ei] + bdy;
|
||||||
|
var ex = other.X[ej] + bdx;
|
||||||
|
var ey = other.Y[ej] + bdy;
|
||||||
|
count = ClipHalfSpace(bufA, count, sx, sy, ex, ey, true, bufB);
|
||||||
|
if (count >= MaxClipVertices)
|
||||||
|
return count;
|
||||||
|
for (var v = 0; v < count * 2; v++)
|
||||||
|
bufA[v] = bufB[v];
|
||||||
|
}
|
||||||
|
for (var v = 0; v < System.Math.Min(count, MaxClipVertices) * 2; v++)
|
||||||
|
piece[v] = bufA[v];
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sutherland-Hodgman clip against one directed edge's half-plane; identical
|
||||||
|
/// classification, interpolation and dedupe to Collision.ClipHalfSpace.
|
||||||
|
/// </summary>
|
||||||
|
private static int ClipHalfSpace(
|
||||||
|
double[] verts,
|
||||||
|
int count,
|
||||||
|
double sx,
|
||||||
|
double sy,
|
||||||
|
double ex,
|
||||||
|
double ey,
|
||||||
|
bool inside,
|
||||||
|
double[] outBuf
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var kept = 0;
|
||||||
|
var cap = outBuf.Length / 2;
|
||||||
|
var edgeX = ex - sx;
|
||||||
|
var edgeY = ey - sy;
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var j = (i + 1) % count;
|
||||||
|
var cx = verts[i * 2];
|
||||||
|
var cy = verts[i * 2 + 1];
|
||||||
|
var nx = verts[j * 2];
|
||||||
|
var ny = verts[j * 2 + 1];
|
||||||
|
var cd = edgeX * (cy - sy) - edgeY * (cx - sx);
|
||||||
|
var nd = edgeX * (ny - sy) - edgeY * (nx - sx);
|
||||||
|
if (inside ? cd >= 0 : cd <= 0)
|
||||||
|
{
|
||||||
|
if (kept >= cap)
|
||||||
|
return cap; // overflow: caller treats as undecided
|
||||||
|
kept = AddDistinct(outBuf, kept, cx, cy);
|
||||||
|
}
|
||||||
|
if ((cd < 0 && nd > 0) || (cd > 0 && nd < 0))
|
||||||
|
{
|
||||||
|
if (kept >= cap)
|
||||||
|
return cap; // overflow
|
||||||
|
var t = cd / (cd - nd);
|
||||||
|
kept = AddDistinct(
|
||||||
|
outBuf, kept, cx + t * (nx - cx), cy + t * (ny - cy)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kept > 1 && outBuf[0] == outBuf[(kept - 1) * 2] && outBuf[1] == outBuf[(kept - 1) * 2 + 1])
|
||||||
|
kept--;
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AddDistinct(double[] buf, int count, double x, double y)
|
||||||
|
{
|
||||||
|
if (count > 0 && buf[(count - 1) * 2] == x && buf[(count - 1) * 2 + 1] == y)
|
||||||
|
return count;
|
||||||
|
buf[count * 2] = x;
|
||||||
|
buf[count * 2 + 1] = y;
|
||||||
|
return count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Twice the area, relative to vertex 0 (cancellation-safe).</summary>
|
||||||
|
private static double TwiceArea(double[] verts, int count)
|
||||||
|
{
|
||||||
|
var twiceArea = 0.0;
|
||||||
|
for (var i = 1; i + 1 < count; i++)
|
||||||
|
twiceArea +=
|
||||||
|
(verts[i * 2] - verts[0]) * (verts[(i + 1) * 2 + 1] - verts[1])
|
||||||
|
- (verts[i * 2 + 1] - verts[1]) * (verts[(i + 1) * 2] - verts[0]);
|
||||||
|
return System.Math.Abs(twiceArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
using System;
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -31,7 +31,7 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
unchecked
|
unchecked
|
||||||
{
|
{
|
||||||
// Use a simple but effective hash combine.
|
// Use a simple but effective hash combine.
|
||||||
// We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet
|
// We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet
|
||||||
// during a single operation, raw bits or slightly rounded is usually fine.
|
// during a single operation, raw bits or slightly rounded is usually fine.
|
||||||
// However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision.
|
// However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user