Compare commits

20 Commits
Author SHA1 Message Date
aj e7cbd99db6 Separate WinForms tests so main test suite runs on Linux 2026-09-20 17:40:06 -04:00
aj 27684c3782 style: apply CSharpier formatting to files merged from arc-tangency branch 2026-09-20 16:54:16 -04:00
aj 54694f9b17 Merge branch 'chore/csharpier-sweep'
# Conflicts:
#	OpenNest.Core/Geometry/ArcFit.cs
#	OpenNest.Core/Geometry/GeometrySimplifier.cs
#	OpenNest.Posts.GravographIS/GravographISWriter.cs
#	OpenNest.Posts.GravographIS/NestPolylineExtractor.cs
2026-09-20 16:53:52 -04:00
aj de1248589a Merge branch 'feat/outer-profile-only'
# Conflicts:
#	CLAUDE.md
2026-09-20 16:52:18 -04:00
aj a9ebd8bb55 chore: add .git-blame-ignore-revs for the CSharpier sweep
Skip the formatting commit in git blame.
Enable with: git config blame.ignoreRevsFile .git-blame-ignore-revs
2026-09-20 16:42:45 -04:00
aj aec0523062 style: apply CSharpier formatting to all C# sources
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.

Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).

Formatting is now enforceable: dotnet csharpier check . passes.
2026-09-20 16:41:50 -04:00
aj 8e6fa677fb chore: add .editorconfig and pinned CSharpier tool manifest
Mirrors CSharpier conventions (4-space indent, Allman braces,
System-first usings, 100-col wraps) so IDE auto-format and
'dotnet format' agree with the canonical formatter.
Usage: dotnet tool restore && dotnet csharpier format .
2026-09-20 16:04:04 -04:00
aj 589d341455 feat(io): conservative opt-in bend repair with tests and console CLI
Add OpenNest.IO/Bending/BendRepair: opt-in repair of unambiguous paired
ETCH/SCRIBE bend ticks, bounded to <=3.175 mm endpoint movement with
explicit source units. Cut geometry is never modified.

- CadImportOptions.BendRepair configures it; CadImportResult exposes
  per-bend BendRepairReports; CadImporter/Dxf wire it into import.
- Console: --repair-bends-mm <limit> --cad-units inches|mm prints
  per-bend reports for newly imported DXFs.
- New OpenNest.IO.Tests project (net8.0, synthetic DXFs, 30 tests)
  covering bend detection and repair, added to the solution.
- Update README.md and CLAUDE.md for the new pipeline and build/test
  instructions.
2026-09-20 15:22:46 -04:00
aj 1a05391d94 fix(engine): reject small corner overlaps in placement validation
The witness-probe overlap test missed small corner intersections: its
candidate points (crossing-edge midpoints and vertex-centroid midpoints)
can all land on a part boundary or outside the intersection, so two 10x10
parts at (0,0) and (9,9) with zero spacing were accepted despite sharing
a 1x1 unit of material.

Route the overlap decision through Collision, which clips triangulated
polygons and keeps only positive-area regions, catching corner overlaps,
containment, and coincident poses while legal edge/corner contact stays
legal. Collision's hole subtraction was conservative (partially-clipped
triangles were kept whole), so a part inside another part's cutout could
false-positive depending on triangulation alignment; subtract holes
exactly instead: a piece outside a convex hole triangle is the union of
its clips against each edge's outside half-space.
2026-09-20 13:40:45 -04:00
aj f5d27652f4 Merge branch 'fix/simplifier-arc-tangency'
Arc-tangency fitting fix in GeometrySimplifier/ArcFit plus layered
engrave/cut passes for the GravographIS post processor.
2026-09-19 12:02:08 -04:00
aj ea4bd836cd Add tested caller-stock StockLadder baseline with strict geometry validation 2026-09-19 11:24:36 -04:00
ajandClaude Sonnet 5 9b69c67572 fix(engine): use required spacing, not a sampled gap, when resequencing shrink-fill strips
SortStrips measured the gap between only the first two strips in original
placement order and replayed that single value between every strip after
reordering by height/width. Real (non-uniform) geometry produces varying
inter-strip gaps, so resequencing could expand the total footprint beyond
the plate's already-fitted work area, crashing StripPlateNester with
"Candidate placement falls outside the usable stock area." Using the
actual required spacing guarantees the resequenced span never exceeds
the original, since real gaps are always >= spacing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 10:04:33 -04:00
ajandClaude Sonnet 5 aa88eee484 fix(benchmark): use reference-based drawing identity in NestValidator, fix duplicate-sheet-size crash, document Engines/ plugin contract
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:44:09 -04:00
ajandClaude Sonnet 5 e0e3b96bed fix(benchmark): match drawing identity across materialization boundary in NestValidator
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:29:48 -04:00
aj 424ff15ebc docs: describe INestingEngine-based benchmark comparison 2026-09-19 08:25:57 -04:00
aj 9888fe6083 feat(benchmark): switch CLI to NestingEngineRegistry and its Engines/ plugin directory
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 08:23:31 -04:00
ajandClaude Sonnet 5 a2dcfc7484 refactor(benchmark): drive engines through INestingEngine.Solve instead of a hand-rolled multi-plate loop
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:19:08 -04:00
aj ae704478af feat(engine): add NestingEngineRegistry for whole-job INestingEngine plugin discovery 2026-09-19 08:15:40 -04:00
ajandClaude Haiku 4.5 ecca71e185 feat(engine): add FixedStrategyNestingEngine adapting IPlateNester strategies to INestingEngine
Implements a sealed adapter class that forces a fixed IPlateNester strategy onto
any NestJob, overriding the job's own PlacementStrategy while preserving MaxPlates.
Delegates all multi-plate allocation and stock selection to NestJobRunner.

This allows single-plate nesting strategies to compete as full whole-job
INestingEngine solvers in benchmarks, enabling comparative performance testing
of placement algorithms across various job configurations.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-19 08:12:10 -04:00
ajandClaude Opus 4.6 a085339ba9 fix: improve arc-tangency fitting and add layered engrave/cut passes for GravographIS
GeometrySimplifier/ArcFit now fit arcs that pass exactly through run
endpoints while balancing tangency error between trusted and estimated
directions, fixing arcs that previously bulged or broke tangent
continuity at fillet/compound-curve junctions.

