Compare commits

37 Commits
Author SHA1 Message Date
ajandClaude Sonnet 5 02fc0ea3db chore: remove unused NFP nesting code
Delete OpenNest.Engine/Nfp (AutoNester, BottomLeftFill, NfpCache,
SimulatedAnnealing, INestOptimizer, PlacedPart, SequenceEntry), the Core
InnerFitPolygon, and the NestPhase.Nfp member. None had callers outside the
folder: console --autonest and MCP autonest_plate call engine.Nest(), not
AutoNester.

Drop the Nfp cases from NestPhaseExtensionsTests, fix the --autonest help
text, and update CLAUDE.md. NoFitPolygon stays; BestFit pair evaluation
still uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:20:30 -04:00
ajandClaude Sonnet 5 451876c02f fix(engine): stop PairFiller toggling the global fill-strategy registry
PairFiller.EvaluateCandidates wrapped its candidate loop in
FillStrategyRegistry.SetEnabled(...) and reset it with SetEnabled(null)
afterwards. Nothing inside that window re-enters the strategy pipeline
(EvaluateCandidate and FillRemnantBox only use FillLinear), so the
restriction had no effect on the running solve. It did mutate process-global
state: concurrent solves (e.g. benchmark --parallel) could observe the
narrowed strategy list, and the reset cleared any filter set elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:20:30 -04:00
ajandClaude Sonnet 5 5061b41a5d feat(benchmark): build jobs from DXF manifests and run solves in parallel
Benchmark jobs could only come from .nest files. A JSON manifest now lists
DXF files with quantities (plus sheet sizes, spacing, edge spacing, quadrant
and per-part allowRotation), imported through CadImporter. DXF paths resolve
relative to the manifest; sheet sizes are required from the manifest or
--sheet-sizes and are read in the DXFs' own units. Folder scans pick up
*.nest and *.manifest.json, and invalid manifests fail loudly.

BenchmarkRunner now runs (job x engine) solves concurrently, capped by
--parallel N (CLI default 3; --parallel 1 is sequential). Results are written
by index so report order is unchanged. Concurrent solves compete for cores,
so Time(ms) is only clean at --parallel 1; the run prints a note when N > 1.

Also fixes --output for manifest jobs, which tried to read the manifest as a
.nest to copy metadata from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 21:43:41 -04:00
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
aj f0fe79f0f1 Merge remote-tracking branch 'origin/master' 2026-09-19 07:44:40 -04:00
aj a68e252ac7 docs: describe whole-job engine architecture 2026-09-18 20:18:24 -04:00
aj bc6bcae419 docs: document whole-job nesting contracts and migration boundaries 2026-09-18 16:27:32 -04:00
aj 02141f6ca5 refactor(engine): separate plate placement from job allocation 2026-09-18 08:38:23 -04:00
aj ad69023c17 feat(api): accept complete nesting jobs and report fulfillment
Task 6 of the whole-job engine API: adapt the public NestRequest/NestRunner/
NestResponse surface to delegate to the whole-job runner instead of a manual
quantity loop.

- NestRequest: optional explicit Plates stock list (null keeps the legacy
  unlimited SheetSize fallback; empty list means no available stock),
  optional per-part Id (derived as part-{index} when absent), and an explicit
  PlacementStrategy that takes precedence over the legacy Strategy.
- NestRequestPlate: one physical-stock type (id, size, quantity, spacing,
  quadrant).
- NestRunner: imports each DXF once, propagates priority/rotation constraints,
  runs a single NestJobRunner solve, materializes ID/pose placements exactly
  once, and reports aggregate utilization as total placed part area over total
  physical sheet area.
- NestResponse: exposes status, stop reason, part fulfillment, stock usage,
  and plate-to-stock mapping; .nestquote save/load gains a schema version and
  reports completion as unknown for old archives lacking fulfillment metadata.
- Tests: extend the Api request/runner/persistence suites for legacy SheetSize,
  explicit mixed finite stock, stock exhaustion, weighted utilization, old
  archive loading, and new-archive round trips.

Verification: cross-compiles clean on net8.0-windows (Linux). The Api tests
require a Windows runner (net8.0-windows) and are NOT executed here; the
delegated engine logic is covered by the 70-test net8.0 Engine.Tests suite
(committed in Task 5). Windows runtime verification remains outstanding.
2026-09-18 06:15:04 -04:00
aj 2b0b962c8f fix(engine): enforce whole-job safety invariants
Task 5 of the whole-job engine API: add a geometry safety gate that
validates every candidate trial before the runner commits accounting.

- NestJobPlacementValidator: closed-contour validity, rotation-policy
  compliance, work-area containment per quadrant, hole-aware material
  overlap, and required part spacing. Overlap is interior-only, so
  zero-clearance edge/corner contact remains a valid placement.
- NestJobValidator: route candidate validation through the geometry
  gate; reject unusable/unclosed/degenerate contours up front.
- NestJobRunner: wrap candidate evaluation in a progress bridge that
  tags legacy engine detail with the current candidate context.
- LegacyPlateNesterAdapter: forward IProgress to the legacy engine so
  its progress surfaces under the active candidate.
- Tests: geometry (quadrants, rotations, touching, containment, holes,
  empty stock, real Default/Strip smoke), validation, and cancellation
  suites; repaired test fakes that emitted out-of-bounds or overlapping
  placements the gate now correctly rejects.

Engine.Tests: 70 passed, 0 failed, 0 skipped in Debug and Release.
Windows-only OpenNest.Tests not run on Linux.
2026-09-18 05:56:37 -04:00
aj 75c8adc76c feat(engine): select from mixed plate inventory 2026-09-18 02:09:06 -04:00
aj 67f5fb8eca refactor(jobs): single-source strategy resolution in adapter
LegacyPlateNesterAdapter.Create now delegates to PlateNesterFactory
instead of carrying its own minimal Default-only switch, so built-in
strategy resolution has one source of truth. Behavior unchanged:
unknown keys still reject; all four built-ins now resolvable.

44 net8.0 tests pass in Debug and Release; 0 warnings.
2026-09-17 16:26:45 -04:00
aj 5b88d85937 fix(engine): isolate job identity and engine selection
Replace name-based quantity deduction with reference-based drawing
identity in the engine paths the whole-job runner reaches
(NestEngineBase fill/pack, StripNestEngine deduction, RemnantFiller
ledger, IterativeShrinkFiller leftovers). Add instance-scoped
PlateNesterFactory that resolves built-in strategies without touching
the global NestEngineRegistry. Add identity and engine-selection tests.

44 net8.0 tests pass in Debug and Release; no new warnings.
2026-09-17 15:35:59 -04:00
aj 0963b051be feat(engine): execute inventory-bounded multi-plate jobs 2026-09-17 13:55:11 -04:00
aj 71dffce72c feat(engine): introduce whole-job nesting contracts 2026-09-17 13:44:10 -04:00
aj a9e0f8a1d4 Rework OpenNest.Benchmark into a full multi-plate, multi-size nest
Previously each job fixed one plate size and ran a single Nest() call,
which doesn't reflect the actual problem: a real job is fulfilled
across however many plates are needed, drawn from a pool of standard
sheet sizes, not forced onto one fixed sheet.

NestEngineBase.Nest() has no way to pick its own plate's size - it
fills whatever Plate it's given - so size selection now lives in the
harness itself, applied identically to every engine:

- BenchmarkJob carries the full candidate size pool (CandidateSizes)
  instead of one fixed PlateSize; one job per file, not one per size.
