Compare commits
30
Commits
ad69023c17
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02fc0ea3db | ||
|
|
451876c02f | ||
|
|
5061b41a5d | ||
|
|
e7cbd99db6 | ||
|
|
27684c3782 | ||
|
|
54694f9b17 | ||
|
|
de1248589a | ||
|
|
a9ebd8bb55 | ||
|
|
aec0523062 | ||
|
|
8e6fa677fb | ||
|
|
589d341455 | ||
|
|
1a05391d94 | ||
|
|
f5d27652f4 | ||
|
|
ea4bd836cd | ||
|
|
9b69c67572 | ||
|
|
aa88eee484 | ||
|
|
e0e3b96bed | ||
|
|
424ff15ebc | ||
|
|
9888fe6083 | ||
|
|
a2dcfc7484 | ||
|
|
ae704478af | ||
|
|
ecca71e185 | ||
|
|
f0fe79f0f1 | ||
|
|
a68e252ac7 | ||
|
|
bc6bcae419 | ||
|
|
02141f6ca5 | ||
|
|
a9e0f8a1d4 | ||
|
|
20da5477b6 | ||
|
|
6a0fba0fec | ||
|
|
a085339ba9 |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"csharpier": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"commands": [
|
||||||
|
"csharpier"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# CSharpier formats only C# sources; project/config XML keeps its layout.
|
||||||
|
**/*.csproj
|
||||||
|
**/*.config
|
||||||
|
**/*.xml
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Unified code style for OpenNest.
|
||||||
|
# Canonical formatter: CSharpier (see .config/dotnet-tools.json).
|
||||||
|
# dotnet tool restore
|
||||||
|
# dotnet csharpier check . # verify before committing
|
||||||
|
# dotnet csharpier format . # fix
|
||||||
|
# These settings match CSharpier's conventions so IDE auto-formatting
|
||||||
|
# (VS / Rider / VS Code) stays consistent with the formatter.
|
||||||
|
|
||||||
|
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
|
||||||
|
# CSharpier wraps code at 100 columns; dotnet format cannot re-wrap, but
|
||||||
|
# IDEs surface 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,24 +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 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.
|
||||||
|
|
||||||
|
`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), `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).
|
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).
|
||||||
|
|
||||||
## 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`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
|
||||||
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
- **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.
|
||||||
@@ -35,9 +41,11 @@ 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 with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine.
|
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.
|
||||||
|
|
||||||
- **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/`.
|
- **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` retains legacy name-based helpers and is deliberately outside the runner path.
|
||||||
|
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. Existing desktop, CLI, and MCP callers remain on this compatibility path until separate migrations preserve their existing-plate, preview, and accept/cancel semantics.
|
||||||
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
|
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
|
||||||
- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
|
- **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.
|
||||||
- **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`.
|
||||||
@@ -45,7 +53,6 @@ Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the
|
|||||||
- **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.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.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.
|
||||||
@@ -59,9 +66,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.
|
||||||
@@ -69,6 +77,17 @@ 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 via each part's own world-space polygon, inflated by the spacing — works on arbitrary concave/holed shapes, not just bounding boxes), and no drawing over its requested quantity. An invalid, throwing, or timed-out run scores zero for that job.
|
||||||
|
- Scoring matches `Plate.Utilization()` (placed drawing area / full sheet area, `Plate.Area()`). If an engine placed every requested part, ties are broken by fewer plates used (`Report`'s ranking rule) — using fewer sheets to do the same job wastes less material.
|
||||||
|
- `--engines Name1,Name2` filters to specific registered engines (default: all); `--csv <path>` writes a flat per-job CSV alongside the console report.
|
||||||
|
|
||||||
### 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/`.
|
||||||
|
|
||||||
@@ -110,7 +129,7 @@ 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.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`.
|
||||||
- `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.
|
||||||
@@ -119,4 +138,5 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
|
|||||||
- `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` (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; }
|
||||||
|
|||||||
@@ -25,10 +25,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 +45,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,7 +63,9 @@ 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,
|
||||||
|
new NestResponseArchiveDto
|
||||||
{
|
{
|
||||||
SchemaVersion = CurrentSchemaVersion,
|
SchemaVersion = CurrentSchemaVersion,
|
||||||
SheetCount = SheetCount,
|
SheetCount = SheetCount,
|
||||||
@@ -70,10 +74,16 @@ public class NestResponse
|
|||||||
ElapsedTicks = Elapsed.Ticks,
|
ElapsedTicks = Elapsed.Ticks,
|
||||||
Status = Status,
|
Status = Status,
|
||||||
StopReason = StopReason,
|
StopReason = StopReason,
|
||||||
Fulfillment = Fulfillment is null ? [] : new List<NestPartFulfillment>(Fulfillment),
|
Fulfillment = Fulfillment is null
|
||||||
|
? []
|
||||||
|
: new List<NestPartFulfillment>(Fulfillment),
|
||||||
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
|
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
|
||||||
PlateStockMappings = PlateStockMappings is null ? [] : new List<NestPlateStockMapping>(PlateStockMappings)
|
PlateStockMappings = PlateStockMappings is null
|
||||||
}, JsonOptions);
|
? []
|
||||||
|
: new List<NestPlateStockMapping>(PlateStockMappings),
|
||||||
|
},
|
||||||
|
JsonOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nestEntry = zip.CreateEntry("nest.nest");
|
var nestEntry = zip.CreateEntry("nest.nest");
|
||||||
@@ -91,16 +101,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 +123,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 +161,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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+90
-32
@@ -16,10 +16,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 +35,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 +69,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,7 +87,8 @@ 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,
|
SheetCount = nest.Plates.Count,
|
||||||
Utilization = CalculateUtilization(nest),
|
Utilization = CalculateUtilization(nest),
|
||||||
@@ -79,27 +96,47 @@ public static class NestRunner
|
|||||||
Elapsed = sw.Elapsed,
|
Elapsed = sw.Elapsed,
|
||||||
Status = result.Status,
|
Status = result.Status,
|
||||||
StopReason = result.StopReason,
|
StopReason = result.StopReason,
|
||||||
Fulfillment = result.Fulfillment
|
Fulfillment = result
|
||||||
.Select(value => new NestPartFulfillment(value.PartId, value.Requested, value.Placed, value.Unplaced))
|
.Fulfillment.Select(value => new NestPartFulfillment(
|
||||||
|
value.PartId,
|
||||||
|
value.Requested,
|
||||||
|
value.Placed,
|
||||||
|
value.Unplaced
|
||||||
|
))
|
||||||
.ToArray(),
|
.ToArray(),
|
||||||
StockUsage = result.StockUsage
|
StockUsage = result
|
||||||
.Select(value => new NestStockUsage(value.StockId, value.Used, value.Remaining))
|
.StockUsage.Select(value => new NestStockUsage(
|
||||||
|
value.StockId,
|
||||||
|
value.Used,
|
||||||
|
value.Remaining
|
||||||
|
))
|
||||||
.ToArray(),
|
.ToArray(),
|
||||||
PlateStockMappings = result.Plates
|
PlateStockMappings = result
|
||||||
.Select(value => new NestPlateStockMapping(value.PlateIndex, value.StockId))
|
.Plates.Select(value => new NestPlateStockMapping(
|
||||||
|
value.PlateIndex,
|
||||||
|
value.StockId
|
||||||
|
))
|
||||||
.ToArray(),
|
.ToArray(),
|
||||||
Nest = nest,
|
Nest = nest,
|
||||||
Request = request
|
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 +154,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 +167,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 +199,31 @@ public static class NestRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolvePlacementStrategy(NestRequest request) => request.PlacementStrategy ?? request.Strategy switch
|
private static string ResolvePlacementStrategy(NestRequest request) =>
|
||||||
|
request.PlacementStrategy
|
||||||
|
?? request.Strategy switch
|
||||||
{
|
{
|
||||||
NestStrategy.Auto => "Default",
|
NestStrategy.Auto => "Default",
|
||||||
_ => throw new NotSupportedException($"Unknown legacy nesting strategy: {request.Strategy}.")
|
_ => 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,75 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
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; }
|
||||||
|
public List<DrawingRequest> Requests { get; init; }
|
||||||
|
|
||||||
|
public string Name => Path.GetFileNameWithoutExtension(SourceFile);
|
||||||
|
|
||||||
|
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
|
||||||
|
|
||||||
|
/// <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 = 0,
|
||||||
|
double minimumSalvageDimension = 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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, minimumSalvageDimension)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
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 = 0,
|
||||||
|
double minimumSalvageDimension = 0,
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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());
|
||||||
|
|
||||||
|
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 = 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 = salvageRate,
|
||||||
|
MinimumSalvageDimension = minimumSalvageDimension,
|
||||||
|
EstimatedNetArea = jobResult.Plates.Sum(p =>
|
||||||
|
StockLadderNestingEngine.EstimateNetArea(nestJob, p)
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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 (!Size.TryParse(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,162 @@
|
|||||||
|
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,
|
||||||
|
Requests = requests,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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,35 @@
|
|||||||
|
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 always scores zero utilization, per the
|
||||||
|
/// benchmark rules.
|
||||||
|
/// </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; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Benchmark
|
||||||
|
{
|
||||||
|
public class ValidationResult
|
||||||
|
{
|
||||||
|
public bool Valid => Violations.Count == 0;
|
||||||
|
public List<string> Violations { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a (possibly multi-plate) placed layout against the benchmark
|
||||||
|
/// rules: on every plate, every part must lie within that plate's work
|
||||||
|
/// area and every pair of parts must be at least PartSpacing apart; across
|
||||||
|
/// all plates combined, no drawing may have more parts placed than
|
||||||
|
/// requested (the quantity limit is a property of the whole order, not of
|
||||||
|
/// any one plate). Geometry checks work on arbitrary (concave, holed)
|
||||||
|
/// polygons by reusing the same world-space extraction Part.Intersects
|
||||||
|
/// uses internally, so no engine gets an advantage or penalty from shape
|
||||||
|
/// complexity.
|
||||||
|
/// </summary>
|
||||||
|
public static class NestValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// requirements maps each materialized part's BaseDrawing (by reference - materialized
|
||||||
|
/// Drawing instances are freshly reconstructed per NestResultMaterializer.Materialize, so
|
||||||
|
/// identity must never be inferred from Name, which is only incidentally seeded from the
|
||||||
|
/// originating NestJobPart id) to its original quantity limit and display name.
|
||||||
|
/// </summary>
|
||||||
|
public static ValidationResult Validate(
|
||||||
|
List<(Plate Plate, List<Part> Parts)> plateRuns,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var result = new ValidationResult();
|
||||||
|
var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
|
||||||
|
|
||||||
|
if (allParts.Count == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
ValidateQuantities(allParts, requirements, result);
|
||||||
|
|
||||||
|
foreach (var (plate, parts) in plateRuns)
|
||||||
|
{
|
||||||
|
if (parts.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ValidateBounds(parts, plate, requirements, result);
|
||||||
|
ValidateAreaBudget(parts, plate, result);
|
||||||
|
ValidateSpacing(parts, plate.PartSpacing, requirements, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateQuantities(
|
||||||
|
List<Part> parts,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
|
||||||
|
ValidationResult result
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var placedCounts = parts
|
||||||
|
.GroupBy<Part, Drawing>(p => p.BaseDrawing, ReferenceEqualityComparer.Instance)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
foreach (var (drawing, placed) in placedCounts)
|
||||||
|
{
|
||||||
|
if (!requirements.TryGetValue(drawing, out var requirement))
|
||||||
|
{
|
||||||
|
result.Violations.Add(
|
||||||
|
$"Placed drawing '{drawing.Name}' which was not requested for this job"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placed > requirement.Quantity)
|
||||||
|
{
|
||||||
|
result.Violations.Add(
|
||||||
|
$"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateBounds(
|
||||||
|
List<Part> parts,
|
||||||
|
Plate plate,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
|
||||||
|
ValidationResult result
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var workArea = plate.WorkArea();
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
var bb = part.BoundingBox;
|
||||||
|
|
||||||
|
var outLeft = bb.Left < workArea.X - Tolerance.Epsilon;
|
||||||
|
var outBottom = bb.Bottom < workArea.Y - Tolerance.Epsilon;
|
||||||
|
var outRight = bb.Right > workArea.Right + Tolerance.Epsilon;
|
||||||
|
var outTop = bb.Top > workArea.Top + Tolerance.Epsilon;
|
||||||
|
|
||||||
|
if (outLeft || outBottom || outRight || outTop)
|
||||||
|
{
|
||||||
|
result.Violations.Add(
|
||||||
|
$"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area "
|
||||||
|
+ $"of a {plate.Size} plate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hard mathematical backstop: non-overlapping parts confined to the
|
||||||
|
/// work area can never have a combined area greater than the work
|
||||||
|
/// area itself. This catches overlap that the polygon-based
|
||||||
|
/// ValidateSpacing check can miss - Collision.HasOverlap (and
|
||||||
|
/// Part.Intersects, which uses the same algorithm) has been observed
|
||||||
|
/// to return false negatives on real, complex production geometry, so
|
||||||
|
/// this check does not depend on it.
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateAreaBudget(
|
||||||
|
List<Part> parts,
|
||||||
|
Plate plate,
|
||||||
|
ValidationResult result
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var workArea = plate.WorkArea();
|
||||||
|
var budget = workArea.Width * workArea.Length;
|
||||||
|
var placedArea = parts.Sum(p => p.BaseDrawing.Area);
|
||||||
|
|
||||||
|
if (placedArea > budget + Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
result.Violations.Add(
|
||||||
|
$"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - "
|
||||||
|
+ "parts must overlap even though the polygon overlap check did not flag a pair"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateSpacing(
|
||||||
|
List<Part> parts,
|
||||||
|
double spacing,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
|
||||||
|
ValidationResult result
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var worldPolygons = new Polygon[parts.Count];
|
||||||
|
var inflatedPolygons = new Polygon[parts.Count];
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
worldPolygons[i] = WorldPolygon(parts[i], 0);
|
||||||
|
inflatedPolygons[i] =
|
||||||
|
spacing > Tolerance.Epsilon
|
||||||
|
? WorldPolygon(parts[i], spacing)
|
||||||
|
: worldPolygons[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
if (worldPolygons[i] == null || inflatedPolygons[i] == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
if (worldPolygons[j] == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j]))
|
||||||
|
{
|
||||||
|
result.Violations.Add(
|
||||||
|
$"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Friendly name for a violation message, falling back to the materialized
|
||||||
|
/// Drawing's own Name (the raw partId string) if this part wasn't in requirements at all -
|
||||||
|
/// that mismatch is already reported by ValidateQuantities, so this is display-only.</summary>
|
||||||
|
private static string DisplayName(
|
||||||
|
Part part,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
|
||||||
|
) =>
|
||||||
|
requirements.TryGetValue(part.BaseDrawing, out var requirement)
|
||||||
|
? requirement.Name
|
||||||
|
: part.BaseDrawing.Name;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts a part's perimeter as a world-space polygon, optionally inflated
|
||||||
|
/// outward by the given spacing, mirroring Part.Intersects' own geometry
|
||||||
|
/// extraction (part.Program is already rotated; only a Location offset is needed).
|
||||||
|
/// </summary>
|
||||||
|
private static Polygon WorldPolygon(Part part, double inflateBy)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram
|
||||||
|
.ToGeometry(part.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (entities.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var perimeter = new ShapeProfile(entities).Perimeter;
|
||||||
|
|
||||||
|
if (perimeter == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (inflateBy > Tolerance.Epsilon)
|
||||||
|
perimeter = perimeter.OffsetOutward(inflateBy) ?? perimeter;
|
||||||
|
|
||||||
|
// Adaptive tolerance instead of Shape.ToPolygon()'s default (up to 1000
|
||||||
|
// segments per arc) - arc-heavy real parts otherwise produce thousands
|
||||||
|
// of vertices, which is needlessly slow for a spacing check.
|
||||||
|
var polygon = perimeter.ToPolygonWithTolerance(0.01, circumscribe: true);
|
||||||
|
|
||||||
|
if (polygon == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,304 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest;
|
||||||
|
using OpenNest.Benchmark;
|
||||||
|
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}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
|
||||||
|
|
||||||
|
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]);
|
||||||
|
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 (Size.TryParse(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. Scoring: aggregate material utilization across every"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"plate used, then (if everything requested was placed) fewer plates as the"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"thrown exception, or a run exceeding its time budget all score zero."
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
" --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 0)"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --min-salvage-dimension <value> Both offcut dimensions must qualify; 0 disables credit"
|
||||||
|
);
|
||||||
|
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,183 @@
|
|||||||
|
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; higher aggregate utilization wins; if utilization
|
||||||
|
/// ties and both engines fully placed every requested part, fewer plates
|
||||||
|
/// used wins (the multi-plate analogue of "smaller remnant" - both are
|
||||||
|
/// proxies for wasting less material). Ties beyond that are a shared win.
|
||||||
|
/// </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%", -8} {"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 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, -8} {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),
|
||||||
|
TotalUtilization = g.Sum(r => r.Utilization),
|
||||||
|
TotalPlates = g.Sum(r => r.PlatesUsed),
|
||||||
|
TotalTimeMs = g.Sum(r => r.ElapsedMs),
|
||||||
|
})
|
||||||
|
.OrderByDescending(e => e.TotalUtilization)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var wins = CountWins(results);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"{"Engine", -16} {"Jobs", -6} {"Valid", -7} {"Complete", -9} {"Wins", -6} {"AvgUtil%", -10} {"Plates", -8} {"TotalTime(ms)", -14}"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var e in byEngine)
|
||||||
|
{
|
||||||
|
var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 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} {winCount, -6} {avgUtil, -10: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,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.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, then higher
|
||||||
|
/// aggregate utilization, then (if both fully placed) fewer plates used.</summary>
|
||||||
|
private static int Compare(JobResult a, JobResult b)
|
||||||
|
{
|
||||||
|
if (a.Valid != b.Valid)
|
||||||
|
return a.Valid ? -1 : 1;
|
||||||
|
|
||||||
|
if (!a.Valid)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var utilDiff = b.Utilization - a.Utilization;
|
||||||
|
|
||||||
|
if (System.Math.Abs(utilDiff) > Epsilon)
|
||||||
|
return utilDiff > 0 ? 1 : -1;
|
||||||
|
|
||||||
|
if (a.FullyPlaced && b.FullyPlaced && 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>
|
||||||
|
|||||||
+178
-47
@@ -1,13 +1,15 @@
|
|||||||
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;
|
||||||
|
|
||||||
return NestConsole.Run(args);
|
return NestConsole.Run(args);
|
||||||
|
|
||||||
@@ -20,6 +22,22 @@ 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);
|
||||||
@@ -82,6 +100,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;
|
||||||
@@ -149,10 +187,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 +209,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 +238,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 +250,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 +262,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 +319,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 +328,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 =
|
||||||
|
options.DrawingName != null
|
||||||
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
|
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
|
||||||
: nest.Drawings.FirstOrDefault();
|
: nest.Drawings.FirstOrDefault();
|
||||||
|
|
||||||
if (drawing != null)
|
if (drawing != null)
|
||||||
return drawing;
|
return drawing;
|
||||||
|
|
||||||
Console.Error.WriteLine(options.DrawingName != null
|
Console.Error.WriteLine(
|
||||||
|
options.DrawingName != null
|
||||||
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
||||||
: "Error: nest file contains no drawings");
|
: "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(
|
||||||
|
options.KeepParts
|
||||||
? $"Keeping {existingCount} existing parts"
|
? $"Keeping {existingCount} existing parts"
|
||||||
: $"Cleared {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,7 +392,9 @@ 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);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var nestParts = engine.Nest(nestItems, null, CancellationToken.None);
|
var nestParts = engine.Nest(nestItems, null, CancellationToken.None);
|
||||||
@@ -334,9 +418,11 @@ static class NestConsole
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
||||||
Console.WriteLine(hasOverlaps
|
Console.WriteLine(
|
||||||
|
hasOverlaps
|
||||||
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
||||||
: "Overlap check: PASS");
|
: "Overlap check: PASS"
|
||||||
|
);
|
||||||
|
|
||||||
return overlapPts.Count;
|
return overlapPts.Count;
|
||||||
}
|
}
|
||||||
@@ -355,9 +441,12 @@ static class NestConsole
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var firstInput = options.InputFiles[0];
|
var firstInput = options.InputFiles[0];
|
||||||
var outputFile = options.OutputFile ?? Path.Combine(
|
var outputFile =
|
||||||
|
options.OutputFile
|
||||||
|
?? Path.Combine(
|
||||||
Path.GetDirectoryName(firstInput),
|
Path.GetDirectoryName(firstInput),
|
||||||
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}");
|
$"{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 +457,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 +477,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 +490,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 +513,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 +524,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 +548,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 +561,58 @@ 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 Use mixed-part autonesting (engine Nest) instead of linear fill"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --keep-parts Don't clear existing parts before filling"
|
||||||
|
);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
" --check-overlaps Run overlap detection after fill (exit code 1 if found)"
|
||||||
|
);
|
||||||
Console.Error.WriteLine(" --no-save Skip saving output file");
|
Console.Error.WriteLine(" --no-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");
|
||||||
}
|
}
|
||||||
@@ -506,5 +635,7 @@ static class NestConsole
|
|||||||
public string PostOutput;
|
public string PostOutput;
|
||||||
public string PostsDir;
|
public string PostsDir;
|
||||||
public bool ListPosts;
|
public bool ListPosts;
|
||||||
|
public double? RepairBendsMillimeters;
|
||||||
|
public BendRepairUnits CadUnits;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-11
@@ -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, p2) =>
|
||||||
|
horizontal
|
||||||
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
|
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
|
||||||
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y));
|
: 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,
|
Id = key,
|
||||||
Program = program.SubPrograms[key],
|
Program = program.SubPrograms[key],
|
||||||
Offset = holeCenter
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -264,7 +275,8 @@ namespace OpenNest.CNC
|
|||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
var motion = code as Motion;
|
var motion = code as Motion;
|
||||||
|
|
||||||
if (motion == null) continue;
|
if (motion == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
return motion.EndPoint;
|
return motion.EndPoint;
|
||||||
}
|
}
|
||||||
@@ -280,7 +292,8 @@ namespace OpenNest.CNC
|
|||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
var motion = code as Motion;
|
var motion = code as Motion;
|
||||||
|
|
||||||
if (motion == null) continue;
|
if (motion == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
pos += motion.EndPoint;
|
pos += motion.EndPoint;
|
||||||
}
|
}
|
||||||
@@ -318,9 +331,10 @@ namespace OpenNest.CNC
|
|||||||
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;
|
? frameOrigin + line.EndPoint
|
||||||
|
: line.EndPoint + pos;
|
||||||
|
|
||||||
if (pt.X > maxX)
|
if (pt.X > maxX)
|
||||||
maxX = pt.X;
|
maxX = pt.X;
|
||||||
@@ -340,7 +354,8 @@ namespace OpenNest.CNC
|
|||||||
case CodeType.RapidMove:
|
case CodeType.RapidMove:
|
||||||
{
|
{
|
||||||
var line = (RapidMove)code;
|
var line = (RapidMove)code;
|
||||||
var pt = Mode == Mode.Absolute
|
var pt =
|
||||||
|
Mode == Mode.Absolute
|
||||||
? frameOrigin + line.EndPoint
|
? frameOrigin + line.EndPoint
|
||||||
: line.EndPoint + pos;
|
: line.EndPoint + pos;
|
||||||
|
|
||||||
@@ -479,11 +494,7 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
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,7 +66,8 @@ namespace OpenNest.CNC
|
|||||||
}
|
}
|
||||||
else if (code is Motion motion)
|
else if (code is Motion motion)
|
||||||
{
|
{
|
||||||
var endpt = pgm.Mode == Mode.Incremental
|
var endpt =
|
||||||
|
pgm.Mode == Mode.Incremental
|
||||||
? motion.EndPoint + pos
|
? motion.EndPoint + pos
|
||||||
: motion.EndPoint + basePos;
|
: motion.EndPoint + basePos;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,8 @@ 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
|
||||||
|
.ToGeometry(drawing.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||||
|
|
||||||
var shapes = ShapeBuilder.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|||||||
@@ -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,24 @@ 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 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.
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
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;
|
||||||
@@ -112,9 +131,28 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-14
@@ -1,14 +1,14 @@
|
|||||||
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
|
||||||
@@ -26,7 +26,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 +44,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 +78,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 +124,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 +141,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 +156,24 @@ 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 target = OffsetOutward(perimeter, clearance) ?? perimeter;
|
||||||
var usedOffset = target != perimeter;
|
var usedOffset = target != perimeter;
|
||||||
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
var cutLine = new Line(
|
||||||
|
MakePoint(cutPosition, lineStart),
|
||||||
|
MakePoint(cutPosition, lineEnd)
|
||||||
|
);
|
||||||
|
|
||||||
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
if (!target.Intersects(cutLine, out var pts) || 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();
|
||||||
|
|
||||||
@@ -184,7 +220,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,
|
Feedrate = 100,
|
||||||
RapidTravelRate = 300,
|
RapidTravelRate = 300,
|
||||||
PierceTime = TimeSpan.FromSeconds(0.5),
|
PierceTime = TimeSpan.FromSeconds(0.5),
|
||||||
Units = OpenNest.Units.Inches
|
Units = OpenNest.Units.Inches,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-12
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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 => entity.Layer != SpecialLayers.Rapid);
|
||||||
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,11 +246,10 @@ 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 =
|
||||||
|
circumscribe && segments > 0
|
||||||
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
|
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
|
||||||
: Radius;
|
: Radius;
|
||||||
|
|
||||||
@@ -254,9 +257,12 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var angle = stepAngle * i + StartAngle;
|
var angle = stepAngle * i + StartAngle;
|
||||||
|
|
||||||
points.Add(new Vector(
|
points.Add(
|
||||||
|
new Vector(
|
||||||
System.Math.Cos(angle) * r + Center.X,
|
System.Math.Cos(angle) * r + Center.X,
|
||||||
System.Math.Sin(angle) * r + Center.Y));
|
System.Math.Sin(angle) * r + Center.Y
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
@@ -470,7 +476,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 +507,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(
|
||||||
|
new Vector(
|
||||||
System.Math.Cos(angle) * r + Center.X,
|
System.Math.Cos(angle) * r + Center.X,
|
||||||
System.Math.Sin(angle) * r + Center.Y));
|
System.Math.Sin(angle) * r + Center.Y
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
@@ -278,11 +275,9 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
if (side == OffsetSide.Left && Rotation == RotationType.CCW)
|
if (side == OffsetSide.Left && Rotation == RotationType.CCW)
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -294,11 +289,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 +310,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 +344,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>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
using OpenNest.Math;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
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 +50,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 +65,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 +88,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 +107,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;
|
||||||
}
|
}
|
||||||
@@ -164,13 +171,19 @@ namespace OpenNest.Geometry
|
|||||||
var output = new List<Vector>(subject.Vertices);
|
var output = new List<Vector>(subject.Vertices);
|
||||||
|
|
||||||
// Remove closing vertex if present
|
// Remove closing vertex if present
|
||||||
if (output.Count > 1 && output[0].X == output[output.Count - 1].X
|
if (
|
||||||
&& output[0].Y == output[output.Count - 1].Y)
|
output.Count > 1
|
||||||
|
&& output[0].X == output[output.Count - 1].X
|
||||||
|
&& output[0].Y == output[output.Count - 1].Y
|
||||||
|
)
|
||||||
output.RemoveAt(output.Count - 1);
|
output.RemoveAt(output.Count - 1);
|
||||||
|
|
||||||
var clipVerts = new List<Vector>(clip.Vertices);
|
var clipVerts = new List<Vector>(clip.Vertices);
|
||||||
if (clipVerts.Count > 1 && clipVerts[0].X == clipVerts[clipVerts.Count - 1].X
|
if (
|
||||||
&& clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y)
|
clipVerts.Count > 1
|
||||||
|
&& clipVerts[0].X == clipVerts[clipVerts.Count - 1].X
|
||||||
|
&& clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y
|
||||||
|
)
|
||||||
clipVerts.RemoveAt(clipVerts.Count - 1);
|
clipVerts.RemoveAt(clipVerts.Count - 1);
|
||||||
|
|
||||||
for (var i = 0; i < clipVerts.Count; i++)
|
for (var i = 0; i < clipVerts.Count; i++)
|
||||||
@@ -255,12 +268,17 @@ namespace OpenNest.Geometry
|
|||||||
/// <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 +304,10 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subtracts hole triangles from a region. Conservative: partial overlaps
|
/// Subtracts hole triangles from a region. Exact: a piece outside a convex hole
|
||||||
/// keep the full piece triangle (acceptable for visual shading).
|
/// triangle equals the union of its clips against each triangle edge's outside
|
||||||
|
/// half-space, so overlap confined to a cutout disappears while any material
|
||||||
|
/// sliver outside the hole survives.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
|
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
|
||||||
{
|
{
|
||||||
@@ -295,29 +315,32 @@ 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);
|
if (!BoundingBoxesOverlap(piece.BoundingBox, holeTri.BoundingBox))
|
||||||
|
{
|
||||||
|
next.Add(piece);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var pieceTri in pieceTris)
|
foreach (var pieceTri in TriangulateWithBounds(piece))
|
||||||
{
|
{
|
||||||
var inside = ClipConvex(pieceTri, holeTri);
|
var holeVerts = holeTri.Vertices;
|
||||||
if (inside == null)
|
var holeCount = holeTri.IsClosed() ? holeVerts.Count - 1 : holeVerts.Count;
|
||||||
{
|
var survived = false;
|
||||||
// No overlap with hole - keep
|
for (var i = 0; i < holeCount; i++)
|
||||||
next.Add(pieceTri);
|
survived |= AddIfPositiveArea(
|
||||||
}
|
next,
|
||||||
else if (inside.Area() < pieceTri.Area() - Tolerance.Epsilon)
|
ClipOutsideHalfSpace(
|
||||||
{
|
pieceTri,
|
||||||
// Partial overlap - keep the piece (conservative)
|
holeVerts[i],
|
||||||
next.Add(pieceTri);
|
holeVerts[(i + 1) % holeCount]
|
||||||
}
|
)
|
||||||
// else: fully inside hole - discard
|
);
|
||||||
|
if (!survived)
|
||||||
|
continue; // piece lies entirely within the hole
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,5 +349,49 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sutherland-Hodgman clip of a convex polygon to the strict outside of the
|
||||||
|
/// infinite line edgeStart->edgeEnd of a CCW hole edge (Cross < -Epsilon).
|
||||||
|
/// </summary>
|
||||||
|
private static List<Vector> ClipOutsideHalfSpace(
|
||||||
|
Polygon piece,
|
||||||
|
Vector edgeStart,
|
||||||
|
Vector edgeEnd
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var verts = piece.Vertices;
|
||||||
|
var count = piece.IsClosed() ? verts.Count - 1 : verts.Count;
|
||||||
|
var kept = new List<Vector>();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var current = verts[i];
|
||||||
|
var next = verts[(i + 1) % count];
|
||||||
|
var currentInside = Cross(edgeStart, edgeEnd, current) >= -Tolerance.Epsilon;
|
||||||
|
var nextInside = Cross(edgeStart, edgeEnd, next) >= -Tolerance.Epsilon;
|
||||||
|
if (!currentInside)
|
||||||
|
kept.Add(current);
|
||||||
|
if (currentInside == nextInside)
|
||||||
|
continue;
|
||||||
|
var intersection = LineIntersection(edgeStart, edgeEnd, current, next);
|
||||||
|
if (intersection.IsValid())
|
||||||
|
kept.Add(intersection);
|
||||||
|
}
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AddIfPositiveArea(List<Polygon> polygons, List<Vector> vertices)
|
||||||
|
{
|
||||||
|
if (vertices.Count < 3)
|
||||||
|
return false;
|
||||||
|
var polygon = new Polygon();
|
||||||
|
polygon.Vertices.AddRange(vertices);
|
||||||
|
polygon.Close();
|
||||||
|
polygon.UpdateBounds();
|
||||||
|
if (polygon.Area() <= Tolerance.Epsilon)
|
||||||
|
return false;
|
||||||
|
polygons.Add(polygon);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,19 +200,32 @@ 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,
|
||||||
|
lines.Count,
|
||||||
|
index =>
|
||||||
{
|
{
|
||||||
var compareLine = lines[index];
|
var compareLine = lines[index];
|
||||||
|
|
||||||
@@ -203,7 +239,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
collinearLines.Add(compareLine);
|
collinearLines.Add(compareLine);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return collinearLines;
|
return collinearLines;
|
||||||
}
|
}
|
||||||
@@ -212,7 +249,10 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var coradialArcs = new List<Arc>();
|
var coradialArcs = new List<Arc>();
|
||||||
|
|
||||||
Parallel.For(startIndex, arcs.Count, index =>
|
Parallel.For(
|
||||||
|
startIndex,
|
||||||
|
arcs.Count,
|
||||||
|
index =>
|
||||||
{
|
{
|
||||||
var compareArc = arcs[index];
|
var compareArc = arcs[index];
|
||||||
|
|
||||||
@@ -226,7 +266,8 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
coradialArcs.Add(compareArc);
|
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,7 +367,8 @@ 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,
|
StartIndex = j,
|
||||||
EndIndex = result.EndIndex,
|
EndIndex = result.EndIndex,
|
||||||
@@ -358,37 +377,53 @@ public class GeometrySimplifier
|
|||||||
BoundingBox = result.Points.GetBoundingBox(),
|
BoundingBox = result.Points.GetBoundingBox(),
|
||||||
FirstPoint = arc.StartPoint(),
|
FirstPoint = arc.StartPoint(),
|
||||||
LastPoint = arc.EndPoint(),
|
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 (dc.IsValid() && dd <= Tolerance)
|
|
||||||
{
|
|
||||||
var isRev = SumSignedAngles(dc, points) < 0;
|
|
||||||
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev);
|
|
||||||
if (aDev <= Tolerance)
|
|
||||||
return (dc, dr, System.Math.Max(dd, aDev));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to start-tangent-only, then mirror axis
|
|
||||||
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent);
|
|
||||||
if (!center.IsValid() || dev > Tolerance)
|
if (!center.IsValid() || dev > Tolerance)
|
||||||
(center, radius, dev) = FitMirrorAxis(points);
|
continue;
|
||||||
if (!center.IsValid() || dev > Tolerance)
|
|
||||||
return (Vector.Invalid, 0, 0);
|
|
||||||
|
|
||||||
// Check that the arc doesn't bulge away from the original line segments
|
// Check that the arc doesn't bulge away from the original line segments
|
||||||
var isReversed = SumSignedAngles(center, points) < 0;
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
|
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
|
||||||
if (arcDev > Tolerance)
|
if (arcDev > Tolerance)
|
||||||
return (Vector.Invalid, 0, 0);
|
continue;
|
||||||
|
|
||||||
return (center, radius, System.Math.Max(dev, arcDev));
|
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
|
||||||
|
{
|
||||||
|
Line l => l.EndPoint,
|
||||||
|
Arc a => a.EndPoint(),
|
||||||
|
_ => Vector.Invalid,
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chord = new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
||||||
|
if (points.Count >= 3)
|
||||||
|
return new TangentEstimate(
|
||||||
|
EstimateVertexTangent(points[0], points[1], points[2], chord),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return new TangentEstimate(chord, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,9 +803,13 @@ 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),
|
Line line => new Vector(
|
||||||
|
line.EndPoint.X - line.StartPoint.X,
|
||||||
|
line.EndPoint.Y - line.StartPoint.Y
|
||||||
|
),
|
||||||
Arc arc => arc.IsReversed
|
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))
|
||||||
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
|
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
|
||||||
@@ -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 =>
|
||||||
|
Angle.IsBetweenRad(
|
||||||
arc.Center.AngleTo(pt),
|
arc.Center.AngleTo(pt),
|
||||||
arc.StartAngle,
|
arc.StartAngle,
|
||||||
arc.EndAngle,
|
arc.EndAngle,
|
||||||
arc.IsReversed)).ToList();
|
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 =>
|
||||||
|
Angle.IsBetweenRad(
|
||||||
arc.Center.AngleTo(pt),
|
arc.Center.AngleTo(pt),
|
||||||
arc.StartAngle,
|
arc.StartAngle,
|
||||||
arc.EndAngle,
|
arc.EndAngle,
|
||||||
arc.IsReversed)).ToList();
|
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 =>
|
||||||
|
Angle.IsBetweenRad(
|
||||||
arc.Center.AngleTo(pt),
|
arc.Center.AngleTo(pt),
|
||||||
arc.StartAngle,
|
arc.StartAngle,
|
||||||
arc.EndAngle,
|
arc.EndAngle,
|
||||||
arc.IsReversed)).ToList();
|
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 =>
|
||||||
|
Angle.IsBetweenRad(
|
||||||
arc.Center.AngleTo(pt),
|
arc.Center.AngleTo(pt),
|
||||||
arc.StartAngle,
|
arc.StartAngle,
|
||||||
arc.EndAngle,
|
arc.EndAngle,
|
||||||
arc.IsReversed)).ToList();
|
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)
|
||||||
{
|
{
|
||||||
@@ -429,13 +423,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 =
|
||||||
|
point.DistanceTo(StartPoint) < Tolerance.Epsilon
|
||||||
? null
|
? null
|
||||||
: new Line(StartPoint, point);
|
: 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 System.Collections.Generic;
|
||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -105,7 +105,8 @@ namespace OpenNest.Geometry
|
|||||||
// 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);
|
||||||
|
|
||||||
@@ -132,10 +133,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 +152,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 +207,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -354,10 +360,19 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var prev = (i - 1 + count) % count;
|
var prev = (i - 1 + count) % count;
|
||||||
|
|
||||||
var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
|
var a1 = new Vector(
|
||||||
var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
|
Vertices[prev].X + normals[prev].X,
|
||||||
|
Vertices[prev].Y + normals[prev].Y
|
||||||
|
);
|
||||||
|
var a2 = new Vector(
|
||||||
|
Vertices[i].X + normals[prev].X,
|
||||||
|
Vertices[i].Y + normals[prev].Y
|
||||||
|
);
|
||||||
var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
|
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 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 edgeA = new Line(a1, a2);
|
||||||
var edgeB = new Line(b1, b2);
|
var edgeB = new Line(b1, b2);
|
||||||
@@ -365,7 +380,9 @@ namespace OpenNest.Geometry
|
|||||||
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
|
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
|
||||||
result.Vertices.Add(pt);
|
result.Vertices.Add(pt);
|
||||||
else
|
else
|
||||||
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
|
result.Vertices.Add(
|
||||||
|
new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Close();
|
result.Close();
|
||||||
@@ -379,8 +396,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);
|
||||||
@@ -581,13 +600,25 @@ namespace OpenNest.Geometry
|
|||||||
var bj = edgeBounds[j];
|
var bj = edgeBounds[j];
|
||||||
|
|
||||||
// Prune with bounding box check.
|
// Prune with bounding box check.
|
||||||
if (bi.maxX < bj.minX || bj.maxX < bi.minX ||
|
if (
|
||||||
bi.maxY < bj.minY || bj.maxY < bi.minY)
|
bi.maxX < bj.minX
|
||||||
|
|| bj.maxX < bi.minX
|
||||||
|
|| bi.maxY < bj.minY
|
||||||
|
|| bj.maxY < bi.minY
|
||||||
|
)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
|
if (
|
||||||
|
SegmentsIntersect(
|
||||||
|
Vertices[i],
|
||||||
|
Vertices[i + 1],
|
||||||
|
Vertices[j],
|
||||||
|
Vertices[j + 1],
|
||||||
|
out pt
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
edgeI = i;
|
edgeI = i;
|
||||||
edgeJ = j;
|
edgeJ = j;
|
||||||
@@ -620,7 +651,13 @@ namespace OpenNest.Geometry
|
|||||||
return areaA >= areaB ? loopA : loopB;
|
return areaA >= areaB ? loopA : loopB;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt)
|
private static bool SegmentsIntersect(
|
||||||
|
Vector a1,
|
||||||
|
Vector a2,
|
||||||
|
Vector b1,
|
||||||
|
Vector b2,
|
||||||
|
out Vector pt
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var da = a2 - a1;
|
var da = a2 - a1;
|
||||||
var db = b2 - b1;
|
var db = b2 - b1;
|
||||||
@@ -636,8 +673,12 @@ namespace OpenNest.Geometry
|
|||||||
var t = (dc.X * db.Y - dc.Y * db.X) / cross;
|
var t = (dc.X * db.Y - dc.Y * db.X) / cross;
|
||||||
var u = (dc.X * da.Y - dc.Y * da.X) / cross;
|
var u = (dc.X * da.Y - dc.Y * da.X) / cross;
|
||||||
|
|
||||||
if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon &&
|
if (
|
||||||
u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon)
|
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);
|
pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y);
|
||||||
return true;
|
return true;
|
||||||
@@ -701,8 +742,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;
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -320,21 +316,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:
|
||||||
@@ -462,9 +458,7 @@ 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
@@ -500,9 +494,14 @@ namespace OpenNest.Geometry
|
|||||||
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
|
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
|
||||||
{
|
{
|
||||||
JoinOffsetLines(
|
JoinOffsetLines(
|
||||||
(Line)lastEntity, (Line)lastOffsetEntity,
|
(Line)lastEntity,
|
||||||
line, offsetLine,
|
(Line)lastOffsetEntity,
|
||||||
distance, side, offsetShape);
|
line,
|
||||||
|
offsetLine,
|
||||||
|
distance,
|
||||||
|
side,
|
||||||
|
offsetShape
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
offsetShape.Entities.Add(offsetLine);
|
offsetShape.Entities.Add(offsetLine);
|
||||||
@@ -519,27 +518,42 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close the shape: join last offset entity back to first
|
// Close the shape: join last offset entity back to first
|
||||||
if (lastOffsetEntity != null && firstOffsetEntity != null
|
if (
|
||||||
|
lastOffsetEntity != null
|
||||||
|
&& firstOffsetEntity != null
|
||||||
&& lastOffsetEntity != firstOffsetEntity
|
&& lastOffsetEntity != firstOffsetEntity
|
||||||
&& lastOffsetEntity.Type == EntityType.Line
|
&& lastOffsetEntity.Type == EntityType.Line
|
||||||
&& firstOffsetEntity.Type == EntityType.Line)
|
&& firstOffsetEntity.Type == EntityType.Line
|
||||||
|
)
|
||||||
{
|
{
|
||||||
JoinOffsetLines(
|
JoinOffsetLines(
|
||||||
(Line)lastEntity, (Line)lastOffsetEntity,
|
(Line)lastEntity,
|
||||||
(Line)firstEntity, (Line)firstOffsetEntity,
|
(Line)lastOffsetEntity,
|
||||||
distance, side, offsetShape);
|
(Line)firstEntity,
|
||||||
|
(Line)firstOffsetEntity,
|
||||||
|
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 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 +562,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)
|
||||||
{
|
{
|
||||||
@@ -563,7 +578,9 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
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;
|
||||||
@@ -596,8 +613,11 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
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 +631,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,8 +662,11 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
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 +680,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,17 +13,28 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -41,8 +52,10 @@ namespace OpenNest.Geometry
|
|||||||
var ix = p1x + t * (p2x - p1x);
|
var ix = p1x + t * (p2x - p1x);
|
||||||
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
|
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
|
||||||
|
|
||||||
if (dist > Tolerance.Epsilon) return dist;
|
if (dist > Tolerance.Epsilon)
|
||||||
if (dist >= -Tolerance.Epsilon) return 0;
|
return dist;
|
||||||
|
if (dist >= -Tolerance.Epsilon)
|
||||||
|
return 0;
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +73,10 @@ namespace OpenNest.Geometry
|
|||||||
var iy = p1y + t * (p2y - p1y);
|
var iy = p1y + t * (p2y - p1y);
|
||||||
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
|
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
|
||||||
|
|
||||||
if (dist > Tolerance.Epsilon) return dist;
|
if (dist > Tolerance.Epsilon)
|
||||||
if (dist >= -Tolerance.Epsilon) return 0;
|
return dist;
|
||||||
|
if (dist >= -Tolerance.Epsilon)
|
||||||
|
return 0;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using OpenNest.Math;
|
using System;
|
||||||
using System;
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class Material
|
public class Material
|
||||||
{
|
{
|
||||||
public Material()
|
public Material() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Material(string name)
|
public Material(string name)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,8 +90,7 @@
|
|||||||
a1 = Angle.NormalizeRad(angle - a1);
|
a1 = Angle.NormalizeRad(angle - a1);
|
||||||
a2 = Angle.NormalizeRad(a2 - angle);
|
a2 = Angle.NormalizeRad(a2 - angle);
|
||||||
|
|
||||||
return diff >= a1 - Tolerance.Epsilon ||
|
return diff >= a1 - Tolerance.Epsilon || diff >= a2 - Tolerance.Epsilon;
|
||||||
diff >= a2 - Tolerance.Epsilon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,8 +115,7 @@
|
|||||||
a1 = Angle.NormalizeRad(angle - a1);
|
a1 = Angle.NormalizeRad(angle - a1);
|
||||||
a2 = Angle.NormalizeRad(a2 - angle);
|
a2 = Angle.NormalizeRad(a2 - angle);
|
||||||
|
|
||||||
return diff >= a1 - Tolerance.Epsilon ||
|
return diff >= a1 - Tolerance.Epsilon || diff >= a2 - Tolerance.Epsilon;
|
||||||
diff >= a2 - Tolerance.Epsilon;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user