GravographIS post processor gains per-layer (engrave/cut) tool passes
via a new GravographISPostConfig, so ENGRAVE/ETCH-tagged geometry runs
as a separate scribe pass with its own feed/depth and an operator
pause before the cut pass (spring-floated spindle needs a tool swap).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-06 23:15:18 -04:00
500 changed files with 19651 additions and 7856 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
+4
View File
@@ -0,0 +1,4 @@
# CSharpier formats only C# sources; project/config XML keeps its layout.
**/*.csproj
**/*.config
**/*.xml
+68
View File
@@ -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
+3
View File
@@ -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
+15 -8
View File
@@ -8,13 +8,17 @@ OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts
## Build ## Build
This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-windows`. Build with: This is a .NET 8 solution using SDK-style `.csproj` files. The desktop app and Windows-dependent projects target `net8.0-windows`; the core libraries and `OpenNest.Console` target `net8.0`. Build the full solution on Windows with:
```bash ```bash
dotnet build OpenNest.sln dotnet build OpenNest.sln
``` ```
Cross-platform whole-job engine tests (net8.0, runs on Linux/macOS/Windows without the desktop project or DXF fixtures): `dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj`. The existing `OpenNest.Tests` suite targets `net8.0-windows` and requires a Windows runner; cross-compiling on Linux is not Windows runtime verification. Cross-platform whole-job engine tests (net8.0, runs on Linux/macOS/Windows without the desktop project or DXF fixtures): `dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj`. The main `OpenNest.Tests` suite also targets `net8.0`: run `dotnet test OpenNest.Tests/OpenNest.Tests.csproj` independently on Linux/macOS/Windows. It must not reference the WinForms `OpenNest` project. The API, Data, Cincinnati, and GravographIS libraries target `net8.0`; post-processor build deployment still targets the desktop app's `net8.0-windows/Posts` directory. Optional CHR-font fixtures are configured through `OpenNest.Tests/test-config.json` and skip when absent.
`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).
@@ -63,9 +67,10 @@ File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types. - `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers. - `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`. - `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
- `Bending/BendRepair` — conservative opt-in repair configured by `CadImportOptions.BendRepair`. Requires explicit inches/mm source units and an endpoint movement limit above 0.001 and at most 3.175 physical mm. Only unambiguous paired ETCH/SCRIBE ticks may move along the existing bend axis; cut geometry and unrelated marks must remain unchanged. Opt-in imports preserve source marks without blanket etch regeneration and expose per-bend outcomes in `CadImportResult.BendRepairReports`.
### OpenNest.Console (console app, depends on Core + Engine + IO) ### OpenNest.Console (console app, depends on Core + Engine + IO)
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`). Command-line interface for batch nesting (`net8.0`). Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--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.
@@ -74,12 +79,13 @@ GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` impleme
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) ### OpenNest.Benchmark (console app, depends on Core + Engine + IO)
Compares registered `NestEngineBase` implementations against each other on real `.nest` files. Fully generic — it never hardcodes drawing geometry, just reads whatever drawings/quantities/plate settings each input file already has. 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. - `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.
- `BenchmarkRunner` gives each (job, engine) pair a fresh `Plate`/`NestItem` list (`BenchmarkJob.CreatePlate()`/`CreateItems()`) so engines can't see each other's mutated state, then calls the engine's `Nest()` and times it. - `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).
- `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 or throwing run scores zero for that job. - `BenchmarkRunner` 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.
- Scoring matches `Plate.Utilization()` (placed drawing area / full sheet area, `Plate.Area()`). If an engine placed every requested part, ties are broken by the smaller used-bounding-box (`Report`'s ranking rule) — a more compact layout leaves a bigger usable remnant. - `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. - `--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)
@@ -132,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).
+3
View File
@@ -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;
+1
View File
@@ -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; }
+41 -25
View File
@@ -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.01.0 ratio.</summary> /// <summary>Placed-part area divided by total materialized physical-sheet area, as a 0.01.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,19 +63,27 @@ public class NestResponse
var responseEntry = zip.CreateEntry("response.json"); var responseEntry = zip.CreateEntry("response.json");
await using (var stream = responseEntry.Open()) await using (var stream = responseEntry.Open())
{ {
await JsonSerializer.SerializeAsync(stream, new NestResponseArchiveDto await JsonSerializer.SerializeAsync(
{ stream,
SchemaVersion = CurrentSchemaVersion, new NestResponseArchiveDto
SheetCount = SheetCount, {
Utilization = Utilization, SchemaVersion = CurrentSchemaVersion,
CutTimeTicks = CutTime.Ticks, SheetCount = SheetCount,
ElapsedTicks = Elapsed.Ticks, Utilization = Utilization,
Status = Status, CutTimeTicks = CutTime.Ticks,
StopReason = StopReason, ElapsedTicks = Elapsed.Ticks,
Fulfillment = Fulfillment is null ? [] : new List<NestPartFulfillment>(Fulfillment), Status = Status,
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage), StopReason = StopReason,
PlateStockMappings = PlateStockMappings is null ? [] : new List<NestPlateStockMapping>(PlateStockMappings) Fulfillment = Fulfillment is null
}, JsonOptions); ? []
: new List<NestPartFulfillment>(Fulfillment),
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
PlateStockMappings = PlateStockMappings is null
? []
: new List<NestPlateStockMapping>(PlateStockMappings),
},
JsonOptions
);
} }
var nestEntry = zip.CreateEntry("nest.nest"); var nestEntry = zip.CreateEntry("nest.nest");
@@ -91,16 +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,
}; };
} }
+104 -46
View File
@@ -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,35 +87,56 @@ public static class NestRunner
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting); var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
sw.Stop(); sw.Stop();
return Task.FromResult(new NestResponse return Task.FromResult(
{ new NestResponse
SheetCount = nest.Plates.Count, {
Utilization = CalculateUtilization(nest), SheetCount = nest.Plates.Count,
CutTime = cutTime, Utilization = CalculateUtilization(nest),
Elapsed = sw.Elapsed, CutTime = cutTime,
Status = result.Status, Elapsed = sw.Elapsed,
StopReason = result.StopReason, Status = result.Status,
Fulfillment = result.Fulfillment StopReason = result.StopReason,
.Select(value => new NestPartFulfillment(value.PartId, value.Requested, value.Placed, value.Unplaced)) Fulfillment = result
.ToArray(), .Fulfillment.Select(value => new NestPartFulfillment(
StockUsage = result.StockUsage value.PartId,
.Select(value => new NestStockUsage(value.StockId, value.Used, value.Remaining)) value.Requested,
.ToArray(), value.Placed,
PlateStockMappings = result.Plates value.Unplaced
.Select(value => new NestPlateStockMapping(value.PlateIndex, value.StockId)) ))
.ToArray(), .ToArray(),
Nest = nest, StockUsage = result
Request = request .StockUsage.Select(value => new NestStockUsage(
}); value.StockId,
value.Used,
value.Remaining
))
.ToArray(),
PlateStockMappings = result
.Plates.Select(value => new NestPlateStockMapping(
value.PlateIndex,
value.StockId
))
.ToArray(),
Nest = nest,
Request = request,
}
);
} }
private static IReadOnlyList<IdentifiedRequestPart> IdentifyParts(IReadOnlyList<NestRequestPart> requestParts) private static IReadOnlyList<IdentifiedRequestPart> IdentifyParts(
IReadOnlyList<NestRequestPart> requestParts
)
{ {
var identified = new List<IdentifiedRequestPart>(requestParts.Count); var identified = new List<IdentifiedRequestPart>(requestParts.Count);
var ids = new HashSet<string>(StringComparer.Ordinal); var ids = new HashSet<string>(StringComparer.Ordinal);
for (var index = 0; index < requestParts.Count; index++) for (var index = 0; index < requestParts.Count; index++)
{ {
var part = requestParts[index] ?? throw new ArgumentException("Request parts must not contain null entries.", nameof(requestParts)); var part =
requestParts[index]
?? throw new ArgumentException(
"Request parts must not contain null entries.",
nameof(requestParts)
);
var id = part.Id ?? $"part-{index}"; var id = part.Id ?? $"part-{index}";
if (string.IsNullOrWhiteSpace(id)) if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Part IDs must not be blank.", nameof(requestParts)); throw new ArgumentException("Part IDs must not be blank.", nameof(requestParts));
@@ -117,8 +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
NestStrategy.Auto => "Default", ?? request.Strategy switch
_ => throw new NotSupportedException($"Unknown legacy nesting strategy: {request.Strategy}.") {
}; NestStrategy.Auto => "Default",
_ => throw new NotSupportedException(
$"Unknown legacy nesting strategy: {request.Strategy}."
),
};
private static double CalculateUtilization(Nest nest) private static double CalculateUtilization(Nest nest)
{ {
var sheetArea = nest.Plates.Sum(plate => plate.Area()); var sheetArea = nest.Plates.Sum(plate => plate.Area());
if (sheetArea == 0) return 0; if (sheetArea == 0)
var placedArea = nest.Plates.Sum(plate => plate.Parts return 0;
.Where(part => !part.BaseDrawing.IsCutOff) var placedArea = nest.Plates.Sum(plate =>
.Sum(part => part.BaseDrawing.Area)); plate.Parts.Where(part => !part.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area)
);
return placedArea / sheetArea; return placedArea / sheetArea;
} }
private sealed record IdentifiedRequestPart(string Id, NestRequestPart Request); private sealed record IdentifiedRequestPart(string Id, NestRequestPart Request);
private sealed class JobProgressBridge(IProgress<NestProgress> progress) : IProgress<NestJobProgress> private sealed class JobProgressBridge(IProgress<NestProgress> progress)
: IProgress<NestJobProgress>
{ {
public void Report(NestJobProgress value) public void Report(NestJobProgress value)
{ {
+4 -1
View File
@@ -1,3 +1,6 @@
namespace OpenNest.Api; namespace OpenNest.Api;
public enum NestStrategy { Auto } public enum NestStrategy
{
Auto,
}
+1 -1
View File
@@ -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>
+28 -43
View File
@@ -1,7 +1,7 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Benchmark namespace OpenNest.Benchmark
{ {
@@ -41,50 +41,35 @@ namespace OpenNest.Benchmark
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity); public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
/// <summary> /// <summary>
/// A blank plate carrying only the job's spacing/quadrant template. /// Builds the whole-job request this job represents: one NestJobPart per
/// MultiPlateNester.CreatePlate copies these settings onto whichever /// requested drawing, and one NestPlateStock per candidate sheet size
/// size it ultimately picks; its Size is only the fallback used when /// (unlimited quantity - the engine under test decides how many of each
/// nothing in the candidate pool fits, so it's set to the largest /// size it actually uses, and how demand splits across plates). The
/// candidate rather than an arbitrary one. /// engine owns its own multi-plate/size strategy; this harness no
/// longer picks plate sizes on the engine's behalf.
/// </summary> /// </summary>
public Plate CreateTemplatePlate() public NestJob BuildNestJob(
int maxPlates,
double salvageRate = 0,
double minimumSalvageDimension = 0
)
{ {
var fallbackSize = CandidateSizes var parts = Requests.Select(r =>
.OrderByDescending(s => s.Width * s.Length) DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)
.FirstOrDefault(); );
var stock = CandidateSizes.Select(size => new NestPlateStock(
return new Plate(fallbackSize) size.ToString(1),
{ size,
EdgeSpacing = EdgeSpacing, null,
PartSpacing = PartSpacing, PartSpacing,
Quadrant = Quadrant, EdgeSpacing,
}; Quadrant
} ));
return new NestJob(
/// <summary> parts,
/// The candidate sizes as PlateOptions for MultiPlateNester.CreatePlate. stock,
/// Cost is area-proportional since no real per-size material pricing is new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension)
/// available here - this only affects which size is preferred when more );
/// than one candidate fits, favoring the smaller/cheaper sheet.
/// </summary>
public List<PlateOption> BuildPlateOptions()
{
return CandidateSizes
.Select(s => new PlateOption { Width = s.Width, Length = s.Length, Cost = s.Width * s.Length })
.ToList();
}
public List<NestItem> CreateItems()
{
return Requests.Select(r => new NestItem
{
Drawing = r.Drawing,
Quantity = r.Quantity,
Priority = r.Priority,
StepAngle = r.StepAngle,
RotationStart = r.RotationStart,
RotationEnd = r.RotationEnd,
}).ToList();
} }
} }
} }
+148 -100
View File
@@ -7,23 +7,29 @@ using System.Threading;
namespace OpenNest.Benchmark namespace OpenNest.Benchmark
{ {
/// <summary> /// <summary>
/// Runs every candidate engine against every job. A job may need several /// Runs every candidate engine against every job. Each engine is a full
/// plates to place everything it asks for; this drives that loop itself, /// INestingEngine: it owns its own plate/size selection and multi-plate
/// since NestEngineBase.Nest() fills exactly one already-sized plate and /// strategy for the whole job, rather than being handed one already-sized
/// has no say in picking its own size. For each plate the loop needs, the /// plate at a time by this harness. A per-run timeout guards against a
/// smallest candidate size that fits the largest still-unplaced drawing is /// runaway or hanging engine — cooperative cancellation, so it reliably
/// chosen via the codebase's own MultiPlateNester.CreatePlate, then the /// stops engines built on NestJobRunner (all four built-ins) but can't
/// engine's Nest() fills that plate with whatever of the remaining items /// forcibly interrupt an engine that never checks its token.
/// fit. This is applied identically to every engine, so no engine gets to
/// (or has to) implement sheet-size selection itself.
/// </summary> /// </summary>
public static class BenchmarkRunner public static class BenchmarkRunner
{ {
/// <summary>Safety cap so a degenerate engine (placing almost nothing /// <summary>Physical-sheet cap passed to every job's NestJobOptions.MaxPlates.</summary>
/// per plate) can't loop indefinitely.</summary>
private const int MaxPlates = 40; private const int MaxPlates = 40;
public static List<JobResult> Run(List<BenchmarkJob> jobs, IReadOnlyList<NestEngineInfo> engines) /// <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
)
{ {
var results = new List<JobResult>(jobs.Count * engines.Count); var results = new List<JobResult>(jobs.Count * engines.Count);
@@ -31,67 +37,140 @@ namespace OpenNest.Benchmark
{ {
foreach (var engineInfo in engines) foreach (var engineInfo in engines)
{ {
results.Add(RunOne(job, engineInfo)); results.Add(
RunOne(
job,
engineInfo,
salvageRate,
minimumSalvageDimension,
outputDirectory
)
);
} }
} }
return results; return results;
} }
private static JobResult RunOne(BenchmarkJob job, NestEngineInfo engineInfo) private static JobResult RunOne(
BenchmarkJob job,
NestingEngineInfo engineInfo,
double salvageRate,
double minimumSalvageDimension,
string outputDirectory
)
{ {
var template = job.CreateTemplatePlate();
var options = job.BuildPlateOptions();
var remaining = job.CreateItems();
var requested = job.TotalRequestedQuantity; var requested = job.TotalRequestedQuantity;
var plateRuns = new List<(Plate Plate, List<Part> Parts)>();
string engineError = null;
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
try try
{ {
while (remaining.Any(i => i.Quantity > 0) && plateRuns.Count < MaxPlates) 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)
{ {
var largest = remaining System.IO.Directory.CreateDirectory(outputDirectory);
.Where(i => i.Quantity > 0) // Keep names and job metadata for a useful inspectable output; never modify source.
.OrderByDescending(i => BoundsArea(i)) var source = new OpenNest.IO.NestReader(job.SourceFile).Read();
.First(); materialized.Nest.Name = source.Name;
materialized.Nest.Units = source.Units;
var plate = MultiPlateNester.CreatePlate(template, options, largest.Drawing.Program.BoundingBox()); materialized.Nest.Material = source.Material;
var engine = engineInfo.Factory(plate); materialized.Nest.Thickness = source.Thickness;
var itemsClone = CloneItems(remaining); materialized.Nest.SalvageRate = salvageRate;
foreach (var request in job.Requests)
var parts = engine.Nest(itemsClone, null, CancellationToken.None) ?? new List<Part>(); materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request
.Drawing
if (parts.Count == 0) .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
{ {
// Not even the largest available candidate size could fit Source = job.SourceFile,
// the current largest remaining part - stop here rather Engine = engineInfo.Name,
// than loop forever; whatever's left is reported unplaced. jobResult.Status,
break; jobResult.StopReason,
} Requested = requested,
Placed = totalPlaced,
plateRuns.Add((plate, parts)); SheetArea = plateArea,
PlacedArea = placedArea,
foreach (var item in remaining) SalvageRate = salvageRate,
{ MinimumSalvageDimension = minimumSalvageDimension,
var placed = parts.Count(p => p.BaseDrawing.Id == item.Drawing.Id); EstimatedNetArea = jobResult.Plates.Sum(p =>
StockLadderNestingEngine.EstimateNetArea(nestJob, p)
if (placed > 0) ),
item.Quantity = System.Math.Max(0, item.Quantity - placed); 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();
catch (Exception ex)
{
engineError = $"{ex.GetType().Name}: {ex.Message}";
}
sw.Stop(); return new JobResult
{
if (engineError != null) 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 return new JobResult
{ {
EngineName = engineInfo.Name, EngineName = engineInfo.Name,
@@ -99,53 +178,22 @@ namespace OpenNest.Benchmark
Valid = false, Valid = false,
PartsRequested = requested, PartsRequested = requested,
ElapsedMs = sw.ElapsedMilliseconds, ElapsedMs = sw.ElapsedMilliseconds,
Error = engineError, Error = $"Timed out after {SolveTimeout.TotalMinutes:F0} minute(s)",
}; };
} }
catch (Exception ex)
var validation = NestValidator.Validate(plateRuns, job);
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());
return new JobResult
{ {
EngineName = engineInfo.Name, sw.Stop();
JobName = job.Name, return new JobResult
Valid = validation.Valid, {
Violations = validation.Violations, EngineName = engineInfo.Name,
PartsPlaced = totalPlaced, JobName = job.Name,
PartsRequested = requested, Valid = false,
PlacedArea = placedArea, PartsRequested = requested,
PlateArea = plateArea, ElapsedMs = sw.ElapsedMilliseconds,
PlatesUsed = plateRuns.Count, Error = $"{ex.GetType().Name}: {ex.Message}",
SizeBreakdown = sizeBreakdown, };
ElapsedMs = sw.ElapsedMilliseconds, }
};
}
private static double BoundsArea(NestItem item)
{
var bb = item.Drawing.Program.BoundingBox();
return bb.Width * bb.Length;
}
private static List<NestItem> CloneItems(List<NestItem> items)
{
return items.Select(i => new NestItem
{
Drawing = i.Drawing,
Quantity = i.Quantity,
Priority = i.Priority,
StepAngle = i.StepAngle,
RotationStart = i.RotationStart,
RotationEnd = i.RotationEnd,
}).ToList();
} }
} }
} }
+44 -29
View File
@@ -1,9 +1,9 @@
using OpenNest.Geometry;
using OpenNest.IO;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Benchmark namespace OpenNest.Benchmark
{ {
@@ -17,8 +17,11 @@ namespace OpenNest.Benchmark
/// </summary> /// </summary>
public static class JobLoader public static class JobLoader
{ {
public static List<BenchmarkJob> Load(string inputPath, IReadOnlyList<Size> sheetSizeOverrides = null, public static List<BenchmarkJob> Load(
double? partSpacingOverride = null) string inputPath,
IReadOnlyList<Size> sheetSizeOverrides = null,
double? partSpacingOverride = null
)
{ {
var files = ResolveFiles(inputPath); var files = ResolveFiles(inputPath);
var jobs = new List<BenchmarkJob>(); var jobs = new List<BenchmarkJob>();
@@ -33,7 +36,9 @@ namespace OpenNest.Benchmark
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"[JobLoader] Skipping '{file}': failed to read ({ex.Message})"); Console.Error.WriteLine(
$"[JobLoader] Skipping '{file}': failed to read ({ex.Message})"
);
continue; continue;
} }
@@ -41,24 +46,29 @@ namespace OpenNest.Benchmark
if (requests.Count == 0) if (requests.Count == 0)
{ {
Console.Error.WriteLine($"[JobLoader] Skipping '{file}': no drawings with quantity > 0"); Console.Error.WriteLine(
$"[JobLoader] Skipping '{file}': no drawings with quantity > 0"
);
continue; continue;
} }
var template = ResolvePlateTemplate(nest); var template = ResolvePlateTemplate(nest);
var sizes = sheetSizeOverrides != null && sheetSizeOverrides.Count > 0 var sizes =
? sheetSizeOverrides.ToList() sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
: ResolveSheetSizes(nest); ? sheetSizeOverrides.ToList()
: ResolveSheetSizes(nest);
jobs.Add(new BenchmarkJob jobs.Add(
{ new BenchmarkJob
SourceFile = file, {
CandidateSizes = sizes, SourceFile = file,
EdgeSpacing = template.EdgeSpacing, CandidateSizes = sizes,
PartSpacing = partSpacingOverride ?? template.PartSpacing, EdgeSpacing = template.EdgeSpacing,
Quadrant = template.Quadrant, PartSpacing = partSpacingOverride ?? template.PartSpacing,
Requests = requests, Quadrant = template.Quadrant,
}); Requests = requests,
}
);
} }
return jobs; return jobs;
@@ -68,7 +78,8 @@ namespace OpenNest.Benchmark
{ {
if (Directory.Exists(inputPath)) if (Directory.Exists(inputPath))
{ {
return Directory.GetFiles(inputPath, "*.nest", SearchOption.AllDirectories) return Directory
.GetFiles(inputPath, "*.nest", SearchOption.AllDirectories)
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase) .OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
} }
@@ -92,21 +103,25 @@ namespace OpenNest.Benchmark
var constraints = drawing.Constraints; var constraints = drawing.Constraints;
requests.Add(new DrawingRequest requests.Add(
{ new DrawingRequest
Drawing = drawing, {
Quantity = qty, Drawing = drawing,
Priority = drawing.Priority, Quantity = qty,
StepAngle = constraints?.StepAngle ?? 0, Priority = drawing.Priority,
RotationStart = constraints?.StartAngle ?? 0, StepAngle = constraints?.StepAngle ?? 0,
RotationEnd = constraints?.EndAngle ?? 0, RotationStart = constraints?.StartAngle ?? 0,
}); RotationEnd = constraints?.EndAngle ?? 0,
}
);
} }
return requests; return requests;
} }
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(Nest nest) private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(
Nest nest
)
{ {
var source = nest.Plates?.FirstOrDefault(); var source = nest.Plates?.FirstOrDefault();
+72 -25
View File
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters; using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Benchmark namespace OpenNest.Benchmark
{ {
@@ -25,7 +25,16 @@ namespace OpenNest.Benchmark
/// </summary> /// </summary>
public static class NestValidator public static class NestValidator
{ {
public static ValidationResult Validate(List<(Plate Plate, List<Part> Parts)> plateRuns, BenchmarkJob job) /// <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 result = new ValidationResult();
var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList(); var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
@@ -33,45 +42,56 @@ namespace OpenNest.Benchmark
if (allParts.Count == 0) if (allParts.Count == 0)
return result; return result;
ValidateQuantities(allParts, job, result); ValidateQuantities(allParts, requirements, result);
foreach (var (plate, parts) in plateRuns) foreach (var (plate, parts) in plateRuns)
{ {
if (parts.Count == 0) if (parts.Count == 0)
continue; continue;
ValidateBounds(parts, plate, result); ValidateBounds(parts, plate, requirements, result);
ValidateAreaBudget(parts, plate, result); ValidateAreaBudget(parts, plate, result);
ValidateSpacing(parts, plate.PartSpacing, result); ValidateSpacing(parts, plate.PartSpacing, requirements, result);
} }
return result; return result;
} }
private static void ValidateQuantities(List<Part> parts, BenchmarkJob job, ValidationResult result) private static void ValidateQuantities(
List<Part> parts,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
ValidationResult result
)
{ {
var allowed = job.Requests.ToDictionary(r => r.Drawing.Id, r => r.Quantity);
var placedCounts = parts var placedCounts = parts
.GroupBy(p => p.BaseDrawing.Id) .GroupBy<Part, Drawing>(p => p.BaseDrawing, ReferenceEqualityComparer.Instance)
.ToDictionary(g => g.Key, g => g.Count()); .ToDictionary(g => g.Key, g => g.Count());
foreach (var (drawingId, placed) in placedCounts) foreach (var (drawing, placed) in placedCounts)
{ {
if (!allowed.TryGetValue(drawingId, out var max)) if (!requirements.TryGetValue(drawing, out var requirement))
{ {
result.Violations.Add($"Placed drawing id={drawingId} which was not requested for this job"); result.Violations.Add(
$"Placed drawing '{drawing.Name}' which was not requested for this job"
);
continue; continue;
} }
if (placed > max) if (placed > requirement.Quantity)
{ {
var name = parts.First(p => p.BaseDrawing.Id == drawingId).BaseDrawing.Name; result.Violations.Add(
result.Violations.Add($"'{name}': placed {placed} across all plates but only {max} were requested"); $"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested"
);
} }
} }
} }
private static void ValidateBounds(List<Part> parts, Plate plate, ValidationResult result) private static void ValidateBounds(
List<Part> parts,
Plate plate,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
ValidationResult result
)
{ {
var workArea = plate.WorkArea(); var workArea = plate.WorkArea();
@@ -87,8 +107,9 @@ namespace OpenNest.Benchmark
if (outLeft || outBottom || outRight || outTop) if (outLeft || outBottom || outRight || outTop)
{ {
result.Violations.Add( result.Violations.Add(
$"'{part.BaseDrawing.Name}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " + $"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area "
$"of a {plate.Size} plate"); + $"of a {plate.Size} plate"
);
} }
} }
} }
@@ -102,7 +123,11 @@ namespace OpenNest.Benchmark
/// to return false negatives on real, complex production geometry, so /// to return false negatives on real, complex production geometry, so
/// this check does not depend on it. /// this check does not depend on it.
/// </summary> /// </summary>
private static void ValidateAreaBudget(List<Part> parts, Plate plate, ValidationResult result) private static void ValidateAreaBudget(
List<Part> parts,
Plate plate,
ValidationResult result
)
{ {
var workArea = plate.WorkArea(); var workArea = plate.WorkArea();
var budget = workArea.Width * workArea.Length; var budget = workArea.Width * workArea.Length;
@@ -111,12 +136,18 @@ namespace OpenNest.Benchmark
if (placedArea > budget + Tolerance.Epsilon) if (placedArea > budget + Tolerance.Epsilon)
{ {
result.Violations.Add( result.Violations.Add(
$"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - " + $"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"); + "parts must overlap even though the polygon overlap check did not flag a pair"
);
} }
} }
private static void ValidateSpacing(List<Part> parts, double spacing, ValidationResult result) 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 worldPolygons = new Polygon[parts.Count];
var inflatedPolygons = new Polygon[parts.Count]; var inflatedPolygons = new Polygon[parts.Count];
@@ -124,7 +155,10 @@ namespace OpenNest.Benchmark
for (var i = 0; i < parts.Count; i++) for (var i = 0; i < parts.Count; i++)
{ {
worldPolygons[i] = WorldPolygon(parts[i], 0); worldPolygons[i] = WorldPolygon(parts[i], 0);
inflatedPolygons[i] = spacing > Tolerance.Epsilon ? WorldPolygon(parts[i], spacing) : worldPolygons[i]; inflatedPolygons[i] =
spacing > Tolerance.Epsilon
? WorldPolygon(parts[i], spacing)
: worldPolygons[i];
} }
for (var i = 0; i < parts.Count; i++) for (var i = 0; i < parts.Count; i++)
@@ -140,12 +174,24 @@ namespace OpenNest.Benchmark
if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j])) if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j]))
{ {
result.Violations.Add( result.Violations.Add(
$"'{parts[i].BaseDrawing.Name}' and '{parts[j].BaseDrawing.Name}' are closer than the required spacing ({spacing:F3})"); $"'{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> /// <summary>
/// Extracts a part's perimeter as a world-space polygon, optionally inflated /// Extracts a part's perimeter as a world-space polygon, optionally inflated
/// outward by the given spacing, mirroring Part.Intersects' own geometry /// outward by the given spacing, mirroring Part.Intersects' own geometry
@@ -153,7 +199,8 @@ namespace OpenNest.Benchmark
/// </summary> /// </summary>
private static Polygon WorldPolygon(Part part, double inflateBy) private static Polygon WorldPolygon(Part part, double inflateBy)
{ {
var entities = ConvertProgram.ToGeometry(part.Program) var entities = ConvertProgram
.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid) .Where(e => e.Layer != SpecialLayers.Rapid)
.ToList(); .ToList();
+1 -1
View File
@@ -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.Benchmark</RootNamespace> <RootNamespace>OpenNest.Benchmark</RootNamespace>
<AssemblyName>OpenNest.Benchmark</AssemblyName> <AssemblyName>OpenNest.Benchmark</AssemblyName>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
+114 -27
View File
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest; using OpenNest;
using OpenNest.Benchmark; using OpenNest.Benchmark;
using OpenNest.Geometry; using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Linq;
return BenchmarkConsole.Run(args); return BenchmarkConsole.Run(args);
@@ -36,22 +37,36 @@ static class BenchmarkConsole
if (jobs.Count == 0) if (jobs.Count == 0)
{ {
Console.Error.WriteLine("No benchmark jobs found (no .nest files with any drawing quantity > 0)."); Console.Error.WriteLine(
"No benchmark jobs found (no .nest files with any drawing quantity > 0)."
);
return 1; return 1;
} }
var engines = NestEngineRegistry.AvailableEngines; var enginesDir = Path.Combine(AppContext.BaseDirectory, "Engines");
NestingEngineRegistry.LoadPlugins(enginesDir);
var engines = NestingEngineRegistry.AvailableEngines;
if (options.EngineNames.Count > 0) if (options.EngineNames.Count > 0)
{ {
engines = engines engines = engines
.Where(e => options.EngineNames.Any(n => n.Equals(e.Name, StringComparison.OrdinalIgnoreCase))) .Where(e =>
options.EngineNames.Any(n =>
n.Equals(e.Name, StringComparison.OrdinalIgnoreCase)
)
)
.ToList(); .ToList();
if (engines.Count == 0) if (engines.Count == 0)
{ {
Console.Error.WriteLine("None of the requested engines are registered. Available: " + Console.Error.WriteLine(
string.Join(", ", NestEngineRegistry.AvailableEngines.Select(e => e.Name))); "None of the requested engines are registered. Available: "
+ string.Join(
", ",
NestingEngineRegistry.AvailableEngines.Select(e => e.Name)
)
);
return 1; return 1;
} }
} }
@@ -61,12 +76,20 @@ static class BenchmarkConsole
foreach (var job in jobs) foreach (var job in jobs)
{ {
var sizes = string.Join(", ", job.CandidateSizes.Select(s => s.ToString(1))); 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(
$" {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))}"); Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
var results = BenchmarkRunner.Run(jobs, engines); var results = BenchmarkRunner.Run(
jobs,
engines,
options.SalvageRate,
options.MinimumSalvageDimension,
options.OutputDirectory
);
Report.PrintDetailed(results); Report.PrintDetailed(results);
Report.PrintSummary(results); Report.PrintSummary(results);
@@ -99,7 +122,10 @@ static class BenchmarkConsole
case "--engines" when i + 1 < args.Length: case "--engines" when i + 1 < args.Length:
o.EngineNames = args[++i] o.EngineNames = args[++i]
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
)
.ToList(); .ToList();
break; break;
@@ -107,6 +133,22 @@ static class BenchmarkConsole
o.CsvPath = args[++i]; o.CsvPath = args[++i];
break; 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 "--help": case "--help":
PrintUsage(); PrintUsage();
return null; return null;
@@ -125,7 +167,12 @@ static class BenchmarkConsole
{ {
var sizes = new List<Size>(); var sizes = new List<Size>();
foreach (var token in arg.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) foreach (
var token in arg.Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
)
)
{ {
if (Size.TryParse(token, out var size)) if (Size.TryParse(token, out var size))
sizes.Add(size); sizes.Add(size);
@@ -133,31 +180,68 @@ static class BenchmarkConsole
Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping"); Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping");
} }
return sizes; return sizes.Distinct().ToList();
} }
private static void PrintUsage() private static void PrintUsage()
{ {
Console.Error.WriteLine("OpenNest.Benchmark - compare registered nesting engines on a set of .nest files"); Console.Error.WriteLine(
"OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files"
);
Console.Error.WriteLine(); Console.Error.WriteLine();
Console.Error.WriteLine("For each .nest file, every drawing with quantity > 0 is nested (mixed together),"); Console.Error.WriteLine(
Console.Error.WriteLine("once per registered engine. This is a full nest, not a single fixed-size plate:"); "For each .nest file, every drawing with quantity > 0 is nested (mixed together),"
Console.Error.WriteLine("as many plates as needed are created, one at a time, each sized by picking the"); );
Console.Error.WriteLine("smallest candidate sheet size that fits the largest still-unplaced drawing -"); Console.Error.WriteLine(
Console.Error.WriteLine("applied identically to every engine, since Nest() itself has no say over its"); "once per registered INestingEngine. Each engine is handed the full job - every"
Console.Error.WriteLine("own plate's size. Scoring: aggregate material utilization across every plate"); );
Console.Error.WriteLine("used, then (if everything requested was placed) fewer plates as the tie-break."); Console.Error.WriteLine(
Console.Error.WriteLine("An invalid layout (out of bounds, overlapping, or over-quantity) scores zero."); "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();
Console.Error.WriteLine("Usage:"); Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(" OpenNest.Benchmark <file.nest | folder> [options]"); Console.Error.WriteLine(" OpenNest.Benchmark <file.nest | folder> [options]");
Console.Error.WriteLine(); Console.Error.WriteLine();
Console.Error.WriteLine("Options:"); Console.Error.WriteLine("Options:");
Console.Error.WriteLine(" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest"); Console.Error.WriteLine(
Console.Error.WriteLine(" (default: the distinct sizes already in each file)"); " --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest"
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(
Console.Error.WriteLine(" --csv <path> Write a flat CSV of all results"); " (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(" --help Show this message"); Console.Error.WriteLine(" --help Show this message");
} }
@@ -168,5 +252,8 @@ static class BenchmarkConsole
public double? PartSpacing; public double? PartSpacing;
public List<string> EngineNames = new(); public List<string> EngineNames = new();
public string CsvPath; public string CsvPath;
public string OutputDirectory;
public double SalvageRate;
public double MinimumSalvageDimension;
} }
} }
+38 -13
View File
@@ -28,19 +28,27 @@ namespace OpenNest.Benchmark
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList(); var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList();
var best = ranked.Count > 0 ? ranked[0] : null; var best = ranked.Count > 0 ? ranked[0] : null;
Console.WriteLine($"{"Engine",-16} {"Result",-9} {"Parts",-10} {"Util%",-8} {"Plates",-18} {"Time(ms)",-9} Notes"); Console.WriteLine(
$"{"Engine", -16} {"Result", -9} {"Parts", -10} {"Util%", -8} {"Plates", -18} {"Time(ms)", -9} Notes"
);
foreach (var r in ranked) foreach (var r in ranked)
{ {
var isWinner = best != null && Compare(r, best) == 0 && r.Valid; var isWinner = best != null && Compare(r, best) == 0 && r.Valid;
var marker = isWinner ? "*" : " "; var marker = isWinner ? "*" : " ";
var status = r.Crashed ? "CRASH" : r.Valid ? "ok" : "INVALID"; var status =
r.Crashed ? "CRASH"
: r.Valid ? "ok"
: "INVALID";
var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}"; var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}";
var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-"; var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-";
var platesCol = r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-"; var platesCol =
r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-";
var notes = r.Crashed ? r.Error : string.Join("; ", r.Violations.Take(2)); 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}"); Console.WriteLine(
$"{marker}{r.EngineName, -15} {status, -9} {partsCol, -10} {utilCol, -8} {platesCol, -18} {r.ElapsedMs, -9} {notes}"
);
} }
} }
} }
@@ -68,30 +76,47 @@ namespace OpenNest.Benchmark
var wins = CountWins(results); var wins = CountWins(results);
Console.WriteLine($"{"Engine",-16} {"Jobs",-6} {"Valid",-7} {"Complete",-9} {"Wins",-6} {"AvgUtil%",-10} {"Plates",-8} {"TotalTime(ms)",-14}"); Console.WriteLine(
$"{"Engine", -16} {"Jobs", -6} {"Valid", -7} {"Complete", -9} {"Wins", -6} {"AvgUtil%", -10} {"Plates", -8} {"TotalTime(ms)", -14}"
);
foreach (var e in byEngine) foreach (var e in byEngine)
{ {
var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 100 : 0; var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 100 : 0;
var winCount = wins.TryGetValue(e.Engine, out var w) ? w : 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}"); 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) public static void WriteCsv(string path, List<JobResult> results)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes"); sb.AppendLine(
"Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes"
);
foreach (var r in results) foreach (var r in results)
{ {
var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations); var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations);
sb.AppendLine(string.Join(",", sb.AppendLine(
Csv(r.JobName), Csv(r.EngineName), r.Valid, r.Crashed, r.FullyPlaced, string.Join(
r.PartsPlaced, r.PartsRequested, ",",
r.Utilization.ToString("F4", CultureInfo.InvariantCulture), Csv(r.JobName),
r.PlatesUsed, Csv(SizeSummary(r.SizeBreakdown)), Csv(r.EngineName),
r.ElapsedMs, Csv(notes))); 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()); File.WriteAllText(path, sb.ToString());
+1 -1
View File
@@ -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>
+184 -53
View File
@@ -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 =
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName) options.DrawingName != null
: nest.Drawings.FirstOrDefault(); ? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
: nest.Drawings.FirstOrDefault();
if (drawing != null) if (drawing != null)
return drawing; return drawing;
Console.Error.WriteLine(options.DrawingName != null Console.Error.WriteLine(
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}" options.DrawingName != null
: "Error: nest file contains no drawings"); ? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
: "Error: nest file contains no drawings"
);
return null; return null;
} }
static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options) static void PrintHeader(
Nest nest,
Plate plate,
Drawing drawing,
int existingCount,
Options options
)
{ {
Console.WriteLine($"Nest: {nest.Name}"); Console.WriteLine($"Nest: {nest.Name}");
var wa = plate.WorkArea(); var wa = plate.WorkArea();
Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}"); Console.WriteLine(
$"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}"
);
Console.WriteLine($"Drawing: {drawing.Name}"); Console.WriteLine($"Drawing: {drawing.Name}");
Console.WriteLine(options.KeepParts Console.WriteLine(
? $"Keeping {existingCount} existing parts" options.KeepParts
: $"Cleared {existingCount} existing parts"); ? $"Keeping {existingCount} existing parts"
: $"Cleared {existingCount} existing parts"
);
Console.WriteLine("---"); Console.WriteLine("---");
} }
static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options) static (bool success, long elapsedMs) Fill(
Nest nest,
Plate plate,
Drawing drawing,
Options options
)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
bool success; bool success;
@@ -310,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(
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points" hasOverlaps
: "Overlap check: PASS"); ? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
: "Overlap check: PASS"
);
return overlapPts.Count; return overlapPts.Count;
} }
@@ -355,9 +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 =
Path.GetDirectoryName(firstInput), options.OutputFile
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}"); ?? Path.Combine(
Path.GetDirectoryName(firstInput),
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}"
);
new NestWriter(nest).Write(outputFile); new NestWriter(nest).Write(outputFile);
Console.WriteLine($"Saved: {outputFile}"); Console.WriteLine($"Saved: {outputFile}");
@@ -368,8 +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 NFP-based mixed-part autonesting instead of linear fill"
);
Console.Error.WriteLine(
" --keep-parts Don't clear existing parts before filling"
);
Console.Error.WriteLine(
" --check-overlaps Run overlap detection after fill (exit code 1 if found)"
);
Console.Error.WriteLine(" --no-save Skip saving output file"); Console.Error.WriteLine(" --no-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;
} }
} }
+35 -12
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry; using System.Collections.Generic;
using System.Collections.Generic; using OpenNest.Geometry;
namespace OpenNest namespace OpenNest
{ {
@@ -7,7 +7,10 @@ namespace OpenNest
{ {
public static void Vertically(Entity fixedEntity, Entity movableEntity) public static void Vertically(Entity fixedEntity, Entity movableEntity)
{ {
movableEntity.Offset(fixedEntity.BoundingBox.Center.X - movableEntity.BoundingBox.Center.X, 0); movableEntity.Offset(
fixedEntity.BoundingBox.Center.X - movableEntity.BoundingBox.Center.X,
0
);
} }
public static void Vertically(Entity fixedEntity, List<Entity> entities) public static void Vertically(Entity fixedEntity, List<Entity> entities)
@@ -17,7 +20,10 @@ namespace OpenNest
public static void Vertically(Part fixedPart, Part movablePart) public static void Vertically(Part fixedPart, Part movablePart)
{ {
movablePart.Offset(fixedPart.BoundingBox.Center.X - movablePart.BoundingBox.Center.X, 0); movablePart.Offset(
fixedPart.BoundingBox.Center.X - movablePart.BoundingBox.Center.X,
0
);
} }
public static void Vertically(Part fixedPart, List<Part> parts) public static void Vertically(Part fixedPart, List<Part> parts)
@@ -27,7 +33,10 @@ namespace OpenNest
public static void Horizontally(Entity fixedEntity, Entity movableEntity) public static void Horizontally(Entity fixedEntity, Entity movableEntity)
{ {
movableEntity.Offset(0, fixedEntity.BoundingBox.Center.Y - movableEntity.BoundingBox.Center.Y); movableEntity.Offset(
0,
fixedEntity.BoundingBox.Center.Y - movableEntity.BoundingBox.Center.Y
);
} }
public static void Horizontally(Entity fixedEntity, List<Entity> entities) public static void Horizontally(Entity fixedEntity, List<Entity> entities)
@@ -37,7 +46,10 @@ namespace OpenNest
public static void Horizontally(Part fixedPart, Part movablePart) public static void Horizontally(Part fixedPart, Part movablePart)
{ {
movablePart.Offset(0, fixedPart.BoundingBox.Center.Y - movablePart.BoundingBox.Center.Y); movablePart.Offset(
0,
fixedPart.BoundingBox.Center.Y - movablePart.BoundingBox.Center.Y
);
} }
public static void Horizontally(Part fixedPart, List<Part> parts) public static void Horizontally(Part fixedPart, List<Part> parts)
@@ -67,7 +79,10 @@ namespace OpenNest
public static void Right(Entity fixedEntity, Entity movableEntity) public static void Right(Entity fixedEntity, Entity movableEntity)
{ {
movableEntity.Offset(fixedEntity.BoundingBox.Right - movableEntity.BoundingBox.Right, 0); movableEntity.Offset(
fixedEntity.BoundingBox.Right - movableEntity.BoundingBox.Right,
0
);
} }
public static void Right(Entity fixedEntity, List<Entity> entities) public static void Right(Entity fixedEntity, List<Entity> entities)
@@ -107,7 +122,10 @@ namespace OpenNest
public static void Bottom(Entity fixedEntity, Entity movableEntity) public static void Bottom(Entity fixedEntity, Entity movableEntity)
{ {
movableEntity.Offset(0, fixedEntity.BoundingBox.Bottom - movableEntity.BoundingBox.Bottom); movableEntity.Offset(
0,
fixedEntity.BoundingBox.Bottom - movableEntity.BoundingBox.Bottom
);
} }
public static void Bottom(Entity fixedEntity, List<Entity> entities) public static void Bottom(Entity fixedEntity, List<Entity> entities)
@@ -137,14 +155,19 @@ namespace OpenNest
return; return;
var list = new List<Part>(parts); var list = new List<Part>(parts);
list.Sort((p1, p2) => horizontal list.Sort(
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X) (p1, p2) =>
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y)); horizontal
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y)
);
var lastIndex = list.Count - 1; var lastIndex = list.Count - 1;
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y; var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y; var end = horizontal
? list[lastIndex].BoundingBox.Center.X
: list[lastIndex].BoundingBox.Center.Y;
var spacing = (end - start) / lastIndex; var spacing = (end - start) / lastIndex;
+2 -3
View File
@@ -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,
} }
} }
+16 -9
View File
@@ -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()
+1 -1
View File
@@ -4,6 +4,6 @@ namespace OpenNest.Bending
{ {
Unknown, Unknown,
Up, Up,
Down Down,
} }
} }
+19 -12
View File
@@ -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);
} }
} }
} }
+2 -3
View File
@@ -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,
} }
} }
+1 -3
View File
@@ -2,9 +2,7 @@
{ {
public class Comment : ICode public class Comment : ICode
{ {
public Comment() public Comment() { }
{
}
public Comment(string value) public Comment(string value)
{ {
@@ -1,7 +1,7 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -48,7 +48,12 @@ namespace OpenNest.CNC.CuttingStrategy
for (var iter = 0; iter < 3; iter++) for (var iter = 0; iter < 3; iter++)
{ {
var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point; var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point;
perimeterSeed = FindPerimeterIntersection(profile.Perimeter, lastCutoutPt, nextPartStart, out _); perimeterSeed = FindPerimeterIntersection(
profile.Perimeter,
lastCutoutPt,
nextPartStart,
out _
);
orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed); orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed);
orderedCutouts.Reverse(); orderedCutouts.Reverse();
@@ -56,7 +61,12 @@ namespace OpenNest.CNC.CuttingStrategy
} }
var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point; var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point;
perimeterPt = FindPerimeterIntersection(profile.Perimeter, finalLastCutout, nextPartStart, out perimeterEntity); perimeterPt = FindPerimeterIntersection(
profile.Perimeter,
finalLastCutout,
nextPartStart,
out perimeterEntity
);
} }
else else
{ {
@@ -79,18 +89,25 @@ namespace OpenNest.CNC.CuttingStrategy
if (!profile.Perimeter.IsClosed()) if (!profile.Perimeter.IsClosed())
EmitRawContour(result, profile.Perimeter); EmitRawContour(result, profile.Perimeter);
else else
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External); EmitContour(
result,
profile.Perimeter,
perimeterPt,
perimeterEntity,
ContourType.External
);
result.Mode = Mode.Incremental; result.Mode = Mode.Incremental;
return new CuttingResult return new CuttingResult { Program = result, LastCutPoint = perimeterPt };
{
Program = result,
LastCutPoint = perimeterPt
};
} }
public CuttingResult ApplySingle(Program partProgram, Vector point, Entity entity, ContourType contourType) public CuttingResult ApplySingle(
Program partProgram,
Vector point,
Entity entity,
ContourType contourType
)
{ {
var entities = partProgram.ToGeometry(); var entities = partProgram.ToGeometry();
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid); entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
@@ -141,14 +158,14 @@ namespace OpenNest.CNC.CuttingStrategy
result.Mode = Mode.Incremental; result.Mode = Mode.Incremental;
return new CuttingResult return new CuttingResult { Program = result, LastCutPoint = point };
{
Program = result,
LastCutPoint = point
};
} }
private static (Shape Shape, Entity Entity) FindTargetShape(ShapeProfile profile, Vector point, Entity clickedEntity) private static (Shape Shape, Entity Entity) FindTargetShape(
ShapeProfile profile,
Vector point,
Entity clickedEntity
)
{ {
var matched = FindMatchingEntity(profile.Perimeter, clickedEntity); var matched = FindMatchingEntity(profile.Perimeter, clickedEntity);
if (matched != null) if (matched != null)
@@ -190,20 +207,26 @@ namespace OpenNest.CNC.CuttingStrategy
if (shapeEntity is Line sLine && clickedEntity is Line cLine) if (shapeEntity is Line sLine && clickedEntity is Line cLine)
{ {
if (sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon if (
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon) sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon
)
return shapeEntity; return shapeEntity;
} }
else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc) else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc)
{ {
if (System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon if (
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon) System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon
)
return shapeEntity; return shapeEntity;
} }
else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle) else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle)
{ {
if (System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon if (
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon) System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon
)
return shapeEntity; return shapeEntity;
} }
} }
@@ -218,7 +241,10 @@ namespace OpenNest.CNC.CuttingStrategy
program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint)); program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint));
} }
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint) private static List<ContourEntry> ResolveLeadInPoints(
List<Shape> cutouts,
Vector startPoint
)
{ {
var entries = new ContourEntry[cutouts.Count]; var entries = new ContourEntry[cutouts.Count];
var currentPoint = startPoint; var currentPoint = startPoint;
@@ -235,7 +261,12 @@ namespace OpenNest.CNC.CuttingStrategy
return new List<ContourEntry>(entries); return new List<ContourEntry>(entries);
} }
private static Vector FindPerimeterIntersection(Shape perimeter, Vector lastCutout, Vector nextPartStart, out Entity entity) private static Vector FindPerimeterIntersection(
Shape perimeter,
Vector lastCutout,
Vector nextPartStart,
out Entity entity
)
{ {
var ray = new Line(lastCutout, nextPartStart); var ray = new Line(lastCutout, nextPartStart);
@@ -269,7 +300,13 @@ namespace OpenNest.CNC.CuttingStrategy
return HashCode.Combine(r, a); return HashCode.Combine(r, a);
} }
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null) private void EmitContour(
Program program,
Shape shape,
Vector point,
Entity entity,
ContourType? forceType = null
)
{ {
var contourType = forceType ?? DetectContourType(shape); var contourType = forceType ?? DetectContourType(shape);
var winding = DetermineWinding(shape); var winding = DetermineWinding(shape);
@@ -289,7 +326,8 @@ namespace OpenNest.CNC.CuttingStrategy
var outwardAngle = normal - System.Math.PI; var outwardAngle = normal - System.Math.PI;
point = new Vector( point = new Vector(
circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle), circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle),
circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle)); circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle)
);
} }
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal); leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
@@ -297,7 +335,10 @@ namespace OpenNest.CNC.CuttingStrategy
// Build hole sub-program relative to (0,0) // Build hole sub-program relative to (0,0)
var holeCenter = circle.Center; var holeCenter = circle.Center;
var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y); var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y);
var relativeCircle = new Circle(new Vector(0, 0), circle.Radius) { Rotation = circle.Rotation }; var relativeCircle = new Circle(new Vector(0, 0), circle.Radius)
{
Rotation = circle.Rotation,
};
var relativeShape = new Shape(); var relativeShape = new Shape();
relativeShape.Entities.Add(relativeCircle); relativeShape.Entities.Add(relativeCircle);
@@ -314,12 +355,14 @@ namespace OpenNest.CNC.CuttingStrategy
if (!program.SubPrograms.ContainsKey(key)) if (!program.SubPrograms.ContainsKey(key))
program.SubPrograms[key] = subPgm; program.SubPrograms[key] = subPgm;
program.Codes.Add(new SubProgramCall program.Codes.Add(
{ new SubProgramCall
Id = key, {
Program = program.SubPrograms[key], Id = key,
Offset = holeCenter Program = program.SubPrograms[key],
}); Offset = holeCenter,
}
);
return; return;
} }
@@ -328,7 +371,11 @@ namespace OpenNest.CNC.CuttingStrategy
var reindexedShape = shape.ReindexAt(point, entity); var reindexedShape = shape.ReindexAt(point, entity);
if (Parameters.TabsEnabled && Parameters.TabConfig != null && contourType == ContourType.External) if (
Parameters.TabsEnabled
&& Parameters.TabConfig != null
&& contourType == ContourType.External
)
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size); reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point)); program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
@@ -337,7 +384,8 @@ namespace OpenNest.CNC.CuttingStrategy
private void EmitScribeContours(Program program, List<Entity> scribeEntities) private void EmitScribeContours(Program program, List<Entity> scribeEntities)
{ {
if (scribeEntities.Count == 0) return; if (scribeEntities.Count == 0)
return;
var shapes = ShapeBuilder.GetShapes(scribeEntities); var shapes = ShapeBuilder.GetShapes(scribeEntities);
foreach (var shape in shapes) foreach (var shape in shapes)
@@ -388,8 +436,12 @@ namespace OpenNest.CNC.CuttingStrategy
return ContourType.Internal; return ContourType.Internal;
} }
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType, public static double ComputeNormal(
RotationType winding = RotationType.CW) Vector point,
Entity entity,
ContourType contourType,
RotationType winding = RotationType.CW
)
{ {
double normal; double normal;
@@ -442,7 +494,12 @@ namespace OpenNest.CNC.CuttingStrategy
return polygon.RotationDirection(); return polygon.RotationDirection();
} }
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle) private LeadIn ClampLeadInForCircle(
LeadIn leadIn,
Circle circle,
Vector contourPoint,
double normalAngle
)
{ {
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0) if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
return leadIn; return leadIn;
@@ -492,7 +549,7 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn, ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn,
ContourType.Internal => Parameters.InternalLeadIn, ContourType.Internal => Parameters.InternalLeadIn,
_ => Parameters.ExternalLeadIn _ => Parameters.ExternalLeadIn,
}; };
} }
@@ -502,7 +559,7 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut, ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut,
ContourType.Internal => Parameters.InternalLeadOut, ContourType.Internal => Parameters.InternalLeadOut,
_ => Parameters.ExternalLeadOut _ => Parameters.ExternalLeadOut,
}; };
} }
@@ -565,12 +622,18 @@ namespace OpenNest.CNC.CuttingStrategy
private static Vector EntityStartPoint(Entity entity) private static Vector EntityStartPoint(Entity entity)
{ {
if (entity is Line line) return line.StartPoint; if (entity is Line line)
if (entity is Arc arc) return arc.StartPoint(); return line.StartPoint;
if (entity is Arc arc)
return arc.StartPoint();
return Vector.Zero; return Vector.Zero;
} }
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display) private List<ICode> ConvertShapeToMoves(
Shape shape,
Vector startPoint,
LayerType layer = LayerType.Display
)
{ {
var moves = new List<ICode>(); var moves = new List<ICode>();
@@ -582,15 +645,28 @@ namespace OpenNest.CNC.CuttingStrategy
} }
else if (entity is Arc arc) else if (entity is Arc arc)
{ {
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer }); moves.Add(
new ArcMove(
arc.EndPoint(),
arc.Center,
arc.IsReversed ? RotationType.CW : RotationType.CCW
)
{
Layer = layer,
}
);
} }
else if (entity is Circle circle) else if (entity is Circle circle)
{ {
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer }); moves.Add(
new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer }
);
} }
else else
{ {
throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}"); throw new System.InvalidOperationException(
$"Unsupported entity type: {entity.Type}"
);
} }
} }
@@ -600,9 +676,12 @@ namespace OpenNest.CNC.CuttingStrategy
private static Vector GetShapeStartPoint(Shape shape) private static Vector GetShapeStartPoint(Shape shape)
{ {
var first = shape.Entities[0]; var first = shape.Entities[0];
if (first is Line line) return line.StartPoint; if (first is Line line)
if (first is Arc arc) return arc.StartPoint(); return line.StartPoint;
if (first is Circle circle) return new Vector(circle.Center.X + circle.Radius, circle.Center.Y); if (first is Arc arc)
return arc.StartPoint();
if (first is Circle circle)
return new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
return Vector.Zero; return Vector.Zero;
} }
} }
@@ -4,6 +4,6 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
External, External,
Internal, Internal,
ArcCircle ArcCircle,
} }
} }
@@ -15,7 +15,8 @@ namespace OpenNest.CNC.CuttingStrategy
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn(); public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut(); public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }; public LeadIn InternalLeadIn { get; set; } =
new LineLeadIn { Length = 0.125, ApproachAngle = 90 };
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut(); public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn(); public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -7,19 +7,23 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
public double Radius { get; set; } public double Radius { get; set; }
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
var arcCenter = new Vector( var arcCenter = new Vector(
contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle), contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle),
contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle)); contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
}; };
} }
@@ -30,10 +34,10 @@ namespace OpenNest.CNC.CuttingStrategy
return new Vector( return new Vector(
arcCenterX + Radius * System.Math.Cos(contourNormalAngle), arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
arcCenterY + Radius * System.Math.Sin(contourNormalAngle)); arcCenterY + Radius * System.Math.Sin(contourNormalAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) => new ArcLeadIn { Radius = Radius * factor };
new ArcLeadIn { Radius = Radius * factor };
} }
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double ArcRadius { get; set; } public double ArcRadius { get; set; }
public double Kerf { get; set; } public double Kerf { get; set; }
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
@@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0); var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
var arcStart = new Vector( var arcStart = new Vector(
arcCenterX + ArcRadius * System.Math.Cos(lineAngle), arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
arcCenterY + ArcRadius * System.Math.Sin(lineAngle)); arcCenterY + ArcRadius * System.Math.Sin(lineAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(arcStart) { Layer = LayerType.Leadin }, new LinearMove(arcStart) { Layer = LayerType.Leadin },
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
}; };
} }
@@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy
return new Vector( return new Vector(
arcStartX + LineLength * System.Math.Cos(lineAngle), arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle)); arcStartY + LineLength * System.Math.Sin(lineAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf }; new CleanHoleLeadIn
{
LineLength = LineLength * factor,
ArcRadius = ArcRadius * factor,
Kerf = Kerf,
};
} }
} }
@@ -1,12 +1,15 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public abstract class LeadIn public abstract class LeadIn
{ {
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public abstract List<ICode> Generate(
RotationType winding = RotationType.CW); Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle); public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double ApproachAngle { get; set; } = 135.0; public double ApproachAngle { get; set; } = 135.0;
public double ArcRadius { get; set; } public double ArcRadius { get; set; }
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
@@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle); var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
var arcStart = new Vector( var arcStart = new Vector(
arcCenterX + ArcRadius * System.Math.Cos(lineAngle), arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
arcCenterY + ArcRadius * System.Math.Sin(lineAngle)); arcCenterY + ArcRadius * System.Math.Sin(lineAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(arcStart) { Layer = LayerType.Leadin }, new LinearMove(arcStart) { Layer = LayerType.Leadin },
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
}; };
} }
@@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy
return new Vector( return new Vector(
arcStartX + LineLength * System.Math.Cos(lineAngle), arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle)); arcStartY + LineLength * System.Math.Sin(lineAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle }; new LineArcLeadIn
{
LineLength = LineLength * factor,
ArcRadius = ArcRadius * factor,
ApproachAngle = ApproachAngle,
};
} }
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -9,15 +9,18 @@ namespace OpenNest.CNC.CuttingStrategy
public double Length { get; set; } public double Length { get; set; }
public double ApproachAngle { get; set; } = 90.0; public double ApproachAngle { get; set; } = 90.0;
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin } new LinearMove(contourStartPoint) { Layer = LayerType.Leadin },
}; };
} }
@@ -26,7 +29,8 @@ namespace OpenNest.CNC.CuttingStrategy
var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle); var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle);
return new Vector( return new Vector(
contourStartPoint.X + Length * System.Math.Cos(approachAngle), contourStartPoint.X + Length * System.Math.Cos(approachAngle),
contourStartPoint.Y + Length * System.Math.Sin(approachAngle)); contourStartPoint.Y + Length * System.Math.Sin(approachAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -11,21 +11,25 @@ namespace OpenNest.CNC.CuttingStrategy
public double Length2 { get; set; } public double Length2 { get; set; }
public double ApproachAngle2 { get; set; } = 90.0; public double ApproachAngle2 { get; set; } = 90.0;
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1); var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
var midPoint = new Vector( var midPoint = new Vector(
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle), contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle)); contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(midPoint) { Layer = LayerType.Leadin }, new LinearMove(midPoint) { Layer = LayerType.Leadin },
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin } new LinearMove(contourStartPoint) { Layer = LayerType.Leadin },
}; };
} }
@@ -38,10 +42,17 @@ namespace OpenNest.CNC.CuttingStrategy
var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2); var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2);
return new Vector( return new Vector(
midX + Length1 * System.Math.Cos(firstAngle), midX + Length1 * System.Math.Cos(firstAngle),
midY + Length1 * System.Math.Sin(firstAngle)); midY + Length1 * System.Math.Sin(firstAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 }; new LineLineLeadIn
{
Length1 = Length1 * factor,
ApproachAngle1 = ApproachAngle1,
Length2 = Length2 * factor,
ApproachAngle2 = ApproachAngle2,
};
} }
} }
@@ -1,17 +1,17 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public class NoLeadIn : LeadIn public class NoLeadIn : LeadIn
{ {
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
return new List<ICode> return new List<ICode> { new RapidMove(contourStartPoint) };
{
new RapidMove(contourStartPoint)
};
} }
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle) public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -7,8 +7,11 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
public double Radius { get; set; } public double Radius { get; set; }
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle); var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle);
var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle); var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
@@ -16,11 +19,12 @@ namespace OpenNest.CNC.CuttingStrategy
var endPoint = new Vector( var endPoint = new Vector(
arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2), arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2),
arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2)); arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2)
);
return new List<ICode> return new List<ICode>
{ {
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout } new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout },
}; };
} }
} }
@@ -1,11 +1,14 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public abstract class LeadOut public abstract class LeadOut
{ {
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public abstract List<ICode> Generate(
RotationType winding = RotationType.CW); Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
} }
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -9,18 +9,19 @@ namespace OpenNest.CNC.CuttingStrategy
public double Length { get; set; } public double Length { get; set; }
public double ApproachAngle { get; set; } = 90.0; public double ApproachAngle { get; set; } = 90.0;
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle); var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle);
var endPoint = new Vector( var endPoint = new Vector(
contourEndPoint.X + Length * System.Math.Cos(overcutAngle), contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle)); contourEndPoint.Y + Length * System.Math.Sin(overcutAngle)
);
return new List<ICode> return new List<ICode> { new LinearMove(endPoint) { Layer = LayerType.Leadout } };
{
new LinearMove(endPoint) { Layer = LayerType.Leadout }
};
} }
} }
} }
@@ -1,12 +1,15 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public class NoLeadOut : LeadOut public class NoLeadOut : LeadOut
{ {
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
return new List<ICode>(); return new List<ICode>();
} }
@@ -9,7 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
BottomSide = 4, BottomSide = 4,
EdgeStart = 5, EdgeStart = 5,
LeftSide = 7, LeftSide = 7,
RightSideAlt = 8 RightSideAlt = 8,
} }
public class SequenceParameters public class SequenceParameters
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double BreakerAngle { get; set; } public double BreakerAngle { get; set; }
public override List<ICode> Generate( public override List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW) Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var codes = new List<ICode>(); var codes = new List<ICode>();
@@ -21,7 +24,8 @@ namespace OpenNest.CNC.CuttingStrategy
var scoreAngle = contourNormalAngle + System.Math.PI; var scoreAngle = contourNormalAngle + System.Math.PI;
var scoreEnd = new Vector( var scoreEnd = new Vector(
tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle), tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle),
tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle)); tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle)
);
codes.Add(new LinearMove(scoreEnd)); codes.Add(new LinearMove(scoreEnd));
codes.Add(new RapidMove(tabEndPoint)); codes.Add(new RapidMove(tabEndPoint));
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -8,13 +8,13 @@ namespace OpenNest.CNC.CuttingStrategy
public int MachineTabId { get; set; } public int MachineTabId { get; set; }
public override List<ICode> Generate( public override List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW) Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
return new List<ICode> return new List<ICode> { new RapidMove(tabEndPoint) };
{
new RapidMove(tabEndPoint)
};
} }
} }
} }
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -11,8 +11,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double CutoutMaxHeight { get; set; } public double CutoutMaxHeight { get; set; }
public override List<ICode> Generate( public override List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW) Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var codes = new List<ICode>(); var codes = new List<ICode>();
@@ -29,8 +32,10 @@ namespace OpenNest.CNC.CuttingStrategy
public bool AppliesToCutout(double cutoutWidth, double cutoutHeight) public bool AppliesToCutout(double cutoutWidth, double cutoutHeight)
{ {
return cutoutWidth >= CutoutMinWidth && cutoutWidth <= CutoutMaxWidth return cutoutWidth >= CutoutMinWidth
&& cutoutHeight >= CutoutMinHeight && cutoutHeight <= CutoutMaxHeight; && cutoutWidth <= CutoutMaxWidth
&& cutoutHeight >= CutoutMinHeight
&& cutoutHeight <= CutoutMaxHeight;
} }
} }
} }
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,7 +10,10 @@ namespace OpenNest.CNC.CuttingStrategy
public LeadOut TabLeadOut { get; set; } public LeadOut TabLeadOut { get; set; }
public abstract List<ICode> Generate( public abstract List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW); Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
} }
} }
+1 -3
View File
@@ -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)
{ {
+2 -3
View File
@@ -1,10 +1,9 @@
 namespace OpenNest.CNC
namespace OpenNest.CNC
{ {
public enum KerfType public enum KerfType
{ {
None, None,
Left, Left,
Right Right,
} }
} }
+2 -3
View File
@@ -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,
} }
} }
+4 -7
View File
@@ -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,
}; };
} }
+2 -3
View File
@@ -1,9 +1,8 @@
 namespace OpenNest.CNC
namespace OpenNest.CNC
{ {
public enum Mode public enum Mode
{ {
Absolute, Absolute,
Incremental Incremental,
} }
} }
+186 -175
View File
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using OpenNest.Converters; using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System;
using System.Collections.Generic;
namespace OpenNest.CNC namespace OpenNest.CNC
{ {
@@ -10,7 +10,8 @@ namespace OpenNest.CNC
{ {
public List<ICode> Codes; public List<ICode> Codes;
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, VariableDefinition> Variables { get; } =
new(StringComparer.OrdinalIgnoreCase);
public Dictionary<int, Program> SubPrograms { get; } = new(); public Dictionary<int, Program> SubPrograms { get; } = new();
@@ -66,9 +67,17 @@ namespace OpenNest.CNC
{ {
if (code is Motion m) if (code is Motion m)
{ {
var cmd = m is RapidMove ? "G00" : (m is ArcMove am ? (am.Rotation == RotationType.CW ? "G02" : "G03") : "G01"); var cmd =
m is RapidMove
? "G00"
: (
m is ArcMove am
? (am.Rotation == RotationType.CW ? "G02" : "G03")
: "G01"
);
sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}"); sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}");
if (m is ArcMove arc) sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}"); if (m is ArcMove arc)
sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}");
sb.AppendLine(); sb.AppendLine();
} }
} }
@@ -97,7 +106,8 @@ namespace OpenNest.CNC
var dy = subpgm.Offset.Y - origin.Y; var dy = subpgm.Offset.Y - origin.Y;
subpgm.Offset = new Geometry.Vector( subpgm.Offset = new Geometry.Vector(
origin.X + dx * cos - dy * sin, origin.X + dx * cos - dy * sin,
origin.Y + dx * sin + dy * cos); origin.Y + dx * sin + dy * cos
);
} }
if (subpgm.Program != null) if (subpgm.Program != null)
@@ -130,8 +140,7 @@ namespace OpenNest.CNC
if (code is SubProgramCall subpgm) if (code is SubProgramCall subpgm)
{ {
subpgm.Offset = new Geometry.Vector( subpgm.Offset = new Geometry.Vector(subpgm.Offset.X + x, subpgm.Offset.Y + y);
subpgm.Offset.X + x, subpgm.Offset.Y + y);
} }
if (code is Motion == false) if (code is Motion == false)
@@ -159,7 +168,9 @@ namespace OpenNest.CNC
if (code is SubProgramCall subpgm) if (code is SubProgramCall subpgm)
{ {
subpgm.Offset = new Geometry.Vector( subpgm.Offset = new Geometry.Vector(
subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y); subpgm.Offset.X + voffset.X,
subpgm.Offset.Y + voffset.Y
);
} }
if (code is Motion == false) if (code is Motion == false)
@@ -258,35 +269,37 @@ namespace OpenNest.CNC
switch (Mode) switch (Mode)
{ {
case Mode.Absolute: case Mode.Absolute:
{
for (int i = Codes.Count; i >= 0; --i)
{ {
for (int i = Codes.Count; i >= 0; --i) var code = Codes[i];
{ var motion = code as Motion;
var code = Codes[i];
var motion = code as Motion;
if (motion == null) continue; if (motion == null)
continue;
return motion.EndPoint; return motion.EndPoint;
}
break;
} }
break;
}
case Mode.Incremental: case Mode.Incremental:
{
var pos = new Vector(0, 0);
for (int i = 0; i < Codes.Count; ++i)
{ {
var pos = new Vector(0, 0); var code = Codes[i];
var motion = code as Motion;
for (int i = 0; i < Codes.Count; ++i) if (motion == null)
{ continue;
var code = Codes[i];
var motion = code as Motion;
if (motion == null) continue; pos += motion.EndPoint;
pos += motion.EndPoint;
}
return pos;
} }
return pos;
}
} }
return new Vector(0, 0); return new Vector(0, 0);
@@ -316,161 +329,163 @@ namespace OpenNest.CNC
switch (code.Type) switch (code.Type)
{ {
case CodeType.LinearMove: case CodeType.LinearMove:
{ {
var line = (LinearMove)code; var line = (LinearMove)code;
var pt = Mode == Mode.Absolute ? var pt =
frameOrigin + line.EndPoint : Mode == Mode.Absolute
line.EndPoint + pos;
if (pt.X > maxX)
maxX = pt.X;
else if (pt.X < minX)
minX = pt.X;
if (pt.Y > maxY)
maxY = pt.Y;
else if (pt.Y < minY)
minY = pt.Y;
pos = pt;
break;
}
case CodeType.RapidMove:
{
var line = (RapidMove)code;
var pt = Mode == Mode.Absolute
? frameOrigin + line.EndPoint ? frameOrigin + line.EndPoint
: line.EndPoint + pos; : line.EndPoint + pos;
if (pt.X > maxX) if (pt.X > maxX)
maxX = pt.X; maxX = pt.X;
else if (pt.X < minX) else if (pt.X < minX)
minX = pt.X; minX = pt.X;
if (pt.Y > maxY) if (pt.Y > maxY)
maxY = pt.Y; maxY = pt.Y;
else if (pt.Y < minY) else if (pt.Y < minY)
minY = pt.Y; minY = pt.Y;
pos = pt; pos = pt;
break; break;
} }
case CodeType.RapidMove:
{
var line = (RapidMove)code;
var pt =
Mode == Mode.Absolute
? frameOrigin + line.EndPoint
: line.EndPoint + pos;
if (pt.X > maxX)
maxX = pt.X;
else if (pt.X < minX)
minX = pt.X;
if (pt.Y > maxY)
maxY = pt.Y;
else if (pt.Y < minY)
minY = pt.Y;
pos = pt;
break;
}
case CodeType.ArcMove: case CodeType.ArcMove:
{
var arc = (ArcMove)code;
var radius = arc.CenterPoint.DistanceTo(arc.EndPoint);
Vector endpt;
Vector centerpt;
if (Mode == Mode.Incremental)
{ {
var arc = (ArcMove)code; endpt = arc.EndPoint + pos;
var radius = arc.CenterPoint.DistanceTo(arc.EndPoint); centerpt = arc.CenterPoint + pos;
Vector endpt;
Vector centerpt;
if (Mode == Mode.Incremental)
{
endpt = arc.EndPoint + pos;
centerpt = arc.CenterPoint + pos;
}
else
{
endpt = frameOrigin + arc.EndPoint;
centerpt = frameOrigin + arc.CenterPoint;
}
double minX1;
double minY1;
double maxX1;
double maxY1;
if (pos.X < endpt.X)
{
minX1 = pos.X;
maxX1 = endpt.X;
}
else
{
minX1 = endpt.X;
maxX1 = pos.X;
}
if (pos.Y < endpt.Y)
{
minY1 = pos.Y;
maxY1 = endpt.Y;
}
else
{
minY1 = endpt.Y;
maxY1 = pos.Y;
}
var startAngle = pos.AngleFrom(centerpt);
var endAngle = endpt.AngleFrom(centerpt);
// switch the angle to counter clockwise.
if (arc.Rotation == RotationType.CW)
Generic.Swap(ref startAngle, ref endAngle);
startAngle = Angle.NormalizeRad(startAngle);
endAngle = Angle.NormalizeRad(endAngle);
if (Angle.IsBetweenRad(Angle.HalfPI, startAngle, endAngle))
maxY1 = centerpt.Y + radius;
if (Angle.IsBetweenRad(System.Math.PI, startAngle, endAngle))
minX1 = centerpt.X - radius;
const double oneHalfPI = System.Math.PI * 1.5;
if (Angle.IsBetweenRad(oneHalfPI, startAngle, endAngle))
minY1 = centerpt.Y - radius;
if (Angle.IsBetweenRad(Angle.TwoPI, startAngle, endAngle))
maxX1 = centerpt.X + radius;
if (maxX1 > maxX)
maxX = maxX1;
if (minX1 < minX)
minX = minX1;
if (maxY1 > maxY)
maxY = maxY1;
if (minY1 < minY)
minY = minY1;
pos = endpt;
break;
} }
else
{
endpt = frameOrigin + arc.EndPoint;
centerpt = frameOrigin + arc.CenterPoint;
}
double minX1;
double minY1;
double maxX1;
double maxY1;
if (pos.X < endpt.X)
{
minX1 = pos.X;
maxX1 = endpt.X;
}
else
{
minX1 = endpt.X;
maxX1 = pos.X;
}
if (pos.Y < endpt.Y)
{
minY1 = pos.Y;
maxY1 = endpt.Y;
}
else
{
minY1 = endpt.Y;
maxY1 = pos.Y;
}
var startAngle = pos.AngleFrom(centerpt);
var endAngle = endpt.AngleFrom(centerpt);
// switch the angle to counter clockwise.
if (arc.Rotation == RotationType.CW)
Generic.Swap(ref startAngle, ref endAngle);
startAngle = Angle.NormalizeRad(startAngle);
endAngle = Angle.NormalizeRad(endAngle);
if (Angle.IsBetweenRad(Angle.HalfPI, startAngle, endAngle))
maxY1 = centerpt.Y + radius;
if (Angle.IsBetweenRad(System.Math.PI, startAngle, endAngle))
minX1 = centerpt.X - radius;
const double oneHalfPI = System.Math.PI * 1.5;
if (Angle.IsBetweenRad(oneHalfPI, startAngle, endAngle))
minY1 = centerpt.Y - radius;
if (Angle.IsBetweenRad(Angle.TwoPI, startAngle, endAngle))
maxX1 = centerpt.X + radius;
if (maxX1 > maxX)
maxX = maxX1;
if (minX1 < minX)
minX = minX1;
if (maxY1 > maxY)
maxY = maxY1;
if (minY1 < minY)
minY = minY1;
pos = endpt;
break;
}
case CodeType.SubProgramCall: case CodeType.SubProgramCall:
{ {
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
if (subpgm.Program == null) if (subpgm.Program == null)
break;
// Sub-program frame origin in this program's frame
// is frameOrigin + Offset, regardless of current pos.
pos = frameOrigin + subpgm.Offset;
var box = subpgm.Program.BoundingBox(ref pos);
if (box.Left < minX)
minX = box.Left;
if (box.Right > maxX)
maxX = box.Right;
if (box.Bottom < minY)
minY = box.Bottom;
if (box.Top > maxY)
maxY = box.Top;
break; break;
}
// Sub-program frame origin in this program's frame
// is frameOrigin + Offset, regardless of current pos.
pos = frameOrigin + subpgm.Offset;
var box = subpgm.Program.BoundingBox(ref pos);
if (box.Left < minX)
minX = box.Left;
if (box.Right > maxX)
maxX = box.Right;
if (box.Bottom < minY)
minY = box.Bottom;
if (box.Top > maxY)
maxY = box.Top;
break;
}
} }
} }
@@ -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];
+2 -2
View File
@@ -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();
+12 -5
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC namespace OpenNest.CNC
{ {
@@ -36,7 +36,13 @@ namespace OpenNest.CNC
return basePos; return basePos;
} }
private static void Walk(Program pgm, Vector basePos, ref Vector pos, bool skipFirst, List<Segment> results) private static void Walk(
Program pgm,
Vector basePos,
ref Vector pos,
bool skipFirst,
List<Segment> results
)
{ {
var skipped = !skipFirst; var skipped = !skipFirst;
@@ -60,9 +66,10 @@ namespace OpenNest.CNC
} }
else if (code is Motion motion) else if (code is Motion motion)
{ {
var endpt = pgm.Mode == Mode.Incremental var endpt =
? motion.EndPoint + pos pgm.Mode == Mode.Incremental
: motion.EndPoint + basePos; ? motion.EndPoint + pos
: motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove) if (code.Type == CodeType.RapidMove)
{ {
+2 -1
View File
@@ -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,
}; };
} }
+1 -3
View File
@@ -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)
{ {
+7 -2
View File
@@ -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;
+3 -2
View File
@@ -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> { }
{
}
} }
+17 -6
View File
@@ -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())
+35 -9
View File
@@ -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;
}
} }
} }
+53 -15
View File
@@ -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
View File
@@ -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();
+1 -1
View File
@@ -3,7 +3,7 @@ namespace OpenNest
public enum CutDirection public enum CutDirection
{ {
TowardOrigin, TowardOrigin,
AwayFromOrigin AwayFromOrigin,
} }
public class CutOffSettings public class CutOffSettings
+8 -7
View File
@@ -11,11 +11,12 @@ public class CutParameters
public string PostProcessor { get; set; } public string PostProcessor { get; set; }
public Units Units { get; set; } public Units Units { get; set; }
public static CutParameters Default => new() public static CutParameters Default =>
{ new()
Feedrate = 100, {
RapidTravelRate = 300, Feedrate = 100,
PierceTime = TimeSpan.FromSeconds(0.5), RapidTravelRate = 300,
Units = OpenNest.Units.Inches PierceTime = TimeSpan.FromSeconds(0.5),
}; Units = OpenNest.Units.Inches,
};
} }
+22 -24
View File
@@ -1,12 +1,12 @@
using OpenNest.Bending; using System;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest namespace OpenNest
{ {
@@ -18,18 +18,18 @@ namespace OpenNest
public static Color[] PartColors = new Color[] public static Color[] PartColors = new Color[]
{ {
Color.FromArgb(205, 92, 92), // Indian Red Color.FromArgb(205, 92, 92), // Indian Red
Color.FromArgb(148, 103, 189), // Medium Purple Color.FromArgb(148, 103, 189), // Medium Purple
Color.FromArgb(75, 180, 175), // Teal Color.FromArgb(75, 180, 175), // Teal
Color.FromArgb(210, 190, 75), // Goldenrod Color.FromArgb(210, 190, 75), // Goldenrod
Color.FromArgb(190, 85, 175), // Orchid Color.FromArgb(190, 85, 175), // Orchid
Color.FromArgb(185, 115, 85), // Sienna Color.FromArgb(185, 115, 85), // Sienna
Color.FromArgb(120, 100, 190), // Slate Blue Color.FromArgb(120, 100, 190), // Slate Blue
Color.FromArgb(200, 100, 140), // Rose Color.FromArgb(200, 100, 140), // Rose
Color.FromArgb(80, 175, 155), // Sea Green Color.FromArgb(80, 175, 155), // Sea Green
Color.FromArgb(195, 160, 85), // Dark Khaki Color.FromArgb(195, 160, 85), // Dark Khaki
Color.FromArgb(175, 95, 160), // Plum Color.FromArgb(175, 95, 160), // Plum
Color.FromArgb(215, 130, 130), // Light Coral Color.FromArgb(215, 130, 130), // Light Coral
}; };
public static Color GetNextColor() public static Color GetNextColor()
@@ -40,14 +40,10 @@ namespace OpenNest
} }
public Drawing() public Drawing()
: this(string.Empty, new Program()) : this(string.Empty, new Program()) { }
{
}
public Drawing(string name) public Drawing(string name)
: this(name, new Program()) : this(name, new Program()) { }
{
}
public Drawing(string name, Program pgm) public Drawing(string name, Program pgm)
{ {
@@ -127,7 +123,9 @@ namespace OpenNest
public void UpdateArea() public void UpdateArea()
{ {
var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid); var geometry = ConvertProgram
.ToGeometry(Program)
.Where(entity => entity.Layer != SpecialLayers.Rapid);
var shapes = ShapeBuilder.GetShapes(geometry); var shapes = ShapeBuilder.GetShapes(geometry);
if (shapes.Count == 0) if (shapes.Count == 0)
+38 -30
View File
@@ -1,6 +1,6 @@
using OpenNest.Math; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Math;
namespace OpenNest.Geometry namespace OpenNest.Geometry
{ {
@@ -12,16 +12,18 @@ namespace OpenNest.Geometry
private Vector center; private Vector center;
private bool reversed; private bool reversed;
public Arc() public Arc() { }
{
}
public Arc(double x, double y, double r, double a1, double a2, bool reversed = false) public Arc(double x, double y, double r, double a1, double a2, bool reversed = false)
: this(new Vector(x, y), r, a1, a2, reversed) : this(new Vector(x, y), r, a1, a2, reversed) { }
{
}
public Arc(Vector center, double radius, double startAngle, double endAngle, bool reversed = false) public Arc(
Vector center,
double radius,
double startAngle,
double endAngle,
bool reversed = false
)
{ {
this.center = center; this.center = center;
this.radius = radius; this.radius = radius;
@@ -93,8 +95,7 @@ namespace OpenNest.Geometry
} }
} }
public bool IsFullCircle() => public bool IsFullCircle() => SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon;
/// <summary> /// <summary>
/// Angle in radians between start and end angles. /// Angle in radians between start and end angles.
@@ -130,10 +131,7 @@ namespace OpenNest.Geometry
public RotationType Rotation public RotationType Rotation
{ {
get { return IsReversed ? RotationType.CW : RotationType.CCW; } get { return IsReversed ? RotationType.CW : RotationType.CCW; }
set set { IsReversed = (value == RotationType.CW); }
{
IsReversed = (value == RotationType.CW);
}
} }
/// <summary> /// <summary>
@@ -144,7 +142,8 @@ namespace OpenNest.Geometry
{ {
return new Vector( return new Vector(
Center.X + Radius * System.Math.Cos(StartAngle), Center.X + Radius * System.Math.Cos(StartAngle),
Center.Y + Radius * System.Math.Sin(StartAngle)); Center.Y + Radius * System.Math.Sin(StartAngle)
);
} }
/// <summary> /// <summary>
@@ -155,7 +154,8 @@ namespace OpenNest.Geometry
{ {
return new Vector( return new Vector(
Center.X + Radius * System.Math.Cos(EndAngle), Center.X + Radius * System.Math.Cos(EndAngle),
Center.Y + Radius * System.Math.Sin(EndAngle)); Center.Y + Radius * System.Math.Sin(EndAngle)
);
} }
/// <summary> /// <summary>
@@ -166,7 +166,8 @@ namespace OpenNest.Geometry
var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2); var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
return new Vector( return new Vector(
Center.X + Radius * System.Math.Cos(midAngle), Center.X + Radius * System.Math.Cos(midAngle),
Center.Y + Radius * System.Math.Sin(midAngle)); Center.Y + Radius * System.Math.Sin(midAngle)
);
} }
/// <summary> /// <summary>
@@ -231,7 +232,10 @@ namespace OpenNest.Geometry
return 1; return 1;
var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius); var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius);
return System.Math.Max(1, (int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle)); return System.Math.Max(
1,
(int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle)
);
} }
/// <summary> /// <summary>
@@ -242,21 +246,23 @@ namespace OpenNest.Geometry
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false) public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{ {
var points = new List<Vector>(); var points = new List<Vector>();
var stepAngle = reversed var stepAngle = reversed ? -SweepAngle() / segments : SweepAngle() / segments;
? -SweepAngle() / segments
: SweepAngle() / segments;
var r = circumscribe && segments > 0 var r =
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0) circumscribe && segments > 0
: Radius; ? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
: Radius;
for (int i = 0; i <= segments; ++i) for (int i = 0; i <= segments; ++i)
{ {
var angle = stepAngle * i + StartAngle; var angle = stepAngle * i + StartAngle;
points.Add(new Vector( points.Add(
System.Math.Cos(angle) * r + Center.X, new Vector(
System.Math.Sin(angle) * r + Center.Y)); System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y
)
);
} }
return points; return points;
@@ -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>
+55 -41
View File
@@ -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;
} }
+24 -12
View File
@@ -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);
+21 -13
View File
@@ -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)
+25 -29
View File
@@ -1,5 +1,5 @@
using OpenNest.Math; using System.Collections.Generic;
using System.Collections.Generic; using OpenNest.Math;
namespace OpenNest.Geometry namespace OpenNest.Geometry
{ {
@@ -8,14 +8,10 @@ namespace OpenNest.Geometry
private Vector center; private Vector center;
private double radius; private double radius;
public Circle() public Circle() { }
{
}
public Circle(double x, double y, double radius) public Circle(double x, double y, double radius)
: this(new Vector(x, y), radius) : this(new Vector(x, y), radius) { }
{
}
public Circle(Vector center, double radius) public Circle(Vector center, double radius)
{ {
@@ -137,21 +133,22 @@ namespace OpenNest.Geometry
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false) public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{ {
var points = new List<Vector>(); var points = new List<Vector>();
var stepAngle = Rotation == RotationType.CW var stepAngle =
? -Angle.TwoPI / segments Rotation == RotationType.CW ? -Angle.TwoPI / segments : Angle.TwoPI / segments;
: Angle.TwoPI / segments;
var r = circumscribe && segments > 0 var r =
? Radius / System.Math.Cos(stepAngle / 2.0) circumscribe && segments > 0 ? Radius / System.Math.Cos(stepAngle / 2.0) : Radius;
: Radius;
for (int i = 0; i <= segments; ++i) for (int i = 0; i <= segments; ++i)
{ {
var angle = stepAngle * i; var angle = stepAngle * i;
points.Add(new Vector( points.Add(
System.Math.Cos(angle) * r + Center.X, new Vector(
System.Math.Sin(angle) * r + Center.Y)); System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y
)
);
} }
return points; return points;
@@ -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>
+109 -42
View File
@@ -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++)
@@ -231,7 +244,7 @@ namespace OpenNest.Geometry
private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point) private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point)
{ {
return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y) return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y)
- (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X); - (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
} }
/// <summary> /// <summary>
@@ -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))
foreach (var pieceTri in pieceTris)
{ {
var inside = ClipConvex(pieceTri, holeTri); next.Add(piece);
if (inside == null) continue;
{ }
// No overlap with hole - keep
next.Add(pieceTri); foreach (var pieceTri in TriangulateWithBounds(piece))
} {
else if (inside.Area() < pieceTri.Area() - Tolerance.Epsilon) var holeVerts = holeTri.Vertices;
{ var holeCount = holeTri.IsClosed() ? holeVerts.Count - 1 : holeVerts.Count;
// Partial overlap - keep the piece (conservative) var survived = false;
next.Add(pieceTri); for (var i = 0; i < holeCount; i++)
} survived |= AddIfPositiveArea(
// else: fully inside hole - discard next,
ClipOutsideHalfSpace(
pieceTri,
holeVerts[i],
holeVerts[(i + 1) % holeCount]
)
);
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 &lt; -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;
}
} }
} }
+10 -2
View File
@@ -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;
+13 -4
View File
@@ -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)
+8 -2
View File
@@ -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);
+140 -39
View File
@@ -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;
+7 -3
View File
@@ -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)
+2 -3
View File
@@ -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,
} }
} }
+88 -47
View File
@@ -1,21 +1,25 @@
using OpenNest.Math;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenNest.Math;
namespace OpenNest.Geometry namespace OpenNest.Geometry
{ {
public static class GeometryOptimizer public static class GeometryOptimizer
{ {
public static void Optimize(IList<Arc> arcs) => public static void Optimize(IList<Arc> arcs) =>
MergePass(arcs, MergePass(
arcs,
(list, item, i) => list.GetCoradialArs(item, i), (list, item, i) => list.GetCoradialArs(item, i),
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined)); (Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined)
);
public static void Optimize(IList<Line> lines) => public static void Optimize(IList<Line> lines) =>
MergePass(lines, MergePass(
lines,
(list, item, i) => list.GetCollinearLines(item, i), (list, item, i) => list.GetCollinearLines(item, i),
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined)); (Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined)
);
public static void Deduplicate(IList<Circle> circles) public static void Deduplicate(IList<Circle> circles)
{ {
@@ -23,8 +27,10 @@ namespace OpenNest.Geometry
{ {
for (var j = i - 1; j >= 0; j--) for (var j = i - 1; j >= 0; j--)
{ {
if (circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon if (
&& circles[i].Radius.IsEqualTo(circles[j].Radius)) circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon
&& circles[i].Radius.IsEqualTo(circles[j].Radius)
)
{ {
circles.RemoveAt(i); circles.RemoveAt(i);
break; break;
@@ -39,9 +45,11 @@ namespace OpenNest.Geometry
{ {
for (var j = arcs.Count - 1; j >= 0; j--) for (var j = arcs.Count - 1; j >= 0; j--)
{ {
if (arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon if (
arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon
&& arcs[j].Radius.IsEqualTo(circles[i].Radius) && arcs[j].Radius.IsEqualTo(circles[i].Radius)
&& arcs[j].IsFullCircle()) && arcs[j].IsFullCircle()
)
{ {
arcs.RemoveAt(j); arcs.RemoveAt(j);
} }
@@ -51,9 +59,12 @@ namespace OpenNest.Geometry
private delegate bool TryJoin<T>(T a, T b, out T joined); private delegate bool TryJoin<T>(T a, T b, out T joined);
private static void MergePass<T>(IList<T> items, private static void MergePass<T>(
IList<T> items,
Func<IList<T>, T, int, List<T>> findCandidates, Func<IList<T>, T, int, List<T>> findCandidates,
TryJoin<T> tryJoin) where T : class TryJoin<T> tryJoin
)
where T : class
{ {
for (var i = 0; i < items.Count; ++i) for (var i = 0; i < items.Count; ++i)
{ {
@@ -117,10 +128,14 @@ namespace OpenNest.Geometry
if (!onPoint) if (!onPoint)
{ {
if (t1 < b2 - Tolerance.Epsilon) return false; if (t1 < b2 - Tolerance.Epsilon)
if (b1 > t2 + Tolerance.Epsilon) return false; return false;
if (l1 > r2 + Tolerance.Epsilon) return false; if (b1 > t2 + Tolerance.Epsilon)
if (r1 < l2 - Tolerance.Epsilon) return false; return false;
if (l1 > r2 + Tolerance.Epsilon)
return false;
if (r1 < l2 - Tolerance.Epsilon)
return false;
} }
var l = l1 < l2 ? l1 : l2; var l = l1 < l2 ? l1 : l2;
@@ -129,9 +144,17 @@ namespace OpenNest.Geometry
var b = b1 < b2 ? b1 : b2; var b = b1 < b2 ? b1 : b2;
if (!line1.IsVertical() && line1.Slope() < 0) if (!line1.IsVertical() && line1.Slope() < 0)
lineOut = new Line(new Vector(l, t), new Vector(r, b)) { Layer = line1.Layer, Color = line1.Color }; lineOut = new Line(new Vector(l, t), new Vector(r, b))
{
Layer = line1.Layer,
Color = line1.Color,
};
else else
lineOut = new Line(new Vector(l, b), new Vector(r, t)) { Layer = line1.Layer, Color = line1.Color }; lineOut = new Line(new Vector(l, b), new Vector(r, t))
{
Layer = line1.Layer,
Color = line1.Color,
};
return true; return true;
} }
@@ -177,33 +200,47 @@ namespace OpenNest.Geometry
if (sweep >= Angle.TwoPI - Tolerance.Epsilon) if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
return false; return false;
if (startAngle < 0) startAngle += Angle.TwoPI; if (startAngle < 0)
if (endAngle < 0) endAngle += Angle.TwoPI; startAngle += Angle.TwoPI;
if (endAngle < 0)
endAngle += Angle.TwoPI;
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color }; arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle)
{
Layer = arc1.Layer,
Color = arc1.Color,
};
return true; return true;
} }
private static List<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex) private static List<Line> GetCollinearLines(
this IList<Line> lines,
Line line,
int startIndex
)
{ {
var collinearLines = new List<Line>(); var collinearLines = new List<Line>();
Parallel.For(startIndex, lines.Count, index => Parallel.For(
{ startIndex,
var compareLine = lines[index]; lines.Count,
index =>
if (Object.ReferenceEquals(line, compareLine))
return;
if (!line.IsCollinearTo(compareLine))
return;
lock (collinearLines)
{ {
collinearLines.Add(compareLine); var compareLine = lines[index];
if (Object.ReferenceEquals(line, compareLine))
return;
if (!line.IsCollinearTo(compareLine))
return;
lock (collinearLines)
{
collinearLines.Add(compareLine);
}
} }
}); );
return collinearLines; return collinearLines;
} }
@@ -212,21 +249,25 @@ namespace OpenNest.Geometry
{ {
var coradialArcs = new List<Arc>(); var coradialArcs = new List<Arc>();
Parallel.For(startIndex, arcs.Count, index => Parallel.For(
{ startIndex,
var compareArc = arcs[index]; arcs.Count,
index =>
if (Object.ReferenceEquals(arc, compareArc))
return;
if (!arc.IsCoradialTo(compareArc))
return;
lock (coradialArcs)
{ {
coradialArcs.Add(compareArc); var compareArc = arcs[index];
if (Object.ReferenceEquals(arc, compareArc))
return;
if (!arc.IsCoradialTo(compareArc))
return;
lock (coradialArcs)
{
coradialArcs.Add(compareArc);
}
} }
}); );
return coradialArcs; return coradialArcs;
} }
+350 -92
View File
@@ -15,8 +15,10 @@ public class ArcCandidate
public double MaxDeviation { get; set; } public double MaxDeviation { get; set; }
public Box BoundingBox { get; set; } public Box BoundingBox { get; set; }
public bool IsSelected { get; set; } = true; public bool IsSelected { get; set; } = true;
/// <summary>First point of the original line segments this candidate covers.</summary> /// <summary>First point of the original line segments this candidate covers.</summary>
public Vector FirstPoint { get; set; } public Vector FirstPoint { get; set; }
/// <summary>Last point of the original line segments this candidate covers.</summary> /// <summary>Last point of the original line segments this candidate covers.</summary>
public Vector LastPoint { get; set; } public Vector LastPoint { get; set; }
} }
@@ -46,9 +48,7 @@ public class MirrorAxisResult
var dx = p.X - Point.X; var dx = p.X - Point.X;
var dy = p.Y - Point.Y; var dy = p.Y - Point.Y;
var dot = dx * Direction.X + dy * Direction.Y; var dot = dx * Direction.X + dy * Direction.Y;
return new Vector( return new Vector(p.X - 2 * (dx - dot * Direction.X), p.Y - 2 * (dy - dot * Direction.Y));
p.X - 2 * (dx - dot * Direction.X),
p.Y - 2 * (dy - dot * Direction.Y));
} }
} }
@@ -74,9 +74,14 @@ public class GeometrySimplifier
var runStart = i; var runStart = i;
var layerName = entities[i].Layer?.Name; var layerName = entities[i].Layer?.Name;
var lineCount = 0; var lineCount = 0;
while (i < entities.Count && (entities[i] is Line || entities[i] is Arc) && entities[i].Layer?.Name == layerName) while (
i < entities.Count
&& (entities[i] is Line || entities[i] is Arc)
&& entities[i].Layer?.Name == layerName
)
{ {
if (entities[i] is Line) lineCount++; if (entities[i] is Line)
lineCount++;
i++; i++;
} }
var runEnd = i - 1; var runEnd = i - 1;
@@ -90,10 +95,7 @@ public class GeometrySimplifier
public Shape Apply(Shape shape, List<ArcCandidate> candidates) public Shape Apply(Shape shape, List<ArcCandidate> candidates)
{ {
var selected = candidates var selected = candidates.Where(c => c.IsSelected).OrderBy(c => c.StartIndex).ToList();
.Where(c => c.IsSelected)
.OrderBy(c => c.StartIndex)
.ToList();
var newEntities = new List<Entity>(); var newEntities = new List<Entity>();
var i = 0; var i = 0;
@@ -132,11 +134,10 @@ public class GeometrySimplifier
foreach (var e in shape.Entities) foreach (var e in shape.Entities)
midpoints.Add(e.BoundingBox.Center); midpoints.Add(e.BoundingBox.Center);
if (midpoints.Count < 4) return MirrorAxisResult.None; if (midpoints.Count < 4)
return MirrorAxisResult.None;
var centroid = new Vector( var centroid = new Vector(midpoints.Average(p => p.X), midpoints.Average(p => p.Y));
midpoints.Average(p => p.X),
midpoints.Average(p => p.Y));
var cx = centroid.X; var cx = centroid.X;
var cy = centroid.Y; var cy = centroid.Y;
@@ -190,8 +191,7 @@ public class GeometrySimplifier
return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None; return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None;
} }
private static double NormalizeAngle(double angle) => private static double NormalizeAngle(double angle) => angle < 0 ? angle + Angle.TwoPI : angle;
angle < 0 ? angle + Angle.TwoPI : angle;
private static Vector Normalize(Vector v) private static Vector Normalize(Vector v)
{ {
@@ -231,7 +231,8 @@ public class GeometrySimplifier
for (var j = 0; j < points.Count; j++) for (var j = 0; j < points.Count; j++)
{ {
if (i == j) continue; if (i == j)
continue;
var d = reflected.DistanceTo(points[j]); var d = reflected.DistanceTo(points[j]);
if (d < matchTol) if (d < matchTol)
{ {
@@ -251,17 +252,20 @@ public class GeometrySimplifier
/// </summary> /// </summary>
public void Symmetrize(List<ArcCandidate> candidates, MirrorAxisResult axis) public void Symmetrize(List<ArcCandidate> candidates, MirrorAxisResult axis)
{ {
if (!axis.IsValid || candidates.Count < 2) return; if (!axis.IsValid || candidates.Count < 2)
return;
var paired = new HashSet<int>(); var paired = new HashSet<int>();
for (var i = 0; i < candidates.Count; i++) for (var i = 0; i < candidates.Count; i++)
{ {
if (paired.Contains(i)) continue; if (paired.Contains(i))
continue;
var ci = candidates[i]; var ci = candidates[i];
var ciCenter = ci.BoundingBox.Center; var ciCenter = ci.BoundingBox.Center;
if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) continue; // on the axis if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1)
continue; // on the axis
var mirrorCenter = axis.Reflect(ciCenter); var mirrorCenter = axis.Reflect(ciCenter);
@@ -269,7 +273,8 @@ public class GeometrySimplifier
var bestDist = double.MaxValue; var bestDist = double.MaxValue;
for (var j = i + 1; j < candidates.Count; j++) for (var j = i + 1; j < candidates.Count; j++)
{ {
if (paired.Contains(j)) continue; if (paired.Contains(j))
continue;
var d = mirrorCenter.DistanceTo(candidates[j].BoundingBox.Center); var d = mirrorCenter.DistanceTo(candidates[j].BoundingBox.Center);
if (d < bestDist) if (d < bestDist)
{ {
@@ -279,7 +284,8 @@ public class GeometrySimplifier
} }
var matchTol = System.Math.Max(ci.BoundingBox.Width, ci.BoundingBox.Length) * 0.5; var matchTol = System.Math.Max(ci.BoundingBox.Width, ci.BoundingBox.Length) * 0.5;
if (bestJ < 0 || bestDist > matchTol) continue; if (bestJ < 0 || bestDist > matchTol)
continue;
paired.Add(i); paired.Add(i);
paired.Add(bestJ); paired.Add(bestJ);
@@ -287,7 +293,10 @@ public class GeometrySimplifier
var cj = candidates[bestJ]; var cj = candidates[bestJ];
var sourceIdx = i; var sourceIdx = i;
var targetIdx = bestJ; var targetIdx = bestJ;
if (cj.LineCount > ci.LineCount || (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation)) if (
cj.LineCount > ci.LineCount
|| (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation)
)
{ {
sourceIdx = bestJ; sourceIdx = bestJ;
targetIdx = i; targetIdx = i;
@@ -323,8 +332,12 @@ public class GeometrySimplifier
var mirrorEp = axis.Reflect(ep); var mirrorEp = axis.Reflect(ep);
// Mirroring reverses winding — swap start/end to preserve arc direction // Mirroring reverses winding — swap start/end to preserve arc direction
var mirrorStart = NormalizeAngle(System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X)); var mirrorStart = NormalizeAngle(
var mirrorEnd = NormalizeAngle(System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X)); System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X)
);
var mirrorEnd = NormalizeAngle(
System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X)
);
var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed); var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed);
result.Layer = arc.Layer; result.Layer = arc.Layer;
@@ -332,7 +345,12 @@ public class GeometrySimplifier
return result; return result;
} }
private void FindCandidatesInRun(List<Entity> entities, int runStart, int runEnd, List<ArcCandidate> candidates) private void FindCandidatesInRun(
List<Entity> entities,
int runStart,
int runEnd,
List<ArcCandidate> candidates
)
{ {
var j = runStart; var j = runStart;
var chainedTangent = Vector.Invalid; var chainedTangent = Vector.Invalid;
@@ -349,46 +367,63 @@ public class GeometrySimplifier
chainedTangent = ComputeEndTangent(result.Center, result.Points); chainedTangent = ComputeEndTangent(result.Center, result.Points);
var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]); var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]);
candidates.Add(new ArcCandidate candidates.Add(
{ new ArcCandidate
StartIndex = j, {
EndIndex = result.EndIndex, StartIndex = j,
FittedArc = arc, EndIndex = result.EndIndex,
MaxDeviation = result.Deviation, FittedArc = arc,
BoundingBox = result.Points.GetBoundingBox(), MaxDeviation = result.Deviation,
FirstPoint = arc.StartPoint(), BoundingBox = result.Points.GetBoundingBox(),
LastPoint = arc.EndPoint(), FirstPoint = arc.StartPoint(),
}); LastPoint = arc.EndPoint(),
}
);
j = result.EndIndex + 1; j = result.EndIndex + 1;
} }
} }
private record ArcFitResult(Vector Center, double Radius, double Deviation, List<Vector> Points, int EndIndex); private record ArcFitResult(
Vector Center,
double Radius,
double Deviation,
List<Vector> Points,
int EndIndex
);
private ArcFitResult TryFitArcAt(List<Entity> entities, int start, int runEnd, Vector chainedTangent) private ArcFitResult TryFitArcAt(
List<Entity> entities,
int start,
int runEnd,
Vector chainedTangent
)
{ {
var k = start + MinLines - 1; var k = start + MinLines - 1;
if (k > runEnd) return null; if (k > runEnd)
return null;
var points = CollectPoints(entities, start, k); var points = CollectPoints(entities, start, k);
if (points.Count < 3) return null; if (points.Count < 3)
return null;
var startTangent = chainedTangent.IsValid() var startTangent = EstimateStartTangent(entities, start, points, chainedTangent);
? chainedTangent var endTangent = EstimateEndTangent(entities, k, points);
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
var endTangent = GetExitDirection(entities[k]);
var (center, radius, dev) = TryFit(points, startTangent, endTangent); var (center, radius, dev) = TryFit(points, startTangent, endTangent);
if (!center.IsValid()) return null; if (!center.IsValid())
return null;
// Extend the arc as far as possible // Extend the arc as far as possible
while (k + 1 <= runEnd) while (k + 1 <= runEnd)
{ {
var extPoints = CollectPoints(entities, start, k + 1); var extPoints = CollectPoints(entities, start, k + 1);
var extEndTangent = GetExitDirection(entities[k + 1]); if (extPoints.Count < 3)
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent, extEndTangent) : (Vector.Invalid, 0, 0d); break;
if (!nc.IsValid()) break;
var extEndTangent = EstimateEndTangent(entities, k + 1, extPoints);
var (nc, nr, nd) = TryFit(extPoints, startTangent, extEndTangent);
if (!nc.IsValid())
break;
k++; k++;
center = nc; center = nc;
@@ -407,37 +442,228 @@ public class GeometrySimplifier
return new ArcFitResult(center, radius, dev, points, k); return new ArcFitResult(center, radius, dev, points, k);
} }
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent, Vector endTangent) private (Vector center, double radius, double deviation) TryFit(
List<Vector> points,
TangentEstimate start,
TangentEstimate end
)
{ {
// Try dual-tangent fit first (matches direction at both endpoints) foreach (var (center, radius, dev) in FitAttempts(points, start, end))
if (endTangent.IsValid())
{ {
var (dc, dr, dd) = ArcFit.FitWithDualTangent(points, startTangent, endTangent); if (!center.IsValid() || dev > Tolerance)
if (dc.IsValid() && dd <= Tolerance) continue;
// Check that the arc doesn't bulge away from the original line segments
var isReversed = SumSignedAngles(center, points) < 0;
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
if (arcDev > Tolerance)
continue;
return (center, radius, System.Math.Max(dev, arcDev));
}
return (Vector.Invalid, 0, 0);
}
/// <summary>
/// Yields fit attempts in preference order. A trusted tangent (chained from the
/// previous arc, an adjacent original arc, or a long straight edge) is enforced
/// exactly on its side; otherwise the tangency error is balanced between both
/// endpoints. The unconstrained mirror-axis fit is the last resort. Every attempt
/// passes exactly through both endpoints, so no gaps are introduced.
/// </summary>
private IEnumerable<(Vector center, double radius, double deviation)> FitAttempts(
List<Vector> points,
TangentEstimate start,
TangentEstimate end
)
{
if (start.Trusted && !end.Trusted)
{
yield return ArcFit.FitWithStartTangent(points, start.Direction);
yield return ArcFit.FitThroughEndpointsWithTangents(
points,
start.Direction,
end.Direction
);
yield return FitWithEndTangent(points, end.Direction);
}
else if (end.Trusted && !start.Trusted)
{
yield return FitWithEndTangent(points, end.Direction);
yield return ArcFit.FitThroughEndpointsWithTangents(
points,
start.Direction,
end.Direction
);
yield return ArcFit.FitWithStartTangent(points, start.Direction);
}
else
{
yield return ArcFit.FitThroughEndpointsWithTangents(
points,
start.Direction,
end.Direction
);
yield return ArcFit.FitWithStartTangent(points, start.Direction);
yield return FitWithEndTangent(points, end.Direction);
}
yield return FitMirrorAxis(points);
}
/// <summary>
/// Fits an arc through both endpoints with an exact tangent at the last point,
/// by running the start-tangent fit on the reversed point sequence.
/// </summary>
private static (Vector center, double radius, double deviation) FitWithEndTangent(
List<Vector> points,
Vector endTangent
)
{
var reversed = new List<Vector>(points);
reversed.Reverse();
return ArcFit.FitWithStartTangent(reversed, new Vector(-endTangent.X, -endTangent.Y));
}
/// <summary>
/// An estimated tangent direction at a fit endpoint. Trusted estimates come from
/// exact geometry (a chained arc, an adjacent original arc, or a long straight
/// edge) and are enforced exactly; untrusted ones are derived from the polyline
/// vertices and only guide the fit.
/// </summary>
private readonly record struct TangentEstimate(Vector Direction, bool Trusted);
/// <summary>Segment-length ratio above which a neighboring line counts as a true
/// straight edge (rather than another chord of the tessellated curve).</summary>
private const double NeighborEdgeFactor = 3.0;
private static TangentEstimate EstimateStartTangent(
List<Entity> entities,
int start,
List<Vector> points,
Vector chainedTangent
)
{
if (chainedTangent.IsValid())
return new TangentEstimate(chainedTangent, true);
if (entities[start] is Arc startArc)
return new TangentEstimate(GetEntryDirection(startArc), true);
var firstChordLen = points[0].DistanceTo(points[1]);
if (start > 0)
{
var prev = entities[start - 1];
var prevEnd = prev switch
{ {
var isRev = SumSignedAngles(dc, points) < 0; Line l => l.EndPoint,
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev); Arc a => a.EndPoint(),
if (aDev <= Tolerance) _ => Vector.Invalid,
return (dc, dr, System.Math.Max(dd, aDev)); };
if (prevEnd.IsValid() && prevEnd.DistanceTo(points[0]) < 1e-6)
{
if (prev is Arc)
return new TangentEstimate(GetExitDirection(prev), true);
if (
prev is Line prevLine
&& prevLine.StartPoint.DistanceTo(prevLine.EndPoint)
>= NeighborEdgeFactor * firstChordLen
)
return new TangentEstimate(GetExitDirection(prevLine), true);
} }
} }
// Fall back to start-tangent-only, then mirror axis var chord = new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent); if (points.Count >= 3)
if (!center.IsValid() || dev > Tolerance) return new TangentEstimate(
(center, radius, dev) = FitMirrorAxis(points); EstimateVertexTangent(points[0], points[1], points[2], chord),
if (!center.IsValid() || dev > Tolerance) false
return (Vector.Invalid, 0, 0); );
return new TangentEstimate(chord, false);
// Check that the arc doesn't bulge away from the original line segments
var isReversed = SumSignedAngles(center, points) < 0;
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
if (arcDev > Tolerance)
return (Vector.Invalid, 0, 0);
return (center, radius, System.Math.Max(dev, arcDev));
} }
private static TangentEstimate EstimateEndTangent(
List<Entity> entities,
int k,
List<Vector> points
)
{
if (entities[k] is Arc endArc)
return new TangentEstimate(GetExitDirection(endArc), true);
var lastChordLen = points[^1].DistanceTo(points[^2]);
if (k + 1 < entities.Count)
{
var next = entities[k + 1];
var nextStart = next switch
{
Line l => l.StartPoint,
Arc a => a.StartPoint(),
_ => Vector.Invalid,
};
if (nextStart.IsValid() && nextStart.DistanceTo(points[^1]) < 1e-6)
{
if (next is Arc nextArc)
return new TangentEstimate(GetEntryDirection(nextArc), true);
if (
next is Line nextLine
&& nextLine.StartPoint.DistanceTo(nextLine.EndPoint)
>= NeighborEdgeFactor * lastChordLen
)
return new TangentEstimate(GetExitDirection(nextLine), true);
}
}
var chord = new Vector(points[^1].X - points[^2].X, points[^1].Y - points[^2].Y);
if (points.Count >= 3)
return new TangentEstimate(
EstimateVertexTangent(points[^1], points[^2], points[^3], chord),
false
);
return new TangentEstimate(chord, false);
}
/// <summary>
/// Estimates the curve tangent at a polyline vertex from the circle through it and
/// its two nearest neighbors. A raw chord direction is off from the true tangent by
/// half the chord's subtended angle; the circumcircle estimate removes that bias.
/// Falls back to the travel direction when the three points are collinear.
/// </summary>
private static Vector EstimateVertexTangent(Vector at, Vector b, Vector c, Vector travel)
{
var d = 2 * (at.X * (b.Y - c.Y) + b.X * (c.Y - at.Y) + c.X * (at.Y - b.Y));
if (System.Math.Abs(d) < 1e-14)
return travel;
var sqA = at.X * at.X + at.Y * at.Y;
var sqB = b.X * b.X + b.Y * b.Y;
var sqC = c.X * c.X + c.Y * c.Y;
var cx = (sqA * (b.Y - c.Y) + sqB * (c.Y - at.Y) + sqC * (at.Y - b.Y)) / d;
var cy = (sqA * (c.X - b.X) + sqB * (at.X - c.X) + sqC * (b.X - at.X)) / d;
var tangent = new Vector(-(at.Y - cy), at.X - cx);
if (tangent.X * travel.X + tangent.Y * travel.Y < 0)
tangent = new Vector(-tangent.X, -tangent.Y);
return tangent;
}
/// <summary>
/// Returns the entry direction (tangent at start point) of an entity.
/// </summary>
private static Vector GetEntryDirection(Entity entity) =>
entity switch
{
Line line => new Vector(
line.EndPoint.X - line.StartPoint.X,
line.EndPoint.Y - line.StartPoint.Y
),
Arc arc => arc.IsReversed
? new Vector(System.Math.Sin(arc.StartAngle), -System.Math.Cos(arc.StartAngle))
: new Vector(-System.Math.Sin(arc.StartAngle), System.Math.Cos(arc.StartAngle)),
_ => Vector.Invalid,
};
/// <summary> /// <summary>
/// Computes the tangent direction at the last point of a fitted arc, /// Computes the tangent direction at the last point of a fitted arc,
/// used to chain tangent continuity to the next arc. /// used to chain tangent continuity to the next arc.
@@ -488,9 +714,17 @@ public class GeometrySimplifier
var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta); var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta);
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord); var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
var dOpt = GoldenSectionMin(dInit - range, dInit + range, var dOpt = GoldenSectionMin(
d => ArcFit.MaxRadialDeviation(points, mx + d * nx, my + d * ny, dInit - range,
System.Math.Sqrt(halfChord * halfChord + d * d))); dInit + range,
d =>
ArcFit.MaxRadialDeviation(
points,
mx + d * nx,
my + d * ny,
System.Math.Sqrt(halfChord * halfChord + d * d)
)
);
var center = new Vector(mx + dOpt * nx, my + dOpt * ny); var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt); var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
@@ -542,13 +776,22 @@ public class GeometrySimplifier
return points; return points;
} }
private static Arc CreateArc(Vector center, double radius, List<Vector> points, Entity sourceEntity) private static Arc CreateArc(
Vector center,
double radius,
List<Vector> points,
Entity sourceEntity
)
{ {
var firstPoint = points[0]; var firstPoint = points[0];
var lastPoint = points[^1]; var lastPoint = points[^1];
var startAngle = NormalizeAngle(System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X)); var startAngle = NormalizeAngle(
var endAngle = NormalizeAngle(System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X)); System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X)
);
var endAngle = NormalizeAngle(
System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X)
);
var isReversed = SumSignedAngles(center, points) < 0; var isReversed = SumSignedAngles(center, points) < 0;
var arc = new Arc(center, radius, startAngle, endAngle, isReversed); var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
@@ -560,14 +803,18 @@ public class GeometrySimplifier
/// <summary> /// <summary>
/// Returns the exit direction (tangent at endpoint) of an entity. /// Returns the exit direction (tangent at endpoint) of an entity.
/// </summary> /// </summary>
private static Vector GetExitDirection(Entity entity) => entity switch private static Vector GetExitDirection(Entity entity) =>
{ entity switch
Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y), {
Arc arc => arc.IsReversed Line line => new Vector(
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle)) line.EndPoint.X - line.StartPoint.X,
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)), line.EndPoint.Y - line.StartPoint.Y
_ => Vector.Invalid, ),
}; Arc arc => arc.IsReversed
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle))
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
_ => Vector.Invalid,
};
/// <summary> /// <summary>
/// Sums signed angular change traversing consecutive points around a center. /// Sums signed angular change traversing consecutive points around a center.
@@ -581,8 +828,10 @@ public class GeometrySimplifier
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X); var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X); var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
var da = a2 - a1; var da = a2 - a1;
while (da > System.Math.PI) da -= Angle.TwoPI; while (da > System.Math.PI)
while (da < -System.Math.PI) da += Angle.TwoPI; da -= Angle.TwoPI;
while (da < -System.Math.PI)
da += Angle.TwoPI;
total += da; total += da;
} }
return total; return total;
@@ -593,7 +842,12 @@ public class GeometrySimplifier
/// back to the original line segments. This catches cases where points lie /// back to the original line segments. This catches cases where points lie
/// on a large circle but the arc bulges far from the original straight geometry. /// on a large circle but the arc bulges far from the original straight geometry.
/// </summary> /// </summary>
private static double MaxArcToSegmentDeviation(List<Vector> points, Vector center, double radius, bool isReversed) private static double MaxArcToSegmentDeviation(
List<Vector> points,
Vector center,
double radius,
bool isReversed
)
{ {
var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X); var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X);
var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X); var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X);
@@ -601,11 +855,13 @@ public class GeometrySimplifier
var sweep = endAngle - startAngle; var sweep = endAngle - startAngle;
if (isReversed) if (isReversed)
{ {
if (sweep > 0) sweep -= Angle.TwoPI; if (sweep > 0)
sweep -= Angle.TwoPI;
} }
else else
{ {
if (sweep < 0) sweep += Angle.TwoPI; if (sweep < 0)
sweep += Angle.TwoPI;
} }
var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10)); var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10));
@@ -624,9 +880,11 @@ public class GeometrySimplifier
for (var j = 0; j < points.Count - 1; j++) for (var j = 0; j < points.Count - 1; j++)
{ {
var dist = DistanceToSegment(arcPt, points[j], points[j + 1]); var dist = DistanceToSegment(arcPt, points[j], points[j + 1]);
if (dist < minDist) minDist = dist; if (dist < minDist)
minDist = dist;
} }
if (minDist > maxDev) maxDev = minDist; if (minDist > maxDev)
maxDev = minDist;
} }
return maxDev; return maxDev;
} }
+1 -2
View File
@@ -1,5 +1,4 @@
 namespace OpenNest.Geometry
namespace OpenNest.Geometry
{ {
public interface IBoundable public interface IBoundable
{ {
+8 -4
View File
@@ -28,10 +28,14 @@ namespace OpenNest.Geometry
for (var i = 1; i < verts.Count; i++) for (var i = 1; i < verts.Count; i++)
{ {
if (verts[i].X < minX) minX = verts[i].X; if (verts[i].X < minX)
if (verts[i].X > maxX) maxX = verts[i].X; minX = verts[i].X;
if (verts[i].Y < minY) minY = verts[i].Y; if (verts[i].X > maxX)
if (verts[i].Y > maxY) maxY = verts[i].Y; 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 IFP is the work area shrunk inward by the part's extent in each direction.
+60 -31
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry namespace OpenNest.Geometry
{ {
@@ -18,8 +18,19 @@ namespace OpenNest.Geometry
} }
pts = pts.Where(pt => pts = pts.Where(pt =>
Angle.IsBetweenRad(arc1.Center.AngleTo(pt), arc1.StartAngle, arc1.EndAngle, arc1.IsReversed) && Angle.IsBetweenRad(
Angle.IsBetweenRad(arc2.Center.AngleTo(pt), arc2.StartAngle, arc2.EndAngle, arc2.IsReversed)) arc1.Center.AngleTo(pt),
arc1.StartAngle,
arc1.EndAngle,
arc1.IsReversed
)
&& Angle.IsBetweenRad(
arc2.Center.AngleTo(pt),
arc2.StartAngle,
arc2.EndAngle,
arc2.IsReversed
)
)
.ToList(); .ToList();
return pts.Count > 0; return pts.Count > 0;
@@ -35,11 +46,15 @@ namespace OpenNest.Geometry
return false; return false;
} }
pts = pts.Where(pt => Angle.IsBetweenRad( pts = pts.Where(pt =>
arc.Center.AngleTo(pt), Angle.IsBetweenRad(
arc.StartAngle, arc.Center.AngleTo(pt),
arc.EndAngle, arc.StartAngle,
arc.IsReversed)).ToList(); arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -54,11 +69,15 @@ namespace OpenNest.Geometry
return false; return false;
} }
pts = pts.Where(pt => Angle.IsBetweenRad( pts = pts.Where(pt =>
arc.Center.AngleTo(pt), Angle.IsBetweenRad(
arc.StartAngle, arc.Center.AngleTo(pt),
arc.EndAngle, arc.StartAngle,
arc.IsReversed)).ToList(); arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -74,11 +93,15 @@ namespace OpenNest.Geometry
pts2.AddRange(pts3); pts2.AddRange(pts3);
} }
pts = pts2.Where(pt => Angle.IsBetweenRad( pts = pts2.Where(pt =>
arc.Center.AngleTo(pt), Angle.IsBetweenRad(
arc.StartAngle, arc.Center.AngleTo(pt),
arc.EndAngle, arc.StartAngle,
arc.IsReversed)).ToList(); arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -95,11 +118,15 @@ namespace OpenNest.Geometry
pts2.AddRange(pts3); pts2.AddRange(pts3);
} }
pts = pts2.Where(pt => Angle.IsBetweenRad( pts = pts2.Where(pt =>
arc.Center.AngleTo(pt), Angle.IsBetweenRad(
arc.StartAngle, arc.Center.AngleTo(pt),
arc.EndAngle, arc.StartAngle,
arc.IsReversed)).ToList(); arc.EndAngle,
arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -123,20 +150,22 @@ namespace OpenNest.Geometry
} }
var d = circle2.Center - circle1.Center; var d = circle2.Center - circle1.Center;
var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance); var a =
(
circle1.Radius * circle1.Radius
- circle2.Radius * circle2.Radius
+ distance * distance
) / (2.0 * distance);
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a); var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
var pt = new Vector( var pt = new Vector(
circle1.Center.X + (a * d.X) / distance, circle1.Center.X + (a * d.X) / distance,
circle1.Center.Y + (a * d.Y) / distance); circle1.Center.Y + (a * d.Y) / distance
);
var i1 = new Vector( var i1 = new Vector(pt.X + (h * d.Y) / distance, pt.Y - (h * d.X) / distance);
pt.X + (h * d.Y) / distance,
pt.Y - (h * d.X) / distance);
var i2 = new Vector( var i2 = new Vector(pt.X - (h * d.Y) / distance, pt.Y + (h * d.X) / distance);
pt.X - (h * d.Y) / distance,
pt.Y + (h * d.X) / distance);
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 }; pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
+1 -1
View File
@@ -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)
+12 -18
View File
@@ -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 =
? null point.DistanceTo(StartPoint) < Tolerance.Epsilon
: new Line(StartPoint, point); ? null
: new Line(StartPoint, point);
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon var second =
? null point.DistanceTo(EndPoint) < Tolerance.Epsilon ? null : new Line(point, EndPoint);
: new Line(point, EndPoint);
return (first, second); return (first, second);
} }
+13 -7
View File
@@ -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;
} }
+22 -11
View File
@@ -4,12 +4,14 @@ namespace OpenNest.Geometry
{ {
public static class PolyLabel public static class PolyLabel
{ {
public static Vector Find(Polygon outer, IList<Polygon> holes = null, double precision = 0.5) public static Vector Find(
Polygon outer,
IList<Polygon> holes = null,
double precision = 0.5
)
{ {
if (outer.Vertices.Count < 3) if (outer.Vertices.Count < 3)
return outer.Vertices.Count > 0 return outer.Vertices.Count > 0 ? outer.Vertices[0] : new Vector();
? outer.Vertices[0]
: new Vector();
var minX = double.MaxValue; var minX = double.MaxValue;
var minY = double.MaxValue; var minY = double.MaxValue;
@@ -19,10 +21,14 @@ namespace OpenNest.Geometry
for (var i = 0; i < outer.Vertices.Count; i++) for (var i = 0; i < outer.Vertices.Count; i++)
{ {
var v = outer.Vertices[i]; var v = outer.Vertices[i];
if (v.X < minX) minX = v.X; if (v.X < minX)
if (v.Y < minY) minY = v.Y; minX = v.X;
if (v.X > maxX) maxX = v.X; if (v.Y < minY)
if (v.Y > maxY) maxY = v.Y; minY = v.Y;
if (v.X > maxX)
maxX = v.X;
if (v.Y > maxY)
maxY = v.Y;
} }
var width = maxX - minX; var width = maxX - minX;
@@ -37,8 +43,8 @@ namespace OpenNest.Geometry
var queue = new List<Cell>(); var queue = new List<Cell>();
for (var x = minX; x < maxX; x += cellSize) for (var x = minX; x < maxX; x += cellSize)
for (var y = minY; y < maxY; y += cellSize) for (var y = minY; y < maxY; y += cellSize)
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes)); queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes));
queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist)); queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist));
@@ -194,7 +200,12 @@ namespace OpenNest.Geometry
} }
} }
private static double PointToAllEdgesDist(double x, double y, Polygon outer, IList<Polygon> holes) private static double PointToAllEdgesDist(
double x,
double y,
Polygon outer,
IList<Polygon> holes
)
{ {
var minDist = PointToPolygonDist(x, y, outer); var minDist = PointToPolygonDist(x, y, outer);
+64 -21
View File
@@ -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;
} }
+35 -15
View File
@@ -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;
+95 -50
View File
@@ -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)
@@ -493,22 +487,27 @@ namespace OpenNest.Geometry
switch (entity.Type) switch (entity.Type)
{ {
case EntityType.Line: case EntityType.Line:
{
var line = (Line)entity;
var offsetLine = (Line)offsetEntity;
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
{ {
var line = (Line)entity; JoinOffsetLines(
var offsetLine = (Line)offsetEntity; (Line)lastEntity,
(Line)lastOffsetEntity,
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line) line,
{ offsetLine,
JoinOffsetLines( distance,
(Line)lastEntity, (Line)lastOffsetEntity, side,
line, offsetLine, offsetShape
distance, side, offsetShape); );
}
offsetShape.Entities.Add(offsetLine);
break;
} }
offsetShape.Entities.Add(offsetLine);
break;
}
default: default:
offsetShape.Entities.Add(offsetEntity); offsetShape.Entities.Add(offsetEntity);
break; break;
@@ -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)
{ {
@@ -559,11 +574,13 @@ namespace OpenNest.Geometry
line.StartPoint.AngleTo(lastOffsetLine.EndPoint), line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
line.StartPoint.AngleTo(offsetLine.StartPoint), line.StartPoint.AngleTo(offsetLine.StartPoint),
side == OffsetSide.Left side == OffsetSide.Left
); );
offsetShape.Entities.Add(arc); offsetShape.Entities.Add(arc);
} }
else if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)) else if (
Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)
)
{ {
offsetLine.StartPoint = intersection; offsetLine.StartPoint = intersection;
lastOffsetLine.EndPoint = intersection; lastOffsetLine.EndPoint = intersection;
@@ -576,7 +593,7 @@ namespace OpenNest.Geometry
line.StartPoint.AngleTo(lastOffsetLine.EndPoint), line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
line.StartPoint.AngleTo(offsetLine.StartPoint), line.StartPoint.AngleTo(offsetLine.StartPoint),
side == OffsetSide.Left side == OffsetSide.Left
); );
offsetShape.Entities.Add(arc); offsetShape.Entities.Add(arc);
} }
@@ -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;
} }
} }
+10 -3
View File
@@ -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)
{ {
+1 -2
View File
@@ -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();
} }
+2 -1
View File
@@ -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)}";
} }
} }
+408 -157
View File
@@ -1,6 +1,6 @@
using OpenNest.Math;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry namespace OpenNest.Geometry
{ {
@@ -13,57 +13,72 @@ namespace OpenNest.Geometry
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction) private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
{ {
return RayEdgeDistance( return RayEdgeDistance(
vertex.X, vertex.Y, vertex.X,
edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y, vertex.Y,
direction); edge.pt1.X,
edge.pt1.Y,
edge.pt2.X,
edge.pt2.Y,
direction
);
} }
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
private static double RayEdgeDistance( private static double RayEdgeDistance(
double vx, double vy, double vx,
double p1x, double p1y, double p2x, double p2y, double vy,
PushDirection direction) double p1x,
double p1y,
double p2x,
double p2y,
PushDirection direction
)
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: case PushDirection.Left:
case PushDirection.Right: case PushDirection.Right:
{ {
var dy = p2y - p1y; var dy = p2y - p1y;
if (System.Math.Abs(dy) < Tolerance.Epsilon) if (System.Math.Abs(dy) < Tolerance.Epsilon)
return double.MaxValue;
var t = (vy - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
return double.MaxValue; return double.MaxValue;
}
var t = (vy - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon)
return dist;
if (dist >= -Tolerance.Epsilon)
return 0;
return double.MaxValue;
}
case PushDirection.Down: case PushDirection.Down:
case PushDirection.Up: case PushDirection.Up:
{ {
var dx = p2x - p1x; var dx = p2x - p1x;
if (System.Math.Abs(dx) < Tolerance.Epsilon) if (System.Math.Abs(dx) < Tolerance.Epsilon)
return double.MaxValue;
var t = (vx - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0;
return double.MaxValue; return double.MaxValue;
}
var t = (vx - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon)
return dist;
if (dist >= -Tolerance.Epsilon)
return 0;
return double.MaxValue;
}
default: default:
return double.MaxValue; return double.MaxValue;
@@ -75,11 +90,18 @@ namespace OpenNest.Geometry
/// Returns double.MaxValue if the ray does not hit the segment. /// Returns double.MaxValue if the ray does not hit the segment.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayEdgeDistance( public static double RayEdgeDistance(
double vx, double vy, double vx,
double p1x, double p1y, double p2x, double p2y, double vy,
double dirX, double dirY) double p1x,
double p1y,
double p2x,
double p2y,
double dirX,
double dirY
)
{ {
var ex = p2x - p1x; var ex = p2x - p1x;
var ey = p2y - p1y; var ey = p2y - p1y;
@@ -99,8 +121,10 @@ namespace OpenNest.Geometry
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon) if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
return double.MaxValue; return double.MaxValue;
if (t > Tolerance.Epsilon) return t; if (t > Tolerance.Epsilon)
if (t >= -Tolerance.Epsilon) return 0; return t;
if (t >= -Tolerance.Epsilon)
return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -109,12 +133,19 @@ namespace OpenNest.Geometry
/// Returns false if no real intersection exists. /// Returns false if no real intersection exists.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
private static bool SolveRayCircle( private static bool SolveRayCircle(
double vx, double vy, double vx,
double cx, double cy, double r, double vy,
double dirX, double dirY, double cx,
out double t1, out double t2) double cy,
double r,
double dirX,
double dirY,
out double t1,
out double t2
)
{ {
var ox = vx - cx; var ox = vx - cx;
var oy = vy - cy; var oy = vy - cy;
@@ -143,12 +174,20 @@ namespace OpenNest.Geometry
/// angular span. Returns double.MaxValue if no hit. /// angular span. Returns double.MaxValue if no hit.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayArcDistance( public static double RayArcDistance(
double vx, double vy, double vx,
double cx, double cy, double r, double vy,
double startAngle, double endAngle, bool reversed, double cx,
double dirX, double dirY) double cy,
double r,
double startAngle,
double endAngle,
bool reversed,
double dirX,
double dirY
)
{ {
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2)) if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
return double.MaxValue; return double.MaxValue;
@@ -157,16 +196,18 @@ namespace OpenNest.Geometry
if (t1 > -Tolerance.Epsilon) if (t1 > -Tolerance.Epsilon)
{ {
var hitAngle = Angle.NormalizeRad(System.Math.Atan2( var hitAngle = Angle.NormalizeRad(
vy + t1 * dirY - cy, vx + t1 * dirX - cx)); System.Math.Atan2(vy + t1 * dirY - cy, vx + t1 * dirX - cx)
);
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
best = t1 > Tolerance.Epsilon ? t1 : 0; best = t1 > Tolerance.Epsilon ? t1 : 0;
} }
if (t2 > -Tolerance.Epsilon && t2 < best) if (t2 > -Tolerance.Epsilon && t2 < best)
{ {
var hitAngle = Angle.NormalizeRad(System.Math.Atan2( var hitAngle = Angle.NormalizeRad(
vy + t2 * dirY - cy, vx + t2 * dirX - cx)); System.Math.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx)
);
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
best = t2 > Tolerance.Epsilon ? t2 : 0; best = t2 > Tolerance.Epsilon ? t2 : 0;
} }
@@ -179,19 +220,29 @@ namespace OpenNest.Geometry
/// Returns double.MaxValue if no hit. /// Returns double.MaxValue if no hit.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayCircleDistance( public static double RayCircleDistance(
double vx, double vy, double vx,
double cx, double cy, double r, double vy,
double dirX, double dirY) double cx,
double cy,
double r,
double dirX,
double dirY
)
{ {
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2)) if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
return double.MaxValue; return double.MaxValue;
if (t1 > Tolerance.Epsilon) return t1; if (t1 > Tolerance.Epsilon)
if (t1 >= -Tolerance.Epsilon) return 0; return t1;
if (t2 > Tolerance.Epsilon) return t2; if (t1 >= -Tolerance.Epsilon)
if (t2 >= -Tolerance.Epsilon) return 0; return 0;
if (t2 > Tolerance.Epsilon)
return t2;
if (t2 >= -Tolerance.Epsilon)
return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -201,7 +252,11 @@ namespace OpenNest.Geometry
/// any edge of movingLines contacts any edge of stationaryLines. /// any edge of movingLines contacts any edge of stationaryLines.
/// Returns double.MaxValue if no collision path exists. /// Returns double.MaxValue if no collision path exists.
/// </summary> /// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction) public static double DirectionalDistance(
List<Line> movingLines,
List<Line> stationaryLines,
PushDirection direction
)
{ {
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction); return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
} }
@@ -211,8 +266,12 @@ namespace OpenNest.Geometry
/// by (movingDx, movingDy) without creating new Line objects. /// by (movingDx, movingDy) without creating new Line objects.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
List<Line> movingLines, double movingDx, double movingDy, List<Line> movingLines,
List<Line> stationaryLines, PushDirection direction) double movingDx,
double movingDy,
List<Line> stationaryLines,
PushDirection direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var movingOffset = new Vector(movingDx, movingDy); var movingOffset = new Vector(movingDx, movingDy);
@@ -226,7 +285,8 @@ namespace OpenNest.Geometry
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
// Case 2: Each stationary vertex -> each moving edge (opposite direction) // Case 2: Each stationary vertex -> each moving edge (opposite direction)
@@ -239,7 +299,8 @@ namespace OpenNest.Geometry
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
return minDist; return minDist;
@@ -267,9 +328,12 @@ namespace OpenNest.Geometry
/// to avoid all intermediate object allocations. /// to avoid all intermediate object allocations.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
(Vector start, Vector end)[] movingEdges, Vector movingOffset, (Vector start, Vector end)[] movingEdges,
(Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset, Vector movingOffset,
PushDirection direction) (Vector start, Vector end)[] stationaryEdges,
Vector stationaryOffset,
PushDirection direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
@@ -281,7 +345,8 @@ namespace OpenNest.Geometry
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction); var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
// Case 2: Each stationary vertex -> each moving edge (opposite direction) // Case 2: Each stationary vertex -> each moving edge (opposite direction)
@@ -293,15 +358,19 @@ namespace OpenNest.Geometry
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
return minDist; return minDist;
} }
public static double OneWayDistance( public static double OneWayDistance(
Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset, Vector vertex,
PushDirection direction) (Vector start, Vector end)[] edges,
Vector edgeOffset,
PushDirection direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var vx = vertex.X; var vx = vertex.X;
@@ -315,7 +384,9 @@ namespace OpenNest.Geometry
var e1 = edges[i].start + edgeOffset; var e1 = edges[i].start + edgeOffset;
var e2 = edges[i].end + edgeOffset; var e2 = edges[i].end + edgeOffset;
double perpValue, edgeMin, edgeMax; double perpValue,
edgeMin,
edgeMax;
if (horizontal) if (horizontal)
{ {
perpValue = vy; perpValue = vy;
@@ -337,7 +408,8 @@ namespace OpenNest.Geometry
continue; continue;
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
return minDist; return minDist;
@@ -347,11 +419,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return PushDirection.Right; case PushDirection.Left:
case PushDirection.Right: return PushDirection.Left; return PushDirection.Right;
case PushDirection.Up: return PushDirection.Down; case PushDirection.Right:
case PushDirection.Down: return PushDirection.Up; return PushDirection.Left;
default: return direction; case PushDirection.Up:
return PushDirection.Down;
case PushDirection.Down:
return PushDirection.Up;
default:
return direction;
} }
} }
@@ -364,11 +441,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return box.Left - boundary.Left; case PushDirection.Left:
case PushDirection.Right: return boundary.Right - box.Right; return box.Left - boundary.Left;
case PushDirection.Up: return boundary.Top - box.Top; case PushDirection.Right:
case PushDirection.Down: return box.Bottom - boundary.Bottom; return boundary.Right - box.Right;
default: return double.MaxValue; case PushDirection.Up:
return boundary.Top - box.Top;
case PushDirection.Down:
return box.Bottom - boundary.Bottom;
default:
return double.MaxValue;
} }
} }
@@ -376,11 +458,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return new Vector(-distance, 0); case PushDirection.Left:
case PushDirection.Right: return new Vector(distance, 0); return new Vector(-distance, 0);
case PushDirection.Up: return new Vector(0, distance); case PushDirection.Right:
case PushDirection.Down: return new Vector(0, -distance); return new Vector(distance, 0);
default: return new Vector(); case PushDirection.Up:
return new Vector(0, distance);
case PushDirection.Down:
return new Vector(0, -distance);
default:
return new Vector();
} }
} }
@@ -388,11 +475,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return from.Left - to.Right; case PushDirection.Left:
case PushDirection.Right: return to.Left - from.Right; return from.Left - to.Right;
case PushDirection.Up: return to.Bottom - from.Top; case PushDirection.Right:
case PushDirection.Down: return from.Bottom - to.Top; return to.Left - from.Right;
default: return double.MaxValue; case PushDirection.Up:
return to.Bottom - from.Top;
case PushDirection.Down:
return from.Bottom - to.Top;
default:
return double.MaxValue;
} }
} }
@@ -409,23 +501,27 @@ namespace OpenNest.Geometry
if (direction.X < -Tolerance.Epsilon) if (direction.X < -Tolerance.Epsilon)
{ {
var d = (box.Left - boundary.Left) / -direction.X; var d = (box.Left - boundary.Left) / -direction.X;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
else if (direction.X > Tolerance.Epsilon) else if (direction.X > Tolerance.Epsilon)
{ {
var d = (boundary.Right - box.Right) / direction.X; var d = (boundary.Right - box.Right) / direction.X;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
if (direction.Y < -Tolerance.Epsilon) if (direction.Y < -Tolerance.Epsilon)
{ {
var d = (box.Bottom - boundary.Bottom) / -direction.Y; var d = (box.Bottom - boundary.Bottom) / -direction.Y;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
else if (direction.Y > Tolerance.Epsilon) else if (direction.Y > Tolerance.Epsilon)
{ {
var d = (boundary.Top - box.Top) / direction.Y; var d = (boundary.Top - box.Top) / direction.Y;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
return dist < 0 ? 0 : dist; return dist < 0 ? 0 : dist;
@@ -463,7 +559,11 @@ namespace OpenNest.Geometry
/// Computes the minimum translation distance along an arbitrary unit direction /// Computes the minimum translation distance along an arbitrary unit direction
/// before any edge of movingLines contacts any edge of stationaryLines. /// before any edge of movingLines contacts any edge of stationaryLines.
/// </summary> /// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction) public static double DirectionalDistance(
List<Line> movingLines,
List<Line> stationaryLines,
Vector direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var dirX = direction.X; var dirX = direction.X;
@@ -476,8 +576,18 @@ namespace OpenNest.Geometry
for (var i = 0; i < stationaryLines.Count; i++) for (var i = 0; i < stationaryLines.Count; i++)
{ {
var e = stationaryLines[i]; var e = stationaryLines[i];
var d = RayEdgeDistance(mv.X, mv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, dirX, dirY); var d = RayEdgeDistance(
if (d < minDist) minDist = d; mv.X,
mv.Y,
e.pt1.X,
e.pt1.Y,
e.pt2.X,
e.pt2.Y,
dirX,
dirY
);
if (d < minDist)
minDist = d;
} }
} }
@@ -491,8 +601,18 @@ namespace OpenNest.Geometry
for (var i = 0; i < movingLines.Count; i++) for (var i = 0; i < movingLines.Count; i++)
{ {
var e = movingLines[i]; var e = movingLines[i];
var d = RayEdgeDistance(sv.X, sv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, oppX, oppY); var d = RayEdgeDistance(
if (d < minDist) minDist = d; sv.X,
sv.Y,
e.pt1.X,
e.pt1.Y,
e.pt2.X,
e.pt2.Y,
oppX,
oppY
);
if (d < minDist)
minDist = d;
} }
} }
@@ -505,9 +625,16 @@ namespace OpenNest.Geometry
/// stationaryEntities. Delegates to the Vector-based overload. /// stationaryEntities. Delegates to the Vector-based overload.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction) List<Entity> movingEntities,
List<Entity> stationaryEntities,
PushDirection direction
)
{ {
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0)); return DirectionalDistance(
movingEntities,
stationaryEntities,
DirectionToOffset(direction, 1.0)
);
} }
/// <summary> /// <summary>
@@ -517,7 +644,10 @@ namespace OpenNest.Geometry
/// without tessellation. /// without tessellation.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction) List<Entity> movingEntities,
List<Entity> stationaryEntities,
Vector direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var dirX = direction.X; var dirX = direction.X;
@@ -536,7 +666,8 @@ namespace OpenNest.Geometry
if (d < minDist) if (d < minDist)
{ {
minDist = d; minDist = d;
if (d <= 0) return 0; if (d <= 0)
return 0;
} }
} }
} }
@@ -557,7 +688,8 @@ namespace OpenNest.Geometry
if (d < minDist) if (d < minDist)
{ {
minDist = d; minDist = d;
if (d <= 0) return 0; if (d <= 0)
return 0;
} }
} }
} }
@@ -566,10 +698,24 @@ namespace OpenNest.Geometry
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual // Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
// closest point on a small corner arc to a straight edge may lie between // closest point on a small corner arc to a straight edge may lie between
// those samples. Use ClosestPointTo to find it and fire a ray from there. // those samples. Use ClosestPointTo to find it and fire a ray from there.
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist); minDist = ArcToLineClosestDistance(
if (minDist <= 0) return 0; movingEntities,
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist); stationaryEntities,
if (minDist <= 0) return 0; dirX,
dirY,
minDist
);
if (minDist <= 0)
return 0;
minDist = ArcToLineClosestDistance(
stationaryEntities,
movingEntities,
oppX,
oppY,
minDist
);
if (minDist <= 0)
return 0;
// Phase 4: Curve-to-curve direct distance. // Phase 4: Curve-to-curve direct distance.
// The vertex-to-entity approach misses the closest contact between two // The vertex-to-entity approach misses the closest contact between two
@@ -605,20 +751,35 @@ namespace OpenNest.Geometry
if (me is Arc mArc) if (me is Arc mArc)
{ {
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx)); var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed)) if (
!Angle.IsBetweenRad(
angle,
mArc.StartAngle,
mArc.EndAngle,
mArc.IsReversed
)
)
continue; continue;
} }
if (se is Arc sArc) if (se is Arc sArc)
{ {
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx)); var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed)) if (
!Angle.IsBetweenRad(
angle,
sArc.StartAngle,
sArc.EndAngle,
sArc.IsReversed
)
)
continue; continue;
} }
} }
minDist = d; minDist = d;
if (d <= 0) return 0; if (d <= 0)
return 0;
} }
} }
@@ -626,8 +787,12 @@ namespace OpenNest.Geometry
} }
private static double ArcToLineClosestDistance( private static double ArcToLineClosestDistance(
List<Entity> arcEntities, List<Entity> lineEntities, List<Entity> arcEntities,
double dirX, double dirY, double minDist) List<Entity> lineEntities,
double dirX,
double dirY,
double minDist
)
{ {
for (var i = 0; i < arcEntities.Count; i++) for (var i = 0; i < arcEntities.Count; i++)
{ {
@@ -662,15 +827,30 @@ namespace OpenNest.Geometry
{ {
var theta = k == 0 ? theta1 : theta2; var theta = k == 0 ? theta1 : theta2;
if (!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)) if (
!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)
)
continue; continue;
var qx = cx + r * System.Math.Cos(theta); var qx = cx + r * System.Math.Cos(theta);
var qy = cy + r * System.Math.Sin(theta); var qy = cy + r * System.Math.Sin(theta);
var d = RayEdgeDistance(qx, qy, p1x, p1y, line.pt2.X, line.pt2.Y, var d = RayEdgeDistance(
dirX, dirY); qx,
if (d < minDist) { minDist = d; if (d <= 0) return 0; } qy,
p1x,
p1y,
line.pt2.X,
line.pt2.Y,
dirX,
dirY
);
if (d < minDist)
{
minDist = d;
if (d <= 0)
return 0;
}
} }
} }
} }
@@ -678,28 +858,54 @@ namespace OpenNest.Geometry
} }
private static double RayEntityDistance( private static double RayEntityDistance(
double vx, double vy, Entity entity, double dirX, double dirY) double vx,
double vy,
Entity entity,
double dirX,
double dirY
)
{ {
if (entity is Line line) if (entity is Line line)
{ {
return RayEdgeDistance(vx, vy, return RayEdgeDistance(
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y, vx,
dirX, dirY); vy,
line.pt1.X,
line.pt1.Y,
line.pt2.X,
line.pt2.Y,
dirX,
dirY
);
} }
if (entity is Arc arc) if (entity is Arc arc)
{ {
return RayArcDistance(vx, vy, return RayArcDistance(
arc.Center.X, arc.Center.Y, arc.Radius, vx,
arc.StartAngle, arc.EndAngle, arc.IsReversed, vy,
dirX, dirY); arc.Center.X,
arc.Center.Y,
arc.Radius,
arc.StartAngle,
arc.EndAngle,
arc.IsReversed,
dirX,
dirY
);
} }
if (entity is Circle circle) if (entity is Circle circle)
{ {
return RayCircleDistance(vx, vy, return RayCircleDistance(
circle.Center.X, circle.Center.Y, circle.Radius, vx,
dirX, dirY); vy,
circle.Center.X,
circle.Center.Y,
circle.Radius,
dirX,
dirY
);
} }
return double.MaxValue; return double.MaxValue;
@@ -759,7 +965,10 @@ namespace OpenNest.Geometry
return CollectVertices(ToEdgeArray(lines), offset); return CollectVertices(ToEdgeArray(lines), offset);
} }
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset) private static HashSet<Vector> CollectVertices(
(Vector start, Vector end)[] edges,
Vector offset
)
{ {
var vertices = new HashSet<Vector>(); var vertices = new HashSet<Vector>();
for (var i = 0; i < edges.Length; i++) for (var i = 0; i < edges.Length; i++)
@@ -778,26 +987,48 @@ namespace OpenNest.Geometry
return edges; return edges;
} }
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction) private static void SortEdgesForPruning(
(Vector start, Vector end)[] edges,
PushDirection direction
)
{ {
if (direction == PushDirection.Left || direction == PushDirection.Right) if (direction == PushDirection.Left || direction == PushDirection.Right)
System.Array.Sort(edges, (a, b) => System.Array.Sort(
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y))); edges,
(a, b) =>
System
.Math.Min(a.start.Y, a.end.Y)
.CompareTo(System.Math.Min(b.start.Y, b.end.Y))
);
else else
System.Array.Sort(edges, (a, b) => System.Array.Sort(
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X))); edges,
(a, b) =>
System
.Math.Min(a.start.X, a.end.X)
.CompareTo(System.Math.Min(b.start.X, b.end.X))
);
} }
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r) private static bool TryGetCurveParams(
Entity entity,
out double cx,
out double cy,
out double r
)
{ {
if (entity is Circle circle) if (entity is Circle circle)
{ {
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius; cx = circle.Center.X;
cy = circle.Center.Y;
r = circle.Radius;
return true; return true;
} }
if (entity is Arc arc) if (entity is Arc arc)
{ {
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius; cx = arc.Center.X;
cy = arc.Center.Y;
r = arc.Radius;
return true; return true;
} }
cx = cy = r = 0; cx = cy = r = 0;
@@ -850,7 +1081,13 @@ namespace OpenNest.Geometry
return new Box(lft, btm, rgt - lft, top - btm); return new Box(lft, btm, rgt - lft, top - btm);
} }
private static bool FindVerticalLimits(Vector pt, Box bounds, List<Box> boxes, out double top, out double btm) private static bool FindVerticalLimits(
Vector pt,
Box bounds,
List<Box> boxes,
out double top,
out double btm
)
{ {
top = double.MaxValue; top = double.MaxValue;
btm = double.MinValue; btm = double.MinValue;
@@ -868,20 +1105,30 @@ namespace OpenNest.Geometry
if (top == double.MaxValue) if (top == double.MaxValue)
{ {
if (bounds.Top > pt.Y) top = bounds.Top; if (bounds.Top > pt.Y)
else return false; top = bounds.Top;
else
return false;
} }
if (btm == double.MinValue) if (btm == double.MinValue)
{ {
if (bounds.Bottom < pt.Y) btm = bounds.Bottom; if (bounds.Bottom < pt.Y)
else return false; btm = bounds.Bottom;
else
return false;
} }
return true; return true;
} }
private static bool FindHorizontalLimits(Vector pt, Box bounds, List<Box> boxes, out double lft, out double rgt) private static bool FindHorizontalLimits(
Vector pt,
Box bounds,
List<Box> boxes,
out double lft,
out double rgt
)
{ {
lft = double.MinValue; lft = double.MinValue;
rgt = double.MaxValue; rgt = double.MaxValue;
@@ -899,14 +1146,18 @@ namespace OpenNest.Geometry
if (rgt == double.MaxValue) if (rgt == double.MaxValue)
{ {
if (bounds.Right > pt.X) rgt = bounds.Right; if (bounds.Right > pt.X)
else return false; rgt = bounds.Right;
else
return false;
} }
if (lft == double.MinValue) if (lft == double.MinValue)
{ {
if (bounds.Left < pt.X) lft = bounds.Left; if (bounds.Left < pt.X)
else return false; lft = bounds.Left;
else
return false;
} }
return true; return true;
+32 -16
View File
@@ -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);
} }
+2 -2
View File
@@ -1,5 +1,5 @@
using OpenNest.Math; using System;
using System; using OpenNest.Math;
namespace OpenNest.Geometry namespace OpenNest.Geometry
{ {
+1 -3
View File
@@ -2,9 +2,7 @@
{ {
public class Material public class Material
{ {
public Material() public Material() { }
{
}
public Material(string name) public Material(string name)
{ {
+2 -4
View File
@@ -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;
} }
} }
} }
+30 -9
View File
@@ -10,13 +10,18 @@ namespace OpenNest.Math
/// </summary> /// </summary>
public static class ExpressionEvaluator public static class ExpressionEvaluator
{ {
public static double Evaluate(string expression, IReadOnlyDictionary<string, double> variables) public static double Evaluate(
string expression,
IReadOnlyDictionary<string, double> variables
)
{ {
var parser = new Parser(expression, variables); var parser = new Parser(expression, variables);
var result = parser.ParseExpression(); var result = parser.ParseExpression();
parser.SkipWhitespace(); parser.SkipWhitespace();
if (!parser.IsEnd) if (!parser.IsEnd)
throw new FormatException($"Unexpected character at position {parser.Position}: '{parser.Current}'"); throw new FormatException(
$"Unexpected character at position {parser.Position}: '{parser.Current}'"
);
return result; return result;
} }
@@ -52,10 +57,12 @@ namespace OpenNest.Math
while (true) while (true)
{ {
SkipWhitespace(); SkipWhitespace();
if (IsEnd) break; if (IsEnd)
break;
var op = Current; var op = Current;
if (op != '+' && op != '-') break; if (op != '+' && op != '-')
break;
_pos++; _pos++;
SkipWhitespace(); SkipWhitespace();
@@ -75,10 +82,12 @@ namespace OpenNest.Math
while (true) while (true)
{ {
SkipWhitespace(); SkipWhitespace();
if (IsEnd) break; if (IsEnd)
break;
var op = Current; var op = Current;
if (op != '*' && op != '/') break; if (op != '*' && op != '/')
break;
_pos++; _pos++;
SkipWhitespace(); SkipWhitespace();
@@ -129,7 +138,10 @@ namespace OpenNest.Math
{ {
_pos++; // consume '$' _pos++; // consume '$'
var start = _pos; var start = _pos;
while (_pos < _input.Length && (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_')) while (
_pos < _input.Length
&& (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_')
)
_pos++; _pos++;
if (_pos == start) if (_pos == start)
throw new FormatException("Expected variable name after '$'."); throw new FormatException("Expected variable name after '$'.");
@@ -145,10 +157,19 @@ namespace OpenNest.Math
_pos++; _pos++;
if (_pos == numStart) if (_pos == numStart)
throw new FormatException($"Unexpected character '{Current}' at position {_pos}."); throw new FormatException(
$"Unexpected character '{Current}' at position {_pos}."
);
var numSpan = _input.Slice(numStart, _pos - numStart).ToString(); var numSpan = _input.Slice(numStart, _pos - numStart).ToString();
if (!double.TryParse(numSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) if (
!double.TryParse(
numSpan,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var number
)
)
throw new FormatException($"Invalid number: '{numSpan}'"); throw new FormatException($"Invalid number: '{numSpan}'");
return number; return number;
+5 -3
View File
@@ -7,8 +7,9 @@ namespace OpenNest.Math
{ {
public static class Fraction public static class Fraction
{ {
public static readonly Regex FractionRegex = public static readonly Regex FractionRegex = new Regex(
new Regex(@"((?<WholeNum>\d+)(\ |-))?(?<Fraction>\d+\/\d+)"); @"((?<WholeNum>\d+)(\ |-))?(?<Fraction>\d+\/\d+)"
);
public static bool IsValid(string s) public static bool IsValid(string s)
{ {
@@ -59,7 +60,8 @@ namespace OpenNest.Math
{ {
var sb = new StringBuilder(input); var sb = new StringBuilder(input);
var fractionMatches = FractionRegex.Matches(sb.ToString()) var fractionMatches = FractionRegex
.Matches(sb.ToString())
.Cast<Match>() .Cast<Match>()
.OrderByDescending(m => m.Index); .OrderByDescending(m => m.Index);

Some files were not shown because too many files have changed in this diff Show More