- BenchmarkRunner drives a loop: while items remain, pick the smallest
  candidate size that fits the largest still-unplaced drawing (reusing
  the codebase's own MultiPlateNester.CreatePlate/FitsBounds), build a
  fresh plate of that size, and run one Nest() call to fill it. Repeat
  until everything is placed, no candidate size fits what's left, or a
  safety cap (40 plates) is hit.
- NestValidator now validates bounds/spacing per plate but the
  quantity cap once globally across all plates, since that limit
  belongs to the whole order, not any one sheet.
- JobResult/Report report PlatesUsed and a per-size breakdown instead
  of a single-plate bounding-box compactness metric; utilization is
  now aggregated across every plate the engine used. Ranking keeps the
  same rule (utilization first), with fewer plates as the tie-break
  when both are fully placed and tied - the natural multi-plate
  analogue of the old single-plate compactness tie-break.

Smoke-tested against the synthetic sample across 5 candidate sizes:
correctly builds one job, picks the smallest fitting size, uses
however many plates each engine needs (1-2 here), and still catches
StripNestEngine's pre-existing out-of-bounds bug.
2026-09-15 21:36:31 -04:00
aj 20da5477b6 Fix NestValidator: add area-budget backstop, cheaper polygon conversion
Collision.HasOverlap (and Part.Intersects, which shares the same
underlying algorithm) was observed to return false negatives on real,
complex production geometry: a layout with a combined placed area over
7x the plate's work area passed the polygon-based spacing/overlap
check with zero flagged pairs. This is a pre-existing gap in
OpenNest.Core's Collision detection, not something introduced here,
but it let an obviously-invalid layout score as "valid".

ValidateAreaBudget adds a hard mathematical backstop that does not
depend on Collision at all: non-overlapping parts confined to the work
area can never have a combined area greater than the work area itself.

Also switch WorldPolygon from Shape.ToPolygon() (default up to 1000
segments per arc) to ToPolygonWithTolerance(0.01), matching the
convention already used elsewhere in the codebase (e.g.
BestFit.PolygonHelper) - arc-heavy real parts were producing
thousands-of-vertex polygons for a simple spacing check.
2026-09-15 20:54:12 -04:00
aj 6a0fba0fec Add OpenNest.Benchmark: generic head-to-head engine comparison harness
Loads any .nest file (or folder of them) via NestReader and nests every
drawing with quantity > 0 using each registered NestEngineBase, so it
works sight-unseen against arbitrary real jobs without any hardcoded
geometry. Optionally sweeps a fixed --sheet-sizes list instead of each
file's own plate size.

- BenchmarkJob/JobLoader build immutable job specs; a fresh Plate and
  NestItem list is created per (job, engine) run so state never leaks
  between engines or jobs.
- NestValidator rejects a layout if any part falls outside the work
  area, any two parts are closer than PartSpacing (checked via each
  part's own world-space polygon inflated by the spacing, so it holds
  for arbitrary concave/holed geometry, not just bounding boxes), or a
  drawing gets more parts than requested.
- Scoring matches Plate.Utilization() (placed area / full sheet area);
  ties among fully-placed layouts break on the smaller used bounding
  box (more usable remnant).
- Report prints a per-job ranked breakdown plus a per-engine summary
  (wins, avg utilization, time), and can write a flat CSV.

Verified end-to-end against a synthetic .nest file (not committed)
against the four built-in engines; caught a genuine out-of-work-area
bug in StripNestEngine in the process.
2026-09-15 18:31:36 -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
511 changed files with 23596 additions and 8077 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
+29 -9
View File
@@ -8,24 +8,30 @@ OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts
## Build ## Build
This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-windows`. Build with: This is a .NET 8 solution using SDK-style `.csproj` files. The desktop app and Windows-dependent projects target `net8.0-windows`; the core libraries and `OpenNest.Console` target `net8.0`. Build the full solution on Windows with:
```bash ```bash
dotnet build OpenNest.sln dotnet build OpenNest.sln
``` ```
Cross-platform whole-job engine tests (net8.0, runs on Linux/macOS/Windows without the desktop project or DXF fixtures): `dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj`. The main `OpenNest.Tests` suite also targets `net8.0`: run `dotnet test OpenNest.Tests/OpenNest.Tests.csproj` independently on Linux/macOS/Windows. It must not reference the WinForms `OpenNest` project. The API, Data, Cincinnati, and GravographIS libraries target `net8.0`; post-processor build deployment still targets the desktop app's `net8.0-windows/Posts` directory. Optional CHR-font fixtures are configured through `OpenNest.Tests/test-config.json` and skip when absent.
`OpenNest.WinForms.Tests` contains the desktop-assembly-dependent `CadBendNoteTests` (`CadText`) and `CuttingParametersSerializerTests` (`CuttingParametersSerializer`). It targets `net8.0-windows`, references `OpenNest`, and requires a Windows runner: `dotnet test OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj`. Keep future desktop-dependent tests here rather than in `OpenNest.Tests`. Linux cross-compilation uses `dotnet build OpenNest.WinForms.Tests/OpenNest.WinForms.Tests.csproj -p:EnableWindowsTargeting=true`; cross-compilation is not Windows runtime verification.
Cross-platform CAD import tests: `dotnet test OpenNest.IO.Tests/OpenNest.IO.Tests.csproj`. These synthetic-DXF and bend-repair tests target `net8.0`, require no external fixtures, and are included in the solution. Build the headless console independently with `dotnet build OpenNest.Console/OpenNest.Console.csproj`.
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp), `Microsoft.ML.OnnxRuntime` (in OpenNest.Engine for ML angle prediction), `Microsoft.EntityFrameworkCore.Sqlite` (in OpenNest.Training). NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp), `Microsoft.ML.OnnxRuntime` (in OpenNest.Engine for ML angle prediction), `Microsoft.EntityFrameworkCore.Sqlite` (in OpenNest.Training).
## Architecture ## Architecture
Eight projects form a layered architecture: Nine projects form a layered architecture:
### OpenNest.Core (class library) ### OpenNest.Core (class library)
Domain model, geometry, and CNC primitives organized into namespaces: Domain model, geometry, and CNC primitives organized into namespaces:
- **Root** (`namespace OpenNest`): Domain model — `Nest``Plate[]``Part[]``Drawing``Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`. - **Root** (`namespace OpenNest`): Domain model — `Nest``Plate[]``Part[]``Drawing``Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output. - **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output.
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction). - **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental). - **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed. - **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration. - **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
@@ -35,9 +41,11 @@ Domain model, geometry, and CNC primitives organized into namespaces:
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning. - **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
### OpenNest.Engine (class library, depends on Core) ### OpenNest.Engine (class library, depends on Core)
Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine. Nesting algorithms provide both a legacy single-plate API and a whole-job API. The legacy path centers on `NestEngineBase`, `DefaultNestEngine` (formerly `NestEngine`), and the global `NestEngineRegistry`. New job callers use immutable, ID-based contracts in `Jobs/`: `INestingEngine.Solve(NestJob)` returns `NestJobResult`; `NestJobRunner` alone commits demand and finite/unlimited stock accounting; `IPlateNester` only proposes a one-sheet candidate; and `PlateNesterFactory` resolves a named strategy without reading or changing the process-global registry.
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. - **Whole-job API (`Jobs/`)**: `NestJob` owns part requirements, physical stock, and options for one material/thickness/unit system. `PartGeometrySnapshot` contains owned flat rapid/line/arc geometry; results contain stock IDs and placement poses (radians), not mutable desktop models. `NestJobPlacementValidator` validates contours, rotation, usable work area, overlap, and spacing before accounting commits. The runner selects valid trial candidates greedily by priority vector, sheet area, envelope, and input order; an incomplete result reports why but does not prove geometric impossibility. `DrawingJobMapper` and `NestResultMaterializer` are the domain-boundary adapters.
- **Placement boundary (`Jobs/Placement/`, `Jobs/Adapters/`)**: `DefaultPlateNester` and `StripPlateNester` are migrated built-ins with run-scoped private geometry; `LegacyPlateNesterAdapter` remains for remnant strategies and legacy plugins/callers during rollout. Job-path identity is reference-based rather than drawing name; `PlateOptimizer` retains legacy name-based helpers and is deliberately outside the runner path.
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`. Existing desktop, CLI, and MCP callers remain on this compatibility path until separate migrations preserve their existing-plate, preview, and accept/cancel semantics.
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`. - **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency. - **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`. - **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`.
@@ -45,7 +53,6 @@ Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups. - **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions. - **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts. - **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts.
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): Internal NFP-based single-part placement utilities — `AutoNester` (NFP placement with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`OptimizationResult`. Not exposed as a nest engine; used internally for individual part placement.
- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data). - **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints. - `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback. - `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
@@ -59,9 +66,10 @@ File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types. - `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers. - `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`. - `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
- `Bending/BendRepair` — conservative opt-in repair configured by `CadImportOptions.BendRepair`. Requires explicit inches/mm source units and an endpoint movement limit above 0.001 and at most 3.175 physical mm. Only unambiguous paired ETCH/SCRIBE ticks may move along the existing bend axis; cut geometry and unrelated marks must remain unchanged. Opt-in imports preserve source marks without blanket etch regeneration and expose per-bend outcomes in `CadImportResult.BendRepairReports`.
### OpenNest.Console (console app, depends on Core + Engine + IO) ### OpenNest.Console (console app, depends on Core + Engine + IO)
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`). Command-line interface for batch nesting (`net8.0`). Supports DXF import, plate configuration, linear fill, and multi-drawing auto-nesting through the active engine's `Nest()` (`--autonest`). `--repair-bends-mm <limit> --cad-units inches|mm` opts newly imported DXFs into conservative bend repair and prints per-bend reports; it does not rescale coordinates or repair saved nests.
### OpenNest.Gpu (class library, depends on Core + Engine) ### OpenNest.Gpu (class library, depends on Core + Engine)
GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` implements `IPairEvaluator`, `GpuSlideComputer` implements `ISlideComputer`, and `PartBitmap` handles rasterization. `GpuEvaluatorFactory` provides factory methods. GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` implements `IPairEvaluator`, `GpuSlideComputer` implements `ISlideComputer`, and `PartBitmap` handles rasterization. `GpuEvaluatorFactory` provides factory methods.
@@ -69,6 +77,17 @@ GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` impleme
### OpenNest.Training (console app, depends on Core + Engine) ### OpenNest.Training (console app, depends on Core + Engine)
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training. Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
### OpenNest.Benchmark (console app, depends on Core + Engine + IO)
Compares registered `INestingEngine` implementations against each other on real `.nest` files. Each engine solves the whole job — it owns its own multi-plate/size strategy rather than being handed one already-sized plate at a time. Fully generic — it never hardcodes drawing geometry, just reads whatever drawings/quantities/plate settings each input file already has.
- `JobLoader` builds `BenchmarkJob`s from a `.nest` file or a folder of them via `NestReader`, using every drawing with `Quantity.Required > 0`. `--sheet-sizes` can sweep a fixed list of plate sizes instead of each file's own.
- `DxfManifestLoader` builds a `BenchmarkJob` from a JSON manifest (`sheetSizes`, `spacing`, `edgeSpacing`, `quadrant`, `parts[] { dxf, quantity, allowRotation }`) instead of a `.nest`, importing each DXF with `CadImporter.ImportDrawing`. DXF paths resolve relative to the manifest; sheet sizes are required (manifest or `--sheet-sizes`, which overrides). `allowRotation: false` locks rotation the same way `NestRunner` does. `JobLoader.Load` routes `*.json` inputs to it, and folder scans pick up `*.nest` plus `*.manifest.json` (plain `*.json` is ignored so `--output` reports are never read as manifests). Invalid manifests throw rather than being skipped.
- `BenchmarkJob.BuildNestJob(maxPlates)` converts the job into a `NestJob`: one `NestJobPart` per requested drawing (via `DrawingJobMapper.FromDrawing`) and one `NestPlateStock` per candidate sheet size (unlimited quantity — the engine decides how many of each size it uses).
- `BenchmarkRunner` fans the (job × engine) pairs out with `Parallel.ForEach` (`NoBuffering`, `MaxDegreeOfParallelism` from `--parallel`, CLI default 3, `Run`'s own default 1) and writes results by index so report order stays job-then-engine. Each solve builds its own `NestJob` snapshot and materialized drawings, so solves share no mutable drawing state. Concurrent solves compete for cores, so `Time(ms)` is only clean at `--parallel 1`. It calls each engine's `INestingEngine.Solve(NestJob)` once per job, under a wall-clock timeout so a runaway or hanging engine can't stall the whole benchmark run, then materializes the result back into legacy `Plate`/`Part` objects via `NestResultMaterializer` for scoring.
- `NestValidator` checks the returned layout: every part inside `Plate.WorkArea()`, every pair at least `Plate.PartSpacing` apart (checked geometrically via each part's own world-space polygon, inflated by the spacing — works on arbitrary concave/holed shapes, not just bounding boxes), and no drawing over its requested quantity. An invalid, throwing, or timed-out run scores zero for that job.
- Scoring matches `Plate.Utilization()` (placed drawing area / full sheet area, `Plate.Area()`). If an engine placed every requested part, ties are broken by fewer plates used (`Report`'s ranking rule) — using fewer sheets to do the same job wastes less material.
- `--engines Name1,Name2` filters to specific registered engines (default: all); `--csv <path>` writes a flat per-job CSV alongside the console report.
### OpenNest.Mcp (console app, depends on Core + Engine + IO) ### OpenNest.Mcp (console app, depends on Core + Engine + IO)
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`. MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
@@ -110,7 +129,7 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
## Key Patterns ## Key Patterns
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`. - OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Nfp` (NFP-based nesting, not yet integrated), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`. - OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`.
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings. - `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion). - Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations. - `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
@@ -119,4 +138,5 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies. - `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load). - **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity. - **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). Console, MCP, API, and Training projects use `ImportDrawing` for headless conversion. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata. - **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). MCP, API, and Training projects use `ImportDrawing` for headless conversion. The console uses `Import` followed by `BuildDrawing` so it can report bend-repair outcomes. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
- **GravographIS engrave/cut passes**: The `OpenNest.Posts.GravographIS` post splits geometry by `LayerType` into ordered tool passes — engrave (`Scribe`) then cut (`Cut`/`Leadin`/`Leadout`); `Display` is skipped. `ConvertGeometry` tags DXF layers `ENGRAVE`/`ETCH` (lines, arcs, circles) as `Scribe`; the layer round-trips through `.nest` via `NestWriter`/`ProgramReader`. `NestPolylineExtractor.ExtractLayered` carries `LayerType` per polyline (splitting a continuous chain at any layer change); `GravographISPostProcessor.BuildPasses` groups them and `GravographISWriter.Write(IReadOnlyList<GravographPass>, …)` emits each pass at its own feed/depth, parking to origin and emitting an operator pause (motor off → aux off → `LB` console message → motor on) before any pass whose config has `PauseBefore`. Per-pass parameters live in `GravographISPostConfig` (an `IConfigurablePostProcessor` config with `Engrave`/`Cut` `LayerCutConfig` blocks), edited in the shared `PostProcessorConfigForm` PropertyGrid and persisted to JSON. The cut block pauses by default so the operator can swap/adjust the tool (the spring-floated spindle means programmed `DZ` depth is not the real cut depth).
+11
View File
@@ -6,10 +6,21 @@ namespace OpenNest.Api;
public class NestRequest public class NestRequest
{ {
public IReadOnlyList<NestRequestPart> Parts { get; init; } = []; public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
/// <summary>
/// Explicit available physical stock. Null keeps the legacy unlimited SheetSize fallback;
/// an empty list deliberately means no stock is available.
/// </summary>
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>
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>
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;
} }
+2
View File
@@ -2,6 +2,8 @@ namespace OpenNest.Api;
public class NestRequestPart public class NestRequestPart
{ {
/// <summary>Optional stable requirement identity. NestRunner derives part-{requestIndex} when omitted.</summary>
public string Id { get; init; }
public string DxfPath { get; init; } public string DxfPath { get; init; }
public int Quantity { get; init; } = 1; public int Quantity { get; init; } = 1;
public bool AllowRotation { get; init; } = true; public bool AllowRotation { get; init; } = true;
+16
View File
@@ -0,0 +1,16 @@
using OpenNest.Geometry;
namespace OpenNest.Api;
/// <summary>One explicit physical-stock type for a whole nesting job.</summary>
public class NestRequestPlate
{
public string Id { get; init; }
public Size Size { get; init; }
/// <summary>Available physical sheets; null means unlimited.</summary>
public int? Quantity { get; init; }
public double PartSpacing { get; init; }
public Spacing EdgeSpacing { get; init; }
public int Quadrant { get; init; } = 1;
}
+99 -30
View File
@@ -1,18 +1,42 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenNest.IO; using OpenNest.IO;
namespace OpenNest.Api; namespace OpenNest.Api;
/// <summary>Stable fulfillment metadata for one requested part identity.</summary>
public sealed record NestPartFulfillment(string PartId, int Requested, int Placed, int Unplaced);
/// <summary>Physical-sheet usage for one stock identity.</summary>
public sealed record NestStockUsage(string StockId, int Used, int? Remaining);
/// <summary>Maps each materialized physical sheet to its source stock identity.</summary>
public sealed record NestPlateStockMapping(int PlateIndex, string StockId);
public class NestResponse public class NestResponse
{ {
public const int CurrentSchemaVersion = 2;
/// <summary>Zero identifies an archive written before response metadata was versioned.</summary>
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>
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>
public NestJobStatus? Status { get; init; }
public NestJobStopReason? StopReason { get; init; }
public IReadOnlyList<NestPartFulfillment> Fulfillment { get; init; } = [];
public IReadOnlyList<NestStockUsage> StockUsage { get; init; } = [];
public IReadOnlyList<NestPlateStockMapping> PlateStockMappings { get; init; } = [];
public Nest Nest { get; init; } public Nest Nest { get; init; }
public NestRequest Request { get; init; } public NestRequest Request { get; init; }
@@ -20,7 +44,8 @@ public class NestResponse
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true, WriteIndented = true,
IncludeFields = true // Required for OpenNest.Geometry.Size (public fields) IncludeFields = true, // Required for OpenNest.Geometry.Size and Spacing public fields.
Converters = { new JsonStringEnumConverter() },
}; };
public async Task SaveAsync(string path) public async Task SaveAsync(string path)
@@ -28,32 +53,42 @@ public class NestResponse
using var fs = new FileStream(path, FileMode.Create); using var fs = new FileStream(path, FileMode.Create);
using var zip = new ZipArchive(fs, ZipArchiveMode.Create); using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
// Write request.json
var requestEntry = zip.CreateEntry("request.json"); var requestEntry = zip.CreateEntry("request.json");
await using (var stream = requestEntry.Open()) await using (var stream = requestEntry.Open())
{ {
await JsonSerializer.SerializeAsync(stream, Request, JsonOptions); await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
} }
// Write response.json (metrics only) // Keep persisted data versioned and detached from the live mutable Nest graph.
var metrics = new
{
SheetCount,
Utilization,
CutTimeTicks = CutTime.Ticks,
ElapsedTicks = Elapsed.Ticks
};
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, metrics, JsonOptions); await JsonSerializer.SerializeAsync(
stream,
new NestResponseArchiveDto
{
SchemaVersion = CurrentSchemaVersion,
SheetCount = SheetCount,
Utilization = Utilization,
CutTimeTicks = CutTime.Ticks,
ElapsedTicks = Elapsed.Ticks,
Status = Status,
StopReason = StopReason,
Fulfillment = Fulfillment is null
? []
: new List<NestPartFulfillment>(Fulfillment),
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
PlateStockMappings = PlateStockMappings is null
? []
: new List<NestPlateStockMapping>(PlateStockMappings),
},
JsonOptions
);
} }
// Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry
var nestEntry = zip.CreateEntry("nest.nest"); var nestEntry = zip.CreateEntry("nest.nest");
using var nestMs = new MemoryStream(); using var nestMs = new MemoryStream();
var writer = new NestWriter(Nest); new NestWriter(Nest).Write(nestMs);
writer.Write(nestMs);
nestMs.Position = 0; nestMs.Position = 0;
await using (var stream = nestEntry.Open()) await using (var stream = nestEntry.Open())
{ {
@@ -66,26 +101,41 @@ 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);
// Read request.json var requestEntry =
var requestEntry = zip.GetEntry("request.json") 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");
} }
// Read response.json var responseEntry =
var responseEntry = zip.GetEntry("response.json") zip.GetEntry("response.json")
?? throw new InvalidOperationException("Missing response.json in .nestquote file"); ?? throw new InvalidOperationException("Missing response.json in .nestquote file");
JsonElement metricsJson; NestResponseArchiveDto archive;
var hasSchemaVersion = false;
var hasStatusMetadata = false;
await using (var stream = responseEntry.Open()) await using (var stream = responseEntry.Open())
using (var document = await JsonDocument.ParseAsync(stream))
{ {
metricsJson = await JsonSerializer.DeserializeAsync<JsonElement>(stream, JsonOptions); var root = document.RootElement;
hasSchemaVersion = root.TryGetProperty("schemaVersion", out _);
hasStatusMetadata =
root.TryGetProperty("status", out _)
|| root.TryGetProperty("stopReason", out _)
|| root.TryGetProperty("fulfillment", out _)
|| root.TryGetProperty("stockUsage", out _)
|| root.TryGetProperty("plateStockMappings", out _);
archive =
root.Deserialize<NestResponseArchiveDto>(JsonOptions)
?? throw new InvalidOperationException("Invalid response.json in .nestquote file");
} }
// Read embedded nest.nest via NestReader(Stream) var nestEntry =
var nestEntry = zip.GetEntry("nest.nest") 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())
@@ -95,18 +145,37 @@ public class NestResponse
await stream.CopyToAsync(nestMs); await stream.CopyToAsync(nestMs);
} }
nestMs.Position = 0; nestMs.Position = 0;
var reader = new NestReader(nestMs); nest = new NestReader(nestMs).Read();
nest = reader.Read();
} }
return new NestResponse return new NestResponse
{ {
SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(), SchemaVersion = hasSchemaVersion ? archive.SchemaVersion : 0,
Utilization = metricsJson.GetProperty("utilization").GetDouble(), SheetCount = archive.SheetCount,
CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()), Utilization = archive.Utilization,
Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()), CutTime = TimeSpan.FromTicks(archive.CutTimeTicks),
Elapsed = TimeSpan.FromTicks(archive.ElapsedTicks),
Status = hasStatusMetadata ? archive.Status : null,
StopReason = hasStatusMetadata ? archive.StopReason : null,
Fulfillment = hasStatusMetadata ? archive.Fulfillment ?? [] : [],
StockUsage = hasStatusMetadata ? archive.StockUsage ?? [] : [],
PlateStockMappings = hasStatusMetadata ? archive.PlateStockMappings ?? [] : [],
Nest = nest, Nest = nest,
Request = request Request = request,
}; };
} }
private sealed class NestResponseArchiveDto
{
public int SchemaVersion { get; init; }
public int SheetCount { get; init; }
public double Utilization { get; init; }
public long CutTimeTicks { get; init; }
public long ElapsedTicks { get; init; }
public NestJobStatus? Status { get; init; }
public NestJobStopReason? StopReason { get; init; }
public List<NestPartFulfillment> Fulfillment { get; init; } = [];
public List<NestStockUsage> StockUsage { get; init; } = [];
public List<NestPlateStockMapping> PlateStockMappings { get; init; } = [];
}
} }
+183 -83
View File
@@ -11,125 +11,225 @@ namespace OpenNest.Api;
public static class NestRunner public static class NestRunner
{ {
private const string LegacyStockId = "legacy-sheet";
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
)
{ {
if (request.Parts.Count == 0) ArgumentNullException.ThrowIfNull(request);
var requestParts =
request.Parts
?? throw new ArgumentException("Request parts must not be null.", nameof(request));
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));
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var parts = IdentifyParts(requestParts);
var importedByPath = new Dictionary<string, Drawing>(StringComparer.Ordinal);
var jobParts = new List<NestJobPart>(parts.Count);
// 1. Import DXFs → Drawings foreach (var part in parts)
var drawings = new List<Drawing>();
foreach (var part in request.Parts)
{ {
if (!File.Exists(part.DxfPath)) token.ThrowIfCancellationRequested();
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath); if (!File.Exists(part.Request.DxfPath))
throw new FileNotFoundException(
$"DXF file not found: {part.Request.DxfPath}",
part.Request.DxfPath
);
Drawing drawing; if (!importedByPath.TryGetValue(part.Request.DxfPath, out var drawing))
{
try try
{ {
drawing = CadImporter.ImportDrawing(part.DxfPath, drawing = CadImporter.ImportDrawing(
new CadImportOptions { Quantity = part.Quantity }); part.Request.DxfPath,
new CadImportOptions { Quantity = part.Request.Quantity }
);
} }
catch (System.Exception ex) catch (Exception exception)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"Failed to import DXF: {part.DxfPath}", ex); $"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.DxfPath}"); throw new InvalidOperationException(
$"Failed to import DXF: {part.Request.DxfPath}"
);
drawings.Add(drawing); importedByPath.Add(part.Request.DxfPath, drawing);
} }
// 2. Build NestItems ConfigureDrawingForRequirement(drawing, part.Request);
var items = new List<NestItem>(); jobParts.Add(DrawingJobMapper.FromDrawing(part.Id, drawing, part.Request.Quantity));
for (var i = 0; i < request.Parts.Count; i++)
{
var part = request.Parts[i];
items.Add(new NestItem
{
Drawing = drawings[i],
Quantity = part.Quantity,
Priority = part.Priority,
StepAngle = part.AllowRotation ? 0 : OpenNest.Math.Angle.TwoPI,
});
} }
// 3. Multi-plate loop var job = new NestJob(
var nest = new Nest(); jobParts,
CreateStock(request),
new NestJobOptions(ResolvePlacementStrategy(request))
);
var jobProgress = progress == null ? null : new JobProgressBridge(progress);
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job, jobProgress, token);
// This is the sole translation from immutable result poses to mutable legacy output objects.
var materialized = NestResultMaterializer.Materialize(job, result);
var nest = materialized.Nest;
nest.Thickness = request.Thickness; nest.Thickness = request.Thickness;
nest.Material = new Material(request.Material); nest.Material = new Material(request.Material);
var remaining = items.Select(item => item.Quantity).ToList();
while (remaining.Any(q => q > 0))
{
token.ThrowIfCancellationRequested();
var plate = new Plate(request.SheetSize)
{
PartSpacing = request.Spacing,
};
// Build items for this pass with remaining quantities
var passItems = new List<NestItem>();
for (var i = 0; i < items.Count; i++)
{
if (remaining[i] <= 0) continue;
passItems.Add(new NestItem
{
Drawing = items[i].Drawing,
Quantity = remaining[i],
Priority = items[i].Priority,
StepAngle = items[i].StepAngle,
});
}
// Run engine
var engine = NestEngineRegistry.Create(plate);
var parts = engine.Nest(passItems, progress, token);
if (parts.Count == 0)
break; // No progress — part doesn't fit on fresh sheet
// Add parts to plate and nest
foreach (var p in parts)
plate.Parts.Add(p);
nest.Plates.Add(plate);
// Deduct placed quantities
foreach (var p in parts)
{
var idx = drawings.IndexOf(p.BaseDrawing);
if (idx >= 0)
remaining[idx]--;
}
}
// 4. Compute timing
var timingInfo = Timing.GetTimingInfo(nest); var timingInfo = Timing.GetTimingInfo(nest);
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting); var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
sw.Stop(); sw.Stop();
// 5. Build response return Task.FromResult(
var response = new NestResponse new NestResponse
{ {
SheetCount = nest.Plates.Count, SheetCount = nest.Plates.Count,
Utilization = nest.Plates.Count > 0 Utilization = CalculateUtilization(nest),
? nest.Plates.Average(p => p.Utilization())
: 0,
CutTime = cutTime, CutTime = cutTime,
Elapsed = sw.Elapsed, Elapsed = sw.Elapsed,
Status = result.Status,
StopReason = result.StopReason,
Fulfillment = result
.Fulfillment.Select(value => new NestPartFulfillment(
value.PartId,
value.Requested,
value.Placed,
value.Unplaced
))
.ToArray(),
StockUsage = result
.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, Nest = nest,
Request = request Request = request,
}
);
}
private static IReadOnlyList<IdentifiedRequestPart> IdentifyParts(
IReadOnlyList<NestRequestPart> requestParts
)
{
var identified = new List<IdentifiedRequestPart>(requestParts.Count);
var ids = new HashSet<string>(StringComparer.Ordinal);
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 id = part.Id ?? $"part-{index}";
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Part IDs must not be blank.", nameof(requestParts));
if (!ids.Add(id))
throw new ArgumentException("Part IDs must be unique.", nameof(requestParts));
identified.Add(new IdentifiedRequestPart(id, part));
}
return identified;
}
private static IReadOnlyList<NestPlateStock> CreateStock(NestRequest request)
{
if (request.Plates is null)
{
return
[
new NestPlateStock(
LegacyStockId,
request.SheetSize,
quantity: null,
partSpacing: request.Spacing
),
];
}
var stock = new List<NestPlateStock>(request.Plates.Count);
foreach (var plate in request.Plates)
{
if (plate is null)
throw new ArgumentException(
"Request plates must not contain null entries.",
nameof(request)
);
stock.Add(
new NestPlateStock(
plate.Id,
plate.Size,
plate.Quantity,
plate.PartSpacing,
plate.EdgeSpacing,
plate.Quadrant
)
);
}
return stock;
}
private static void ConfigureDrawingForRequirement(Drawing drawing, NestRequestPart part)
{
drawing.Priority = part.Priority;
drawing.Constraints ??= new NestConstraints();
if (!part.AllowRotation)
{
// A zero legacy step means automatic rotation to DrawingJobMapper, so lock it explicitly.
drawing.Constraints.StepAngle = OpenNest.Math.Angle.TwoPI;
drawing.Constraints.StartAngle = 0;
drawing.Constraints.EndAngle = 0;
}
}
private static string ResolvePlacementStrategy(NestRequest request) =>
request.PlacementStrategy
?? request.Strategy switch
{
NestStrategy.Auto => "Default",
_ => throw new NotSupportedException(
$"Unknown legacy nesting strategy: {request.Strategy}."
),
}; };
return Task.FromResult(response); private static double CalculateUtilization(Nest nest)
{
var sheetArea = nest.Plates.Sum(plate => plate.Area());
if (sheetArea == 0)
return 0;
var placedArea = nest.Plates.Sum(plate =>
plate.Parts.Where(part => !part.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area)
);
return placedArea / sheetArea;
}
private sealed record IdentifiedRequestPart(string Id, NestRequestPart Request);
private sealed class JobProgressBridge(IProgress<NestProgress> progress)
: IProgress<NestJobProgress>
{
public void Report(NestJobProgress value)
{
ArgumentNullException.ThrowIfNull(value);
if (value.LegacyProgress is not null)
progress.Report(value.LegacyProgress);
}
} }
} }
+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>
+75
View File
@@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Benchmark
{
/// <summary>
/// One request to nest a specific drawing, with the quantity and rotation
/// constraints pulled from its source .nest file.
/// </summary>
public class DrawingRequest
{
public Drawing Drawing { get; init; }
public int Quantity { get; init; }
public int Priority { get; init; }
public double StepAngle { get; init; }
public double RotationStart { get; init; }
public double RotationEnd { get; init; }
}
/// <summary>
/// An immutable specification for one benchmark job: the full set of
/// drawings/quantities that must be nested, and the pool of sheet sizes the
/// engine may draw from while doing it. A single run may use several
/// plates - possibly of different sizes - to place everything, the same
/// way a real production job spreads across whatever plates it needs
/// rather than being handed one fixed-size sheet.
/// </summary>
public class BenchmarkJob
{
public string SourceFile { get; init; }
public List<Size> CandidateSizes { get; init; }
public Spacing EdgeSpacing { get; init; }
public double PartSpacing { get; init; }
public int Quadrant { get; init; }
public List<DrawingRequest> Requests { get; init; }
public string Name => Path.GetFileNameWithoutExtension(SourceFile);
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
/// <summary>
/// Builds the whole-job request this job represents: one NestJobPart per
/// requested drawing, and one NestPlateStock per candidate sheet size
/// (unlimited quantity - the engine under test decides how many of each
/// size it actually uses, and how demand splits across plates). The
/// engine owns its own multi-plate/size strategy; this harness no
/// longer picks plate sizes on the engine's behalf.
/// </summary>
public NestJob BuildNestJob(
int maxPlates,
double salvageRate = 0,
double minimumSalvageDimension = 0
)
{
var parts = Requests.Select(r =>
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)
);
var stock = CandidateSizes.Select(size => new NestPlateStock(
size.ToString(1),
size,
null,
PartSpacing,
EdgeSpacing,
Quadrant
));
return new NestJob(
parts,
stock,
new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension)
);
}
}
}
+219
View File
@@ -0,0 +1,219 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace OpenNest.Benchmark
{
/// <summary>
/// Runs every candidate engine against every job. Each engine is a full
/// INestingEngine: it owns its own plate/size selection and multi-plate
/// strategy for the whole job, rather than being handed one already-sized
/// plate at a time by this harness. A per-run timeout guards against a
/// runaway or hanging engine — cooperative cancellation, so it reliably
/// stops engines built on NestJobRunner (all four built-ins) but can't
/// forcibly interrupt an engine that never checks its token.
/// </summary>
public static class BenchmarkRunner
{
/// <summary>Physical-sheet cap passed to every job's NestJobOptions.MaxPlates.</summary>
private const int MaxPlates = 40;
/// <summary>Wall-clock budget for one engine solving one job.</summary>
private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5);
public static List<JobResult> Run(
List<BenchmarkJob> jobs,
IReadOnlyList<NestingEngineInfo> engines,
double salvageRate = 0,
double minimumSalvageDimension = 0,
string outputDirectory = null,
int maxParallelism = 1
)
{
var pairs = jobs.SelectMany(job => engines.Select(engine => (Job: job, Engine: engine)))
.ToList();
var results = new JobResult[pairs.Count];
var options = new ParallelOptions
{
MaxDegreeOfParallelism = System.Math.Max(1, maxParallelism),
};
// NoBuffering hands out one pair at a time: solves run for seconds to minutes,
// so chunked partitioning would leave workers idle behind a slow engine.
Parallel.ForEach(
Partitioner.Create(
Enumerable.Range(0, pairs.Count),
EnumerablePartitionerOptions.NoBuffering
),
options,
i =>
results[i] = RunOne(
pairs[i].Job,
pairs[i].Engine,
salvageRate,
minimumSalvageDimension,
outputDirectory
)
);
// Indexed writes keep the report in job-then-engine order whatever finishes first.
return results.ToList();
}
private static JobResult RunOne(
BenchmarkJob job,
NestingEngineInfo engineInfo,
double salvageRate,
double minimumSalvageDimension,
string outputDirectory
)
{
var requested = job.TotalRequestedQuantity;
var sw = Stopwatch.StartNew();
try
{
var nestJob = job.BuildNestJob(MaxPlates, salvageRate, minimumSalvageDimension);
var engine = engineInfo.Factory();
using var cts = new CancellationTokenSource(SolveTimeout);
var jobResult = engine.Solve(nestJob, null, cts.Token);
var materialized = NestResultMaterializer.Materialize(nestJob, jobResult);
var plateRuns = materialized
.Nest.Plates.Select(plate => (Plate: plate, Parts: plate.Parts.ToList()))
.ToList();
var requirements = job.Requests.ToDictionary<
DrawingRequest,
Drawing,
(string Name, int Quantity)
>(
r => materialized.DrawingsByPartId[r.Drawing.Id.ToString()],
r => (r.Drawing.Name, r.Quantity),
ReferenceEqualityComparer.Instance
);
var validation = NestValidator.Validate(plateRuns, requirements);
var totalPlaced = plateRuns.Sum(pr => pr.Parts.Count);
var placedArea = validation.Valid
? plateRuns.Sum(pr => pr.Parts.Sum(p => p.BaseDrawing.Area))
: 0;
var plateArea = plateRuns.Sum(pr => pr.Plate.Area());
var sizeBreakdown = plateRuns
.GroupBy(pr => pr.Plate.Size.ToString(1))
.OrderByDescending(g => g.Count())
.ToDictionary(g => g.Key, g => g.Count());
if (validation.Valid && outputDirectory != null)
{
System.IO.Directory.CreateDirectory(outputDirectory);
// Keep names and job metadata for a useful inspectable output; never modify source.
// Manifest jobs have no source nest to copy from, so they keep the job's name.
if (job.SourceFile.EndsWith(".nest", StringComparison.OrdinalIgnoreCase))
{
var source = new OpenNest.IO.NestReader(job.SourceFile).Read();
materialized.Nest.Name = source.Name;
materialized.Nest.Units = source.Units;
materialized.Nest.Material = source.Material;
materialized.Nest.Thickness = source.Thickness;
}
else
{
materialized.Nest.Name = job.Name;
}
materialized.Nest.SalvageRate = salvageRate;
foreach (var request in job.Requests)
materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request
.Drawing
.Name;
var path = System.IO.Path.Combine(
outputDirectory,
$"{job.Name}-{engineInfo.Name}.nest"
);
if (
System.IO.Path.GetFullPath(path)
== System.IO.Path.GetFullPath(job.SourceFile)
)
throw new InvalidOperationException(
"Output must not overwrite the source nest."
);
new OpenNest.IO.NestWriter(materialized.Nest).Write(path);
var report = new
{
Source = job.SourceFile,
Engine = engineInfo.Name,
jobResult.Status,
jobResult.StopReason,
Requested = requested,
Placed = totalPlaced,
SheetArea = plateArea,
PlacedArea = placedArea,
SalvageRate = salvageRate,
MinimumSalvageDimension = minimumSalvageDimension,
EstimatedNetArea = jobResult.Plates.Sum(p =>
StockLadderNestingEngine.EstimateNetArea(nestJob, p)
),
Fulfillment = jobResult.Fulfillment,
StockUsage = jobResult.StockUsage,
Plates = jobResult.Plates,
validation.Violations,
};
System.IO.File.WriteAllText(
System.IO.Path.ChangeExtension(path, ".json"),
System.Text.Json.JsonSerializer.Serialize(
report,
new System.Text.Json.JsonSerializerOptions { WriteIndented = true }
)
);
}
sw.Stop();
return new JobResult
{
EngineName = engineInfo.Name,
JobName = job.Name,
Valid = validation.Valid,
Violations = validation.Violations,
PartsPlaced = totalPlaced,
PartsRequested = requested,
PlacedArea = placedArea,
PlateArea = plateArea,
PlatesUsed = plateRuns.Count,
SizeBreakdown = sizeBreakdown,
ElapsedMs = sw.ElapsedMilliseconds,
};
}
catch (OperationCanceledException)
{
sw.Stop();
return new JobResult
{
EngineName = engineInfo.Name,
JobName = job.Name,
Valid = false,
PartsRequested = requested,
ElapsedMs = sw.ElapsedMilliseconds,
Error = $"Timed out after {SolveTimeout.TotalMinutes:F0} minute(s)",
};
}
catch (Exception ex)
{
sw.Stop();
return new JobResult
{
EngineName = engineInfo.Name,
JobName = job.Name,
Valid = false,
PartsRequested = requested,
ElapsedMs = sw.ElapsedMilliseconds,
Error = $"{ex.GetType().Name}: {ex.Message}",
};
}
}
}
}
+175
View File
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Benchmark
{
/// <summary>
/// Builds a BenchmarkJob from a JSON manifest that lists DXF files and the
/// quantity of each to nest. DXF paths resolve relative to the manifest.
/// Sheet sizes come from the manifest or the caller's override; unlike a
/// .nest file there is no plate to inherit them from, so a job with none is
/// an error. Sheet sizes must use the same units as the DXFs.
/// </summary>
public static class DxfManifestLoader
{
/// <summary>Suffix a manifest needs to be picked up when scanning a folder.</summary>
public const string FolderSuffix = ".manifest.json";
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
public static BenchmarkJob Load(
string manifestPath,
IReadOnlyList<Size> sheetSizeOverrides = null,
double? partSpacingOverride = null
)
{
var manifest = ReadManifest(manifestPath);
var baseDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath));
if (manifest.Parts == null || manifest.Parts.Count == 0)
throw new InvalidOperationException(
$"Manifest '{manifestPath}' has no parts. Add entries to \"parts\"."
);
var sizes = ResolveSheetSizes(manifest, sheetSizeOverrides, manifestPath);
var requests = manifest.Parts.Select(p => BuildRequest(p, baseDir)).ToList();
return new BenchmarkJob
{
SourceFile = manifestPath,
CandidateSizes = sizes,
EdgeSpacing = new Spacing(manifest.EdgeSpacing, manifest.EdgeSpacing),
PartSpacing = partSpacingOverride ?? manifest.Spacing,
Quadrant = manifest.Quadrant,
Requests = requests,
};
}
private static Manifest ReadManifest(string manifestPath)
{
try
{
return JsonSerializer.Deserialize<Manifest>(
File.ReadAllText(manifestPath),
JsonOptions
) ?? throw new InvalidOperationException("The manifest is empty.");
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"Manifest '{manifestPath}' is not valid JSON: {ex.Message}",
ex
);
}
}
private static List<Size> ResolveSheetSizes(
Manifest manifest,
IReadOnlyList<Size> overrides,
string manifestPath
)
{
if (overrides != null && overrides.Count > 0)
return overrides.ToList();
var sizes = new List<Size>();
foreach (var text in manifest.SheetSizes ?? new List<string>())
{
if (!Size.TryParse(text, out var size))
throw new InvalidOperationException(
$"Manifest '{manifestPath}': could not parse sheet size '{text}' (expected e.g. \"48x96\")."
);
sizes.Add(size);
}
if (sizes.Count == 0)
throw new InvalidOperationException(
$"Manifest '{manifestPath}' has no sheet sizes. Set \"sheetSizes\" or pass --sheet-sizes."
);
return sizes.Distinct().ToList();
}
private static DrawingRequest BuildRequest(ManifestPart part, string baseDir)
{
if (string.IsNullOrWhiteSpace(part.Dxf))
throw new InvalidOperationException("A manifest part is missing \"dxf\".");
if (part.Quantity <= 0)
throw new InvalidOperationException(
$"Manifest part '{part.Dxf}': quantity must be greater than 0 (was {part.Quantity})."
);
var dxfPath = Path.GetFullPath(Path.Combine(baseDir, part.Dxf));
if (!File.Exists(dxfPath))
throw new FileNotFoundException($"DXF file not found: {dxfPath}", dxfPath);
Drawing drawing;
try
{
drawing = CadImporter.ImportDrawing(
dxfPath,
new CadImportOptions { Quantity = part.Quantity }
);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to import DXF: {dxfPath}", ex);
}
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
throw new InvalidOperationException($"Failed to import DXF: {dxfPath}");
// A zero legacy step means automatic rotation to DrawingJobMapper, so lock it explicitly.
if (!part.AllowRotation)
{
drawing.Constraints ??= new NestConstraints();
drawing.Constraints.StepAngle = OpenNest.Math.Angle.TwoPI;
drawing.Constraints.StartAngle = 0;
drawing.Constraints.EndAngle = 0;
}
var constraints = drawing.Constraints;
return new DrawingRequest
{
Drawing = drawing,
Quantity = part.Quantity,
Priority = drawing.Priority,
StepAngle = constraints?.StepAngle ?? 0,
RotationStart = constraints?.StartAngle ?? 0,
RotationEnd = constraints?.EndAngle ?? 0,
};
}
private class Manifest
{
public List<string> SheetSizes { get; set; }
public double Spacing { get; set; }
public double EdgeSpacing { get; set; }
public int Quadrant { get; set; } = 1;
public List<ManifestPart> Parts { get; set; }
}
private class ManifestPart
{
public string Dxf { get; set; }
public int Quantity { get; set; }
public bool AllowRotation { get; set; } = true;
}
}
}
+162
View File
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Benchmark
{
/// <summary>
/// Builds BenchmarkJobs from .nest files on disk. Fully generic: works on
/// any valid .nest file, using whatever drawings/quantities/plate settings
/// it contains. One job per file, carrying the full pool of candidate
/// sheet sizes the engine may use across the whole nest - by default the
/// distinct sizes already present in that file, or a fixed override list
/// (e.g. a standard sheet-size lineup) applied to every file.
/// </summary>
public static class JobLoader
{
public static List<BenchmarkJob> Load(
string inputPath,
IReadOnlyList<Size> sheetSizeOverrides = null,
double? partSpacingOverride = null
)
{
var files = ResolveFiles(inputPath);
var jobs = new List<BenchmarkJob>();
foreach (var file in files)
{
if (file.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
// Hand-written manifests fail loudly rather than being skipped like unreadable .nest files.
jobs.Add(DxfManifestLoader.Load(file, sheetSizeOverrides, partSpacingOverride));
continue;
}
Nest nest;
try
{
nest = new NestReader(file).Read();
}
catch (Exception ex)
{
Console.Error.WriteLine(
$"[JobLoader] Skipping '{file}': failed to read ({ex.Message})"
);
continue;
}
var requests = BuildRequests(nest);
if (requests.Count == 0)
{
Console.Error.WriteLine(
$"[JobLoader] Skipping '{file}': no drawings with quantity > 0"
);
continue;
}
var template = ResolvePlateTemplate(nest);
var sizes =
sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
? sheetSizeOverrides.ToList()
: ResolveSheetSizes(nest);
jobs.Add(
new BenchmarkJob
{
SourceFile = file,
CandidateSizes = sizes,
EdgeSpacing = template.EdgeSpacing,
PartSpacing = partSpacingOverride ?? template.PartSpacing,
Quadrant = template.Quadrant,
Requests = requests,
}
);
}
return jobs;
}
private static List<string> ResolveFiles(string inputPath)
{
if (Directory.Exists(inputPath))
{
return Directory
.EnumerateFiles(inputPath, "*", SearchOption.AllDirectories)
.Where(f =>
f.EndsWith(".nest", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(
DxfManifestLoader.FolderSuffix,
StringComparison.OrdinalIgnoreCase
)
)
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
.ToList();
}
if (File.Exists(inputPath))
return new List<string> { inputPath };
throw new FileNotFoundException($"Benchmark input not found: {inputPath}");
}
private static List<DrawingRequest> BuildRequests(Nest nest)
{
var requests = new List<DrawingRequest>();
foreach (var drawing in nest.Drawings)
{
var qty = drawing.Quantity.Required;
if (qty <= 0)
continue;
var constraints = drawing.Constraints;
requests.Add(
new DrawingRequest
{
Drawing = drawing,
Quantity = qty,
Priority = drawing.Priority,
StepAngle = constraints?.StepAngle ?? 0,
RotationStart = constraints?.StartAngle ?? 0,
RotationEnd = constraints?.EndAngle ?? 0,
}
);
}
return requests;
}
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(
Nest nest
)
{
var source = nest.Plates?.FirstOrDefault();
if (source != null)
return (source.EdgeSpacing, source.PartSpacing, source.Quadrant);
var defaults = nest.PlateDefaults;
return (defaults.EdgeSpacing, defaults.PartSpacing, defaults.Quadrant);
}
private static List<Size> ResolveSheetSizes(Nest nest)
{
var sizes = (nest.Plates ?? Enumerable.Empty<Plate>())
.Select(p => p.Size)
.Distinct()
.ToList();
if (sizes.Count == 0)
sizes.Add(nest.PlateDefaults.Size);
return sizes;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace OpenNest.Benchmark
{
/// <summary>
/// Outcome of running one engine against one job. A job may span several
/// plates (PlatesUsed, SizeBreakdown), since the engine may need more than
/// one sheet - possibly of different sizes - to place everything asked of
/// it. An invalid or crashed run always scores zero utilization, per the
/// benchmark rules.
/// </summary>
public class JobResult
{
public string EngineName { get; init; }
public string JobName { get; init; }
public bool Valid { get; init; }
public List<string> Violations { get; init; } = new();
public string Error { get; init; }
public int PartsPlaced { get; init; }
public int PartsRequested { get; init; }
public double PlacedArea { get; init; }
public double PlateArea { get; init; }
public int PlatesUsed { get; init; }
public Dictionary<string, int> SizeBreakdown { get; init; } = new();
public long ElapsedMs { get; init; }
public bool Crashed => Error != null;
public bool FullyPlaced => Valid && PartsRequested > 0 && PartsPlaced >= PartsRequested;
/// <summary>Aggregate utilization across every plate the engine used:
/// total placed drawing area over total plate area, matching
/// Plate.Utilization()'s per-plate definition summed across the job.</summary>
public double Utilization => Valid && PlateArea > 0 ? PlacedArea / PlateArea : 0;
}
}
+230
View File
@@ -0,0 +1,230 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Benchmark
{
public class ValidationResult
{
public bool Valid => Violations.Count == 0;
public List<string> Violations { get; } = new();
}
/// <summary>
/// Validates a (possibly multi-plate) placed layout against the benchmark
/// rules: on every plate, every part must lie within that plate's work
/// area and every pair of parts must be at least PartSpacing apart; across
/// all plates combined, no drawing may have more parts placed than
/// requested (the quantity limit is a property of the whole order, not of
/// any one plate). Geometry checks work on arbitrary (concave, holed)
/// polygons by reusing the same world-space extraction Part.Intersects
/// uses internally, so no engine gets an advantage or penalty from shape
/// complexity.
/// </summary>
public static class NestValidator
{
/// <summary>
/// requirements maps each materialized part's BaseDrawing (by reference - materialized
/// Drawing instances are freshly reconstructed per NestResultMaterializer.Materialize, so
/// identity must never be inferred from Name, which is only incidentally seeded from the
/// originating NestJobPart id) to its original quantity limit and display name.
/// </summary>
public static ValidationResult Validate(
List<(Plate Plate, List<Part> Parts)> plateRuns,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
)
{
var result = new ValidationResult();
var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
if (allParts.Count == 0)
return result;
ValidateQuantities(allParts, requirements, result);
foreach (var (plate, parts) in plateRuns)
{
if (parts.Count == 0)
continue;
ValidateBounds(parts, plate, requirements, result);
ValidateAreaBudget(parts, plate, result);
ValidateSpacing(parts, plate.PartSpacing, requirements, result);
}
return result;
}
private static void ValidateQuantities(
List<Part> parts,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
ValidationResult result
)
{
var placedCounts = parts
.GroupBy<Part, Drawing>(p => p.BaseDrawing, ReferenceEqualityComparer.Instance)
.ToDictionary(g => g.Key, g => g.Count());
foreach (var (drawing, placed) in placedCounts)
{
if (!requirements.TryGetValue(drawing, out var requirement))
{
result.Violations.Add(
$"Placed drawing '{drawing.Name}' which was not requested for this job"
);
continue;
}
if (placed > requirement.Quantity)
{
result.Violations.Add(
$"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested"
);
}
}
}
private static void ValidateBounds(
List<Part> parts,
Plate plate,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
ValidationResult result
)
{
var workArea = plate.WorkArea();
foreach (var part in parts)
{
var bb = part.BoundingBox;
var outLeft = bb.Left < workArea.X - Tolerance.Epsilon;
var outBottom = bb.Bottom < workArea.Y - Tolerance.Epsilon;
var outRight = bb.Right > workArea.Right + Tolerance.Epsilon;
var outTop = bb.Top > workArea.Top + Tolerance.Epsilon;
if (outLeft || outBottom || outRight || outTop)
{
result.Violations.Add(
$"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area "
+ $"of a {plate.Size} plate"
);
}
}
}
/// <summary>
/// Hard mathematical backstop: non-overlapping parts confined to the
/// work area can never have a combined area greater than the work
/// area itself. This catches overlap that the polygon-based
/// ValidateSpacing check can miss - Collision.HasOverlap (and
/// Part.Intersects, which uses the same algorithm) has been observed
/// to return false negatives on real, complex production geometry, so
/// this check does not depend on it.
/// </summary>
private static void ValidateAreaBudget(
List<Part> parts,
Plate plate,
ValidationResult result
)
{
var workArea = plate.WorkArea();
var budget = workArea.Width * workArea.Length;
var placedArea = parts.Sum(p => p.BaseDrawing.Area);
if (placedArea > budget + Tolerance.Epsilon)
{
result.Violations.Add(
$"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - "
+ "parts must overlap even though the polygon overlap check did not flag a pair"
);
}
}
private static void ValidateSpacing(
List<Part> parts,
double spacing,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
ValidationResult result
)
{
var worldPolygons = new Polygon[parts.Count];
var inflatedPolygons = new Polygon[parts.Count];
for (var i = 0; i < parts.Count; i++)
{
worldPolygons[i] = WorldPolygon(parts[i], 0);
inflatedPolygons[i] =
spacing > Tolerance.Epsilon
? WorldPolygon(parts[i], spacing)
: worldPolygons[i];
}
for (var i = 0; i < parts.Count; i++)
{
if (worldPolygons[i] == null || inflatedPolygons[i] == null)
continue;
for (var j = i + 1; j < parts.Count; j++)
{
if (worldPolygons[j] == null)
continue;
if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j]))
{
result.Violations.Add(
$"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})"
);
}
}
}
}
/// <summary>Friendly name for a violation message, falling back to the materialized
/// Drawing's own Name (the raw partId string) if this part wasn't in requirements at all -
/// that mismatch is already reported by ValidateQuantities, so this is display-only.</summary>
private static string DisplayName(
Part part,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
) =>
requirements.TryGetValue(part.BaseDrawing, out var requirement)
? requirement.Name
: part.BaseDrawing.Name;
/// <summary>
/// Extracts a part's perimeter as a world-space polygon, optionally inflated
/// outward by the given spacing, mirroring Part.Intersects' own geometry
/// extraction (part.Program is already rotated; only a Location offset is needed).
/// </summary>
private static Polygon WorldPolygon(Part part, double inflateBy)
{
var entities = ConvertProgram
.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
if (entities.Count == 0)
return null;
var perimeter = new ShapeProfile(entities).Perimeter;
if (perimeter == null)
return null;
if (inflateBy > Tolerance.Epsilon)
perimeter = perimeter.OffsetOutward(inflateBy) ?? perimeter;
// Adaptive tolerance instead of Shape.ToPolygon()'s default (up to 1000
// segments per arc) - arc-heavy real parts otherwise produce thousands
// of vertices, which is needlessly slow for a spacing check.
var polygon = perimeter.ToPolygonWithTolerance(0.01, circumscribe: true);
if (polygon == null)
return null;
polygon.Offset(part.Location);
return polygon;
}
}
}
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>OpenNest.Benchmark</RootNamespace>
<AssemblyName>OpenNest.Benchmark</AssemblyName>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
</ItemGroup>
</Project>
+304
View File
@@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest;
using OpenNest.Benchmark;
using OpenNest.Geometry;
return BenchmarkConsole.Run(args);
static class BenchmarkConsole
{
public static int Run(string[] args)
{
var options = ParseArgs(args);
if (options == null)
return 0; // --help was requested
if (options.InputPath == null)
{
PrintUsage();
return 1;
}
List<BenchmarkJob> jobs;
try
{
jobs = JobLoader.Load(options.InputPath, options.SheetSizes, options.PartSpacing);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
if (jobs.Count == 0)
{
Console.Error.WriteLine(
"No benchmark jobs found (no .nest files with any drawing quantity > 0, or *.manifest.json files)."
);
return 1;
}
var enginesDir = Path.Combine(AppContext.BaseDirectory, "Engines");
NestingEngineRegistry.LoadPlugins(enginesDir);
var engines = NestingEngineRegistry.AvailableEngines;
if (options.EngineNames.Count > 0)
{
engines = engines
.Where(e =>
options.EngineNames.Any(n =>
n.Equals(e.Name, StringComparison.OrdinalIgnoreCase)
)
)
.ToList();
if (engines.Count == 0)
{
Console.Error.WriteLine(
"None of the requested engines are registered. Available: "
+ string.Join(
", ",
NestingEngineRegistry.AvailableEngines.Select(e => e.Name)
)
);
return 1;
}
}
Console.WriteLine($"Loaded {jobs.Count} job(s) from '{options.InputPath}'");
foreach (var job in jobs)
{
var sizes = string.Join(", ", job.CandidateSizes.Select(s => s.ToString(1)));
Console.WriteLine(
$" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}"
);
}
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
var solves = jobs.Count * engines.Count;
if (options.Parallel > 1 && solves > 1)
{
Console.WriteLine(
$"Running up to {options.Parallel} solves at a time; Time(ms) is measured under that "
+ "concurrent load. Use --parallel 1 for strictly isolated timings."
);
}
var results = BenchmarkRunner.Run(
jobs,
engines,
options.SalvageRate,
options.MinimumSalvageDimension,
options.OutputDirectory,
options.Parallel
);
Report.PrintDetailed(results);
Report.PrintSummary(results);
if (options.CsvPath != null)
{
Report.WriteCsv(options.CsvPath, results);
Console.WriteLine();
Console.WriteLine($"Wrote CSV report to {options.CsvPath}");
}
return 0;
}
private static Options ParseArgs(string[] args)
{
var o = new Options();
for (var i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--sheet-sizes" when i + 1 < args.Length:
o.SheetSizes = ParseSheetSizes(args[++i]);
break;
case "--spacing" when i + 1 < args.Length:
o.PartSpacing = double.Parse(args[++i]);
break;
case "--engines" when i + 1 < args.Length:
o.EngineNames = args[++i]
.Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
)
.ToList();
break;
case "--csv" when i + 1 < args.Length:
o.CsvPath = args[++i];
break;
case "--salvage-rate" when i + 1 < args.Length:
o.SalvageRate = double.Parse(
args[++i],
System.Globalization.CultureInfo.InvariantCulture
);
break;
case "--min-salvage-dimension" when i + 1 < args.Length:
o.MinimumSalvageDimension = double.Parse(
args[++i],
System.Globalization.CultureInfo.InvariantCulture
);
break;
case "--output" when i + 1 < args.Length:
o.OutputDirectory = args[++i];
break;
case "--parallel" when i + 1 < args.Length:
if (int.TryParse(args[++i], out var parallel) && parallel >= 1)
o.Parallel = parallel;
else
Console.Error.WriteLine(
$"Warning: --parallel needs a whole number >= 1, using {o.Parallel}"
);
break;
case "--help":
PrintUsage();
return null;
default:
if (!args[i].StartsWith("--"))
o.InputPath = args[i];
break;
}
}
return o;
}
private static List<Size> ParseSheetSizes(string arg)
{
var sizes = new List<Size>();
foreach (
var token in arg.Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
)
)
{
if (Size.TryParse(token, out var size))
sizes.Add(size);
else
Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping");
}
return sizes.Distinct().ToList();
}
private static void PrintUsage()
{
Console.Error.WriteLine(
"OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files"
);
Console.Error.WriteLine();
Console.Error.WriteLine(
"For each .nest file, every drawing with quantity > 0 is nested (mixed together),"
);
Console.Error.WriteLine(
"once per registered INestingEngine. Each engine is handed the full job - every"
);
Console.Error.WriteLine(
"requested part and the whole pool of candidate sheet sizes - and owns its own"
);
Console.Error.WriteLine(
"multi-plate/size strategy: how many plates it uses, of which sizes, and how"
);
Console.Error.WriteLine(
"demand splits across them. Scoring: aggregate material utilization across every"
);
Console.Error.WriteLine(
"plate used, then (if everything requested was placed) fewer plates as the"
);
Console.Error.WriteLine(
"tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a"
);
Console.Error.WriteLine(
"thrown exception, or a run exceeding its time budget all score zero."
);
Console.Error.WriteLine();
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(
" OpenNest.Benchmark <file.nest | manifest.json | folder> [options]"
);
Console.Error.WriteLine();
Console.Error.WriteLine(
"A manifest.json builds a job straight from DXF files (paths relative to the manifest):"
);
Console.Error.WriteLine(
" { \"sheetSizes\": [\"48x96\"], \"spacing\": 0.25, \"edgeSpacing\": 0.25, \"quadrant\": 1,"
);
Console.Error.WriteLine(
" \"parts\": [ { \"dxf\": \"a.dxf\", \"quantity\": 12 }, { \"dxf\": \"b.dxf\", \"quantity\": 4, \"allowRotation\": false } ] }"
);
Console.Error.WriteLine(
"Sheet sizes must use the same units as the DXFs. A folder is scanned for *.nest and"
);
Console.Error.WriteLine(
"*.manifest.json files. --sheet-sizes and --spacing override the manifest."
);
Console.Error.WriteLine();
Console.Error.WriteLine("Options:");
Console.Error.WriteLine(
" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest"
);
Console.Error.WriteLine(
" (default: the distinct sizes already in each file)"
);
Console.Error.WriteLine(
" --spacing <value> Override part spacing for every job"
);
Console.Error.WriteLine(
" --engines Name1,Name2,... Only benchmark these registered engines (default: all)"
);
Console.Error.WriteLine(
" --csv <path> Write a flat CSV of all results"
);
Console.Error.WriteLine(
" --salvage-rate <0..1> Fraction of eligible offcut area credited (default 0)"
);
Console.Error.WriteLine(
" --min-salvage-dimension <value> Both offcut dimensions must qualify; 0 disables credit"
);
Console.Error.WriteLine(
" --output <directory> Save valid layouts as .nest plus detailed JSON reports"
);
Console.Error.WriteLine(
" --parallel <n> Solves to run at once (default 3; 1 = strictly sequential,"
);
Console.Error.WriteLine(
" which gives the cleanest per-engine timings)"
);
Console.Error.WriteLine(" --help Show this message");
}
private class Options
{
public string InputPath;
public List<Size> SheetSizes = new();
public double? PartSpacing;
public List<string> EngineNames = new();
public string CsvPath;
public string OutputDirectory;
public double SalvageRate;
public double MinimumSalvageDimension;
public int Parallel = 3;
}
}
+183
View File
@@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace OpenNest.Benchmark
{
/// <summary>
/// Console + CSV reporting for benchmark results. Ranking rule per job:
/// valid beats invalid; higher aggregate utilization wins; if utilization
/// ties and both engines fully placed every requested part, fewer plates
/// used wins (the multi-plate analogue of "smaller remnant" - both are
/// proxies for wasting less material). Ties beyond that are a shared win.
/// </summary>
public static class Report
{
private const double Epsilon = 1e-6;
public static void PrintDetailed(List<JobResult> results)
{
foreach (var jobGroup in results.GroupBy(r => r.JobName))
{
Console.WriteLine();
Console.WriteLine($"=== {jobGroup.Key} ===");
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList();
var best = ranked.Count > 0 ? ranked[0] : null;
Console.WriteLine(
$"{"Engine", -16} {"Result", -9} {"Parts", -10} {"Util%", -8} {"Plates", -18} {"Time(ms)", -9} Notes"
);
foreach (var r in ranked)
{
var isWinner = best != null && Compare(r, best) == 0 && r.Valid;
var marker = isWinner ? "*" : " ";
var status =
r.Crashed ? "CRASH"
: r.Valid ? "ok"
: "INVALID";
var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}";
var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-";
var platesCol =
r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-";
var notes = r.Crashed ? r.Error : string.Join("; ", r.Violations.Take(2));
Console.WriteLine(
$"{marker}{r.EngineName, -15} {status, -9} {partsCol, -10} {utilCol, -8} {platesCol, -18} {r.ElapsedMs, -9} {notes}"
);
}
}
}
public static void PrintSummary(List<JobResult> results)
{
Console.WriteLine();
Console.WriteLine("=== Summary ===");
var byEngine = results
.GroupBy(r => r.EngineName)
.Select(g => new
{
Engine = g.Key,
Jobs = g.Count(),
Valid = g.Count(r => r.Valid),
Crashed = g.Count(r => r.Crashed),
FullyPlaced = g.Count(r => r.FullyPlaced),
TotalUtilization = g.Sum(r => r.Utilization),
TotalPlates = g.Sum(r => r.PlatesUsed),
TotalTimeMs = g.Sum(r => r.ElapsedMs),
})
.OrderByDescending(e => e.TotalUtilization)
.ToList();
var wins = CountWins(results);
Console.WriteLine(
$"{"Engine", -16} {"Jobs", -6} {"Valid", -7} {"Complete", -9} {"Wins", -6} {"AvgUtil%", -10} {"Plates", -8} {"TotalTime(ms)", -14}"
);
foreach (var e in byEngine)
{
var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 100 : 0;
var winCount = wins.TryGetValue(e.Engine, out var w) ? w : 0;
Console.WriteLine(
$"{e.Engine, -16} {e.Jobs, -6} {e.Valid, -7} {e.FullyPlaced, -9} {winCount, -6} {avgUtil, -10:F1} {e.TotalPlates, -8} {e.TotalTimeMs, -14}"
);
}
}
public static void WriteCsv(string path, List<JobResult> results)
{
var sb = new StringBuilder();
sb.AppendLine(
"Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes"
);
foreach (var r in results)
{
var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations);
sb.AppendLine(
string.Join(
",",
Csv(r.JobName),
Csv(r.EngineName),
r.Valid,
r.Crashed,
r.FullyPlaced,
r.PartsPlaced,
r.PartsRequested,
r.Utilization.ToString("F4", CultureInfo.InvariantCulture),
r.PlatesUsed,
Csv(SizeSummary(r.SizeBreakdown)),
r.ElapsedMs,
Csv(notes)
)
);
}
File.WriteAllText(path, sb.ToString());
}
private static string SizeSummary(Dictionary<string, int> breakdown)
{
if (breakdown == null || breakdown.Count == 0)
return "-";
return string.Join("; ", breakdown.Select(kv => $"{kv.Key}×{kv.Value}"));
}
private static string Csv(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
private static Dictionary<string, int> CountWins(List<JobResult> results)
{
var wins = new Dictionary<string, int>();
foreach (var jobGroup in results.GroupBy(r => r.JobName))
{
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.Create(Compare)).ToList();
if (ranked.Count == 0 || !ranked[0].Valid)
continue;
foreach (var r in ranked.TakeWhile(r => Compare(r, ranked[0]) == 0))
wins[r.EngineName] = wins.GetValueOrDefault(r.EngineName) + 1;
}
return wins;
}
/// <summary>Lower sorts first (better). Valid beats invalid, then higher
/// aggregate utilization, then (if both fully placed) fewer plates used.</summary>
private static int Compare(JobResult a, JobResult b)
{
if (a.Valid != b.Valid)
return a.Valid ? -1 : 1;
if (!a.Valid)
return 0;
var utilDiff = b.Utilization - a.Utilization;
if (System.Math.Abs(utilDiff) > Epsilon)
return utilDiff > 0 ? 1 : -1;
if (a.FullyPlaced && b.FullyPlaced && a.PlatesUsed != b.PlatesUsed)
return a.PlatesUsed > b.PlatesUsed ? 1 : -1;
return 0;
}
}
}
+1 -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>
+178 -47
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 =
options.DrawingName != null
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName) ? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
: nest.Drawings.FirstOrDefault(); : nest.Drawings.FirstOrDefault();
if (drawing != null) if (drawing != null)
return drawing; return drawing;
Console.Error.WriteLine(options.DrawingName != null Console.Error.WriteLine(
options.DrawingName != null
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}" ? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
: "Error: nest file contains no drawings"); : "Error: nest file contains no drawings"
);
return null; return null;
} }
static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options) static void PrintHeader(
Nest nest,
Plate plate,
Drawing drawing,
int existingCount,
Options options
)
{ {
Console.WriteLine($"Nest: {nest.Name}"); Console.WriteLine($"Nest: {nest.Name}");
var wa = plate.WorkArea(); var wa = plate.WorkArea();
Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}"); Console.WriteLine(
$"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}"
);
Console.WriteLine($"Drawing: {drawing.Name}"); Console.WriteLine($"Drawing: {drawing.Name}");
Console.WriteLine(options.KeepParts Console.WriteLine(
options.KeepParts
? $"Keeping {existingCount} existing parts" ? $"Keeping {existingCount} existing parts"
: $"Cleared {existingCount} existing parts"); : $"Cleared {existingCount} existing parts"
);
Console.WriteLine("---"); Console.WriteLine("---");
} }
static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options) static (bool success, long elapsedMs) Fill(
Nest nest,
Plate plate,
Drawing drawing,
Options options
)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
bool success; bool success;
@@ -310,7 +392,9 @@ static class NestConsole
nestItems.Add(new NestItem { Drawing = d, Quantity = qty }); nestItems.Add(new NestItem { Drawing = d, Quantity = qty });
} }
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts"); Console.WriteLine(
$"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts"
);
var engine = NestEngineRegistry.Create(plate); var engine = NestEngineRegistry.Create(plate);
var nestParts = engine.Nest(nestItems, null, CancellationToken.None); var nestParts = engine.Nest(nestItems, null, CancellationToken.None);
@@ -334,9 +418,11 @@ static class NestConsole
return 0; return 0;
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts); var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
Console.WriteLine(hasOverlaps Console.WriteLine(
hasOverlaps
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points" ? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
: "Overlap check: PASS"); : "Overlap check: PASS"
);
return overlapPts.Count; return overlapPts.Count;
} }
@@ -355,9 +441,12 @@ static class NestConsole
return; return;
var firstInput = options.InputFiles[0]; var firstInput = options.InputFiles[0];
var outputFile = options.OutputFile ?? Path.Combine( var outputFile =
options.OutputFile
?? Path.Combine(
Path.GetDirectoryName(firstInput), Path.GetDirectoryName(firstInput),
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}"); $"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}"
);
new NestWriter(nest).Write(outputFile); new NestWriter(nest).Write(outputFile);
Console.WriteLine($"Saved: {outputFile}"); Console.WriteLine($"Saved: {outputFile}");
@@ -368,8 +457,8 @@ static class NestConsole
if (options.PostsDir != null) if (options.PostsDir != null)
return options.PostsDir; return options.PostsDir;
var exePath = Assembly.GetEntryAssembly()?.Location var exePath =
?? typeof(NestConsole).Assembly.Location; Assembly.GetEntryAssembly()?.Location ?? typeof(NestConsole).Assembly.Location;
return Path.Combine(Path.GetDirectoryName(exePath), "Posts"); return Path.Combine(Path.GetDirectoryName(exePath), "Posts");
} }
@@ -388,7 +477,11 @@ static class NestConsole
foreach (var type in assembly.GetTypes()) foreach (var type in assembly.GetTypes())
{ {
if (!typeof(IPostProcessor).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract) if (
!typeof(IPostProcessor).IsAssignableFrom(type)
|| type.IsInterface
|| type.IsAbstract
)
continue; continue;
if (Activator.CreateInstance(type) is IPostProcessor processor) if (Activator.CreateInstance(type) is IPostProcessor processor)
@@ -397,7 +490,9 @@ static class NestConsole
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}"); Console.Error.WriteLine(
$"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}"
);
} }
} }
@@ -418,7 +513,7 @@ static class NestConsole
Console.WriteLine($"Post processors ({postsDir}):"); Console.WriteLine($"Post processors ({postsDir}):");
foreach (var p in processors) foreach (var p in processors)
Console.WriteLine($" {p.Name,-30} {p.Description}"); Console.WriteLine($" {p.Name, -30} {p.Description}");
} }
static void PostProcess(Nest nest, Options options) static void PostProcess(Nest nest, Options options)
@@ -429,14 +524,17 @@ static class NestConsole
var postsDir = ResolvePostsDir(options); var postsDir = ResolvePostsDir(options);
var processors = LoadPostProcessors(postsDir); var processors = LoadPostProcessors(postsDir);
var post = processors.FirstOrDefault(p => var post = processors.FirstOrDefault(p =>
p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase)); p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase)
);
if (post == null) if (post == null)
{ {
Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found"); Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found");
if (processors.Count > 0) if (processors.Count > 0)
Console.Error.WriteLine($"Available: {string.Join(", ", processors.Select(p => p.Name))}"); Console.Error.WriteLine(
$"Available: {string.Join(", ", processors.Select(p => p.Name))}"
);
else else
Console.Error.WriteLine($"No post processors found in: {postsDir}"); Console.Error.WriteLine($"No post processors found in: {postsDir}");
@@ -450,7 +548,8 @@ static class NestConsole
var firstInput = options.InputFiles[0]; var firstInput = options.InputFiles[0];
outputFile = Path.Combine( outputFile = Path.Combine(
Path.GetDirectoryName(firstInput), Path.GetDirectoryName(firstInput),
$"{Path.GetFileNameWithoutExtension(firstInput)}.cnc"); $"{Path.GetFileNameWithoutExtension(firstInput)}.cnc"
);
} }
post.Post(nest, outputFile); post.Post(nest, outputFile);
@@ -462,28 +561,58 @@ static class NestConsole
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]"); Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
Console.Error.WriteLine(); Console.Error.WriteLine();
Console.Error.WriteLine("Arguments:"); Console.Error.WriteLine("Arguments:");
Console.Error.WriteLine(" input-files One or more .nest nest files or .dxf/.dwg drawing files"); Console.Error.WriteLine(
" input-files One or more .nest nest files or .dxf/.dwg drawing files"
);
Console.Error.WriteLine(); Console.Error.WriteLine();
Console.Error.WriteLine("Modes:"); Console.Error.WriteLine("Modes:");
Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)"); Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)");
Console.Error.WriteLine(" <part.dxf> --size WxL Import DXF, create plate, and fill"); Console.Error.WriteLine(" <part.dxf> --size WxL Import DXF, create plate, and fill");
Console.Error.WriteLine(" <nest.nest> <part.dxf> Load nest and add imported DXF drawings"); Console.Error.WriteLine(
" <nest.nest> <part.dxf> Load nest and add imported DXF drawings"
);
Console.Error.WriteLine(); Console.Error.WriteLine();
Console.Error.WriteLine("Options:"); Console.Error.WriteLine("Options:");
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)"); Console.Error.WriteLine(
" --repair-bends-mm <n> Opt-in endpoint/tick repair, limit >0.001 to 3.175 physical mm"
);
Console.Error.WriteLine(
" --cad-units inches|mm Explicit source coordinate units required for bend repair"
);
Console.Error.WriteLine(
" --drawing <name> Drawing name to fill with (default: first drawing)"
);
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)"); Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)"); Console.Error.WriteLine(
" --quantity <n> Max parts to place (default: 0 = unlimited)"
);
Console.Error.WriteLine(" --spacing <value> Override part spacing"); Console.Error.WriteLine(" --spacing <value> Override part spacing");
Console.Error.WriteLine(" --size <WxL> Override plate size (e.g. 60x120); required for DXF-only mode"); Console.Error.WriteLine(
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.nest)"); " --size <WxL> Override plate size (e.g. 60x120); required for DXF-only mode"
Console.Error.WriteLine(" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)"); );
Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill"); Console.Error.WriteLine(
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling"); " --output <path> Output nest file path (default: <input>-result.nest)"
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)"); );
Console.Error.WriteLine(
" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)"
);
Console.Error.WriteLine(
" --autonest Use mixed-part autonesting (engine Nest) instead of linear fill"
);
Console.Error.WriteLine(
" --keep-parts Don't clear existing parts before filling"
);
Console.Error.WriteLine(
" --check-overlaps Run overlap detection after fill (exit code 1 if found)"
);
Console.Error.WriteLine(" --no-save Skip saving output file"); Console.Error.WriteLine(" --no-save Skip saving output file");
Console.Error.WriteLine(" --post <name> Run a post processor after nesting"); Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)"); Console.Error.WriteLine(
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)"); " --post-output <path> Output file for post processor (default: <input>.cnc)"
);
Console.Error.WriteLine(
" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)"
);
Console.Error.WriteLine(" --list-posts List available post processors and exit"); Console.Error.WriteLine(" --list-posts List available post processors and exit");
Console.Error.WriteLine(" -h, --help Show this help"); Console.Error.WriteLine(" -h, --help Show this help");
} }
@@ -506,5 +635,7 @@ static class NestConsole
public string PostOutput; public string PostOutput;
public string PostsDir; public string PostsDir;
public bool ListPosts; public bool ListPosts;
public double? RepairBendsMillimeters;
public BendRepairUnits CadUnits;
} }
} }
+34 -11
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, p2) =>
horizontal
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X) ? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y)); : p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y)
);
var lastIndex = list.Count - 1; var lastIndex = list.Count - 1;
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y; var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y; var end = horizontal
? list[lastIndex].BoundingBox.Center.X
: list[lastIndex].BoundingBox.Center.Y;
var spacing = (end - start) / lastIndex; var spacing = (end - start) / lastIndex;
+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, Id = key,
Program = program.SubPrograms[key], Program = program.SubPrograms[key],
Offset = holeCenter Offset = holeCenter,
}); }
);
return; return;
} }
@@ -328,7 +371,11 @@ namespace OpenNest.CNC.CuttingStrategy
var reindexedShape = shape.ReindexAt(point, entity); var reindexedShape = shape.ReindexAt(point, entity);
if (Parameters.TabsEnabled && Parameters.TabConfig != null && contourType == ContourType.External) if (
Parameters.TabsEnabled
&& Parameters.TabConfig != null
&& contourType == ContourType.External
)
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size); reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point)); program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
@@ -337,7 +384,8 @@ namespace OpenNest.CNC.CuttingStrategy
private void EmitScribeContours(Program program, List<Entity> scribeEntities) private void EmitScribeContours(Program program, List<Entity> scribeEntities)
{ {
if (scribeEntities.Count == 0) return; if (scribeEntities.Count == 0)
return;
var shapes = ShapeBuilder.GetShapes(scribeEntities); var shapes = ShapeBuilder.GetShapes(scribeEntities);
foreach (var shape in shapes) foreach (var shape in shapes)
@@ -388,8 +436,12 @@ namespace OpenNest.CNC.CuttingStrategy
return ContourType.Internal; return ContourType.Internal;
} }
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType, public static double ComputeNormal(
RotationType winding = RotationType.CW) Vector point,
Entity entity,
ContourType contourType,
RotationType winding = RotationType.CW
)
{ {
double normal; double normal;
@@ -442,7 +494,12 @@ namespace OpenNest.CNC.CuttingStrategy
return polygon.RotationDirection(); return polygon.RotationDirection();
} }
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle) private LeadIn ClampLeadInForCircle(
LeadIn leadIn,
Circle circle,
Vector contourPoint,
double normalAngle
)
{ {
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0) if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
return leadIn; return leadIn;
@@ -492,7 +549,7 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn, ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn,
ContourType.Internal => Parameters.InternalLeadIn, ContourType.Internal => Parameters.InternalLeadIn,
_ => Parameters.ExternalLeadIn _ => Parameters.ExternalLeadIn,
}; };
} }
@@ -502,7 +559,7 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut, ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut,
ContourType.Internal => Parameters.InternalLeadOut, ContourType.Internal => Parameters.InternalLeadOut,
_ => Parameters.ExternalLeadOut _ => Parameters.ExternalLeadOut,
}; };
} }
@@ -565,12 +622,18 @@ namespace OpenNest.CNC.CuttingStrategy
private static Vector EntityStartPoint(Entity entity) private static Vector EntityStartPoint(Entity entity)
{ {
if (entity is Line line) return line.StartPoint; if (entity is Line line)
if (entity is Arc arc) return arc.StartPoint(); return line.StartPoint;
if (entity is Arc arc)
return arc.StartPoint();
return Vector.Zero; return Vector.Zero;
} }
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display) private List<ICode> ConvertShapeToMoves(
Shape shape,
Vector startPoint,
LayerType layer = LayerType.Display
)
{ {
var moves = new List<ICode>(); var moves = new List<ICode>();
@@ -582,15 +645,28 @@ namespace OpenNest.CNC.CuttingStrategy
} }
else if (entity is Arc arc) else if (entity is Arc arc)
{ {
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer }); moves.Add(
new ArcMove(
arc.EndPoint(),
arc.Center,
arc.IsReversed ? RotationType.CW : RotationType.CCW
)
{
Layer = layer,
}
);
} }
else if (entity is Circle circle) else if (entity is Circle circle)
{ {
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer }); moves.Add(
new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer }
);
} }
else else
{ {
throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}"); throw new System.InvalidOperationException(
$"Unsupported entity type: {entity.Type}"
);
} }
} }
@@ -600,9 +676,12 @@ namespace OpenNest.CNC.CuttingStrategy
private static Vector GetShapeStartPoint(Shape shape) private static Vector GetShapeStartPoint(Shape shape)
{ {
var first = shape.Entities[0]; var first = shape.Entities[0];
if (first is Line line) return line.StartPoint; if (first is Line line)
if (first is Arc arc) return arc.StartPoint(); return line.StartPoint;
if (first is Circle circle) return new Vector(circle.Center.X + circle.Radius, circle.Center.Y); if (first is Arc arc)
return arc.StartPoint();
if (first is Circle circle)
return new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
return Vector.Zero; return Vector.Zero;
} }
} }
@@ -4,6 +4,6 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
External, External,
Internal, Internal,
ArcCircle ArcCircle,
} }
} }
@@ -15,7 +15,8 @@ namespace OpenNest.CNC.CuttingStrategy
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn(); public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut(); public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }; public LeadIn InternalLeadIn { get; set; } =
new LineLeadIn { Length = 0.125, ApproachAngle = 90 };
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut(); public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn(); public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -7,19 +7,23 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
public double Radius { get; set; } public double Radius { get; set; }
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
var arcCenter = new Vector( var arcCenter = new Vector(
contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle), contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle),
contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle)); contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
}; };
} }
@@ -30,10 +34,10 @@ namespace OpenNest.CNC.CuttingStrategy
return new Vector( return new Vector(
arcCenterX + Radius * System.Math.Cos(contourNormalAngle), arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
arcCenterY + Radius * System.Math.Sin(contourNormalAngle)); arcCenterY + Radius * System.Math.Sin(contourNormalAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) => new ArcLeadIn { Radius = Radius * factor };
new ArcLeadIn { Radius = Radius * factor };
} }
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double ArcRadius { get; set; } public double ArcRadius { get; set; }
public double Kerf { get; set; } public double Kerf { get; set; }
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
@@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0); var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
var arcStart = new Vector( var arcStart = new Vector(
arcCenterX + ArcRadius * System.Math.Cos(lineAngle), arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
arcCenterY + ArcRadius * System.Math.Sin(lineAngle)); arcCenterY + ArcRadius * System.Math.Sin(lineAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(arcStart) { Layer = LayerType.Leadin }, new LinearMove(arcStart) { Layer = LayerType.Leadin },
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
}; };
} }
@@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy
return new Vector( return new Vector(
arcStartX + LineLength * System.Math.Cos(lineAngle), arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle)); arcStartY + LineLength * System.Math.Sin(lineAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf }; new CleanHoleLeadIn
{
LineLength = LineLength * factor,
ArcRadius = ArcRadius * factor,
Kerf = Kerf,
};
} }
} }
@@ -1,12 +1,15 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public abstract class LeadIn public abstract class LeadIn
{ {
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public abstract List<ICode> Generate(
RotationType winding = RotationType.CW); Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle); public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double ApproachAngle { get; set; } = 135.0; public double ApproachAngle { get; set; } = 135.0;
public double ArcRadius { get; set; } public double ArcRadius { get; set; }
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
@@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle); var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
var arcStart = new Vector( var arcStart = new Vector(
arcCenterX + ArcRadius * System.Math.Cos(lineAngle), arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
arcCenterY + ArcRadius * System.Math.Sin(lineAngle)); arcCenterY + ArcRadius * System.Math.Sin(lineAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(arcStart) { Layer = LayerType.Leadin }, new LinearMove(arcStart) { Layer = LayerType.Leadin },
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin },
}; };
} }
@@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy
return new Vector( return new Vector(
arcStartX + LineLength * System.Math.Cos(lineAngle), arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle)); arcStartY + LineLength * System.Math.Sin(lineAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle }; new LineArcLeadIn
{
LineLength = LineLength * factor,
ArcRadius = ArcRadius * factor,
ApproachAngle = ApproachAngle,
};
} }
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -9,15 +9,18 @@ namespace OpenNest.CNC.CuttingStrategy
public double Length { get; set; } public double Length { get; set; }
public double ApproachAngle { get; set; } = 90.0; public double ApproachAngle { get; set; } = 90.0;
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin } new LinearMove(contourStartPoint) { Layer = LayerType.Leadin },
}; };
} }
@@ -26,7 +29,8 @@ namespace OpenNest.CNC.CuttingStrategy
var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle); var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle);
return new Vector( return new Vector(
contourStartPoint.X + Length * System.Math.Cos(approachAngle), contourStartPoint.X + Length * System.Math.Cos(approachAngle),
contourStartPoint.Y + Length * System.Math.Sin(approachAngle)); contourStartPoint.Y + Length * System.Math.Sin(approachAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -11,21 +11,25 @@ namespace OpenNest.CNC.CuttingStrategy
public double Length2 { get; set; } public double Length2 { get; set; }
public double ApproachAngle2 { get; set; } = 90.0; public double ApproachAngle2 { get; set; } = 90.0;
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1); var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
var midPoint = new Vector( var midPoint = new Vector(
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle), contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle)); contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle)
);
return new List<ICode> return new List<ICode>
{ {
new RapidMove(piercePoint), new RapidMove(piercePoint),
new LinearMove(midPoint) { Layer = LayerType.Leadin }, new LinearMove(midPoint) { Layer = LayerType.Leadin },
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin } new LinearMove(contourStartPoint) { Layer = LayerType.Leadin },
}; };
} }
@@ -38,10 +42,17 @@ namespace OpenNest.CNC.CuttingStrategy
var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2); var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2);
return new Vector( return new Vector(
midX + Length1 * System.Math.Cos(firstAngle), midX + Length1 * System.Math.Cos(firstAngle),
midY + Length1 * System.Math.Sin(firstAngle)); midY + Length1 * System.Math.Sin(firstAngle)
);
} }
public override LeadIn Scale(double factor) => public override LeadIn Scale(double factor) =>
new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 }; new LineLineLeadIn
{
Length1 = Length1 * factor,
ApproachAngle1 = ApproachAngle1,
Length2 = Length2 * factor,
ApproachAngle2 = ApproachAngle2,
};
} }
} }
@@ -1,17 +1,17 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public class NoLeadIn : LeadIn public class NoLeadIn : LeadIn
{ {
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
return new List<ICode> return new List<ICode> { new RapidMove(contourStartPoint) };
{
new RapidMove(contourStartPoint)
};
} }
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle) public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -7,8 +7,11 @@ namespace OpenNest.CNC.CuttingStrategy
{ {
public double Radius { get; set; } public double Radius { get; set; }
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle); var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle);
var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle); var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
@@ -16,11 +19,12 @@ namespace OpenNest.CNC.CuttingStrategy
var endPoint = new Vector( var endPoint = new Vector(
arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2), arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2),
arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2)); arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2)
);
return new List<ICode> return new List<ICode>
{ {
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout } new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout },
}; };
} }
} }
@@ -1,11 +1,14 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public abstract class LeadOut public abstract class LeadOut
{ {
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public abstract List<ICode> Generate(
RotationType winding = RotationType.CW); Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
} }
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -9,18 +9,19 @@ namespace OpenNest.CNC.CuttingStrategy
public double Length { get; set; } public double Length { get; set; }
public double ApproachAngle { get; set; } = 90.0; public double ApproachAngle { get; set; } = 90.0;
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle); var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle);
var endPoint = new Vector( var endPoint = new Vector(
contourEndPoint.X + Length * System.Math.Cos(overcutAngle), contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle)); contourEndPoint.Y + Length * System.Math.Sin(overcutAngle)
);
return new List<ICode> return new List<ICode> { new LinearMove(endPoint) { Layer = LayerType.Leadout } };
{
new LinearMove(endPoint) { Layer = LayerType.Leadout }
};
} }
} }
} }
@@ -1,12 +1,15 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
public class NoLeadOut : LeadOut public class NoLeadOut : LeadOut
{ {
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle, public override List<ICode> Generate(
RotationType winding = RotationType.CW) Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
return new List<ICode>(); return new List<ICode>();
} }
@@ -9,7 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
BottomSide = 4, BottomSide = 4,
EdgeStart = 5, EdgeStart = 5,
LeftSide = 7, LeftSide = 7,
RightSideAlt = 8 RightSideAlt = 8,
} }
public class SequenceParameters public class SequenceParameters
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double BreakerAngle { get; set; } public double BreakerAngle { get; set; }
public override List<ICode> Generate( public override List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW) Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var codes = new List<ICode>(); var codes = new List<ICode>();
@@ -21,7 +24,8 @@ namespace OpenNest.CNC.CuttingStrategy
var scoreAngle = contourNormalAngle + System.Math.PI; var scoreAngle = contourNormalAngle + System.Math.PI;
var scoreEnd = new Vector( var scoreEnd = new Vector(
tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle), tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle),
tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle)); tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle)
);
codes.Add(new LinearMove(scoreEnd)); codes.Add(new LinearMove(scoreEnd));
codes.Add(new RapidMove(tabEndPoint)); codes.Add(new RapidMove(tabEndPoint));
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -8,13 +8,13 @@ namespace OpenNest.CNC.CuttingStrategy
public int MachineTabId { get; set; } public int MachineTabId { get; set; }
public override List<ICode> Generate( public override List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW) Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
return new List<ICode> return new List<ICode> { new RapidMove(tabEndPoint) };
{
new RapidMove(tabEndPoint)
};
} }
} }
} }
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -11,8 +11,11 @@ namespace OpenNest.CNC.CuttingStrategy
public double CutoutMaxHeight { get; set; } public double CutoutMaxHeight { get; set; }
public override List<ICode> Generate( public override List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW) Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{ {
var codes = new List<ICode>(); var codes = new List<ICode>();
@@ -29,8 +32,10 @@ namespace OpenNest.CNC.CuttingStrategy
public bool AppliesToCutout(double cutoutWidth, double cutoutHeight) public bool AppliesToCutout(double cutoutWidth, double cutoutHeight)
{ {
return cutoutWidth >= CutoutMinWidth && cutoutWidth <= CutoutMaxWidth return cutoutWidth >= CutoutMinWidth
&& cutoutHeight >= CutoutMinHeight && cutoutHeight <= CutoutMaxHeight; && cutoutWidth <= CutoutMaxWidth
&& cutoutHeight >= CutoutMinHeight
&& cutoutHeight <= CutoutMaxHeight;
} }
} }
} }
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using System.Collections.Generic; using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.CuttingStrategy namespace OpenNest.CNC.CuttingStrategy
{ {
@@ -10,7 +10,10 @@ namespace OpenNest.CNC.CuttingStrategy
public LeadOut TabLeadOut { get; set; } public LeadOut TabLeadOut { get; set; }
public abstract List<ICode> Generate( public abstract List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, Vector tabStartPoint,
RotationType winding = RotationType.CW); Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
} }
} }
+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,
} }
} }
+31 -20
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)
@@ -264,7 +275,8 @@ namespace OpenNest.CNC
var code = Codes[i]; var code = Codes[i];
var motion = code as Motion; var motion = code as Motion;
if (motion == null) continue; if (motion == null)
continue;
return motion.EndPoint; return motion.EndPoint;
} }
@@ -280,7 +292,8 @@ namespace OpenNest.CNC
var code = Codes[i]; var code = Codes[i];
var motion = code as Motion; var motion = code as Motion;
if (motion == null) continue; if (motion == null)
continue;
pos += motion.EndPoint; pos += motion.EndPoint;
} }
@@ -318,9 +331,10 @@ namespace OpenNest.CNC
case CodeType.LinearMove: case CodeType.LinearMove:
{ {
var line = (LinearMove)code; var line = (LinearMove)code;
var pt = Mode == Mode.Absolute ? var pt =
frameOrigin + line.EndPoint : Mode == Mode.Absolute
line.EndPoint + pos; ? frameOrigin + line.EndPoint
: line.EndPoint + pos;
if (pt.X > maxX) if (pt.X > maxX)
maxX = pt.X; maxX = pt.X;
@@ -340,7 +354,8 @@ namespace OpenNest.CNC
case CodeType.RapidMove: case CodeType.RapidMove:
{ {
var line = (RapidMove)code; var line = (RapidMove)code;
var pt = Mode == Mode.Absolute var pt =
Mode == Mode.Absolute
? frameOrigin + line.EndPoint ? frameOrigin + line.EndPoint
: line.EndPoint + pos; : line.EndPoint + pos;
@@ -479,11 +494,7 @@ namespace OpenNest.CNC
public object Clone() public object Clone()
{ {
var pgm = new Program() var pgm = new Program() { mode = this.mode, Rotation = this.Rotation };
{
mode = this.mode,
Rotation = this.Rotation
};
var codes = new ICode[Length]; var codes = new ICode[Length];
+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();
+10 -3
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,7 +66,8 @@ namespace OpenNest.CNC
} }
else if (code is Motion motion) else if (code is Motion motion)
{ {
var endpt = pgm.Mode == Mode.Incremental var endpt =
pgm.Mode == Mode.Incremental
? motion.EndPoint + pos ? motion.EndPoint + pos
: motion.EndPoint + basePos; : motion.EndPoint + basePos;
+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
+3 -2
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, Feedrate = 100,
RapidTravelRate = 300, RapidTravelRate = 300,
PierceTime = TimeSpan.FromSeconds(0.5), PierceTime = TimeSpan.FromSeconds(0.5),
Units = OpenNest.Units.Inches Units = OpenNest.Units.Inches,
}; };
} }
+10 -12
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
{ {
@@ -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)
+35 -27
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,11 +246,10 @@ namespace OpenNest.Geometry
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false) public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{ {
var points = new List<Vector>(); var points = new List<Vector>();
var stepAngle = reversed var stepAngle = reversed ? -SweepAngle() / segments : SweepAngle() / segments;
? -SweepAngle() / segments
: SweepAngle() / segments;
var r = circumscribe && segments > 0 var r =
circumscribe && segments > 0
? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0) ? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0)
: Radius; : Radius;
@@ -254,9 +257,12 @@ namespace OpenNest.Geometry
{ {
var angle = stepAngle * i + StartAngle; var angle = stepAngle * i + StartAngle;
points.Add(new Vector( points.Add(
new Vector(
System.Math.Cos(angle) * r + Center.X, System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y)); System.Math.Sin(angle) * r + Center.Y
)
);
} }
return points; return points;
@@ -470,7 +476,8 @@ namespace OpenNest.Geometry
{ {
return new Vector( return new Vector(
System.Math.Cos(angle) * Radius + Center.X, System.Math.Cos(angle) * Radius + Center.X,
System.Math.Sin(angle) * Radius + Center.Y); System.Math.Sin(angle) * Radius + Center.Y
);
} }
else else
{ {
@@ -500,7 +507,8 @@ namespace OpenNest.Geometry
/// <returns></returns> /// <returns></returns>
public override bool Intersects(Arc arc, out List<Vector> pts) public override bool Intersects(Arc arc, out List<Vector> pts)
{ {
return Intersect.Intersects(this, arc, out pts); ; return Intersect.Intersects(this, arc, out pts);
;
} }
/// <summary> /// <summary>
+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)
+24 -28
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(
new Vector(
System.Math.Cos(angle) * r + Center.X, System.Math.Cos(angle) * r + Center.X,
System.Math.Sin(angle) * r + Center.Y)); System.Math.Sin(angle) * r + Center.Y
)
);
} }
return points; return points;
@@ -278,11 +275,9 @@ namespace OpenNest.Geometry
{ {
if (side == OffsetSide.Left && Rotation == RotationType.CCW) if (side == OffsetSide.Left && Rotation == RotationType.CCW)
{ {
return Radius <= distance ? null : new Circle(center, Radius - distance) return Radius <= distance
{ ? null
Layer = Layer, : new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation };
Rotation = Rotation
};
} }
else else
{ {
@@ -294,11 +289,9 @@ namespace OpenNest.Geometry
{ {
if (ContainsPoint(pt)) if (ContainsPoint(pt))
{ {
return Radius <= distance ? null : new Circle(center, Radius - distance) return Radius <= distance
{ ? null
Layer = Layer, : new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation };
Rotation = Rotation
};
} }
else else
{ {
@@ -317,7 +310,8 @@ namespace OpenNest.Geometry
return new Vector( return new Vector(
System.Math.Cos(angle) * Radius + Center.X, System.Math.Cos(angle) * Radius + Center.X,
System.Math.Sin(angle) * Radius + Center.Y); System.Math.Sin(angle) * Radius + Center.Y
);
} }
/// <summary> /// <summary>
@@ -350,7 +344,9 @@ namespace OpenNest.Geometry
public override bool Intersects(Circle circle) public override bool Intersects(Circle circle)
{ {
var dist = Center.DistanceTo(circle.Center); var dist = Center.DistanceTo(circle.Center);
return (dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius)); return (
dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius)
);
} }
/// <summary> /// <summary>
+107 -40
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++)
@@ -255,12 +268,17 @@ namespace OpenNest.Geometry
/// <summary> /// <summary>
/// Subtracts holes from overlap regions. /// Subtracts holes from overlap regions.
/// </summary> /// </summary>
private static List<Polygon> SubtractHoles(List<Polygon> regions, private static List<Polygon> SubtractHoles(
List<Polygon> holesA, List<Polygon> holesB) List<Polygon> regions,
List<Polygon> holesA,
List<Polygon> holesB
)
{ {
var allHoles = new List<Polygon>(); var allHoles = new List<Polygon>();
if (holesA != null) allHoles.AddRange(holesA); if (holesA != null)
if (holesB != null) allHoles.AddRange(holesB); allHoles.AddRange(holesA);
if (holesB != null)
allHoles.AddRange(holesB);
if (allHoles.Count == 0) if (allHoles.Count == 0)
return regions; return regions;
@@ -286,8 +304,10 @@ namespace OpenNest.Geometry
} }
/// <summary> /// <summary>
/// Subtracts hole triangles from a region. Conservative: partial overlaps /// Subtracts hole triangles from a region. Exact: a piece outside a convex hole
/// keep the full piece triangle (acceptable for visual shading). /// triangle equals the union of its clips against each triangle edge's outside
/// half-space, so overlap confined to a cutout disappears while any material
/// sliver outside the hole survives.
/// </summary> /// </summary>
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris) private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
{ {
@@ -295,29 +315,32 @@ namespace OpenNest.Geometry
foreach (var holeTri in holeTris) foreach (var holeTri in holeTris)
{ {
if (!BoundingBoxesOverlap(region.BoundingBox, holeTri.BoundingBox))
continue;
var next = new List<Polygon>(); var next = new List<Polygon>();
foreach (var piece in current) foreach (var piece in current)
{ {
var pieceTris = TriangulateWithBounds(piece); if (!BoundingBoxesOverlap(piece.BoundingBox, holeTri.BoundingBox))
{
next.Add(piece);
continue;
}
foreach (var pieceTri in pieceTris) foreach (var pieceTri in TriangulateWithBounds(piece))
{ {
var inside = ClipConvex(pieceTri, holeTri); var holeVerts = holeTri.Vertices;
if (inside == null) var holeCount = holeTri.IsClosed() ? holeVerts.Count - 1 : holeVerts.Count;
{ var survived = false;
// No overlap with hole - keep for (var i = 0; i < holeCount; i++)
next.Add(pieceTri); survived |= AddIfPositiveArea(
} next,
else if (inside.Area() < pieceTri.Area() - Tolerance.Epsilon) ClipOutsideHalfSpace(
{ pieceTri,
// Partial overlap - keep the piece (conservative) holeVerts[i],
next.Add(pieceTri); holeVerts[(i + 1) % holeCount]
} )
// else: fully inside hole - discard );
if (!survived)
continue; // piece lies entirely within the hole
} }
} }
@@ -326,5 +349,49 @@ namespace OpenNest.Geometry
return current; return current;
} }
/// <summary>
/// Sutherland-Hodgman clip of a convex polygon to the strict outside of the
/// infinite line edgeStart->edgeEnd of a CCW hole edge (Cross &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,
} }
} }
+66 -25
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,19 +200,32 @@ namespace OpenNest.Geometry
if (sweep >= Angle.TwoPI - Tolerance.Epsilon) if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
return false; return false;
if (startAngle < 0) startAngle += Angle.TwoPI; if (startAngle < 0)
if (endAngle < 0) endAngle += Angle.TwoPI; startAngle += Angle.TwoPI;
if (endAngle < 0)
endAngle += Angle.TwoPI;
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color }; arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle)
{
Layer = arc1.Layer,
Color = arc1.Color,
};
return true; return true;
} }
private static List<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex) private static List<Line> GetCollinearLines(
this IList<Line> lines,
Line line,
int startIndex
)
{ {
var collinearLines = new List<Line>(); var collinearLines = new List<Line>();
Parallel.For(startIndex, lines.Count, index => Parallel.For(
startIndex,
lines.Count,
index =>
{ {
var compareLine = lines[index]; var compareLine = lines[index];
@@ -203,7 +239,8 @@ namespace OpenNest.Geometry
{ {
collinearLines.Add(compareLine); collinearLines.Add(compareLine);
} }
}); }
);
return collinearLines; return collinearLines;
} }
@@ -212,7 +249,10 @@ namespace OpenNest.Geometry
{ {
var coradialArcs = new List<Arc>(); var coradialArcs = new List<Arc>();
Parallel.For(startIndex, arcs.Count, index => Parallel.For(
startIndex,
arcs.Count,
index =>
{ {
var compareArc = arcs[index]; var compareArc = arcs[index];
@@ -226,7 +266,8 @@ namespace OpenNest.Geometry
{ {
coradialArcs.Add(compareArc); coradialArcs.Add(compareArc);
} }
}); }
);
return coradialArcs; return coradialArcs;
} }
+332 -74
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,7 +367,8 @@ public class GeometrySimplifier
chainedTangent = ComputeEndTangent(result.Center, result.Points); chainedTangent = ComputeEndTangent(result.Center, result.Points);
var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]); var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]);
candidates.Add(new ArcCandidate candidates.Add(
new ArcCandidate
{ {
StartIndex = j, StartIndex = j,
EndIndex = result.EndIndex, EndIndex = result.EndIndex,
@@ -358,37 +377,53 @@ public class GeometrySimplifier
BoundingBox = result.Points.GetBoundingBox(), BoundingBox = result.Points.GetBoundingBox(),
FirstPoint = arc.StartPoint(), FirstPoint = arc.StartPoint(),
LastPoint = arc.EndPoint(), LastPoint = arc.EndPoint(),
}); }
);
j = result.EndIndex + 1; j = result.EndIndex + 1;
} }
} }
private record ArcFitResult(Vector Center, double Radius, double Deviation, List<Vector> Points, int EndIndex); private record ArcFitResult(
Vector Center,
double Radius,
double Deviation,
List<Vector> Points,
int EndIndex
);
private ArcFitResult TryFitArcAt(List<Entity> entities, int start, int runEnd, Vector chainedTangent) private ArcFitResult TryFitArcAt(
List<Entity> entities,
int start,
int runEnd,
Vector chainedTangent
)
{ {
var k = start + MinLines - 1; var k = start + MinLines - 1;
if (k > runEnd) return null; if (k > runEnd)
return null;
var points = CollectPoints(entities, start, k); var points = CollectPoints(entities, start, k);
if (points.Count < 3) return null; if (points.Count < 3)
return null;
var startTangent = chainedTangent.IsValid() var startTangent = EstimateStartTangent(entities, start, points, chainedTangent);
? chainedTangent var endTangent = EstimateEndTangent(entities, k, points);
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
var endTangent = GetExitDirection(entities[k]);
var (center, radius, dev) = TryFit(points, startTangent, endTangent); var (center, radius, dev) = TryFit(points, startTangent, endTangent);
if (!center.IsValid()) return null; if (!center.IsValid())
return null;
// Extend the arc as far as possible // Extend the arc as far as possible
while (k + 1 <= runEnd) while (k + 1 <= runEnd)
{ {
var extPoints = CollectPoints(entities, start, k + 1); var extPoints = CollectPoints(entities, start, k + 1);
var extEndTangent = GetExitDirection(entities[k + 1]); if (extPoints.Count < 3)
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent, extEndTangent) : (Vector.Invalid, 0, 0d); break;
if (!nc.IsValid()) break;
var extEndTangent = EstimateEndTangent(entities, k + 1, extPoints);
var (nc, nr, nd) = TryFit(extPoints, startTangent, extEndTangent);
if (!nc.IsValid())
break;
k++; k++;
center = nc; center = nc;
@@ -407,37 +442,228 @@ public class GeometrySimplifier
return new ArcFitResult(center, radius, dev, points, k); return new ArcFitResult(center, radius, dev, points, k);
} }
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent, Vector endTangent) private (Vector center, double radius, double deviation) TryFit(
List<Vector> points,
TangentEstimate start,
TangentEstimate end
)
{ {
// Try dual-tangent fit first (matches direction at both endpoints) foreach (var (center, radius, dev) in FitAttempts(points, start, end))
if (endTangent.IsValid())
{ {
var (dc, dr, dd) = ArcFit.FitWithDualTangent(points, startTangent, endTangent);
if (dc.IsValid() && dd <= Tolerance)
{
var isRev = SumSignedAngles(dc, points) < 0;
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev);
if (aDev <= Tolerance)
return (dc, dr, System.Math.Max(dd, aDev));
}
}
// Fall back to start-tangent-only, then mirror axis
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent);
if (!center.IsValid() || dev > Tolerance) if (!center.IsValid() || dev > Tolerance)
(center, radius, dev) = FitMirrorAxis(points); continue;
if (!center.IsValid() || dev > Tolerance)
return (Vector.Invalid, 0, 0);
// Check that the arc doesn't bulge away from the original line segments // Check that the arc doesn't bulge away from the original line segments
var isReversed = SumSignedAngles(center, points) < 0; var isReversed = SumSignedAngles(center, points) < 0;
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed); var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
if (arcDev > Tolerance) if (arcDev > Tolerance)
return (Vector.Invalid, 0, 0); continue;
return (center, radius, System.Math.Max(dev, arcDev)); return (center, radius, System.Math.Max(dev, arcDev));
} }
return (Vector.Invalid, 0, 0);
}
/// <summary>
/// Yields fit attempts in preference order. A trusted tangent (chained from the
/// previous arc, an adjacent original arc, or a long straight edge) is enforced
/// exactly on its side; otherwise the tangency error is balanced between both
/// endpoints. The unconstrained mirror-axis fit is the last resort. Every attempt
/// passes exactly through both endpoints, so no gaps are introduced.
/// </summary>
private IEnumerable<(Vector center, double radius, double deviation)> FitAttempts(
List<Vector> points,
TangentEstimate start,
TangentEstimate end
)
{
if (start.Trusted && !end.Trusted)
{
yield return ArcFit.FitWithStartTangent(points, start.Direction);
yield return ArcFit.FitThroughEndpointsWithTangents(
points,
start.Direction,
end.Direction
);
yield return FitWithEndTangent(points, end.Direction);
}
else if (end.Trusted && !start.Trusted)
{
yield return FitWithEndTangent(points, end.Direction);
yield return ArcFit.FitThroughEndpointsWithTangents(
points,
start.Direction,
end.Direction
);
yield return ArcFit.FitWithStartTangent(points, start.Direction);
}
else
{
yield return ArcFit.FitThroughEndpointsWithTangents(
points,
start.Direction,
end.Direction
);
yield return ArcFit.FitWithStartTangent(points, start.Direction);
yield return FitWithEndTangent(points, end.Direction);
}
yield return FitMirrorAxis(points);
}
/// <summary>
/// Fits an arc through both endpoints with an exact tangent at the last point,
/// by running the start-tangent fit on the reversed point sequence.
/// </summary>
private static (Vector center, double radius, double deviation) FitWithEndTangent(
List<Vector> points,
Vector endTangent
)
{
var reversed = new List<Vector>(points);
reversed.Reverse();
return ArcFit.FitWithStartTangent(reversed, new Vector(-endTangent.X, -endTangent.Y));
}
/// <summary>
/// An estimated tangent direction at a fit endpoint. Trusted estimates come from
/// exact geometry (a chained arc, an adjacent original arc, or a long straight
/// edge) and are enforced exactly; untrusted ones are derived from the polyline
/// vertices and only guide the fit.
/// </summary>
private readonly record struct TangentEstimate(Vector Direction, bool Trusted);
/// <summary>Segment-length ratio above which a neighboring line counts as a true
/// straight edge (rather than another chord of the tessellated curve).</summary>
private const double NeighborEdgeFactor = 3.0;
private static TangentEstimate EstimateStartTangent(
List<Entity> entities,
int start,
List<Vector> points,
Vector chainedTangent
)
{
if (chainedTangent.IsValid())
return new TangentEstimate(chainedTangent, true);
if (entities[start] is Arc startArc)
return new TangentEstimate(GetEntryDirection(startArc), true);
var firstChordLen = points[0].DistanceTo(points[1]);
if (start > 0)
{
var prev = entities[start - 1];
var prevEnd = prev switch
{
Line l => l.EndPoint,
Arc a => a.EndPoint(),
_ => Vector.Invalid,
};
if (prevEnd.IsValid() && prevEnd.DistanceTo(points[0]) < 1e-6)
{
if (prev is Arc)
return new TangentEstimate(GetExitDirection(prev), true);
if (
prev is Line prevLine
&& prevLine.StartPoint.DistanceTo(prevLine.EndPoint)
>= NeighborEdgeFactor * firstChordLen
)
return new TangentEstimate(GetExitDirection(prevLine), true);
}
}
var chord = new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
if (points.Count >= 3)
return new TangentEstimate(
EstimateVertexTangent(points[0], points[1], points[2], chord),
false
);
return new TangentEstimate(chord, false);
}
private static TangentEstimate EstimateEndTangent(
List<Entity> entities,
int k,
List<Vector> points
)
{
if (entities[k] is Arc endArc)
return new TangentEstimate(GetExitDirection(endArc), true);
var lastChordLen = points[^1].DistanceTo(points[^2]);
if (k + 1 < entities.Count)
{
var next = entities[k + 1];
var nextStart = next switch
{
Line l => l.StartPoint,
Arc a => a.StartPoint(),
_ => Vector.Invalid,
};
if (nextStart.IsValid() && nextStart.DistanceTo(points[^1]) < 1e-6)
{
if (next is Arc nextArc)
return new TangentEstimate(GetEntryDirection(nextArc), true);
if (
next is Line nextLine
&& nextLine.StartPoint.DistanceTo(nextLine.EndPoint)
>= NeighborEdgeFactor * lastChordLen
)
return new TangentEstimate(GetExitDirection(nextLine), true);
}
}
var chord = new Vector(points[^1].X - points[^2].X, points[^1].Y - points[^2].Y);
if (points.Count >= 3)
return new TangentEstimate(
EstimateVertexTangent(points[^1], points[^2], points[^3], chord),
false
);
return new TangentEstimate(chord, false);
}
/// <summary>
/// Estimates the curve tangent at a polyline vertex from the circle through it and
/// its two nearest neighbors. A raw chord direction is off from the true tangent by
/// half the chord's subtended angle; the circumcircle estimate removes that bias.
/// Falls back to the travel direction when the three points are collinear.
/// </summary>
private static Vector EstimateVertexTangent(Vector at, Vector b, Vector c, Vector travel)
{
var d = 2 * (at.X * (b.Y - c.Y) + b.X * (c.Y - at.Y) + c.X * (at.Y - b.Y));
if (System.Math.Abs(d) < 1e-14)
return travel;
var sqA = at.X * at.X + at.Y * at.Y;
var sqB = b.X * b.X + b.Y * b.Y;
var sqC = c.X * c.X + c.Y * c.Y;
var cx = (sqA * (b.Y - c.Y) + sqB * (c.Y - at.Y) + sqC * (at.Y - b.Y)) / d;
var cy = (sqA * (c.X - b.X) + sqB * (at.X - c.X) + sqC * (b.X - at.X)) / d;
var tangent = new Vector(-(at.Y - cy), at.X - cx);
if (tangent.X * travel.X + tangent.Y * travel.Y < 0)
tangent = new Vector(-tangent.X, -tangent.Y);
return tangent;
}
/// <summary>
/// Returns the entry direction (tangent at start point) of an entity.
/// </summary>
private static Vector GetEntryDirection(Entity entity) =>
entity switch
{
Line line => new Vector(
line.EndPoint.X - line.StartPoint.X,
line.EndPoint.Y - line.StartPoint.Y
),
Arc arc => arc.IsReversed
? new Vector(System.Math.Sin(arc.StartAngle), -System.Math.Cos(arc.StartAngle))
: new Vector(-System.Math.Sin(arc.StartAngle), System.Math.Cos(arc.StartAngle)),
_ => Vector.Invalid,
};
/// <summary> /// <summary>
/// Computes the tangent direction at the last point of a fitted arc, /// Computes the tangent direction at the last point of a fitted arc,
/// used to chain tangent continuity to the next arc. /// used to chain tangent continuity to the next arc.
@@ -488,9 +714,17 @@ public class GeometrySimplifier
var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta); var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta);
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord); var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
var dOpt = GoldenSectionMin(dInit - range, dInit + range, var dOpt = GoldenSectionMin(
d => ArcFit.MaxRadialDeviation(points, mx + d * nx, my + d * ny, dInit - range,
System.Math.Sqrt(halfChord * halfChord + d * d))); dInit + range,
d =>
ArcFit.MaxRadialDeviation(
points,
mx + d * nx,
my + d * ny,
System.Math.Sqrt(halfChord * halfChord + d * d)
)
);
var center = new Vector(mx + dOpt * nx, my + dOpt * ny); var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt); var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
@@ -542,13 +776,22 @@ public class GeometrySimplifier
return points; return points;
} }
private static Arc CreateArc(Vector center, double radius, List<Vector> points, Entity sourceEntity) private static Arc CreateArc(
Vector center,
double radius,
List<Vector> points,
Entity sourceEntity
)
{ {
var firstPoint = points[0]; var firstPoint = points[0];
var lastPoint = points[^1]; var lastPoint = points[^1];
var startAngle = NormalizeAngle(System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X)); var startAngle = NormalizeAngle(
var endAngle = NormalizeAngle(System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X)); System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X)
);
var endAngle = NormalizeAngle(
System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X)
);
var isReversed = SumSignedAngles(center, points) < 0; var isReversed = SumSignedAngles(center, points) < 0;
var arc = new Arc(center, radius, startAngle, endAngle, isReversed); var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
@@ -560,9 +803,13 @@ public class GeometrySimplifier
/// <summary> /// <summary>
/// Returns the exit direction (tangent at endpoint) of an entity. /// Returns the exit direction (tangent at endpoint) of an entity.
/// </summary> /// </summary>
private static Vector GetExitDirection(Entity entity) => entity switch private static Vector GetExitDirection(Entity entity) =>
entity switch
{ {
Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y), Line line => new Vector(
line.EndPoint.X - line.StartPoint.X,
line.EndPoint.Y - line.StartPoint.Y
),
Arc arc => arc.IsReversed Arc arc => arc.IsReversed
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle)) ? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle))
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)), : new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
@@ -581,8 +828,10 @@ public class GeometrySimplifier
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X); var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X); var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
var da = a2 - a1; var da = a2 - a1;
while (da > System.Math.PI) da -= Angle.TwoPI; while (da > System.Math.PI)
while (da < -System.Math.PI) da += Angle.TwoPI; da -= Angle.TwoPI;
while (da < -System.Math.PI)
da += Angle.TwoPI;
total += da; total += da;
} }
return total; return total;
@@ -593,7 +842,12 @@ public class GeometrySimplifier
/// back to the original line segments. This catches cases where points lie /// back to the original line segments. This catches cases where points lie
/// on a large circle but the arc bulges far from the original straight geometry. /// on a large circle but the arc bulges far from the original straight geometry.
/// </summary> /// </summary>
private static double MaxArcToSegmentDeviation(List<Vector> points, Vector center, double radius, bool isReversed) private static double MaxArcToSegmentDeviation(
List<Vector> points,
Vector center,
double radius,
bool isReversed
)
{ {
var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X); var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X);
var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X); var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X);
@@ -601,11 +855,13 @@ public class GeometrySimplifier
var sweep = endAngle - startAngle; var sweep = endAngle - startAngle;
if (isReversed) if (isReversed)
{ {
if (sweep > 0) sweep -= Angle.TwoPI; if (sweep > 0)
sweep -= Angle.TwoPI;
} }
else else
{ {
if (sweep < 0) sweep += Angle.TwoPI; if (sweep < 0)
sweep += Angle.TwoPI;
} }
var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10)); var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10));
@@ -624,9 +880,11 @@ public class GeometrySimplifier
for (var j = 0; j < points.Count - 1; j++) for (var j = 0; j < points.Count - 1; j++)
{ {
var dist = DistanceToSegment(arcPt, points[j], points[j + 1]); var dist = DistanceToSegment(arcPt, points[j], points[j + 1]);
if (dist < minDist) minDist = dist; if (dist < minDist)
minDist = dist;
} }
if (minDist > maxDev) maxDev = minDist; if (minDist > maxDev)
maxDev = minDist;
} }
return maxDev; return maxDev;
} }
+1 -2
View File
@@ -1,5 +1,4 @@
 namespace OpenNest.Geometry
namespace OpenNest.Geometry
{ {
public interface IBoundable public interface IBoundable
{ {
-148
View File
@@ -1,148 +0,0 @@
using Clipper2Lib;
namespace OpenNest.Geometry
{
/// <summary>
/// Computes the Inner-Fit Polygon (IFP) — the feasible region where a part's
/// reference point can be placed so the part stays entirely within the plate boundary.
/// For a rectangular plate, the IFP is the plate shrunk by the part's bounding dimensions.
/// </summary>
public static class InnerFitPolygon
{
/// <summary>
/// Computes the IFP for placing a part polygon inside a rectangular work area.
/// The result is a polygon representing all valid reference point positions.
/// </summary>
public static Polygon Compute(Box workArea, Polygon partPolygon)
{
// Get the part's bounding box relative to its reference point (origin).
var verts = partPolygon.Vertices;
if (verts.Count < 3)
return new Polygon();
var minX = verts[0].X;
var maxX = verts[0].X;
var minY = verts[0].Y;
var maxY = verts[0].Y;
for (var i = 1; i < verts.Count; i++)
{
if (verts[i].X < minX) minX = verts[i].X;
if (verts[i].X > maxX) maxX = verts[i].X;
if (verts[i].Y < minY) minY = verts[i].Y;
if (verts[i].Y > maxY) maxY = verts[i].Y;
}
// The IFP is the work area shrunk inward by the part's extent in each direction.
// The reference point can range from (workArea.Left - minX) to (workArea.Right - maxX)
// and (workArea.Bottom - minY) to (workArea.Top - maxY).
var ifpLeft = workArea.X - minX;
var ifpRight = workArea.Right - maxX;
var ifpBottom = workArea.Y - minY;
var ifpTop = workArea.Top - maxY;
// If the part doesn't fit, return an empty polygon.
if (ifpRight < ifpLeft || ifpTop < ifpBottom)
return new Polygon();
var result = new Polygon();
result.Vertices.Add(new Vector(ifpLeft, ifpBottom));
result.Vertices.Add(new Vector(ifpRight, ifpBottom));
result.Vertices.Add(new Vector(ifpRight, ifpTop));
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
result.Close();
result.UpdateBounds();
return result;
}
/// <summary>
/// Computes the feasible region for placing a part given already-placed parts.
/// FeasibleRegion = IFP(plate, part) - union(NFP(placed_i, part))
/// Returns the polygon representing valid placement positions, or an empty
/// polygon if no valid position exists.
/// </summary>
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
{
if (ifp.Vertices.Count < 3)
return new Polygon();
if (nfpPaths == null || nfpPaths.Count == 0)
return ifp;
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
var ifpPaths = new PathsD { ifpPath };
// Subtract the NFPs from the IFP.
// Clipper2 handles the implicit union of the clip paths.
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
if (feasible.Count == 0)
return new Polygon();
// Find the polygon with the bottom-left-most point.
// This ensures we pick the correct region for placement.
PathD bestPath = null;
var bestY = double.MaxValue;
var bestX = double.MaxValue;
foreach (var path in feasible)
{
foreach (var pt in path)
{
if (pt.y < bestY || (pt.y == bestY && pt.x < bestX))
{
bestY = pt.y;
bestX = pt.x;
bestPath = path;
}
}
}
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
}
/// <summary>
/// Computes the feasible region for placing a part given already-placed parts.
/// (Legacy overload for backward compatibility).
/// </summary>
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
{
if (nfps == null || nfps.Length == 0)
return ifp;
var nfpPaths = new PathsD(nfps.Length);
foreach (var nfp in nfps)
{
if (nfp.Vertices.Count >= 3)
nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp));
}
return ComputeFeasibleRegion(ifp, nfpPaths);
}
/// <summary>
/// Finds the bottom-left-most point on a polygon boundary.
/// "Bottom-left" means: minimize Y first, then minimize X.
/// Returns Vector.Invalid if the polygon has no vertices.
/// </summary>
public static Vector FindBottomLeftPoint(Polygon polygon)
{
if (polygon.Vertices.Count == 0)
return Vector.Invalid;
var best = polygon.Vertices[0];
for (var i = 1; i < polygon.Vertices.Count; i++)
{
var v = polygon.Vertices[i];
if (v.Y < best.Y || (v.Y == best.Y && v.X < best.X))
best = v;
}
return best;
}
}
}
+48 -19
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 =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt), arc.Center.AngleTo(pt),
arc.StartAngle, arc.StartAngle,
arc.EndAngle, arc.EndAngle,
arc.IsReversed)).ToList(); arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -54,11 +69,15 @@ namespace OpenNest.Geometry
return false; return false;
} }
pts = pts.Where(pt => Angle.IsBetweenRad( pts = pts.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt), arc.Center.AngleTo(pt),
arc.StartAngle, arc.StartAngle,
arc.EndAngle, arc.EndAngle,
arc.IsReversed)).ToList(); arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -74,11 +93,15 @@ namespace OpenNest.Geometry
pts2.AddRange(pts3); pts2.AddRange(pts3);
} }
pts = pts2.Where(pt => Angle.IsBetweenRad( pts = pts2.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt), arc.Center.AngleTo(pt),
arc.StartAngle, arc.StartAngle,
arc.EndAngle, arc.EndAngle,
arc.IsReversed)).ToList(); arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -95,11 +118,15 @@ namespace OpenNest.Geometry
pts2.AddRange(pts3); pts2.AddRange(pts3);
} }
pts = pts2.Where(pt => Angle.IsBetweenRad( pts = pts2.Where(pt =>
Angle.IsBetweenRad(
arc.Center.AngleTo(pt), arc.Center.AngleTo(pt),
arc.StartAngle, arc.StartAngle,
arc.EndAngle, arc.EndAngle,
arc.IsReversed)).ToList(); arc.IsReversed
)
)
.ToList();
return pts.Count > 0; return pts.Count > 0;
} }
@@ -123,20 +150,22 @@ namespace OpenNest.Geometry
} }
var d = circle2.Center - circle1.Center; var d = circle2.Center - circle1.Center;
var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance); var a =
(
circle1.Radius * circle1.Radius
- circle2.Radius * circle2.Radius
+ distance * distance
) / (2.0 * distance);
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a); var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
var pt = new Vector( var pt = new Vector(
circle1.Center.X + (a * d.X) / distance, circle1.Center.X + (a * d.X) / distance,
circle1.Center.Y + (a * d.Y) / distance); circle1.Center.Y + (a * d.Y) / distance
);
var i1 = new Vector( var i1 = new Vector(pt.X + (h * d.Y) / distance, pt.Y - (h * d.X) / distance);
pt.X + (h * d.Y) / distance,
pt.Y - (h * d.X) / distance);
var i2 = new Vector( var i2 = new Vector(pt.X - (h * d.Y) / distance, pt.Y + (h * d.X) / distance);
pt.X - (h * d.Y) / distance,
pt.Y + (h * d.X) / distance);
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 }; pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
+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)
+10 -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
{ {
@@ -9,14 +9,10 @@ namespace OpenNest.Geometry
internal Vector pt1; internal Vector pt1;
internal Vector pt2; internal Vector pt2;
public Line() public Line() { }
{
}
public Line(double x1, double y1, double x2, double y2) public Line(double x1, double y1, double x2, double y2)
: this(new Vector(x1, y1), new Vector(x2, y2)) : this(new Vector(x1, y1), new Vector(x2, y2)) { }
{
}
public Line(Vector startPoint, Vector endPoint) public Line(Vector startPoint, Vector endPoint)
{ {
@@ -83,9 +79,7 @@ namespace OpenNest.Geometry
return EndPoint; return EndPoint;
else else
{ {
return new Vector( return new Vector(StartPoint.X + param * diff2.X, StartPoint.Y + param * diff2.Y);
StartPoint.X + param * diff2.X,
StartPoint.Y + param * diff2.Y);
} }
} }
@@ -372,7 +366,7 @@ namespace OpenNest.Geometry
/// <summary> /// <summary>
/// Updates the bounding box. /// Updates the bounding box.
/// </summary> /// </summary>
public override sealed void UpdateBounds() public sealed override void UpdateBounds()
{ {
if (StartPoint.X < EndPoint.X) if (StartPoint.X < EndPoint.X)
{ {
@@ -429,13 +423,13 @@ namespace OpenNest.Geometry
/// <returns>A tuple of (first, second) sub-lines.</returns> /// <returns>A tuple of (first, second) sub-lines.</returns>
public (Line first, Line second) SplitAt(Vector point) public (Line first, Line second) SplitAt(Vector point)
{ {
var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon var first =
point.DistanceTo(StartPoint) < Tolerance.Epsilon
? null ? null
: new Line(StartPoint, point); : new Line(StartPoint, point);
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon var second =
? null point.DistanceTo(EndPoint) < Tolerance.Epsilon ? null : new Line(point, EndPoint);
: new Line(point, EndPoint);
return (first, second); return (first, second);
} }
+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;
} }
+20 -9
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;
@@ -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;
+83 -38
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)
@@ -500,9 +494,14 @@ namespace OpenNest.Geometry
if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line) if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line)
{ {
JoinOffsetLines( JoinOffsetLines(
(Line)lastEntity, (Line)lastOffsetEntity, (Line)lastEntity,
line, offsetLine, (Line)lastOffsetEntity,
distance, side, offsetShape); line,
offsetLine,
distance,
side,
offsetShape
);
} }
offsetShape.Entities.Add(offsetLine); offsetShape.Entities.Add(offsetLine);
@@ -519,27 +518,42 @@ namespace OpenNest.Geometry
} }
// Close the shape: join last offset entity back to first // Close the shape: join last offset entity back to first
if (lastOffsetEntity != null && firstOffsetEntity != null if (
lastOffsetEntity != null
&& firstOffsetEntity != null
&& lastOffsetEntity != firstOffsetEntity && lastOffsetEntity != firstOffsetEntity
&& lastOffsetEntity.Type == EntityType.Line && lastOffsetEntity.Type == EntityType.Line
&& firstOffsetEntity.Type == EntityType.Line) && firstOffsetEntity.Type == EntityType.Line
)
{ {
JoinOffsetLines( JoinOffsetLines(
(Line)lastEntity, (Line)lastOffsetEntity, (Line)lastEntity,
(Line)firstEntity, (Line)firstOffsetEntity, (Line)lastOffsetEntity,
distance, side, offsetShape); (Line)firstEntity,
(Line)firstOffsetEntity,
distance,
side,
offsetShape
);
} }
foreach (var cutout in definedShape.Cutouts) foreach (var cutout in definedShape.Cutouts)
offsetShape.Entities.AddRange(((Shape)cutout.OffsetEntity(distance, side)).Entities); offsetShape.Entities.AddRange(
((Shape)cutout.OffsetEntity(distance, side)).Entities
);
return offsetShape; return offsetShape;
} }
private static void JoinOffsetLines( private static void JoinOffsetLines(
Line lastLine, Line lastOffsetLine, Line lastLine,
Line line, Line offsetLine, Line lastOffsetLine,
double distance, OffsetSide side, Shape offsetShape) Line line,
Line offsetLine,
double distance,
OffsetSide side,
Shape offsetShape
)
{ {
// Determine if this is a convex corner using the cross product of // Determine if this is a convex corner using the cross product of
// the original line directions. Convex corners need an arc; concave // the original line directions. Convex corners need an arc; concave
@@ -548,8 +562,9 @@ namespace OpenNest.Geometry
var d2 = line.EndPoint - line.StartPoint; var d2 = line.EndPoint - line.StartPoint;
var cross = d1.X * d2.Y - d1.Y * d2.X; var cross = d1.X * d2.Y - d1.Y * d2.X;
var isConvex = (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) || var isConvex =
(side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon); (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon)
|| (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
if (isConvex) if (isConvex)
{ {
@@ -563,7 +578,9 @@ namespace OpenNest.Geometry
offsetShape.Entities.Add(arc); offsetShape.Entities.Add(arc);
} }
else if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)) else if (
Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)
)
{ {
offsetLine.StartPoint = intersection; offsetLine.StartPoint = intersection;
lastOffsetLine.EndPoint = intersection; lastOffsetLine.EndPoint = intersection;
@@ -596,8 +613,11 @@ namespace OpenNest.Geometry
{ {
var poly = ToPolygon(); var poly = ToPolygon();
if (poly == null || poly.Vertices.Count < 3 if (
|| poly.RotationDirection() == RotationType.CW) poly == null
|| poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CW
)
return OffsetEntity(distance, OffsetSide.Left) as Shape; return OffsetEntity(distance, OffsetSide.Left) as Shape;
// Shape is CCW — reverse to CW so Left offset goes outward. // Shape is CCW — reverse to CW so Left offset goes outward.
@@ -611,10 +631,21 @@ namespace OpenNest.Geometry
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer }); copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
break; break;
case Arc a: case Arc a:
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); copy.Entities.Add(
new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed)
{
Layer = a.Layer,
}
);
break; break;
case Circle c: case Circle c:
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW }); copy.Entities.Add(
new Circle(c.Center, c.Radius)
{
Layer = c.Layer,
Rotation = RotationType.CW,
}
);
break; break;
} }
} }
@@ -631,8 +662,11 @@ namespace OpenNest.Geometry
{ {
var poly = ToPolygon(); var poly = ToPolygon();
if (poly == null || poly.Vertices.Count < 3 if (
|| poly.RotationDirection() == RotationType.CCW) poly == null
|| poly.Vertices.Count < 3
|| poly.RotationDirection() == RotationType.CCW
)
return OffsetEntity(distance, OffsetSide.Left) as Shape; return OffsetEntity(distance, OffsetSide.Left) as Shape;
// Create a reversed copy to avoid mutating shared entity objects. // Create a reversed copy to avoid mutating shared entity objects.
@@ -646,10 +680,21 @@ namespace OpenNest.Geometry
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer }); copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
break; break;
case Arc a: case Arc a:
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); copy.Entities.Add(
new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed)
{
Layer = a.Layer,
}
);
break; break;
case Circle c: case Circle c:
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW }); copy.Entities.Add(
new Circle(c.Center, c.Radius)
{
Layer = c.Layer,
Rotation = RotationType.CCW,
}
);
break; break;
} }
} }
+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)}";
} }
} }
+382 -131
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,17 +13,28 @@ namespace OpenNest.Geometry
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction) private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
{ {
return RayEdgeDistance( return RayEdgeDistance(
vertex.X, vertex.Y, vertex.X,
edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y, vertex.Y,
direction); edge.pt1.X,
edge.pt1.Y,
edge.pt2.X,
edge.pt2.Y,
direction
);
} }
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
private static double RayEdgeDistance( private static double RayEdgeDistance(
double vx, double vy, double vx,
double p1x, double p1y, double p2x, double p2y, double vy,
PushDirection direction) double p1x,
double p1y,
double p2x,
double p2y,
PushDirection direction
)
{ {
switch (direction) switch (direction)
{ {
@@ -41,8 +52,10 @@ namespace OpenNest.Geometry
var ix = p1x + t * (p2x - p1x); var ix = p1x + t * (p2x - p1x);
var dist = direction == PushDirection.Left ? vx - ix : ix - vx; var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
if (dist > Tolerance.Epsilon) return dist; if (dist > Tolerance.Epsilon)
if (dist >= -Tolerance.Epsilon) return 0; return dist;
if (dist >= -Tolerance.Epsilon)
return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -60,8 +73,10 @@ namespace OpenNest.Geometry
var iy = p1y + t * (p2y - p1y); var iy = p1y + t * (p2y - p1y);
var dist = direction == PushDirection.Down ? vy - iy : iy - vy; var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
if (dist > Tolerance.Epsilon) return dist; if (dist > Tolerance.Epsilon)
if (dist >= -Tolerance.Epsilon) return 0; return dist;
if (dist >= -Tolerance.Epsilon)
return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -75,11 +90,18 @@ namespace OpenNest.Geometry
/// Returns double.MaxValue if the ray does not hit the segment. /// Returns double.MaxValue if the ray does not hit the segment.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayEdgeDistance( public static double RayEdgeDistance(
double vx, double vy, double vx,
double p1x, double p1y, double p2x, double p2y, double vy,
double dirX, double dirY) double p1x,
double p1y,
double p2x,
double p2y,
double dirX,
double dirY
)
{ {
var ex = p2x - p1x; var ex = p2x - p1x;
var ey = p2y - p1y; var ey = p2y - p1y;
@@ -99,8 +121,10 @@ namespace OpenNest.Geometry
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon) if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
return double.MaxValue; return double.MaxValue;
if (t > Tolerance.Epsilon) return t; if (t > Tolerance.Epsilon)
if (t >= -Tolerance.Epsilon) return 0; return t;
if (t >= -Tolerance.Epsilon)
return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -109,12 +133,19 @@ namespace OpenNest.Geometry
/// Returns false if no real intersection exists. /// Returns false if no real intersection exists.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
private static bool SolveRayCircle( private static bool SolveRayCircle(
double vx, double vy, double vx,
double cx, double cy, double r, double vy,
double dirX, double dirY, double cx,
out double t1, out double t2) double cy,
double r,
double dirX,
double dirY,
out double t1,
out double t2
)
{ {
var ox = vx - cx; var ox = vx - cx;
var oy = vy - cy; var oy = vy - cy;
@@ -143,12 +174,20 @@ namespace OpenNest.Geometry
/// angular span. Returns double.MaxValue if no hit. /// angular span. Returns double.MaxValue if no hit.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayArcDistance( public static double RayArcDistance(
double vx, double vy, double vx,
double cx, double cy, double r, double vy,
double startAngle, double endAngle, bool reversed, double cx,
double dirX, double dirY) double cy,
double r,
double startAngle,
double endAngle,
bool reversed,
double dirX,
double dirY
)
{ {
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2)) if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
return double.MaxValue; return double.MaxValue;
@@ -157,16 +196,18 @@ namespace OpenNest.Geometry
if (t1 > -Tolerance.Epsilon) if (t1 > -Tolerance.Epsilon)
{ {
var hitAngle = Angle.NormalizeRad(System.Math.Atan2( var hitAngle = Angle.NormalizeRad(
vy + t1 * dirY - cy, vx + t1 * dirX - cx)); System.Math.Atan2(vy + t1 * dirY - cy, vx + t1 * dirX - cx)
);
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
best = t1 > Tolerance.Epsilon ? t1 : 0; best = t1 > Tolerance.Epsilon ? t1 : 0;
} }
if (t2 > -Tolerance.Epsilon && t2 < best) if (t2 > -Tolerance.Epsilon && t2 < best)
{ {
var hitAngle = Angle.NormalizeRad(System.Math.Atan2( var hitAngle = Angle.NormalizeRad(
vy + t2 * dirY - cy, vx + t2 * dirX - cx)); System.Math.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx)
);
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
best = t2 > Tolerance.Epsilon ? t2 : 0; best = t2 > Tolerance.Epsilon ? t2 : 0;
} }
@@ -179,19 +220,29 @@ namespace OpenNest.Geometry
/// Returns double.MaxValue if no hit. /// Returns double.MaxValue if no hit.
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
public static double RayCircleDistance( public static double RayCircleDistance(
double vx, double vy, double vx,
double cx, double cy, double r, double vy,
double dirX, double dirY) double cx,
double cy,
double r,
double dirX,
double dirY
)
{ {
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2)) if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
return double.MaxValue; return double.MaxValue;
if (t1 > Tolerance.Epsilon) return t1; if (t1 > Tolerance.Epsilon)
if (t1 >= -Tolerance.Epsilon) return 0; return t1;
if (t2 > Tolerance.Epsilon) return t2; if (t1 >= -Tolerance.Epsilon)
if (t2 >= -Tolerance.Epsilon) return 0; return 0;
if (t2 > Tolerance.Epsilon)
return t2;
if (t2 >= -Tolerance.Epsilon)
return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -201,7 +252,11 @@ namespace OpenNest.Geometry
/// any edge of movingLines contacts any edge of stationaryLines. /// any edge of movingLines contacts any edge of stationaryLines.
/// Returns double.MaxValue if no collision path exists. /// Returns double.MaxValue if no collision path exists.
/// </summary> /// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction) public static double DirectionalDistance(
List<Line> movingLines,
List<Line> stationaryLines,
PushDirection direction
)
{ {
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction); return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
} }
@@ -211,8 +266,12 @@ namespace OpenNest.Geometry
/// by (movingDx, movingDy) without creating new Line objects. /// by (movingDx, movingDy) without creating new Line objects.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
List<Line> movingLines, double movingDx, double movingDy, List<Line> movingLines,
List<Line> stationaryLines, PushDirection direction) double movingDx,
double movingDy,
List<Line> stationaryLines,
PushDirection direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var movingOffset = new Vector(movingDx, movingDy); var movingOffset = new Vector(movingDx, movingDy);
@@ -226,7 +285,8 @@ namespace OpenNest.Geometry
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
// Case 2: Each stationary vertex -> each moving edge (opposite direction) // Case 2: Each stationary vertex -> each moving edge (opposite direction)
@@ -239,7 +299,8 @@ namespace OpenNest.Geometry
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
return minDist; return minDist;
@@ -267,9 +328,12 @@ namespace OpenNest.Geometry
/// to avoid all intermediate object allocations. /// to avoid all intermediate object allocations.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
(Vector start, Vector end)[] movingEdges, Vector movingOffset, (Vector start, Vector end)[] movingEdges,
(Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset, Vector movingOffset,
PushDirection direction) (Vector start, Vector end)[] stationaryEdges,
Vector stationaryOffset,
PushDirection direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
@@ -281,7 +345,8 @@ namespace OpenNest.Geometry
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction); var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
// Case 2: Each stationary vertex -> each moving edge (opposite direction) // Case 2: Each stationary vertex -> each moving edge (opposite direction)
@@ -293,15 +358,19 @@ namespace OpenNest.Geometry
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
return minDist; return minDist;
} }
public static double OneWayDistance( public static double OneWayDistance(
Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset, Vector vertex,
PushDirection direction) (Vector start, Vector end)[] edges,
Vector edgeOffset,
PushDirection direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var vx = vertex.X; var vx = vertex.X;
@@ -315,7 +384,9 @@ namespace OpenNest.Geometry
var e1 = edges[i].start + edgeOffset; var e1 = edges[i].start + edgeOffset;
var e2 = edges[i].end + edgeOffset; var e2 = edges[i].end + edgeOffset;
double perpValue, edgeMin, edgeMax; double perpValue,
edgeMin,
edgeMax;
if (horizontal) if (horizontal)
{ {
perpValue = vy; perpValue = vy;
@@ -337,7 +408,8 @@ namespace OpenNest.Geometry
continue; continue;
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
if (d < minDist) minDist = d; if (d < minDist)
minDist = d;
} }
return minDist; return minDist;
@@ -347,11 +419,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return PushDirection.Right; case PushDirection.Left:
case PushDirection.Right: return PushDirection.Left; return PushDirection.Right;
case PushDirection.Up: return PushDirection.Down; case PushDirection.Right:
case PushDirection.Down: return PushDirection.Up; return PushDirection.Left;
default: return direction; case PushDirection.Up:
return PushDirection.Down;
case PushDirection.Down:
return PushDirection.Up;
default:
return direction;
} }
} }
@@ -364,11 +441,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return box.Left - boundary.Left; case PushDirection.Left:
case PushDirection.Right: return boundary.Right - box.Right; return box.Left - boundary.Left;
case PushDirection.Up: return boundary.Top - box.Top; case PushDirection.Right:
case PushDirection.Down: return box.Bottom - boundary.Bottom; return boundary.Right - box.Right;
default: return double.MaxValue; case PushDirection.Up:
return boundary.Top - box.Top;
case PushDirection.Down:
return box.Bottom - boundary.Bottom;
default:
return double.MaxValue;
} }
} }
@@ -376,11 +458,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return new Vector(-distance, 0); case PushDirection.Left:
case PushDirection.Right: return new Vector(distance, 0); return new Vector(-distance, 0);
case PushDirection.Up: return new Vector(0, distance); case PushDirection.Right:
case PushDirection.Down: return new Vector(0, -distance); return new Vector(distance, 0);
default: return new Vector(); case PushDirection.Up:
return new Vector(0, distance);
case PushDirection.Down:
return new Vector(0, -distance);
default:
return new Vector();
} }
} }
@@ -388,11 +475,16 @@ namespace OpenNest.Geometry
{ {
switch (direction) switch (direction)
{ {
case PushDirection.Left: return from.Left - to.Right; case PushDirection.Left:
case PushDirection.Right: return to.Left - from.Right; return from.Left - to.Right;
case PushDirection.Up: return to.Bottom - from.Top; case PushDirection.Right:
case PushDirection.Down: return from.Bottom - to.Top; return to.Left - from.Right;
default: return double.MaxValue; case PushDirection.Up:
return to.Bottom - from.Top;
case PushDirection.Down:
return from.Bottom - to.Top;
default:
return double.MaxValue;
} }
} }
@@ -409,23 +501,27 @@ namespace OpenNest.Geometry
if (direction.X < -Tolerance.Epsilon) if (direction.X < -Tolerance.Epsilon)
{ {
var d = (box.Left - boundary.Left) / -direction.X; var d = (box.Left - boundary.Left) / -direction.X;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
else if (direction.X > Tolerance.Epsilon) else if (direction.X > Tolerance.Epsilon)
{ {
var d = (boundary.Right - box.Right) / direction.X; var d = (boundary.Right - box.Right) / direction.X;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
if (direction.Y < -Tolerance.Epsilon) if (direction.Y < -Tolerance.Epsilon)
{ {
var d = (box.Bottom - boundary.Bottom) / -direction.Y; var d = (box.Bottom - boundary.Bottom) / -direction.Y;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
else if (direction.Y > Tolerance.Epsilon) else if (direction.Y > Tolerance.Epsilon)
{ {
var d = (boundary.Top - box.Top) / direction.Y; var d = (boundary.Top - box.Top) / direction.Y;
if (d < dist) dist = d; if (d < dist)
dist = d;
} }
return dist < 0 ? 0 : dist; return dist < 0 ? 0 : dist;
@@ -463,7 +559,11 @@ namespace OpenNest.Geometry
/// Computes the minimum translation distance along an arbitrary unit direction /// Computes the minimum translation distance along an arbitrary unit direction
/// before any edge of movingLines contacts any edge of stationaryLines. /// before any edge of movingLines contacts any edge of stationaryLines.
/// </summary> /// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction) public static double DirectionalDistance(
List<Line> movingLines,
List<Line> stationaryLines,
Vector direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var dirX = direction.X; var dirX = direction.X;
@@ -476,8 +576,18 @@ namespace OpenNest.Geometry
for (var i = 0; i < stationaryLines.Count; i++) for (var i = 0; i < stationaryLines.Count; i++)
{ {
var e = stationaryLines[i]; var e = stationaryLines[i];
var d = RayEdgeDistance(mv.X, mv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, dirX, dirY); var d = RayEdgeDistance(
if (d < minDist) minDist = d; mv.X,
mv.Y,
e.pt1.X,
e.pt1.Y,
e.pt2.X,
e.pt2.Y,
dirX,
dirY
);
if (d < minDist)
minDist = d;
} }
} }
@@ -491,8 +601,18 @@ namespace OpenNest.Geometry
for (var i = 0; i < movingLines.Count; i++) for (var i = 0; i < movingLines.Count; i++)
{ {
var e = movingLines[i]; var e = movingLines[i];
var d = RayEdgeDistance(sv.X, sv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, oppX, oppY); var d = RayEdgeDistance(
if (d < minDist) minDist = d; sv.X,
sv.Y,
e.pt1.X,
e.pt1.Y,
e.pt2.X,
e.pt2.Y,
oppX,
oppY
);
if (d < minDist)
minDist = d;
} }
} }
@@ -505,9 +625,16 @@ namespace OpenNest.Geometry
/// stationaryEntities. Delegates to the Vector-based overload. /// stationaryEntities. Delegates to the Vector-based overload.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction) List<Entity> movingEntities,
List<Entity> stationaryEntities,
PushDirection direction
)
{ {
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0)); return DirectionalDistance(
movingEntities,
stationaryEntities,
DirectionToOffset(direction, 1.0)
);
} }
/// <summary> /// <summary>
@@ -517,7 +644,10 @@ namespace OpenNest.Geometry
/// without tessellation. /// without tessellation.
/// </summary> /// </summary>
public static double DirectionalDistance( public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction) List<Entity> movingEntities,
List<Entity> stationaryEntities,
Vector direction
)
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
var dirX = direction.X; var dirX = direction.X;
@@ -536,7 +666,8 @@ namespace OpenNest.Geometry
if (d < minDist) if (d < minDist)
{ {
minDist = d; minDist = d;
if (d <= 0) return 0; if (d <= 0)
return 0;
} }
} }
} }
@@ -557,7 +688,8 @@ namespace OpenNest.Geometry
if (d < minDist) if (d < minDist)
{ {
minDist = d; minDist = d;
if (d <= 0) return 0; if (d <= 0)
return 0;
} }
} }
} }
@@ -566,10 +698,24 @@ namespace OpenNest.Geometry
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual // Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
// closest point on a small corner arc to a straight edge may lie between // closest point on a small corner arc to a straight edge may lie between
// those samples. Use ClosestPointTo to find it and fire a ray from there. // those samples. Use ClosestPointTo to find it and fire a ray from there.
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist); minDist = ArcToLineClosestDistance(
if (minDist <= 0) return 0; movingEntities,
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist); stationaryEntities,
if (minDist <= 0) return 0; dirX,
dirY,
minDist
);
if (minDist <= 0)
return 0;
minDist = ArcToLineClosestDistance(
stationaryEntities,
movingEntities,
oppX,
oppY,
minDist
);
if (minDist <= 0)
return 0;
// Phase 4: Curve-to-curve direct distance. // Phase 4: Curve-to-curve direct distance.
// The vertex-to-entity approach misses the closest contact between two // The vertex-to-entity approach misses the closest contact between two
@@ -605,20 +751,35 @@ namespace OpenNest.Geometry
if (me is Arc mArc) if (me is Arc mArc)
{ {
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx)); var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed)) if (
!Angle.IsBetweenRad(
angle,
mArc.StartAngle,
mArc.EndAngle,
mArc.IsReversed
)
)
continue; continue;
} }
if (se is Arc sArc) if (se is Arc sArc)
{ {
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx)); var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed)) if (
!Angle.IsBetweenRad(
angle,
sArc.StartAngle,
sArc.EndAngle,
sArc.IsReversed
)
)
continue; continue;
} }
} }
minDist = d; minDist = d;
if (d <= 0) return 0; if (d <= 0)
return 0;
} }
} }
@@ -626,8 +787,12 @@ namespace OpenNest.Geometry
} }
private static double ArcToLineClosestDistance( private static double ArcToLineClosestDistance(
List<Entity> arcEntities, List<Entity> lineEntities, List<Entity> arcEntities,
double dirX, double dirY, double minDist) List<Entity> lineEntities,
double dirX,
double dirY,
double minDist
)
{ {
for (var i = 0; i < arcEntities.Count; i++) for (var i = 0; i < arcEntities.Count; i++)
{ {
@@ -662,15 +827,30 @@ namespace OpenNest.Geometry
{ {
var theta = k == 0 ? theta1 : theta2; var theta = k == 0 ? theta1 : theta2;
if (!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)) if (
!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)
)
continue; continue;
var qx = cx + r * System.Math.Cos(theta); var qx = cx + r * System.Math.Cos(theta);
var qy = cy + r * System.Math.Sin(theta); var qy = cy + r * System.Math.Sin(theta);
var d = RayEdgeDistance(qx, qy, p1x, p1y, line.pt2.X, line.pt2.Y, var d = RayEdgeDistance(
dirX, dirY); qx,
if (d < minDist) { minDist = d; if (d <= 0) return 0; } qy,
p1x,
p1y,
line.pt2.X,
line.pt2.Y,
dirX,
dirY
);
if (d < minDist)
{
minDist = d;
if (d <= 0)
return 0;
}
} }
} }
} }
@@ -678,28 +858,54 @@ namespace OpenNest.Geometry
} }
private static double RayEntityDistance( private static double RayEntityDistance(
double vx, double vy, Entity entity, double dirX, double dirY) double vx,
double vy,
Entity entity,
double dirX,
double dirY
)
{ {
if (entity is Line line) if (entity is Line line)
{ {
return RayEdgeDistance(vx, vy, return RayEdgeDistance(
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y, vx,
dirX, dirY); vy,
line.pt1.X,
line.pt1.Y,
line.pt2.X,
line.pt2.Y,
dirX,
dirY
);
} }
if (entity is Arc arc) if (entity is Arc arc)
{ {
return RayArcDistance(vx, vy, return RayArcDistance(
arc.Center.X, arc.Center.Y, arc.Radius, vx,
arc.StartAngle, arc.EndAngle, arc.IsReversed, vy,
dirX, dirY); arc.Center.X,
arc.Center.Y,
arc.Radius,
arc.StartAngle,
arc.EndAngle,
arc.IsReversed,
dirX,
dirY
);
} }
if (entity is Circle circle) if (entity is Circle circle)
{ {
return RayCircleDistance(vx, vy, return RayCircleDistance(
circle.Center.X, circle.Center.Y, circle.Radius, vx,
dirX, dirY); vy,
circle.Center.X,
circle.Center.Y,
circle.Radius,
dirX,
dirY
);
} }
return double.MaxValue; return double.MaxValue;
@@ -759,7 +965,10 @@ namespace OpenNest.Geometry
return CollectVertices(ToEdgeArray(lines), offset); return CollectVertices(ToEdgeArray(lines), offset);
} }
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset) private static HashSet<Vector> CollectVertices(
(Vector start, Vector end)[] edges,
Vector offset
)
{ {
var vertices = new HashSet<Vector>(); var vertices = new HashSet<Vector>();
for (var i = 0; i < edges.Length; i++) for (var i = 0; i < edges.Length; i++)
@@ -778,26 +987,48 @@ namespace OpenNest.Geometry
return edges; return edges;
} }
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction) private static void SortEdgesForPruning(
(Vector start, Vector end)[] edges,
PushDirection direction
)
{ {
if (direction == PushDirection.Left || direction == PushDirection.Right) if (direction == PushDirection.Left || direction == PushDirection.Right)
System.Array.Sort(edges, (a, b) => System.Array.Sort(
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y))); edges,
(a, b) =>
System
.Math.Min(a.start.Y, a.end.Y)
.CompareTo(System.Math.Min(b.start.Y, b.end.Y))
);
else else
System.Array.Sort(edges, (a, b) => System.Array.Sort(
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X))); edges,
(a, b) =>
System
.Math.Min(a.start.X, a.end.X)
.CompareTo(System.Math.Min(b.start.X, b.end.X))
);
} }
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r) private static bool TryGetCurveParams(
Entity entity,
out double cx,
out double cy,
out double r
)
{ {
if (entity is Circle circle) if (entity is Circle circle)
{ {
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius; cx = circle.Center.X;
cy = circle.Center.Y;
r = circle.Radius;
return true; return true;
} }
if (entity is Arc arc) if (entity is Arc arc)
{ {
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius; cx = arc.Center.X;
cy = arc.Center.Y;
r = arc.Radius;
return true; return true;
} }
cx = cy = r = 0; cx = cy = r = 0;
@@ -850,7 +1081,13 @@ namespace OpenNest.Geometry
return new Box(lft, btm, rgt - lft, top - btm); return new Box(lft, btm, rgt - lft, top - btm);
} }
private static bool FindVerticalLimits(Vector pt, Box bounds, List<Box> boxes, out double top, out double btm) private static bool FindVerticalLimits(
Vector pt,
Box bounds,
List<Box> boxes,
out double top,
out double btm
)
{ {
top = double.MaxValue; top = double.MaxValue;
btm = double.MinValue; btm = double.MinValue;
@@ -868,20 +1105,30 @@ namespace OpenNest.Geometry
if (top == double.MaxValue) if (top == double.MaxValue)
{ {
if (bounds.Top > pt.Y) top = bounds.Top; if (bounds.Top > pt.Y)
else return false; top = bounds.Top;
else
return false;
} }
if (btm == double.MinValue) if (btm == double.MinValue)
{ {
if (bounds.Bottom < pt.Y) btm = bounds.Bottom; if (bounds.Bottom < pt.Y)
else return false; btm = bounds.Bottom;
else
return false;
} }
return true; return true;
} }
private static bool FindHorizontalLimits(Vector pt, Box bounds, List<Box> boxes, out double lft, out double rgt) private static bool FindHorizontalLimits(
Vector pt,
Box bounds,
List<Box> boxes,
out double lft,
out double rgt
)
{ {
lft = double.MinValue; lft = double.MinValue;
rgt = double.MaxValue; rgt = double.MaxValue;
@@ -899,14 +1146,18 @@ namespace OpenNest.Geometry
if (rgt == double.MaxValue) if (rgt == double.MaxValue)
{ {
if (bounds.Right > pt.X) rgt = bounds.Right; if (bounds.Right > pt.X)
else return false; rgt = bounds.Right;
else
return false;
} }
if (lft == double.MinValue) if (lft == double.MinValue)
{ {
if (bounds.Left < pt.X) lft = bounds.Left; if (bounds.Left < pt.X)
else return false; lft = bounds.Left;
else
return false;
} }
return true; return true;
+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)
{ {

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