Compare commits

...

40 Commits

Author SHA1 Message Date
aj 0246073b31 fix: reduce default cut-off part clearance from 0.125 to 0.02
The previous default of 0.125 was too large for typical use, causing
cut-off lines to be pushed unnecessarily far from parts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 01:07:33 -04:00
aj 4801895321 chore: add Cincinnati post processor build dependency to solution
Adds project dependency in .sln and project reference in OpenNest.csproj
so the Cincinnati post processor DLL builds before the main app, ensuring
it's always available in the Posts/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 01:07:28 -04:00
aj 833abfe72e feat: add optional M98 part sub-programs to Cincinnati post processor
Each unique part geometry (drawing + rotation) is written once as a
reusable sub-program called via M98, reducing output size for nests
with repeated parts. G92 coordinate repositioning handles per-instance
plate placement with restore after each call. Cut-offs remain inline.

Controlled by UsePartSubprograms (default false) and PartSubprogramStart
config properties.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:43:44 -04:00
aj 379000bbd8 feat: auto-copy Cincinnati post processor DLL to Posts/ on build
The WinForms app's LoadPosts() scans Posts/ for IPostProcessor DLLs.
A build target copies the Cincinnati DLL there so it appears in the
Nest > Post menu automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:12:47 -04:00
aj 5936272ce4 refactor: move ProgramVariableManager to Core, add config file support
- Move ProgramVariable and ProgramVariableManager from
  OpenNest.Posts.Cincinnati to OpenNest.Core/CNC (namespace OpenNest.CNC)
  so they can be used internally in nest programs
- Add parameterless constructor to CincinnatiPostProcessor that loads
  config from a .json file next to the DLL (creates defaults on first run)
- Enums serialize as readable strings (e.g., "Inches", "ControllerSide")
- Config file: OpenNest.Posts.Cincinnati.json in the Posts/ directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:08:31 -04:00
aj da8e7e6fd3 feat: interactive cut-off selection and drag via line hit-testing
Select cut-offs by clicking their lines instead of a grip point.
Drag is axis-constrained with live regeneration during movement.
Selected cut-off highlighted with bright blue 3.5px line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:49:27 -04:00
aj 53d24ddaf1 feat: implement Polygon.OffsetEntity and use geometric offset for cut-off clearance
Polygon.OffsetEntity now computes proper miter-join offsets using edge
normals and winding direction, with self-intersection cleanup. CutOff
exclusion zones use geometric perimeter offset instead of scalar padding,
giving uniform clearance around parts regardless of cut angle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:46:18 -04:00
aj 8efdc8720c fix: review fixes — culture-invariant formatting, sealed config, threshold boundary
- Use CultureInfo.InvariantCulture in CoordinateFormatter, SpeedClassifier,
  and CincinnatiPreambleWriter to prevent locale-dependent G-code output
- Make CincinnatiPostConfig sealed per spec
- Fix SpeedClassifier.Classify threshold to >= (matching spec)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:45:22 -04:00
aj ca8a0942ab feat: add CincinnatiPostProcessor implementing IPostProcessor
Orchestrates CincinnatiPreambleWriter and CincinnatiSheetWriter to produce
a complete Cincinnati CNC output file from a Nest; includes 4 integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:41:06 -04:00
aj 8c3659a439 feat: add CincinnatiSheetWriter for per-plate subprogram emission
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:38:24 -04:00
aj 95a0815484 feat: add CincinnatiPreambleWriter for main program and variable declaration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:35:27 -04:00
aj e9caa9b8eb feat: add CincinnatiFeatureWriter for per-feature G-code emission
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:33:06 -04:00
aj 95a0db1983 feat: add SpeedClassifier for FAST/MEDIUM/SLOW cut distance classification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:29:14 -04:00
aj a323dcc230 feat: add ProgramVariable and ProgramVariableManager for macro variable declarations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:29:10 -04:00
aj 24cd18da88 feat: add CoordinateFormatter for Cincinnati G-code coordinate output
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:29:06 -04:00
aj 5d26efb552 feat: add CincinnatiPostConfig and supporting enums 2026-03-22 23:26:16 -04:00
aj 60c4545a17 feat: add OpenNest.Posts.Cincinnati project for Cincinnati CL-707 post processor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:25:16 -04:00
aj 4db51b8cdf fix: regenerate cut-offs on part rotation and default cut direction
- Regenerate cut-off programs after RotateSelectedParts so cut lines
  update when parts are rotated, not just moved
- Change default CutDirection from TowardOrigin to AwayFromOrigin so
  cuts start at the origin axis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:58:47 -04:00
aj 1c561d880e refactor: simplify CutOff methods and fix ActionCutOff issues
- Extract MakePoint/AxisBounds/CrossAxisBounds helpers in CutOff to
  eliminate repeated axis-dependent branching
- Simplify BuildProgram loop from 4 code paths to 2
- Use static EmptyExclusions to avoid per-part list allocations
- Fix double event subscription in ActionCutOff constructor
- Dispose debounce timer in DisconnectEvents
- Remove redundant BuildPerimeterCache call in OnMouseDown
- Extract TotalCutLength test helper, remove duplicate test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:51:52 -04:00
aj 17fc9c6cab feat: RegenerateCutOffs uses geometry-based perimeter cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:44:15 -04:00
aj 4287c5fa46 refactor: CutOff uses Dictionary<Part, Entity> instead of index-based list
Replace CutOff.BuildPerimeterCache (List<Shape>) with Plate.BuildPerimeterCache
(Dictionary<Part, Entity>) throughout. Consolidate two Regenerate overloads into
a single method with optional cache parameter. Fix Shape intersection bug where
non-intersecting entities added spurious Vector.Zero points to results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:42:32 -04:00
aj a735884ee9 feat: add Plate.BuildPerimeterCache with ShapeProfile and convex hull fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:34:14 -04:00
aj 22554b0fa3 refactor: extract CollectPoints from FindBestRotation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:30:59 -04:00
aj 48b4849a88 fix: ShapeProfile selects perimeter by largest bounding box area
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:29:00 -04:00
aj f79df4d426 refactor: remove NfpNestEngine
The NFP engine was a thin wrapper that delegated all single-drawing
operations to DefaultNestEngine. Remove the engine and its registry
entry to reduce dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:11:53 -04:00
aj ebb18d9b49 fix: prevent RemnantFiller interleaving and PairFiller recursion
RemnantFiller: add placed parts as a single envelope obstacle instead
of individual bounding boxes to prevent the next drawing from filling
into inter-row gaps. Remove the topmost bounding-box part to create a
clean rectangular boundary.

PairsFillStrategy: guard against recursive invocation — remnant fills
within PairFiller create a new engine that runs the full pipeline,
which would invoke PairsFillStrategy again causing deep recursion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:11:47 -04:00
aj 31a9e6dbad feat: replace best-fit viewer dropdown with DrawingListBox in split view
Swap the ComboBox drawing selector with the same DrawingListBox control
used in EditNestForm, placed in a resizable SplitContainer. Add selection
highlighting and HideQuantity option to DrawingListBox. Show a centered
loading message while computing, and allow switching drawings mid-compute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:53:43 -04:00
aj a576f9fafa fix: correct test helper winding and edge sequencer test data
Test drawings used CCW winding, causing OffsetSide.Left to produce
inward offsets. The BestFit pipeline then positioned pairs so actual
shapes overlapped, failing all 1232 candidates. Changed to CW winding
to match CNC convention where OffsetSide.Left = outward.

Also fixed EdgeStartSequencer test: centerPart at (25,55) was only 4.5
from the top edge (plate Y=60), closer than midPart at (10,10). Moved
to (25,25) for correct ordering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:31:50 -04:00
aj 9453bb51ce docs: update CLAUDE.md with cut-off feature documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:02:42 -04:00
aj ad58332a5d feat: regenerate cut-offs after part drag and fill operations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:01:03 -04:00
aj d4f60d5e8e feat: add cut-off selection, drag-to-reposition, and delete
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:57:26 -04:00
aj 3ea05257eb feat: wire up cut-off action in MainForm menu
Add "Sheet Cut-Off" menu item to the Plate menu that activates
ActionCutOff placement mode on the active PlateView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:54:09 -04:00
aj 7e49ed620b feat: add ActionCutOff for placing sheet cut-offs
Adds a new action that lets users place cut-off lines on plates by
clicking. The action auto-detects horizontal vs vertical axis based on
mouse proximity to plate edges, shows a dashed preview line that
follows the cursor, and adds the cut-off on left click. Pressing
Escape returns to selection mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:50:38 -04:00
aj 57bd0447e9 feat: add PlateView cut-off rendering with grip handle
Add DrawCutOffs and DrawCutOffGrip methods to PlateView for rendering
cut-off lines and selection grip handles. Includes CutOffSettings and
SelectedCutOff properties, plus GetCutOffAtPoint for hit-testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:46:49 -04:00
aj 07d6f08e8b feat: engine-specific TrimAxis and rename ShrinkAxis.Height to Length
Make quantity trimming direction-aware: DefaultNestEngine uses TrimAxis
(virtual property on NestEngineBase) so HorizontalRemnantEngine removes
topmost parts instead of rightmost. Rename ShrinkAxis.Height → Length
for consistency with Width/Length naming used throughout the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:43:29 -04:00
aj 2f19f47a85 feat: serialize/deserialize cut-off definitions in nest file format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:40:01 -04:00
aj d58a446eac feat: add Plate.CutOffs collection with materialization and transform support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:36:05 -04:00
aj 5fc7d1989a feat: add CutOff and CutOffSettings domain classes with segment generation
CutOff computes cut segments along a vertical or horizontal axis,
excluding zones around existing parts with configurable clearance.
CutOffSettings controls part clearance, overtravel, minimum segment
length, and cut direction (toward/away from origin).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:32:33 -04:00
aj 3f6bc2b2a1 feat: guard Plate quantity/utilization/overlap for IsCutOff parts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:14:28 -04:00
aj 7681a1bad0 feat: add Drawing.IsCutOff flag for cut-off support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:12:49 -04:00
58 changed files with 4444 additions and 271 deletions
+6 -4
View File
@@ -30,10 +30,11 @@ Domain model, geometry, and CNC primitives organized into namespaces:
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding). 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.
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
- **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)
Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine. `AutoNester` handles mixed-part NFP-based nesting with simulated annealing (not yet integrated into the registry).
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.
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`.
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
@@ -43,7 +44,7 @@ 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.
- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts.
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): NFP-based nesting (not yet integrated)`AutoNester` (mixed-part nesting with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`NestResult`.
- **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).
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
@@ -79,13 +80,13 @@ The UI application with MDI interface.
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`.
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`, `ActionCutOff`.
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
## File Format
Nest files (`.nest`, ZIP-based) use v2 JSON format:
- `nest.json` — single JSON file containing all nest metadata: nest info (name, units, customer, dates, notes), plate defaults (size, thickness, quadrant, spacing, material, edge spacing), drawings array (id, name, color, quantity, priority, rotation constraints, material, source), and plates array (id, size, material, edge spacing, parts with drawingId/x/y/rotation)
- `nest.json` — single JSON file containing all nest metadata: nest info (name, units, customer, dates, notes), plate defaults (size, thickness, quadrant, spacing, material, edge spacing), drawings array (id, name, color, quantity, priority, rotation constraints, material, source), and plates array (id, size, material, edge spacing, parts with drawingId/x/y/rotation, cutoffs with x/y/axis/startLimit/endLimit)
- `programs/program-N` — G-code text for each drawing's cut program (N = drawing id)
- `bestfits/bestfit-N` — JSON array of best-fit pair evaluation results per drawing, keyed by plate size/spacing (optional, only present if best-fit data was computed)
@@ -113,3 +114,4 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
- Nesting uses async progress/cancellation: `IProgress<NestProgress>` and `CancellationToken` flow through the engine to the UI's `NestProgressForm`.
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
- **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).
+18
View File
@@ -0,0 +1,18 @@
namespace OpenNest.CNC
{
public sealed class ProgramVariable
{
public int Number { get; }
public string Name { get; }
public string Expression { get; set; }
public ProgramVariable(int number, string name, string expression = null)
{
Number = number;
Name = name;
Expression = expression;
}
public string Reference => $"#{Number}";
}
}
@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenNest.CNC
{
public sealed class ProgramVariableManager
{
private readonly Dictionary<int, ProgramVariable> _variables = new();
public ProgramVariable GetOrCreate(string name, int number, string expression = null)
{
if (_variables.TryGetValue(number, out var existing))
return existing;
var variable = new ProgramVariable(number, name, expression);
_variables[number] = variable;
return variable;
}
public List<string> EmitDeclarations()
{
return _variables.Values
.Where(v => v.Expression != null)
.OrderBy(v => v.Number)
.Select(v => $"{v.Reference}={v.Expression} ({FormatComment(v.Name)})")
.ToList();
}
private static string FormatComment(string name)
{
// "LeadInFeedrate" -> "LEAD IN FEEDRATE"
var sb = new StringBuilder();
foreach (var c in name)
{
if (char.IsUpper(c) && sb.Length > 0)
sb.Append(' ');
sb.Append(char.ToUpper(c));
}
return sb.ToString();
}
}
}
+211
View File
@@ -0,0 +1,211 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
{
public enum CutOffAxis
{
Horizontal,
Vertical
}
public class CutOff
{
public Vector Position { get; set; }
public CutOffAxis Axis { get; set; }
public double? StartLimit { get; set; }
public double? EndLimit { get; set; }
public Drawing Drawing { get; private set; }
public CutOff(Vector position, CutOffAxis axis)
{
Position = position;
Axis = axis;
Drawing = new Drawing(GetName()) { IsCutOff = true };
}
public void Regenerate(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache = null)
{
var segments = ComputeSegments(plate, settings, cache);
var program = BuildProgram(segments, settings);
Drawing.Program = program;
}
private string GetName()
{
var axisChar = Axis == CutOffAxis.Vertical ? "V" : "H";
var coord = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
return $"CutOff-{axisChar}-{coord:F2}";
}
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache)
{
var bounds = plate.BoundingBox(includeParts: false);
double lineStart, lineEnd, cutPosition;
if (Axis == CutOffAxis.Vertical)
{
cutPosition = Position.X;
lineStart = StartLimit ?? bounds.Y;
lineEnd = EndLimit ?? (bounds.Y + bounds.Length + settings.Overtravel);
}
else
{
cutPosition = Position.Y;
lineStart = StartLimit ?? bounds.X;
lineEnd = EndLimit ?? (bounds.X + bounds.Width + settings.Overtravel);
}
var exclusions = new List<(double Start, double End)>();
foreach (var part in plate.Parts)
{
if (part.BaseDrawing.IsCutOff)
continue;
Entity perimeter = null;
cache?.TryGetValue(part, out perimeter);
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
exclusions.AddRange(partExclusions);
}
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
var merged = new List<(double Start, double End)>();
foreach (var ex in exclusions)
{
if (merged.Count > 0 && ex.Start <= merged[^1].End)
merged[^1] = (merged[^1].Start, System.Math.Max(merged[^1].End, ex.End));
else
merged.Add(ex);
}
var segments = new List<(double Start, double End)>();
var current = lineStart;
foreach (var ex in merged)
{
var clampedStart = System.Math.Max(ex.Start, lineStart);
var clampedEnd = System.Math.Min(ex.End, lineEnd);
if (clampedStart > current)
segments.Add((current, clampedStart));
current = System.Math.Max(current, clampedEnd);
}
if (current < lineEnd)
segments.Add((current, lineEnd));
segments = segments.Where(s => (s.End - s.Start) >= settings.MinSegmentLength).ToList();
return segments;
}
private static readonly List<(double Start, double End)> EmptyExclusions = new();
private List<(double Start, double End)> GetPartExclusions(
Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
{
var bb = part.BoundingBox;
var (partMin, partMax) = AxisBounds(bb, clearance);
var (partStart, partEnd) = CrossAxisBounds(bb, clearance);
if (cutPosition < partMin || cutPosition > partMax)
return EmptyExclusions;
if (perimeter != null)
{
var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance);
if (perimeterExclusions != null)
return perimeterExclusions;
}
return new List<(double Start, double End)> { (partStart, partEnd) };
}
private List<(double Start, double End)> IntersectPerimeter(
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
{
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
var usedOffset = target != perimeter;
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
return null;
var coords = pts
.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
.OrderBy(c => c)
.ToList();
if (coords.Count % 2 != 0)
return null;
var padding = usedOffset ? 0 : clearance;
var result = new List<(double Start, double End)>();
for (var i = 0; i < coords.Count; i += 2)
result.Add((coords[i] - padding, coords[i + 1] + padding));
return result;
}
private static Entity OffsetOutward(Entity perimeter, double clearance)
{
if (clearance <= 0)
return null;
try
{
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
offset?.UpdateBounds();
return offset;
}
catch
{
return null;
}
}
private Vector MakePoint(double cutCoord, double lineCoord) =>
Axis == CutOffAxis.Vertical
? new Vector(cutCoord, lineCoord)
: new Vector(lineCoord, cutCoord);
private (double Min, double Max) AxisBounds(Box bb, double clearance) =>
Axis == CutOffAxis.Vertical
? (bb.X - clearance, bb.X + bb.Width + clearance)
: (bb.Y - clearance, bb.Y + bb.Length + clearance);
private (double Start, double End) CrossAxisBounds(Box bb, double clearance) =>
Axis == CutOffAxis.Vertical
? (bb.Y - clearance, bb.Y + bb.Length + clearance)
: (bb.X - clearance, bb.X + bb.Width + clearance);
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
{
var program = new Program();
if (segments.Count == 0)
return program;
var toward = settings.CutDirection == CutDirection.TowardOrigin;
segments = toward
? segments.OrderByDescending(s => s.Start).ToList()
: segments.OrderBy(s => s.Start).ToList();
var cutPos = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
foreach (var seg in segments)
{
var (from, to) = toward ? (seg.End, seg.Start) : (seg.Start, seg.End);
program.Codes.Add(new RapidMove(MakePoint(cutPos, from)));
program.Codes.Add(new LinearMove(MakePoint(cutPos, to)));
}
return program;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace OpenNest
{
public enum CutDirection
{
TowardOrigin,
AwayFromOrigin
}
public class CutOffSettings
{
public double PartClearance { get; set; } = 0.02;
public double Overtravel { get; set; }
public double MinSegmentLength { get; set; } = 0.05;
public CutDirection CutDirection { get; set; } = CutDirection.AwayFromOrigin;
}
}
+2
View File
@@ -56,6 +56,8 @@ namespace OpenNest
public Color Color { get; set; }
public bool IsCutOff { get; set; }
public NestConstraints Constraints { get; set; }
public SourceInfo Source { get; set; }
+22 -4
View File
@@ -247,7 +247,7 @@ namespace OpenNest.Geometry
public static class EntityExtensions
{
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
public static List<Vector> CollectPoints(this IEnumerable<Entity> entities)
{
var points = new List<Vector>();
@@ -286,17 +286,35 @@ namespace OpenNest.Geometry
case EntityType.Shape:
var shape = (Shape)entity;
var subResult = shape.Entities.FindBestRotation(startAngle, endAngle);
return subResult;
points.AddRange(shape.Entities.CollectPoints());
break;
}
}
return points;
}
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
{
// Check for Shape entity first (recursive case returns early)
foreach (var entity in entities)
{
if (entity.Type == EntityType.Shape)
{
var shape = (Shape)entity;
var subResult = shape.Entities.FindBestRotation(startAngle, endAngle);
return subResult;
}
}
var points = entities.CollectPoints();
if (points.Count == 0)
return new BoundingRectangleResult(startAngle, 0, 0);
var hull = ConvexHull.Compute(points);
bool constrained = !startAngle.IsEqualTo(0) || !endAngle.IsEqualTo(Angle.TwoPI);
var constrained = !startAngle.IsEqualTo(0) || !endAngle.IsEqualTo(Angle.TwoPI);
return constrained
? RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle)
+2 -3
View File
@@ -249,9 +249,8 @@ namespace OpenNest.Geometry
foreach (var geo in shape.Entities)
{
List<Vector> pts3;
geo.Intersects(line, out pts3);
pts.AddRange(pts3);
if (geo.Intersects(line, out var pts3))
pts.AddRange(pts3);
}
return pts.Count > 0;
+58 -2
View File
@@ -317,12 +317,68 @@ namespace OpenNest.Geometry
public override Entity OffsetEntity(double distance, OffsetSide side)
{
throw new NotImplementedException();
if (Vertices.Count < 3)
return null;
var isClosed = IsClosed();
var count = isClosed ? Vertices.Count - 1 : Vertices.Count;
if (count < 3)
return null;
var ccw = CalculateArea() > 0;
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
var sign = side == outward ? 1.0 : -1.0;
var d = distance * sign;
var normals = new Vector[count];
for (var i = 0; i < count; i++)
{
var next = (i + 1) % count;
var dx = Vertices[next].X - Vertices[i].X;
var dy = Vertices[next].Y - Vertices[i].Y;
var len = System.Math.Sqrt(dx * dx + dy * dy);
if (len < Tolerance.Epsilon)
return null;
normals[i] = new Vector(-dy / len * d, dx / len * d);
}
var result = new Polygon();
for (var i = 0; i < count; i++)
{
var prev = (i - 1 + count) % count;
var a1 = new Vector(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 b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);
var edgeA = new Line(a1, a2);
var edgeB = new Line(b1, b2);
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
result.Vertices.Add(pt);
else
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
}
result.Close();
result.RemoveSelfIntersections();
result.UpdateBounds();
return result;
}
public override Entity OffsetEntity(double distance, Vector pt)
{
throw new NotImplementedException();
var left = OffsetEntity(distance, OffsetSide.Left);
var right = OffsetEntity(distance, OffsetSide.Right);
if (left == null) return right;
if (right == null) return left;
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
return distLeft > distRight ? left : right;
}
/// <summary>
+5 -2
View File
@@ -21,9 +21,12 @@ namespace OpenNest.Geometry
Perimeter = shapes[0];
Cutouts = new List<Shape>();
for (int i = 1; i < shapes.Count; i++)
for (var i = 1; i < shapes.Count; i++)
{
if (shapes[i].Left < Perimeter.Left)
var bb = shapes[i].BoundingBox;
var perimBB = Perimeter.BoundingBox;
if (bb.Width * bb.Length > perimBB.Width * perimBB.Length)
{
Cutouts.Add(Perimeter);
Perimeter = shapes[i];
+144 -14
View File
@@ -47,17 +47,20 @@ namespace OpenNest
Parts = new ObservableList<Part>();
Parts.ItemAdded += Parts_PartAdded;
Parts.ItemRemoved += Parts_PartRemoved;
CutOffs = new ObservableList<CutOff>();
Quadrant = 1;
}
private void Parts_PartAdded(object sender, ItemAddedEventArgs<Part> e)
{
e.Item.BaseDrawing.Quantity.Nested += Quantity;
if (!e.Item.BaseDrawing.IsCutOff)
e.Item.BaseDrawing.Quantity.Nested += Quantity;
}
private void Parts_PartRemoved(object sender, ItemRemovedEventArgs<Part> e)
{
e.Item.BaseDrawing.Quantity.Nested -= Quantity;
if (!e.Item.BaseDrawing.IsCutOff)
e.Item.BaseDrawing.Quantity.Nested -= Quantity;
}
/// <summary>
@@ -90,6 +93,92 @@ namespace OpenNest
/// </summary>
public ObservableList<Part> Parts { get; set; }
/// <summary>
/// The cut-off lines defined on this plate.
/// </summary>
public ObservableList<CutOff> CutOffs { get; set; }
/// <summary>
/// Regenerates all cut-off drawings and materializes them as parts.
/// Existing cut-off parts are removed first, then each cut-off is
/// regenerated and added back if it produces any geometry.
/// </summary>
public void RegenerateCutOffs(CutOffSettings settings)
{
// Remove existing cut-off parts
for (var i = Parts.Count - 1; i >= 0; i--)
{
if (Parts[i].BaseDrawing.IsCutOff)
Parts.RemoveAt(i);
}
var cache = BuildPerimeterCache(this);
// Regenerate and materialize each cut-off
foreach (var cutoff in CutOffs)
{
cutoff.Regenerate(this, settings, cache);
if (cutoff.Drawing.Program.Codes.Count == 0)
continue;
var part = new Part(cutoff.Drawing);
Parts.Add(part);
}
}
/// <summary>
/// Builds a dictionary mapping each non-cut-off part to its perimeter entity.
/// Closed shapes use ShapeProfile; open contours fall back to ConvexHull.
/// </summary>
public static Dictionary<Part, Geometry.Entity> BuildPerimeterCache(Plate plate)
{
var cache = new Dictionary<Part, Geometry.Entity>();
foreach (var part in plate.Parts)
{
if (part.BaseDrawing.IsCutOff)
continue;
Geometry.Entity perimeter = null;
try
{
var entities = Converters.ConvertProgram.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
if (entities.Count > 0)
{
var profile = new Geometry.ShapeProfile(entities);
if (profile.Perimeter.IsClosed())
{
perimeter = profile.Perimeter;
perimeter.Offset(part.Location);
}
else
{
var points = entities.CollectPoints();
if (points.Count >= 3)
{
var hull = Geometry.ConvexHull.Compute(points);
hull.Offset(part.Location);
perimeter = hull;
}
}
}
}
catch
{
perimeter = null;
}
cache[part] = perimeter;
}
return cache;
}
/// <summary>
/// The number of times to cut the plate.
/// </summary>
@@ -240,11 +329,20 @@ namespace OpenNest
/// <param name="angle"></param>
public void Rotate(double angle)
{
for (int i = 0; i < Parts.Count; ++i)
for (var i = Parts.Count - 1; i >= 0; i--)
{
if (Parts[i].BaseDrawing.IsCutOff)
Parts.RemoveAt(i);
}
for (var i = 0; i < Parts.Count; ++i)
{
var part = Parts[i];
part.Rotate(angle);
}
foreach (var cutoff in CutOffs)
cutoff.Position = cutoff.Position.Rotate(angle);
}
/// <summary>
@@ -254,11 +352,24 @@ namespace OpenNest
/// <param name="origin"></param>
public void Rotate(double angle, Vector origin)
{
for (int i = 0; i < Parts.Count; ++i)
for (var i = Parts.Count - 1; i >= 0; i--)
{
if (Parts[i].BaseDrawing.IsCutOff)
Parts.RemoveAt(i);
}
for (var i = 0; i < Parts.Count; ++i)
{
var part = Parts[i];
part.Rotate(angle, origin);
}
foreach (var cutoff in CutOffs)
{
var pos = cutoff.Position - origin;
pos = pos.Rotate(angle);
cutoff.Position = pos + origin;
}
}
/// <summary>
@@ -268,11 +379,22 @@ namespace OpenNest
/// <param name="y"></param>
public void Offset(double x, double y)
{
for (int i = 0; i < Parts.Count; ++i)
// Remove cut-off parts before transforming
for (var i = Parts.Count - 1; i >= 0; i--)
{
if (Parts[i].BaseDrawing.IsCutOff)
Parts.RemoveAt(i);
}
for (var i = 0; i < Parts.Count; ++i)
{
var part = Parts[i];
part.Offset(x, y);
}
// Transform cut-off positions
foreach (var cutoff in CutOffs)
cutoff.Position = new Vector(cutoff.Position.X + x, cutoff.Position.Y + y);
}
/// <summary>
@@ -281,11 +403,20 @@ namespace OpenNest
/// <param name="voffset"></param>
public void Offset(Vector voffset)
{
for (int i = 0; i < Parts.Count; ++i)
for (var i = Parts.Count - 1; i >= 0; i--)
{
if (Parts[i].BaseDrawing.IsCutOff)
Parts.RemoveAt(i);
}
for (var i = 0; i < Parts.Count; ++i)
{
var part = Parts[i];
part.Offset(voffset);
}
foreach (var cutoff in CutOffs)
cutoff.Position = new Vector(cutoff.Position.X + voffset.X, cutoff.Position.Y + voffset.Y);
}
/// <summary>
@@ -454,24 +585,23 @@ namespace OpenNest
/// <returns>Returns a number between 0.0 and 1.0</returns>
public double Utilization()
{
return Parts.Sum(part => part.BaseDrawing.Area) / Area();
return Parts.Where(p => !p.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) / Area();
}
public bool HasOverlappingParts(out List<Vector> pts)
{
pts = new List<Vector>();
var realParts = Parts.Where(p => !p.BaseDrawing.IsCutOff).ToList();
for (int i = 0; i < Parts.Count; i++)
for (var i = 0; i < realParts.Count; i++)
{
var part1 = Parts[i];
var part1 = realParts[i];
for (int j = i + 1; j < Parts.Count; j++)
for (var j = i + 1; j < realParts.Count; j++)
{
var part2 = Parts[j];
var part2 = realParts[j];
List<Vector> pts2;
if (part1.Intersects(part2, out pts2))
if (part1.Intersects(part2, out var pts2))
pts.AddRange(pts2);
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ namespace OpenNest
var best = context.CurrentBest ?? new List<Part>();
if (item.Quantity > 0 && best.Count > item.Quantity)
best = ShrinkFiller.TrimToCount(best, item.Quantity, ShrinkAxis.Width);
best = ShrinkFiller.TrimToCount(best, item.Quantity, TrimAxis);
ReportProgress(progress, new ProgressReport
{
@@ -18,7 +18,7 @@ namespace OpenNest.Engine.Fill
/// <summary>
/// Composes <see cref="RemnantFiller"/> and <see cref="ShrinkFiller"/> with
/// dual-direction shrink selection. Wraps the caller's fill function in a
/// closure that tries both <see cref="ShrinkAxis.Height"/> and
/// closure that tries both <see cref="ShrinkAxis.Length"/> and
/// <see cref="ShrinkAxis.Width"/>, picks the better <see cref="FillScore"/>,
/// and passes the wrapper to <see cref="RemnantFiller.FillItems"/>.
/// </summary>
@@ -85,7 +85,7 @@ namespace OpenNest.Engine.Fill
ShrinkResult widthResult = null;
Parallel.Invoke(
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token,
() => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token,
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar),
() => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token,
targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar)
+45 -2
View File
@@ -102,11 +102,21 @@ namespace OpenNest.Engine.Fill
if (placed == null)
continue;
// Remove the topmost bounding box part to create a clean
// rectangular obstacle boundary. Without this, gaps between
// individual bounding boxes cause the next drawing to fill
// into inter-row spaces, producing an interleaved layout.
if (placed.Count > 1)
RemoveTopmostPart(placed);
allParts.AddRange(placed);
localQty[item.Drawing.Name] = System.Math.Max(0, qty - placed.Count);
foreach (var p in placed)
finder.AddObstacle(p.BoundingBox.Offset(spacing));
// Add the envelope of all placed parts as a single obstacle
// rather than individual bounding boxes, preventing the
// remnant finder from seeing inter-part gaps.
var envelope = ComputeEnvelope(placed, spacing);
finder.AddObstacle(envelope);
return true;
}
@@ -114,6 +124,39 @@ namespace OpenNest.Engine.Fill
return false;
}
private static void RemoveTopmostPart(List<Part> parts)
{
var topIdx = 0;
for (var i = 1; i < parts.Count; i++)
{
if (parts[i].BoundingBox.Top > parts[topIdx].BoundingBox.Top)
topIdx = i;
}
parts.RemoveAt(topIdx);
}
private static Box ComputeEnvelope(List<Part> parts, double spacing)
{
var left = double.MaxValue;
var bottom = double.MaxValue;
var right = double.MinValue;
var top = double.MinValue;
foreach (var p in parts)
{
var bb = p.BoundingBox;
if (bb.Left < left) left = bb.Left;
if (bb.Bottom < bottom) bottom = bb.Bottom;
if (bb.Right > right) right = bb.Right;
if (bb.Top > top) top = bb.Top;
}
return new Box(left - spacing, bottom - spacing,
right - left + spacing * 2, top - bottom + spacing * 2);
}
private static List<Part> TryFillInRemnants(
NestItem item,
int qty,
+3 -3
View File
@@ -7,7 +7,7 @@ using System.Threading;
namespace OpenNest.Engine.Fill
{
public enum ShrinkAxis { Width, Height }
public enum ShrinkAxis { Width, Length }
public class ShrinkResult
{
@@ -101,7 +101,7 @@ namespace OpenNest.Engine.Fill
if (bbox.Width <= 0 || bbox.Length <= 0)
return box;
var maxDim = axis == ShrinkAxis.Height ? box.Length : box.Width;
var maxDim = axis == ShrinkAxis.Length ? box.Length : box.Width;
// Use FillBestFit for a fast, accurate rectangle count on the full box.
var bin = new Bin { Size = new Size(box.Width, box.Length) };
@@ -121,7 +121,7 @@ namespace OpenNest.Engine.Fill
if (estimate <= 0 || estimate >= maxDim)
return box;
return axis == ShrinkAxis.Height
return axis == ShrinkAxis.Length
? new Box(box.X, box.Y, box.Width, estimate)
: new Box(box.X, box.Y, estimate, box.Length);
}
@@ -24,6 +24,8 @@ namespace OpenNest
public override NestDirection? PreferredDirection => NestDirection.Vertical;
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
public override List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
{
var baseAngles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
+2
View File
@@ -43,6 +43,8 @@ namespace OpenNest
public virtual NestDirection? PreferredDirection => null;
public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width;
public virtual List<double> BuildAngles(NestItem item, double bestRotation, Box workArea)
{
return new List<double> { bestRotation, bestRotation + OpenNest.Math.Angle.HalfPI };
-4
View File
@@ -21,10 +21,6 @@ namespace OpenNest
"Strip-based nesting for mixed-drawing layouts",
plate => new StripNestEngine(plate));
Register("NFP",
"NFP-based mixed-part nesting with simulated annealing",
plate => new NfpNestEngine(plate));
Register("Vertical Remnant",
"Optimizes for largest right-side vertical drop",
plate => new VerticalRemnantEngine(plate));
-65
View File
@@ -1,65 +0,0 @@
using OpenNest.Engine.Fill;
using OpenNest.Engine.Nfp;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Threading;
namespace OpenNest
{
public class NfpNestEngine : NestEngineBase
{
public NfpNestEngine(Plate plate) : base(plate)
{
}
public override string Name => "NFP";
public override string Description => "NFP-based mixed-part nesting with simulated annealing";
public override List<Part> Fill(NestItem item, Box workArea,
IProgress<NestProgress> progress, CancellationToken token)
{
var inner = new DefaultNestEngine(Plate);
return inner.Fill(item, workArea, progress, token);
}
public override List<Part> Fill(List<Part> groupParts, Box workArea,
IProgress<NestProgress> progress, CancellationToken token)
{
var inner = new DefaultNestEngine(Plate);
return inner.Fill(groupParts, workArea, progress, token);
}
public override List<Part> PackArea(Box box, List<NestItem> items,
IProgress<NestProgress> progress, CancellationToken token)
{
var inner = new DefaultNestEngine(Plate);
return inner.PackArea(box, items, progress, token);
}
public override List<Part> Nest(List<NestItem> items,
IProgress<NestProgress> progress, CancellationToken token)
{
if (items == null || items.Count == 0)
return new List<Part>();
var parts = AutoNester.Nest(items, Plate, progress, token);
// Compact placed parts toward the origin to close gaps.
Compactor.Settle(parts, Plate.WorkArea(), Plate.PartSpacing);
// Deduct placed quantities from original items.
foreach (var item in items)
{
if (item.Quantity <= 0)
continue;
var placed = parts.FindAll(p => p.BaseDrawing.Name == item.Drawing.Name).Count;
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
return parts;
}
}
}
@@ -1,25 +1,42 @@
using OpenNest.Engine.Fill;
using System.Collections.Generic;
using System.Threading;
namespace OpenNest.Engine.Strategies
{
public class PairsFillStrategy : IFillStrategy
{
private static readonly AsyncLocal<bool> active = new();
public string Name => "Pairs";
public NestPhase Phase => NestPhase.Pairs;
public int Order => 100;
public List<Part> Fill(FillContext context)
{
var comparer = context.Policy?.Comparer;
var dedup = GridDedup.GetOrCreate(context.SharedState);
var filler = new PairFiller(context.Plate, comparer, dedup);
var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress);
// Prevent recursive PairFiller — remnant fills within PairFiller
// create a new engine that runs the full pipeline, which would
// invoke PairsFillStrategy again, causing deep recursion.
if (active.Value)
return null;
context.SharedState["BestFits"] = result.BestFits;
active.Value = true;
try
{
var comparer = context.Policy?.Comparer;
var dedup = GridDedup.GetOrCreate(context.SharedState);
var filler = new PairFiller(context.Plate, comparer, dedup);
var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress);
return result.Parts;
context.SharedState["BestFits"] = result.BestFits;
return result.Parts;
}
finally
{
active.Value = false;
}
}
}
}
+10
View File
@@ -62,6 +62,7 @@ namespace OpenNest.IO
public MaterialDto Material { get; init; } = new();
public SpacingDto EdgeSpacing { get; init; } = new();
public List<PartDto> Parts { get; init; } = new();
public List<CutOffDto> CutOffs { get; init; } = new();
}
public record PartDto
@@ -72,6 +73,15 @@ namespace OpenNest.IO
public double Rotation { get; init; }
}
public record CutOffDto
{
public double X { get; init; }
public double Y { get; init; }
public string Axis { get; init; } = "vertical";
public double? StartLimit { get; init; }
public double? EndLimit { get; init; }
}
public record SizeDto
{
public double Width { get; init; }
+19
View File
@@ -197,6 +197,25 @@ namespace OpenNest.IO
plate.Parts.Add(part);
}
// Cut-offs
if (p.CutOffs != null)
{
foreach (var cutoffDto in p.CutOffs)
{
var axis = cutoffDto.Axis?.ToLowerInvariant() == "horizontal"
? CutOffAxis.Horizontal
: CutOffAxis.Vertical;
var cutoff = new CutOff(new Vector(cutoffDto.X, cutoffDto.Y), axis)
{
StartLimit = cutoffDto.StartLimit,
EndLimit = cutoffDto.EndLimit
};
plate.CutOffs.Add(cutoff);
}
plate.RegenerateCutOffs(new CutOffSettings());
}
nest.Plates.Add(plate);
}
+16 -2
View File
@@ -152,7 +152,7 @@ namespace OpenNest.IO
{
var plate = nest.Plates[i];
var parts = new List<PartDto>();
foreach (var part in plate.Parts)
foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff))
{
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
parts.Add(new PartDto
@@ -164,6 +164,19 @@ namespace OpenNest.IO
});
}
var cutoffs = new List<CutOffDto>();
foreach (var cutoff in plate.CutOffs)
{
cutoffs.Add(new CutOffDto
{
X = cutoff.Position.X,
Y = cutoff.Position.Y,
Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal",
StartLimit = cutoff.StartLimit,
EndLimit = cutoff.EndLimit
});
}
list.Add(new PlateDto
{
Id = i + 1,
@@ -185,7 +198,8 @@ namespace OpenNest.IO
Right = plate.EdgeSpacing.Right,
Bottom = plate.EdgeSpacing.Bottom
},
Parts = parts
Parts = parts,
CutOffs = cutoffs
});
}
return list;
@@ -0,0 +1,230 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Posts.Cincinnati;
/// <summary>
/// Data class carrying all context needed to emit one Cincinnati-format G-code feature block.
/// </summary>
public sealed class FeatureContext
{
public List<ICode> Codes { get; set; } = new();
public int FeatureNumber { get; set; }
public string PartName { get; set; } = "";
public bool IsFirstFeatureOfPart { get; set; }
public bool IsLastFeatureOnSheet { get; set; }
public bool IsSafetyHeadraise { get; set; }
public bool IsExteriorFeature { get; set; }
public string LibraryFile { get; set; } = "";
public double CutDistance { get; set; }
public double SheetDiagonal { get; set; }
}
/// <summary>
/// Emits one Cincinnati-format G-code feature block (one contour) to a TextWriter.
/// Handles rapid positioning, pierce, kerf compensation, anti-dive, feedrate modal
/// suppression, arc I/J conversion (absolute to incremental), and M47 head raise.
/// </summary>
public sealed class CincinnatiFeatureWriter
{
private readonly CincinnatiPostConfig _config;
private readonly CoordinateFormatter _fmt;
private readonly SpeedClassifier _speedClassifier;
public CincinnatiFeatureWriter(CincinnatiPostConfig config)
{
_config = config;
_fmt = new CoordinateFormatter(config.PostedAccuracy);
_speedClassifier = new SpeedClassifier();
}
/// <summary>
/// Writes a complete feature block for the given context.
/// </summary>
public void Write(TextWriter writer, FeatureContext ctx)
{
var currentPos = Vector.Zero;
var lastFeedVar = "";
var kerfEmitted = false;
// Find the pierce point from the first rapid move
var piercePoint = FindPiercePoint(ctx.Codes);
// 1. Rapid to pierce point (with line number if configured)
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint);
// 2. Part name comment on first feature of each part
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
// 3. G89 process params (if RepeatG89BeforeEachFeature)
if (_config.RepeatG89BeforeEachFeature && _config.ProcessParameterMode == G89Mode.LibraryFile)
{
var lib = !string.IsNullOrEmpty(ctx.LibraryFile) ? ctx.LibraryFile : _config.DefaultLibraryFile;
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
writer.WriteLine($"G89 P {lib} ({speedClass} {cutDist})");
}
// 4. Pierce and start cut
writer.WriteLine("G84");
// 5. Anti-dive off
if (_config.UseAntiDive)
writer.WriteLine("M130 (ANTI DIVE OFF)");
// Update current position to pierce point
currentPos = piercePoint;
// 6. Lead-in + contour moves with kerf comp and feedrate variables
foreach (var code in ctx.Codes)
{
if (code is RapidMove)
continue; // skip rapids in contour (already handled above)
if (code is LinearMove linear)
{
var sb = new StringBuilder();
// Kerf compensation on first cutting move
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
{
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
kerfEmitted = true;
}
sb.Append($"G1X{_fmt.FormatCoord(linear.EndPoint.X)}Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
// Feedrate
var feedVar = GetFeedVariable(linear.Layer);
if (feedVar != lastFeedVar)
{
sb.Append($"F{feedVar}");
lastFeedVar = feedVar;
}
writer.WriteLine(sb.ToString());
currentPos = linear.EndPoint;
}
else if (code is ArcMove arc)
{
var sb = new StringBuilder();
// Kerf compensation on first cutting move
if (!kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide)
{
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41" : "G42");
kerfEmitted = true;
}
// G2 = CW, G3 = CCW
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
sb.Append($"{gCode}X{_fmt.FormatCoord(arc.EndPoint.X)}Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
// Convert absolute center to incremental I/J
var i = arc.CenterPoint.X - currentPos.X;
var j = arc.CenterPoint.Y - currentPos.Y;
sb.Append($"I{_fmt.FormatCoord(i)}J{_fmt.FormatCoord(j)}");
// Feedrate — full circles use multiplied feedrate
var isFullCircle = IsFullCircle(currentPos, arc.EndPoint);
var feedVar = isFullCircle ? "[#148*#128]" : GetFeedVariable(arc.Layer);
if (feedVar != lastFeedVar)
{
sb.Append($"F{feedVar}");
lastFeedVar = feedVar;
}
writer.WriteLine(sb.ToString());
currentPos = arc.EndPoint;
}
}
// 7. Cancel kerf compensation
if (kerfEmitted)
writer.WriteLine("G40");
// 8. Beam off
writer.WriteLine(_config.UseSpeedGas ? "M135" : "M35");
// 9. Anti-dive on
if (_config.UseAntiDive)
writer.WriteLine("M131 (ANTI DIVE ON)");
// 10. Head raise (unless last feature on sheet)
if (!ctx.IsLastFeatureOnSheet)
WriteM47(writer, ctx);
}
private Vector FindPiercePoint(List<ICode> codes)
{
foreach (var code in codes)
{
if (code is RapidMove rapid)
return rapid.EndPoint;
}
// If no rapid move, use the endpoint of the first motion
foreach (var code in codes)
{
if (code is Motion motion)
return motion.EndPoint;
}
return Vector.Zero;
}
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint)
{
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}");
sb.Append($"G0X{_fmt.FormatCoord(piercePoint.X)}Y{_fmt.FormatCoord(piercePoint.Y)}");
writer.WriteLine(sb.ToString());
}
private void WriteM47(TextWriter writer, FeatureContext ctx)
{
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
{
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
return;
}
var mode = ctx.IsExteriorFeature ? _config.ExteriorM47 : _config.InteriorM47;
switch (mode)
{
case M47Mode.Always:
writer.WriteLine("M47");
break;
case M47Mode.BlockDelete:
writer.WriteLine("/M47");
break;
case M47Mode.None:
break;
}
}
private static string GetFeedVariable(LayerType layer)
{
return layer switch
{
LayerType.Leadin => "#126",
LayerType.Cut => "#148",
_ => "#148"
};
}
private static bool IsFullCircle(Vector start, Vector end)
{
return Tolerance.IsEqualTo(start.X, end.X) && Tolerance.IsEqualTo(start.Y, end.Y);
}
}
@@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.IO;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Posts.Cincinnati;
/// <summary>
/// Writes a Cincinnati-format part sub-program definition.
/// Each sub-program contains the complete cutting sequence for one unique part geometry
/// (drawing + rotation), with coordinates normalized to origin (0,0).
/// Called via M98 from sheet sub-programs.
/// </summary>
public sealed class CincinnatiPartSubprogramWriter
{
private readonly CincinnatiPostConfig _config;
private readonly CincinnatiFeatureWriter _featureWriter;
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config)
{
_config = config;
_featureWriter = new CincinnatiFeatureWriter(config);
}
/// <summary>
/// Writes a complete part sub-program for the given normalized program.
/// The program coordinates must already be normalized to origin (0,0).
/// </summary>
public void Write(TextWriter w, Program normalizedProgram, string drawingName,
int subNumber, string libraryFile, double sheetDiagonal)
{
var features = SplitFeatures(normalizedProgram.Codes);
if (features.Count == 0)
return;
w.WriteLine("(*****************************************************)");
w.WriteLine($":{subNumber}");
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}"));
for (var i = 0; i < features.Count; i++)
{
var codes = features[i];
var featureNumber = i == 0
? _config.FeatureLineNumberStart
: 1000 + i + 1;
var cutDistance = ComputeCutDistance(codes);
var ctx = new FeatureContext
{
Codes = codes,
FeatureNumber = featureNumber,
PartName = drawingName,
IsFirstFeatureOfPart = false,
IsLastFeatureOnSheet = i == features.Count - 1,
IsSafetyHeadraise = false,
IsExteriorFeature = false,
LibraryFile = libraryFile,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
_featureWriter.Write(w, ctx);
}
w.WriteLine("G0X0Y0");
w.WriteLine($"M99(END OF {drawingName})");
}
/// <summary>
/// Creates a sub-program key for matching parts to their sub-programs.
/// </summary>
internal static (int drawingId, long rotationKey) SubprogramKey(Part part) =>
(part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6));
internal static List<List<ICode>> SplitFeatures(List<ICode> codes)
{
var features = new List<List<ICode>>();
List<ICode> current = null;
foreach (var code in codes)
{
if (code is RapidMove)
{
if (current != null)
features.Add(current);
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
features.Add(current);
return features;
}
internal static double ComputeCutDistance(List<ICode> codes)
{
var distance = 0.0;
var currentPos = Vector.Zero;
foreach (var code in codes)
{
if (code is RapidMove rapid)
currentPos = rapid.EndPoint;
else if (code is LinearMove linear)
{
distance += currentPos.DistanceTo(linear.EndPoint);
currentPos = linear.EndPoint;
}
else if (code is ArcMove arc)
{
distance += currentPos.DistanceTo(arc.EndPoint);
currentPos = arc.EndPoint;
}
}
return distance;
}
}
@@ -0,0 +1,275 @@
namespace OpenNest.Posts.Cincinnati
{
/// <summary>
/// Specifies how coordinate positioning is handled between parts.
/// </summary>
public enum CoordinateMode
{
/// <summary>Set absolute position.</summary>
G92,
/// <summary>Use relative/incremental positioning.</summary>
G91,
/// <summary>Use machine coordinate system.</summary>
G53
}
/// <summary>
/// Specifies how G89 (hole drilling/tapping parameters) are provided.
/// </summary>
public enum G89Mode
{
/// <summary>Use external library file for G89 parameters.</summary>
LibraryFile,
/// <summary>Explicitly define G89 parameters in the program.</summary>
Explicit
}
/// <summary>
/// Specifies where kerf compensation is applied.
/// </summary>
public enum KerfMode
{
/// <summary>Controller side (using cutter compensation codes).</summary>
ControllerSide,
/// <summary>Pre-applied to part geometry during post-processing.</summary>
PreApplied
}
/// <summary>
/// Specifies which side of the cut line kerf compensation is applied to.
/// </summary>
public enum KerfSide
{
/// <summary>Kerf applied to the left side of the cut.</summary>
Left,
/// <summary>Kerf applied to the right side of the cut.</summary>
Right
}
/// <summary>
/// Specifies how M47 (optional stop) commands are used.
/// </summary>
public enum M47Mode
{
/// <summary>Always include M47.</summary>
Always,
/// <summary>Include M47 with block delete functionality.</summary>
BlockDelete,
/// <summary>Automatically determine M47 placement.</summary>
Auto,
/// <summary>Do not use M47.</summary>
None
}
/// <summary>
/// Specifies when pallet exchange occurs.
/// </summary>
public enum PalletMode
{
/// <summary>No pallet exchange.</summary>
None,
/// <summary>Pallet exchange at end of sheet.</summary>
EndOfSheet,
/// <summary>Pallet exchange at start and end of sheet.</summary>
StartAndEnd
}
/// <summary>
/// Configuration for Cincinnati post processor.
/// Defines machine-specific parameters, output format, and cutting strategies.
/// </summary>
public sealed class CincinnatiPostConfig
{
/// <summary>
/// Gets or sets the configuration name/identifier.
/// Default: "CL940"
/// </summary>
public string ConfigurationName { get; set; } = "CL940";
/// <summary>
/// Gets or sets the units for posted output.
/// Default: Units.Inches
/// </summary>
public Units PostedUnits { get; set; } = Units.Inches;
/// <summary>
/// Gets or sets the decimal accuracy for numeric output.
/// Default: 4
/// </summary>
public int PostedAccuracy { get; set; } = 4;
/// <summary>
/// Gets or sets how coordinate positioning is handled between parts.
/// Default: CoordinateMode.G92
/// </summary>
public CoordinateMode CoordModeBetweenParts { get; set; } = CoordinateMode.G92;
/// <summary>
/// Gets or sets whether to use subprograms for sheet operations.
/// Default: true
/// </summary>
public bool UseSheetSubprograms { get; set; } = true;
/// <summary>
/// Gets or sets the starting subprogram number for sheet operations.
/// Default: 101
/// </summary>
public int SheetSubprogramStart { get; set; } = 101;
/// <summary>
/// Gets or sets whether to use M98 sub-programs for part geometry.
/// When enabled, each unique part geometry is written as a reusable sub-program
/// called via M98, reducing output size for nests with repeated parts.
/// Default: false
/// </summary>
public bool UsePartSubprograms { get; set; } = false;
/// <summary>
/// Gets or sets the starting sub-program number for part geometry sub-programs.
/// Default: 200
/// </summary>
public int PartSubprogramStart { get; set; } = 200;
/// <summary>
/// Gets or sets the subprogram number for variable declarations.
/// Default: 100
/// </summary>
public int VariableDeclarationSubprogram { get; set; } = 100;
/// <summary>
/// Gets or sets how G89 parameters are provided.
/// Default: G89Mode.LibraryFile
/// </summary>
public G89Mode ProcessParameterMode { get; set; } = G89Mode.LibraryFile;
/// <summary>
/// Gets or sets the default G89 library file path.
/// Default: empty string
/// </summary>
public string DefaultLibraryFile { get; set; } = "";
/// <summary>
/// Gets or sets whether to repeat G89 before each feature.
/// Default: true
/// </summary>
public bool RepeatG89BeforeEachFeature { get; set; } = true;
/// <summary>
/// Gets or sets whether to use exact stop mode (G61).
/// Default: false
/// </summary>
public bool UseExactStopMode { get; set; } = false;
/// <summary>
/// Gets or sets where kerf compensation is applied.
/// Default: KerfMode.ControllerSide
/// </summary>
public KerfMode KerfCompensation { get; set; } = KerfMode.ControllerSide;
/// <summary>
/// Gets or sets the default side for kerf compensation.
/// Default: KerfSide.Left
/// </summary>
public KerfSide DefaultKerfSide { get; set; } = KerfSide.Left;
/// <summary>
/// Gets or sets how M47 is used in interior cuts.
/// Default: M47Mode.Always
/// </summary>
public M47Mode InteriorM47 { get; set; } = M47Mode.Always;
/// <summary>
/// Gets or sets how M47 is used in exterior cuts.
/// Default: M47Mode.Always
/// </summary>
public M47Mode ExteriorM47 { get; set; } = M47Mode.Always;
/// <summary>
/// Gets or sets the safety head raise distance (in machine units).
/// Default: 2000
/// </summary>
public int? SafetyHeadraiseDistance { get; set; } = 2000;
/// <summary>
/// Gets or sets the distance threshold for M47 override.
/// Default: null
/// </summary>
public double? M47OverrideDistanceThreshold { get; set; } = null;
/// <summary>
/// Gets or sets whether to use anti-dive functionality.
/// Default: true
/// </summary>
public bool UseAntiDive { get; set; } = true;
/// <summary>
/// Gets or sets whether to use smart rapids optimization.
/// Default: false
/// </summary>
public bool UseSmartRapids { get; set; } = false;
/// <summary>
/// Gets or sets when pallet exchange occurs.
/// Default: PalletMode.EndOfSheet
/// </summary>
public PalletMode PalletExchange { get; set; } = PalletMode.EndOfSheet;
/// <summary>
/// Gets or sets whether to use line numbers in output.
/// Default: true
/// </summary>
public bool UseLineNumbers { get; set; } = true;
/// <summary>
/// Gets or sets the starting line number for features.
/// Default: 1
/// </summary>
public int FeatureLineNumberStart { get; set; } = 1;
/// <summary>
/// Gets or sets whether to use speed/gas commands.
/// Default: false
/// </summary>
public bool UseSpeedGas { get; set; } = false;
/// <summary>
/// Gets or sets the feedrate percentage for lead-in moves.
/// Default: 0.5 (50%)
/// </summary>
public double LeadInFeedratePercent { get; set; } = 0.5;
/// <summary>
/// Gets or sets the feedrate percentage for lead-in arc-to-line moves.
/// Default: 0.5 (50%)
/// </summary>
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5;
/// <summary>
/// Gets or sets the feedrate multiplier for circular cuts.
/// Default: 0.8 (80%)
/// </summary>
public double CircleFeedrateMultiplier { get; set; } = 0.8;
/// <summary>
/// Gets or sets the variable number for sheet width.
/// Default: 110
/// </summary>
public int SheetWidthVariable { get; set; } = 110;
/// <summary>
/// Gets or sets the variable number for sheet length.
/// Default: 111
/// </summary>
public int SheetLengthVariable { get; set; } = 111;
}
}
@@ -0,0 +1,167 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenNest.CNC;
namespace OpenNest.Posts.Cincinnati
{
public sealed class CincinnatiPostProcessor : IPostProcessor
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
public string Name => "Cincinnati CL-707";
public string Author => "OpenNest";
public string Description => "Cincinnati CL-707/CL-800/CL-900/CL-940/CLX family";
public CincinnatiPostConfig Config { get; }
public CincinnatiPostProcessor()
{
var configPath = GetConfigPath();
if (File.Exists(configPath))
{
var json = File.ReadAllText(configPath);
Config = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, JsonOptions);
}
else
{
Config = new CincinnatiPostConfig();
SaveConfig();
}
}
public CincinnatiPostProcessor(CincinnatiPostConfig config)
{
Config = config;
}
public void SaveConfig()
{
var configPath = GetConfigPath();
var json = JsonSerializer.Serialize(Config, JsonOptions);
File.WriteAllText(configPath, json);
}
private static string GetConfigPath()
{
var assemblyPath = typeof(CincinnatiPostProcessor).Assembly.Location;
var dir = Path.GetDirectoryName(assemblyPath);
var name = Path.GetFileNameWithoutExtension(assemblyPath);
return Path.Combine(dir, name + ".json");
}
public void Post(Nest nest, Stream outputStream)
{
// 1. Create variable manager and register standard variables
var vars = CreateVariableManager();
// 2. Filter to non-empty plates
var plates = nest.Plates
.Where(p => p.Parts.Count > 0)
.ToList();
// 3. Build part sub-program registry (if enabled)
Dictionary<(int, long), int> partSubprograms = null;
List<(int subNum, string name, Program program)> subprogramEntries = null;
if (Config.UsePartSubprograms)
{
partSubprograms = new Dictionary<(int, long), int>();
subprogramEntries = new List<(int, string, Program)>();
var nextSubNum = Config.PartSubprogramStart;
foreach (var plate in plates)
{
foreach (var part in plate.Parts)
{
if (part.BaseDrawing.IsCutOff) continue;
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
if (!partSubprograms.ContainsKey(key))
{
var subNum = nextSubNum++;
partSubprograms[key] = subNum;
// Create normalized program at origin
var pgm = part.Program.Clone() as Program;
var bbox = pgm.BoundingBox();
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
subprogramEntries.Add((subNum, part.BaseDrawing.Name, pgm));
}
}
}
}
// 4. Create writers
var preamble = new CincinnatiPreambleWriter(Config);
var sheetWriter = new CincinnatiSheetWriter(Config, vars);
// 5. Build material description from first plate
var material = plates.FirstOrDefault()?.Material;
var materialDesc = material != null
? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}"
: "";
// 6. Write to stream
using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true);
// Main program
preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates.Count);
// Variable declaration subprogram
preamble.WriteVariableDeclaration(writer, vars);
// Sheet subprograms
for (var i = 0; i < plates.Count; i++)
{
var sheetIndex = i + 1;
var subNumber = Config.SheetSubprogramStart + i;
sheetWriter.Write(writer, plates[i], nest.Name ?? "NEST", sheetIndex, subNumber,
partSubprograms);
}
// Part sub-programs (if enabled)
if (subprogramEntries != null)
{
var partSubWriter = new CincinnatiPartSubprogramWriter(Config);
var firstPlate = plates.FirstOrDefault();
var sheetDiagonal = firstPlate != null
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
+ firstPlate.Size.Length * firstPlate.Size.Length)
: 100.0;
foreach (var (subNum, name, pgm) in subprogramEntries)
{
partSubWriter.Write(writer, pgm, name, subNum,
Config.DefaultLibraryFile ?? "", sheetDiagonal);
}
}
writer.Flush();
}
public void Post(Nest nest, string outputFile)
{
using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write);
Post(nest, fs);
}
private ProgramVariableManager CreateVariableManager()
{
var vars = new ProgramVariableManager();
vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression
vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]");
vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]");
vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#"));
return vars;
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.IO;
using OpenNest;
using OpenNest.CNC;
namespace OpenNest.Posts.Cincinnati;
/// <summary>
/// Emits the main program header and variable declaration subprogram
/// for a Cincinnati laser post-processor output file.
/// </summary>
public sealed class CincinnatiPreambleWriter
{
private readonly CincinnatiPostConfig _config;
public CincinnatiPreambleWriter(CincinnatiPostConfig config)
{
_config = config;
}
/// <summary>
/// Writes the main program header block.
/// </summary>
public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, int sheetCount)
{
w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}"));
w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}"));
w.WriteLine(CoordinateFormatter.Comment(DateTime.Now.ToString("MM-dd-yyyy hh:mm:ss tt", System.Globalization.CultureInfo.InvariantCulture)));
if (!string.IsNullOrEmpty(materialDescription))
w.WriteLine(CoordinateFormatter.Comment($"Material = {materialDescription}"));
if (_config.UseExactStopMode)
w.WriteLine("G61");
w.WriteLine(CoordinateFormatter.Comment("MAIN PROGRAM"));
w.WriteLine(_config.PostedUnits == Units.Millimeters ? "G21" : "G20");
w.WriteLine("M42");
if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(_config.DefaultLibraryFile))
w.WriteLine($"G89 P {_config.DefaultLibraryFile}");
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
w.WriteLine("GOTO1 (GOTO SHEET NUMBER)");
for (var i = 1; i <= sheetCount; i++)
{
var subNum = _config.SheetSubprogramStart + (i - 1);
w.WriteLine($"N{i}M98 P{subNum} (SHEET {i})");
}
w.WriteLine("M42");
w.WriteLine("M30 (END OF MAIN)");
}
/// <summary>
/// Writes the variable declaration subprogram block.
/// </summary>
public void WriteVariableDeclaration(TextWriter w, ProgramVariableManager vars)
{
w.WriteLine("(*****************************************************)");
w.WriteLine($":{_config.VariableDeclarationSubprogram}");
w.WriteLine("(Variable Declaration Start)");
foreach (var line in vars.EmitDeclarations())
w.WriteLine(line);
w.WriteLine("M99 (Variable Declaration End)");
}
}
@@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Posts.Cincinnati;
/// <summary>
/// Emits one Cincinnati-format sheet subprogram per plate.
/// Supports two modes: inline features (default) or M98 sub-program calls per part.
/// </summary>
public sealed class CincinnatiSheetWriter
{
private readonly CincinnatiPostConfig _config;
private readonly ProgramVariableManager _vars;
private readonly CoordinateFormatter _fmt;
private readonly CincinnatiFeatureWriter _featureWriter;
public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars)
{
_config = config;
_vars = vars;
_fmt = new CoordinateFormatter(config.PostedAccuracy);
_featureWriter = new CincinnatiFeatureWriter(config);
}
/// <summary>
/// Writes a complete sheet subprogram for the given plate.
/// </summary>
/// <param name="partSubprograms">
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
/// </param>
public void Write(TextWriter w, Plate plate, string nestName, int sheetIndex, int subNumber,
Dictionary<(int, long), int> partSubprograms = null)
{
if (plate.Parts.Count == 0)
return;
var width = plate.Size.Width;
var length = plate.Size.Length;
var sheetDiagonal = System.Math.Sqrt(width * width + length * length);
var libraryFile = _config.DefaultLibraryFile ?? "";
var varDeclSub = _config.VariableDeclarationSubprogram;
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
// 1. Sheet header
w.WriteLine("(*****************************************************)");
w.WriteLine($"( START OF {nestName}.{sheetIndex:D3} )");
w.WriteLine($":{subNumber}");
w.WriteLine($"( Sheet {sheetIndex} )");
w.WriteLine($"( Layout {sheetIndex} )");
w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(length)} X {_fmt.FormatCoord(width)} )");
w.WriteLine($"( Total parts on sheet = {partCount} )");
w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)}(SHEET WIDTH FOR CUTOFFS)");
w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)}(SHEET LENGTH FOR CUTOFFS)");
// 2. Coordinate setup
w.WriteLine("M42");
w.WriteLine("N10000");
w.WriteLine("G92X#5021Y#5022");
if (!string.IsNullOrEmpty(libraryFile))
w.WriteLine($"G89 P {libraryFile}");
w.WriteLine($"M98 P{varDeclSub} (Variable Declaration)");
w.WriteLine("G90");
w.WriteLine("M47(CPT)");
if (!string.IsNullOrEmpty(libraryFile))
w.WriteLine($"G89 P {libraryFile}");
w.WriteLine("GOTO1( Goto Feature )");
// 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last
var nonCutoffParts = plate.Parts
.Where(p => !p.BaseDrawing.IsCutOff)
.OrderBy(p => p.Bottom)
.ThenBy(p => p.Left)
.ToList();
var cutoffParts = plate.Parts
.Where(p => p.BaseDrawing.IsCutOff)
.ToList();
var allParts = nonCutoffParts.Concat(cutoffParts).ToList();
// 4. Emit parts
if (partSubprograms != null)
WritePartsWithSubprograms(w, allParts, libraryFile, sheetDiagonal, partSubprograms);
else
WritePartsInline(w, allParts, libraryFile, sheetDiagonal);
// 5. Footer
w.WriteLine("M42");
w.WriteLine("G0X0Y0");
if (_config.PalletExchange != PalletMode.None)
w.WriteLine($"N{sheetIndex + 1}M50");
w.WriteLine($"M99(END OF {nestName}.{sheetIndex:D3})");
}
private void WritePartsWithSubprograms(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal,
Dictionary<(int, long), int> partSubprograms)
{
var lastPartName = "";
var featureIndex = 0;
for (var p = 0; p < allParts.Count; p++)
{
var part = allParts[p];
var partName = part.BaseDrawing.Name;
var isNewPart = partName != lastPartName;
var isSafetyHeadraise = isNewPart && lastPartName != "";
var isLastPart = p == allParts.Count - 1;
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
partSubprograms.TryGetValue(key, out var subNum);
var hasSubprogram = !part.BaseDrawing.IsCutOff && subNum != 0;
if (hasSubprogram)
{
WriteSubprogramCall(w, part, subNum, featureIndex, partName,
isSafetyHeadraise, isLastPart);
featureIndex++;
}
else
{
// Inline features for cutoffs or parts without sub-programs
var features = SplitPartFeatures(part);
for (var f = 0; f < features.Count; f++)
{
var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1;
var isLastFeature = isLastPart && f == features.Count - 1;
var cutDistance = ComputeCutDistance(features[f]);
var ctx = new FeatureContext
{
Codes = features[f],
FeatureNumber = featureNumber,
PartName = partName,
IsFirstFeatureOfPart = isNewPart && f == 0,
IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise && f == 0,
IsExteriorFeature = false,
LibraryFile = libraryFile,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
_featureWriter.Write(w, ctx);
featureIndex++;
}
}
lastPartName = partName;
}
}
private void WriteSubprogramCall(TextWriter w, Part part, int subNum,
int featureIndex, string partName, bool isSafetyHeadraise, bool isLastPart)
{
// Safety headraise before rapid to new part
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value}(Safety Headraise)");
// Rapid to part position (bounding box lower-left)
var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1;
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber}");
sb.Append($"G0X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
w.WriteLine(sb.ToString());
// Part name comment
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
// Set local coordinate system at part position
w.WriteLine("G92X0Y0");
// Call part sub-program
w.WriteLine($"M98P{subNum}({partName})");
// Restore sheet coordinate system
w.WriteLine($"G92X{_fmt.FormatCoord(part.Left)}Y{_fmt.FormatCoord(part.Bottom)}");
// Head raise (unless last part on sheet)
if (!isLastPart)
w.WriteLine("M47");
}
private void WritePartsInline(TextWriter w, List<Part> allParts,
string libraryFile, double sheetDiagonal)
{
// Multi-contour splitting
var features = new List<(Part part, List<ICode> codes)>();
foreach (var part in allParts)
{
List<ICode> current = null;
foreach (var code in part.Program.Codes)
{
if (code is RapidMove)
{
if (current != null)
features.Add((part, current));
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
features.Add((part, current));
}
// Emit features
var lastPartName = "";
for (var i = 0; i < features.Count; i++)
{
var (part, codes) = features[i];
var partName = part.BaseDrawing.Name;
var isFirstFeatureOfPart = partName != lastPartName;
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
var isLastFeature = i == features.Count - 1;
var featureNumber = i == 0
? _config.FeatureLineNumberStart
: 1000 + i + 1;
var cutDistance = ComputeCutDistance(codes);
var ctx = new FeatureContext
{
Codes = codes,
FeatureNumber = featureNumber,
PartName = partName,
IsFirstFeatureOfPart = isFirstFeatureOfPart,
IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = isSafetyHeadraise,
IsExteriorFeature = false,
LibraryFile = libraryFile,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
};
_featureWriter.Write(w, ctx);
lastPartName = partName;
}
}
private static List<List<ICode>> SplitPartFeatures(Part part)
{
var features = new List<List<ICode>>();
List<ICode> current = null;
foreach (var code in part.Program.Codes)
{
if (code is RapidMove)
{
if (current != null)
features.Add(current);
current = new List<ICode> { code };
}
else
{
current ??= new List<ICode>();
current.Add(code);
}
}
if (current != null && current.Count > 0)
features.Add(current);
return features;
}
private static double ComputeCutDistance(List<ICode> codes)
{
var distance = 0.0;
var currentPos = Vector.Zero;
foreach (var code in codes)
{
if (code is RapidMove rapid)
{
currentPos = rapid.EndPoint;
}
else if (code is LinearMove linear)
{
distance += currentPos.DistanceTo(linear.EndPoint);
currentPos = linear.EndPoint;
}
else if (code is ArcMove arc)
{
distance += currentPos.DistanceTo(arc.EndPoint);
currentPos = arc.EndPoint;
}
}
return distance;
}
}
@@ -0,0 +1,24 @@
namespace OpenNest.Posts.Cincinnati
{
public sealed class CoordinateFormatter
{
private readonly int _accuracy;
private readonly string _format;
public CoordinateFormatter(int accuracy)
{
_accuracy = accuracy;
_format = "0." + new string('#', accuracy);
}
public string FormatCoord(double value)
{
return System.Math.Round(value, _accuracy)
.ToString(_format, System.Globalization.CultureInfo.InvariantCulture);
}
public static string Comment(string text) => $"( {text} )";
public static string InlineComment(string text) => $"({text})";
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>OpenNest.Posts.Cincinnati</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
</ItemGroup>
<Target Name="CopyToPostsDir" AfterTargets="Build">
<PropertyGroup>
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
</PropertyGroup>
<MakeDir Directories="$(PostsDir)" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" />
</Target>
</Project>
@@ -0,0 +1,31 @@
namespace OpenNest.Posts.Cincinnati
{
public sealed class SpeedClassifier
{
public double FastThreshold { get; set; } = 0.5;
public double SlowThreshold { get; set; } = 0.1;
public string Classify(double contourLength, double sheetDiagonal)
{
var ratio = contourLength / sheetDiagonal;
if (ratio >= FastThreshold) return "FAST";
if (ratio <= SlowThreshold) return "SLOW";
return "MEDIUM";
}
public string FormatCutDist(double contourLength, double sheetDiagonal)
{
return $"CutDist={FormatValue(contourLength)}/{FormatValue(sheetDiagonal)}";
}
private static string FormatValue(double value)
{
// Cincinnati convention: no leading zero for values < 1 (e.g., ".8702" not "0.8702")
var rounded = System.Math.Round(value, 4);
var str = rounded.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
if (rounded > 0 && rounded < 1 && str.StartsWith("0."))
return str.Substring(1);
return str;
}
}
}
@@ -0,0 +1,437 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class CincinnatiFeatureWriterTests
{
private static CincinnatiPostConfig DefaultConfig() => new()
{
UseLineNumbers = true,
FeatureLineNumberStart = 1,
UseAntiDive = true,
KerfCompensation = KerfMode.ControllerSide,
DefaultKerfSide = KerfSide.Left,
RepeatG89BeforeEachFeature = true,
ProcessParameterMode = G89Mode.LibraryFile,
DefaultLibraryFile = "MILD10",
InteriorM47 = M47Mode.Always,
ExteriorM47 = M47Mode.Always,
UseSpeedGas = false,
PostedAccuracy = 4,
SafetyHeadraiseDistance = 2000
};
private static FeatureContext SimpleContext(List<ICode>? codes = null) => new()
{
Codes = codes ?? new List<ICode>
{
new RapidMove(13.401, 57.4895),
new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin },
new LinearMove(20.0, 57.5) { Layer = LayerType.Cut }
},
FeatureNumber = 1,
PartName = "BRACKET",
IsFirstFeatureOfPart = true,
IsLastFeatureOnSheet = false,
IsSafetyHeadraise = false,
IsExteriorFeature = false,
LibraryFile = "MILD10",
CutDistance = 18.0,
SheetDiagonal = 30.0
};
private static string WriteFeature(CincinnatiPostConfig config, FeatureContext ctx)
{
var writer = new CincinnatiFeatureWriter(config);
using var sw = new StringWriter();
writer.Write(sw, ctx);
return sw.ToString();
}
[Fact]
public void RapidToPiercePoint_WithLineNumber()
{
var config = DefaultConfig();
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("N1G0X13.401Y57.4895", lines[0]);
}
[Fact]
public void RapidToPiercePoint_WithoutLineNumber()
{
var config = DefaultConfig();
config.UseLineNumbers = false;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("G0X13.401Y57.4895", lines[0]);
}
[Fact]
public void G84_PierceEmitted()
{
var config = DefaultConfig();
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.Contains("G84", output);
}
[Fact]
public void AntiDive_M130M131_EmittedWhenEnabled()
{
var config = DefaultConfig();
config.UseAntiDive = true;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.Contains("M130 (ANTI DIVE OFF)", output);
Assert.Contains("M131 (ANTI DIVE ON)", output);
}
[Fact]
public void AntiDive_NotEmittedWhenDisabled()
{
var config = DefaultConfig();
config.UseAntiDive = false;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("M130", output);
Assert.DoesNotContain("M131", output);
}
[Fact]
public void KerfCompensation_G41G40_EmittedWhenControllerSide()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.ControllerSide;
config.DefaultKerfSide = KerfSide.Left;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.Contains("G41", output);
Assert.Contains("G40", output);
}
[Fact]
public void KerfCompensation_G42_EmittedForRightSide()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.ControllerSide;
config.DefaultKerfSide = KerfSide.Right;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.Contains("G42", output);
}
[Fact]
public void KerfCompensation_NotEmittedWhenPreApplied()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.PreApplied;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("G41", output);
Assert.DoesNotContain("G42", output);
Assert.DoesNotContain("G40", output);
}
[Fact]
public void M35_BeamOffEmitted()
{
var config = DefaultConfig();
config.UseSpeedGas = false;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.Contains("M35", output);
}
[Fact]
public void M135_BeamOffEmittedWhenSpeedGas()
{
var config = DefaultConfig();
config.UseSpeedGas = true;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.Contains("M135", output);
}
[Fact]
public void M47_EmittedWhenNotLastFeature()
{
var config = DefaultConfig();
var ctx = SimpleContext();
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
Assert.Contains("M47", output);
}
[Fact]
public void M47_OmittedWhenLastFeatureOnSheet()
{
var config = DefaultConfig();
var ctx = SimpleContext();
ctx.IsLastFeatureOnSheet = true;
var output = WriteFeature(config, ctx);
// M47 should not appear, but M35 should still be there
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
}
[Fact]
public void M47_BlockDeleteMode_EmitsSlashM47()
{
var config = DefaultConfig();
config.InteriorM47 = M47Mode.BlockDelete;
var ctx = SimpleContext();
ctx.IsExteriorFeature = false;
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
Assert.Contains("/M47", output);
}
[Fact]
public void M47_NoneMode_NoM47Emitted()
{
var config = DefaultConfig();
config.InteriorM47 = M47Mode.None;
var ctx = SimpleContext();
ctx.IsExteriorFeature = false;
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.DoesNotContain(lines, l => l.Trim() == "M47" || l.Trim() == "/M47");
}
[Fact]
public void ArcIJ_ConvertedFromAbsoluteToIncremental()
{
var config = DefaultConfig();
// Arc starts at rapid endpoint (10, 20), center at (15, 20) absolute
// So incremental I = 15 - 10 = 5, J = 20 - 20 = 0
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(
endPoint: new Vector(10.0, 20.0),
centerPoint: new Vector(15.0, 20.0),
rotation: RotationType.CW
) { Layer = LayerType.Cut }
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
// Should contain incremental I=5, J=0
Assert.Contains("I5", output);
Assert.Contains("J0", output);
}
[Fact]
public void ArcMove_G2ForCW_G3ForCCW()
{
var config = DefaultConfig();
var cwCodes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
};
var ccwCodes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) { Layer = LayerType.Cut }
};
var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
var ccwOutput = WriteFeature(config, SimpleContext(ccwCodes));
Assert.Contains("G2X", cwOutput);
Assert.Contains("G3X", ccwOutput);
}
[Fact]
public void PartNameComment_EmittedOnFirstFeature()
{
var config = DefaultConfig();
var ctx = SimpleContext();
ctx.IsFirstFeatureOfPart = true;
ctx.PartName = "FLANGE";
var output = WriteFeature(config, ctx);
Assert.Contains("( PART: FLANGE )", output);
}
[Fact]
public void PartNameComment_NotEmittedOnSubsequentFeatures()
{
var config = DefaultConfig();
var ctx = SimpleContext();
ctx.IsFirstFeatureOfPart = false;
ctx.PartName = "FLANGE";
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("PART:", output);
}
[Fact]
public void G89_EmittedWhenRepeatEnabled()
{
var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = true;
config.ProcessParameterMode = G89Mode.LibraryFile;
config.DefaultLibraryFile = "MILD10";
var ctx = SimpleContext();
ctx.LibraryFile = "MILD10";
ctx.CutDistance = 18.0;
ctx.SheetDiagonal = 30.0;
var output = WriteFeature(config, ctx);
Assert.Contains("G89 P MILD10", output);
}
[Fact]
public void G89_NotEmittedWhenRepeatDisabled()
{
var config = DefaultConfig();
config.RepeatG89BeforeEachFeature = false;
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
Assert.DoesNotContain("G89", output);
}
[Fact]
public void FeedrateModalSuppression_OnlyEmitsOnChange()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.PreApplied; // simplify output
var codes = new List<ICode>
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Cut },
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut },
new LinearMove(4.0, 1.0) { Layer = LayerType.Cut }
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
// F#148 should appear only once (on the first cut move)
var count = CountOccurrences(output, "F#148");
Assert.Equal(1, count);
}
[Fact]
public void LeadinFeedrate_UsesVariable126()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.PreApplied; // simplify output
var codes = new List<ICode>
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
Assert.Contains("F#126", output);
}
[Fact]
public void FullCircleArc_UsesMultipliedFeedrate()
{
var config = DefaultConfig();
config.KerfCompensation = KerfMode.PreApplied;
// Full circle: start == end
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
Assert.Contains("F[#148*#128]", output);
}
[Fact]
public void SafetyHeadraise_EmitsM47WithDistance()
{
var config = DefaultConfig();
config.SafetyHeadraiseDistance = 2000;
var ctx = SimpleContext();
ctx.IsSafetyHeadraise = true;
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
Assert.Contains("M47 P2000(Safety Headraise)", output);
}
[Fact]
public void ExteriorM47Mode_UsesExteriorConfig()
{
var config = DefaultConfig();
config.ExteriorM47 = M47Mode.BlockDelete;
config.InteriorM47 = M47Mode.Always;
var ctx = SimpleContext();
ctx.IsExteriorFeature = true;
ctx.IsLastFeatureOnSheet = false;
var output = WriteFeature(config, ctx);
Assert.Contains("/M47", output);
}
[Fact]
public void OutputSequence_CorrectOrder()
{
var config = DefaultConfig();
var ctx = SimpleContext();
var output = WriteFeature(config, ctx);
var lines = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
// Find indices of key lines
var rapidIdx = Array.FindIndex(lines, l => l.Contains("G0X"));
var partIdx = Array.FindIndex(lines, l => l.Contains("PART:"));
var g89Idx = Array.FindIndex(lines, l => l.Contains("G89"));
var g84Idx = Array.FindIndex(lines, l => l.Contains("G84"));
var m130Idx = Array.FindIndex(lines, l => l.Contains("M130"));
var g40Idx = Array.FindIndex(lines, l => l.Contains("G40"));
var m35Idx = Array.FindIndex(lines, l => l.Contains("M35"));
var m131Idx = Array.FindIndex(lines, l => l.Contains("M131"));
var m47Idx = Array.FindIndex(lines, l => l.Trim() == "M47");
Assert.True(rapidIdx < partIdx, "Rapid should come before part comment");
Assert.True(partIdx < g89Idx, "Part comment should come before G89");
Assert.True(g89Idx < g84Idx, "G89 should come before G84");
Assert.True(g84Idx < m130Idx, "G84 should come before M130");
Assert.True(g40Idx < m35Idx, "G40 should come before M35");
Assert.True(m35Idx < m131Idx, "M35 should come before M131");
Assert.True(m131Idx < m47Idx, "M131 should come before M47");
}
private static int CountOccurrences(string text, string pattern)
{
var count = 0;
var idx = 0;
while ((idx = text.IndexOf(pattern, idx, StringComparison.Ordinal)) != -1)
{
count++;
idx += pattern.Length;
}
return count;
}
}
@@ -0,0 +1,322 @@
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class CincinnatiPostProcessorTests
{
[Fact]
public void Post_ProducesOutput_ForSinglePlateNest()
{
var nest = CreateTestNest();
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Main program elements
Assert.Contains("( NEST TestNest )", output);
Assert.Contains("( CONFIGURATION - CL940 )", output);
Assert.Contains("G20", output);
Assert.Contains("M30 (END OF MAIN)", output);
// Variable declaration
Assert.Contains(":100", output);
Assert.Contains("#126=", output);
// Sheet subprogram
Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output);
Assert.Contains("G84", output);
Assert.Contains("M99", output);
}
[Fact]
public void Post_ImplementsIPostProcessor()
{
var post = new CincinnatiPostProcessor(new CincinnatiPostConfig());
IPostProcessor pp = post;
Assert.Equal("Cincinnati CL-707", pp.Name);
Assert.Equal("OpenNest", pp.Author);
}
[Fact]
public void Post_SkipsEmptyPlates()
{
var nest = new Nest("TestNest");
nest.Plates.Add(new Plate(48, 96)); // empty plate
var plate2 = new Plate(48, 96);
plate2.Parts.Add(new Part(new Drawing("Part1", CreateSquareProgram())));
nest.Plates.Add(plate2);
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Should only have one sheet subprogram call in main
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.DoesNotContain("SHEET 2", output);
}
[Fact]
public void Post_ToFile_CreatesFile()
{
var nest = CreateTestNest();
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var post = new CincinnatiPostProcessor(config);
var tempFile = Path.GetTempFileName() + ".CNC";
try
{
post.Post(nest, tempFile);
Assert.True(File.Exists(tempFile));
var content = File.ReadAllText(tempFile);
Assert.Contains("M30", content);
}
finally
{
if (File.Exists(tempFile))
File.Delete(tempFile);
}
}
[Fact]
public void Config_RoundTripsAsJson()
{
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940_CORONA",
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedUnits = Units.Inches,
KerfCompensation = KerfMode.ControllerSide,
UseAntiDive = true
};
var opts = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
var json = JsonSerializer.Serialize(config, opts);
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
Assert.Equal("CL940_CORONA", deserialized.ConfigurationName);
Assert.Equal("MS135N2PANEL.lib", deserialized.DefaultLibraryFile);
Assert.Equal(Units.Inches, deserialized.PostedUnits);
Assert.Equal(KerfMode.ControllerSide, deserialized.KerfCompensation);
Assert.True(deserialized.UseAntiDive);
// Enums serialize as strings
Assert.Contains("\"Inches\"", json);
Assert.Contains("\"ControllerSide\"", json);
}
[Fact]
public void ParameterlessConstructor_LoadsOrCreatesConfig()
{
// The parameterless constructor reads from a .json file next to the assembly,
// or creates defaults if none exists. Either way, Config should be non-null.
var post = new CincinnatiPostProcessor();
Assert.NotNull(post.Config);
Assert.Equal("CL940", post.Config.ConfigurationName);
}
[Fact]
public void Post_WithPartSubprograms_WritesM98Calls()
{
var nest = CreateTestNest();
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
};
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Sheet should contain M98 call to part sub-program
Assert.Contains("M98P200", output);
// Should have G92 for local coordinate positioning
Assert.Contains("G92X0Y0", output);
// Part sub-program definition
Assert.Contains(":200", output);
Assert.Contains("G84", output);
// Sub-program ends with G0X0Y0 and M99
Assert.Contains("G0X0Y0", output);
Assert.Contains("M99(END OF Square)", output);
// G92 restore after M98 call
Assert.Contains("G92X", output);
}
[Fact]
public void Post_WithPartSubprograms_ReusesSameSubprogram()
{
var nest = new Nest("TestNest");
var drawing = new Drawing("Square", CreateSquareProgram());
var plate = new Plate(48, 96);
plate.Parts.Add(new Part(drawing, new Vector(5, 5)));
plate.Parts.Add(new Part(drawing, new Vector(20, 5)));
nest.Plates.Add(plate);
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
};
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Both parts should call the same sub-program
var m98Count = System.Text.RegularExpressions.Regex.Matches(output, "M98P200").Count;
Assert.Equal(2, m98Count);
// Only one sub-program definition
var subDefCount = System.Text.RegularExpressions.Regex.Matches(output, ":200").Count;
Assert.Equal(1, subDefCount);
}
[Fact]
public void Post_WithPartSubprograms_DifferentRotationsGetSeparateSubprograms()
{
var nest = new Nest("TestNest");
var drawing = new Drawing("Square", CreateSquareProgram());
var plate = new Plate(48, 96);
var part1 = new Part(drawing, new Vector(5, 5));
plate.Parts.Add(part1);
var part2 = new Part(drawing, new Vector(20, 5));
part2.Rotate(System.Math.PI / 2); // 90 degrees
plate.Parts.Add(part2);
nest.Plates.Add(plate);
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
};
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Should have two different sub-programs
Assert.Contains(":200", output);
Assert.Contains(":201", output);
Assert.Contains("M98P200", output);
Assert.Contains("M98P201", output);
}
[Fact]
public void Post_WithPartSubprograms_CutoffsAreInline()
{
var nest = new Nest("TestNest");
var drawing = new Drawing("Square", CreateSquareProgram());
var cutoffDrawing = new Drawing("CutOff", CreateSquareProgram()) { IsCutOff = true };
var plate = new Plate(48, 96);
plate.Parts.Add(new Part(drawing, new Vector(5, 5)));
plate.Parts.Add(new Part(cutoffDrawing, new Vector(0, 30)));
nest.Plates.Add(plate);
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
};
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
post.Post(nest, ms);
var output = Encoding.UTF8.GetString(ms.ToArray());
// Regular part uses sub-program
Assert.Contains("M98P200", output);
Assert.Contains(":200", output);
// Cutoff should NOT have its own sub-program
Assert.DoesNotContain(":201", output);
}
[Fact]
public void Post_WithPartSubprograms_ConfigRoundTrips()
{
var config = new CincinnatiPostConfig
{
UsePartSubprograms = true,
PartSubprogramStart = 300
};
var opts = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
var json = JsonSerializer.Serialize(config, opts);
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
Assert.True(deserialized.UsePartSubprograms);
Assert.Equal(300, deserialized.PartSubprogramStart);
}
private static Nest CreateTestNest()
{
var nest = new Nest("TestNest");
var drawing = new Drawing("Square", CreateSquareProgram());
nest.Drawings.Add(drawing);
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(drawing, new Vector(10, 10)));
nest.Plates.Add(plate);
return nest;
}
private static Program CreateSquareProgram()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(2, 0));
pgm.Codes.Add(new LinearMove(2, 2));
pgm.Codes.Add(new LinearMove(0, 2));
pgm.Codes.Add(new LinearMove(0, 0));
return pgm;
}
}
@@ -0,0 +1,98 @@
using System.IO;
using System.Text;
using OpenNest.CNC;
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class CincinnatiPreambleWriterTests
{
[Fact]
public void WriteMainProgram_EmitsHeader()
{
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
PostedUnits = Units.Inches,
DefaultLibraryFile = "MS135N2PANEL.lib"
};
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "TestNest", "Mild Steel, 10GA", 2);
var output = sb.ToString();
Assert.Contains("( NEST TestNest )", output);
Assert.Contains("( CONFIGURATION - CL940 )", output);
Assert.Contains("G20", output);
Assert.Contains("M42", output);
Assert.Contains("G89 P MS135N2PANEL.lib", output);
Assert.Contains("M98 P100 (Variable Declaration)", output);
Assert.Contains("GOTO1 (GOTO SHEET NUMBER)", output);
Assert.Contains("N1M98 P101 (SHEET 1)", output);
Assert.Contains("N2M98 P102 (SHEET 2)", output);
Assert.Contains("M30 (END OF MAIN)", output);
}
[Fact]
public void WriteMainProgram_EmitsG21ForMetric()
{
var config = new CincinnatiPostConfig { PostedUnits = Units.Millimeters };
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
Assert.Contains("G21", sb.ToString());
}
[Fact]
public void WriteMainProgram_EmitsG61_WhenExactStop()
{
var config = new CincinnatiPostConfig { UseExactStopMode = true };
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
Assert.Contains("G61", sb.ToString());
}
[Fact]
public void WriteMainProgram_OmitsG61_WhenNotExactStop()
{
var config = new CincinnatiPostConfig { UseExactStopMode = false };
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteMainProgram(sw, "Test", "", 1);
Assert.DoesNotContain("G61", sb.ToString());
}
[Fact]
public void WriteVariableDeclaration_EmitsSubprogram()
{
var config = new CincinnatiPostConfig();
var vars = new ProgramVariableManager();
vars.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
vars.GetOrCreate("CircleFeedrate", 128, ".8");
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var writer = new CincinnatiPreambleWriter(config);
writer.WriteVariableDeclaration(sw, vars);
var output = sb.ToString();
Assert.Contains(":100", output);
Assert.Contains("(Variable Declaration Start)", output);
Assert.Contains("#126=", output);
Assert.Contains("#128=", output);
Assert.Contains("M99 (Variable Declaration End)", output);
}
}
@@ -0,0 +1,117 @@
using System.IO;
using System.Linq;
using System.Text;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class CincinnatiSheetWriterTests
{
[Fact]
public void WriteSheet_EmitsSheetHeader()
{
var config = new CincinnatiPostConfig
{
DefaultLibraryFile = "MS135N2PANEL.lib",
PostedAccuracy = 4
};
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
var output = sb.ToString();
Assert.Contains(":101", output);
Assert.Contains("( Sheet 1 )", output);
Assert.Contains("#110=", output);
Assert.Contains("#111=", output);
Assert.Contains("G92X#5021Y#5022", output);
Assert.Contains("M99", output);
}
[Fact]
public void WriteSheet_EmitsReturnToOriginAndPalletExchange()
{
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.EndOfSheet,
PostedAccuracy = 4
};
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
var output = sb.ToString();
Assert.Contains("M42", output);
Assert.Contains("G0X0Y0", output);
Assert.Contains("M50", output);
}
[Fact]
public void WriteSheet_SkipsEmptyPlate()
{
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var plate = new Plate(48.0, 96.0);
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
Assert.Equal("", sb.ToString());
}
[Fact]
public void WriteSheet_SplitsMultiContourParts()
{
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var pgm = new Program();
// First contour (hole)
pgm.Codes.Add(new RapidMove(1, 1));
pgm.Codes.Add(new LinearMove(2, 1));
pgm.Codes.Add(new LinearMove(2, 2));
pgm.Codes.Add(new LinearMove(1, 1));
// Second contour (exterior)
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(5, 0));
pgm.Codes.Add(new LinearMove(5, 5));
pgm.Codes.Add(new LinearMove(0, 0));
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("MultiContour", pgm)));
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101);
var output = sb.ToString();
// Should have two G84 pierce commands (one per contour)
var g84Count = output.Split('\n').Count(l => l.Trim() == "G84");
Assert.Equal(2, g84Count);
}
private static Program CreateSimpleProgram()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(1, 0));
pgm.Codes.Add(new LinearMove(1, 1));
pgm.Codes.Add(new LinearMove(0, 1));
pgm.Codes.Add(new LinearMove(0, 0));
return pgm;
}
}
@@ -0,0 +1,40 @@
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class CoordinateFormatterTests
{
[Theory]
[InlineData(13.401, 4, "13.401")]
[InlineData(13.0, 4, "13")]
[InlineData(0.0, 4, "0")]
[InlineData(57.4895, 4, "57.4895")]
[InlineData(13.401, 3, "13.401")]
[InlineData(13.4016, 3, "13.402")]
public void FormatCoord_FormatsCorrectly(double value, int accuracy, string expected)
{
var formatter = new CoordinateFormatter(accuracy);
Assert.Equal(expected, formatter.FormatCoord(value));
}
[Theory]
[InlineData(-5.25, 4, "-5.25")]
[InlineData(-0.001, 4, "-0.001")]
public void FormatCoord_HandlesNegatives(double value, int accuracy, string expected)
{
var formatter = new CoordinateFormatter(accuracy);
Assert.Equal(expected, formatter.FormatCoord(value));
}
[Fact]
public void Comment_FormatsWithSpaces()
{
Assert.Equal("( hello )", CoordinateFormatter.Comment("hello"));
}
[Fact]
public void InlineComment_FormatsWithoutSpaces()
{
Assert.Equal("(hello)", CoordinateFormatter.InlineComment("hello"));
}
}
@@ -0,0 +1,71 @@
using OpenNest.CNC;
namespace OpenNest.Tests.Cincinnati;
public class ProgramVariableManagerTests
{
[Fact]
public void GetOrCreate_ReturnsNewVariable()
{
var mgr = new ProgramVariableManager();
var v = mgr.GetOrCreate("LeadInFeedrate", 126);
Assert.Equal(126, v.Number);
Assert.Equal("LeadInFeedrate", v.Name);
}
[Fact]
public void GetOrCreate_ReturnsSameVariable_WhenCalledTwice()
{
var mgr = new ProgramVariableManager();
var v1 = mgr.GetOrCreate("LeadInFeedrate", 126);
var v2 = mgr.GetOrCreate("LeadInFeedrate", 126);
Assert.Same(v1, v2);
}
[Fact]
public void GetOrCreate_WithExpression_SetsExpression()
{
var mgr = new ProgramVariableManager();
var v = mgr.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
Assert.Equal("[#148*0.5]", v.Expression);
}
[Fact]
public void GetOrCreate_WithLiteral_SetsExpression()
{
var mgr = new ProgramVariableManager();
var v = mgr.GetOrCreate("CircleFeedrate", 128, ".8");
Assert.Equal(".8", v.Expression);
}
[Fact]
public void Reference_ReturnsHashNumber()
{
var v = new ProgramVariable(126, "LeadInFeedrate");
Assert.Equal("#126", v.Reference);
}
[Fact]
public void EmitDeclarations_ProducesCorrectLines()
{
var mgr = new ProgramVariableManager();
mgr.GetOrCreate("LeadInFeedrate", 126, "[#148*0.5]");
mgr.GetOrCreate("CircleFeedrate", 128, ".8");
var lines = mgr.EmitDeclarations();
Assert.Contains("#126=[#148*0.5] (LEAD IN FEEDRATE)", lines);
Assert.Contains("#128=.8 (CIRCLE FEEDRATE)", lines);
}
[Fact]
public void EmitDeclarations_SkipsVariablesWithNoExpression()
{
var mgr = new ProgramVariableManager();
mgr.GetOrCreate("ProcessFeedrate", 148);
var lines = mgr.EmitDeclarations();
Assert.Empty(lines);
}
}
@@ -0,0 +1,27 @@
using OpenNest.Posts.Cincinnati;
namespace OpenNest.Tests.Cincinnati;
public class SpeedClassifierTests
{
[Theory]
[InlineData(20.0, 10.0, "FAST")]
[InlineData(5.0, 10.0, "FAST")]
[InlineData(4.9, 10.0, "MEDIUM")]
[InlineData(0.5, 10.0, "SLOW")]
public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected)
{
var classifier = new SpeedClassifier();
Assert.Equal(expected, classifier.Classify(contourLength, sheetDiagonal));
}
[Theory]
[InlineData(0.8702, 3.927, "CutDist=.8702/3.927")]
[InlineData(18.9722, 3.927, "CutDist=18.9722/3.927")]
[InlineData(0.0, 10.0, "CutDist=0/10")]
public void FormatCutDist_IncludesLengthAndDiagonal(double contour, double diag, string expected)
{
var classifier = new SpeedClassifier();
Assert.Equal(expected, classifier.FormatCutDist(contour, diag));
}
}
+370
View File
@@ -0,0 +1,370 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class CutOffGeometryTests
{
private static readonly CutOffSettings ZeroClearance = new() { PartClearance = 0.0 };
private static double TotalCutLength(Program program, CutOffAxis axis = CutOffAxis.Vertical)
{
var total = 0.0;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
total += axis == CutOffAxis.Vertical
? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y)
: System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X);
}
}
return total;
}
private static Program MakeSquare(double size)
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
return pgm;
}
private static Program MakeCircle(double radius)
{
// Rapid to (radius, 0) relative to center at (0, 0),
// then full-circle arc back to same point.
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(radius, 0)));
pgm.Codes.Add(new ArcMove(new Vector(radius, 0), new Vector(0, 0)));
return pgm;
}
private static Program MakeDiamond(double halfSize)
{
// Diamond: points at (half,0), (2*half,half), (half,2*half), (0,half)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(halfSize, 0)));
pgm.Codes.Add(new LinearMove(new Vector(halfSize * 2, halfSize)));
pgm.Codes.Add(new LinearMove(new Vector(halfSize, halfSize * 2)));
pgm.Codes.Add(new LinearMove(new Vector(0, halfSize)));
pgm.Codes.Add(new LinearMove(new Vector(halfSize, 0)));
return pgm;
}
private static Program MakeTriangle(double width, double height)
{
// Right triangle: (0,0) -> (width,0) -> (0,height) -> (0,0)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(width, 0)));
pgm.Codes.Add(new LinearMove(new Vector(0, height)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
return pgm;
}
[Fact]
public void Square_GeometryExclusionMatchesBoundingBox()
{
// For a square, geometry and BB should produce the same exclusion.
var drawing = new Drawing("sq", MakeSquare(20));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
// Vertical cut at X=20 (through the middle of the square).
// BB exclusion Y = [10, 30]. Geometry should give the same.
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance);
var codes = cutoff.Drawing.Program.Codes;
// Two segments: before and after the square → 4 codes
Assert.Equal(4, codes.Count);
}
[Fact]
public void Circle_GeometryExclusionNarrowerThanBoundingBox()
{
// Circle radius=10, center at (10,10) after placement.
// BB = (0,0,20,20). Vertical cut at X=2 clips the circle edge.
// BB would exclude full Y=[0,20].
// Geometry: at X=2, the chord is much narrower.
var drawing = new Drawing("circ", MakeCircle(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(0, 0);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
// Cut at X=2: inside the BB but near the edge of the circle.
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, cache);
// The circle chord at X=2 from center (10,0) is much shorter than 20.
// With geometry, we get a tighter exclusion, so the segments should
// cover more of the plate than with BB.
// Total cut length should be greater than 80 (BB would give 100-20=80)
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
Assert.True(totalCutLength > 80, $"Geometry should give more cut length than BB. Got {totalCutLength:F2}");
}
[Fact]
public void Diamond_GeometryExclusionNarrowerThanBoundingBox()
{
// Diamond half=10 → points at (10,0), (20,10), (10,20), (0,10).
// BB = (0,0,20,20).
// Vertical cut at X=5: BB excludes Y=[0,20].
// Diamond edge at X=5: intersects at Y=5 and Y=15 → exclusion [5,15].
var drawing = new Drawing("dia", MakeDiamond(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(0, 0);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, cache);
// BB would exclude full 20 → cut length = 80.
// Geometry excludes only 10 → cut length = 90.
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
Assert.True(totalCutLength > 85, $"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}");
}
[Fact]
public void Triangle_AsymmetricExclusion()
{
// Right triangle: (0,0)→(30,0)→(0,30)→(0,0) placed at (10,10).
// Vertical cut at X=20 (10 into the triangle from left).
// The hypotenuse from (40,10) to (10,40): at X=20, Y = 30.
// So geometry exclusion should be roughly [10, 30], not [10, 40] like BB.
var drawing = new Drawing("tri", MakeTriangle(30, 30));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, cache);
// BB would exclude [10,40] = 30 → cut = 70.
// Geometry excludes [10,30] = 20 → cut = 80.
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
Assert.True(totalCutLength > 75, $"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}");
}
[Fact]
public void CutLineMissesPart_NoExclusion()
{
var drawing = new Drawing("sq", MakeSquare(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(50, 50);
plate.Parts.Add(part);
// Vertical cut at X=5: well outside the part at X=[50,60].
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance);
// Single full-length segment → 2 codes
Assert.Equal(2, cutoff.Drawing.Program.Codes.Count);
}
[Fact]
public void HorizontalCut_Circle_UsesGeometry()
{
var drawing = new Drawing("circ", MakeCircle(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(0, 0);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
// Horizontal cut at Y=2: near the edge of the circle.
var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
cutoff.Regenerate(plate, ZeroClearance, cache);
// BB would exclude X=[0,20] → cut = 80.
// Circle chord at Y=2 is much shorter → cut > 80.
var totalCutLength = TotalCutLength(cutoff.Drawing.Program, CutOffAxis.Horizontal);
Assert.True(totalCutLength > 80, $"Circle horizontal cut should use geometry. Got {totalCutLength:F2}");
}
[Fact]
public void Clearance_ExpandsGeometryExclusion()
{
var drawing = new Drawing("sq", MakeSquare(20));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
var settings = new CutOffSettings { PartClearance = 5.0 };
var cache = Plate.BuildPerimeterCache(plate);
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings, cache);
// Square at Y=[10,30]. With 5 clearance → exclusion [5,35].
// Segments: [0,5] and [35,100] → 4 codes.
Assert.Equal(4, cutoff.Drawing.Program.Codes.Count);
}
[Fact]
public void BuildPerimeterCache_OpenContourGetsConvexHull()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(new Drawing("open", pgm)));
var cache = Plate.BuildPerimeterCache(plate);
Assert.Single(cache);
var perimeter = cache[plate.Parts[0]];
Assert.NotNull(perimeter);
Assert.IsType<Polygon>(perimeter);
}
[Fact]
public void NullCache_FallsBackToBoundingBox()
{
// Without a cache, should still work (using BB fallback).
var drawing = new Drawing("sq", MakeSquare(20));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, null);
Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
}
[Fact]
public void MultipleParts_IndependentExclusions()
{
var plate = new Plate(100, 100);
var sq1 = new Drawing("sq1", MakeSquare(10));
var p1 = Part.CreateAtOrigin(sq1);
p1.Location = new Vector(10, 10);
plate.Parts.Add(p1);
var sq2 = new Drawing("sq2", MakeSquare(10));
var p2 = Part.CreateAtOrigin(sq2);
p2.Location = new Vector(10, 50);
plate.Parts.Add(p2);
// Vertical cut at X=15 crosses both parts.
var cutoff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance);
// 3 segments: before p1, between p1 and p2, after p2 → 6 codes
Assert.Equal(6, cutoff.Drawing.Program.Codes.Count);
}
[Fact]
public void CollectPoints_LinesAndArcs_ReturnsAllPoints()
{
var entities = new List<Entity>
{
new Line(new Vector(0, 0), new Vector(10, 0)),
new Arc(new Vector(5, 5), 5, 0, System.Math.PI)
};
var points = entities.CollectPoints();
// Line: 2 points. Arc: 2 endpoints + 4 cardinals = 6. Total = 8.
Assert.Equal(8, points.Count);
}
[Fact]
public void PlatePerimeterCache_ReturnsOneEntryPerPart()
{
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(new Drawing("a", MakeSquare(10))));
plate.Parts.Add(new Part(new Drawing("b", MakeCircle(5))));
plate.Parts.Add(new Part(new Drawing("c", MakeDiamond(8))));
var cache = Plate.BuildPerimeterCache(plate);
Assert.Equal(3, cache.Count);
}
[Fact]
public void PlatePerimeterCache_SkipsCutOffParts()
{
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(new Drawing("real", MakeSquare(10))));
plate.Parts.Add(new Part(new Drawing("cutoff", new Program()) { IsCutOff = true }));
var cache = Plate.BuildPerimeterCache(plate);
Assert.Single(cache);
}
[Fact]
public void RegenerateCutOffs_UsesGeometryExclusions()
{
// Circle radius=10 at origin. Vertical cut at X=2.
// With geometry: tighter exclusion than BB.
var drawing = new Drawing("circ", MakeCircle(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
plate.Parts.Add(part);
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings { PartClearance = 0 });
// Find the materialized cut-off part
var cutPart = plate.Parts.First(p => p.BaseDrawing.IsCutOff);
// BB would give 80 (100 - 20). Geometry should give more.
var totalCutLength = TotalCutLength(cutPart.BaseDrawing.Program);
Assert.True(totalCutLength > 80, $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}");
}
[Fact]
public void ShapeProfile_SelectsLargestShapeAsPerimeter()
{
// Outer square: (5,0)→(25,0)→(25,20)→(5,20)→(5,0)
// Inner cutout: (0,5)→(10,5)→(10,15)→(0,15)→(0,5)
// The cutout has Left=0, perimeter has Left=5.
// Old heuristic would pick the cutout as perimeter.
var outer = new Shape();
outer.Entities.Add(new Line(new Vector(5, 0), new Vector(25, 0)));
outer.Entities.Add(new Line(new Vector(25, 0), new Vector(25, 20)));
outer.Entities.Add(new Line(new Vector(25, 20), new Vector(5, 20)));
outer.Entities.Add(new Line(new Vector(5, 20), new Vector(5, 0)));
var inner = new Shape();
inner.Entities.Add(new Line(new Vector(0, 5), new Vector(10, 5)));
inner.Entities.Add(new Line(new Vector(10, 5), new Vector(10, 15)));
inner.Entities.Add(new Line(new Vector(10, 15), new Vector(0, 15)));
inner.Entities.Add(new Line(new Vector(0, 15), new Vector(0, 5)));
// Combine all entities (simulating what ShapeBuilder.GetShapes would produce)
var entities = new List<Entity>();
entities.AddRange(inner.Entities); // inner first — worst case for old heuristic
entities.AddRange(outer.Entities);
var profile = new ShapeProfile(entities);
// Perimeter should be the outer (larger) shape
var bb = profile.Perimeter.BoundingBox;
Assert.Equal(20.0, bb.Width, 1);
Assert.Equal(20.0, bb.Length, 1);
}
}
+119
View File
@@ -0,0 +1,119 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests;
public class CutOffSerializationTests
{
[Fact]
public void RoundTrip_CutOffsPreserved()
{
var nest = new Nest();
nest.Name = "test";
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var drawing = new Drawing("part1", pgm);
nest.Drawings.Add(drawing);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
plate.CutOffs.Add(new CutOff(new Vector(62.0, 24.0), CutOffAxis.Vertical));
plate.CutOffs.Add(new CutOff(new Vector(48.0, 30.0), CutOffAxis.Horizontal));
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
using var stream = new MemoryStream();
var writer = new NestWriter(nest);
writer.Write(stream);
stream.Position = 0;
var reader = new NestReader(stream);
var loaded = reader.Read();
Assert.Single(loaded.Plates);
var loadedPlate = loaded.Plates[0];
Assert.Equal(2, loadedPlate.CutOffs.Count);
Assert.Equal(CutOffAxis.Vertical, loadedPlate.CutOffs[0].Axis);
Assert.Equal(62.0, loadedPlate.CutOffs[0].Position.X, 5);
Assert.Equal(24.0, loadedPlate.CutOffs[0].Position.Y, 5);
Assert.Equal(CutOffAxis.Horizontal, loadedPlate.CutOffs[1].Axis);
Assert.Single(loadedPlate.Parts.Where(p => !p.BaseDrawing.IsCutOff));
Assert.Single(loaded.Drawings);
}
[Fact]
public void NestWriter_SkipsCutOffPartsInPartsList()
{
var nest = new Nest();
nest.Name = "test";
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var drawing = new Drawing("part1", pgm);
nest.Drawings.Add(drawing);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
plate.CutOffs.Add(new CutOff(new Vector(50, 25), CutOffAxis.Vertical));
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
Assert.Equal(2, plate.Parts.Count);
using var stream = new MemoryStream();
var writer = new NestWriter(nest);
writer.Write(stream);
stream.Position = 0;
var reader = new NestReader(stream);
var loaded = reader.Read();
Assert.Single(loaded.Plates[0].Parts.Where(p => !p.BaseDrawing.IsCutOff));
}
[Fact]
public void RoundTrip_LimitsPreserved()
{
var nest = new Nest();
nest.Name = "test";
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var drawing = new Drawing("part1", pgm);
nest.Drawings.Add(drawing);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 });
plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 });
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
using var stream = new MemoryStream();
var writer = new NestWriter(nest);
writer.Write(stream);
stream.Position = 0;
var reader = new NestReader(stream);
var loaded = reader.Read();
var loadedPlate = loaded.Plates[0];
Assert.Equal(85.0, loadedPlate.CutOffs[0].EndLimit);
Assert.Null(loadedPlate.CutOffs[0].StartLimit);
Assert.Equal(30.0, loadedPlate.CutOffs[1].StartLimit);
Assert.Null(loadedPlate.CutOffs[1].EndLimit);
}
}
+266
View File
@@ -0,0 +1,266 @@
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class CutOffTests
{
[Fact]
public void Drawing_IsCutOff_DefaultsFalse()
{
var drawing = new Drawing("test", new Program());
Assert.False(drawing.IsCutOff);
}
[Fact]
public void Plate_CutOffPart_DoesNotIncrementQuantity()
{
var drawing = new Drawing("cutoff", new Program()) { IsCutOff = true };
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(drawing));
Assert.Equal(0, drawing.Quantity.Nested);
}
[Fact]
public void Plate_Utilization_ExcludesCutOffParts()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Geometry.Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 0)));
var realDrawing = new Drawing("real", pgm);
var cutoffDrawing = new Drawing("cutoff", new Program()) { IsCutOff = true };
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(realDrawing));
plate.Parts.Add(new Part(cutoffDrawing));
var utilization = plate.Utilization();
var expected = realDrawing.Area / plate.Area();
Assert.Equal(expected, utilization, 5);
}
[Fact]
public void Plate_HasOverlappingParts_SkipsCutOffParts()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Geometry.Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Geometry.Vector(0, 0)));
var realDrawing = new Drawing("real", pgm);
var cutoffDrawing = new Drawing("cutoff", pgm) { IsCutOff = true };
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(realDrawing));
plate.Parts.Add(new Part(cutoffDrawing));
var hasOverlap = plate.HasOverlappingParts(out var pts);
Assert.False(hasOverlap);
}
[Fact]
public void CutOff_VerticalCut_GeneratesFullLineOnEmptyPlate()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 20), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
Assert.NotNull(cutoff.Drawing);
Assert.True(cutoff.Drawing.IsCutOff);
Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
}
[Fact]
public void CutOff_HorizontalCut_GeneratesFullLineOnEmptyPlate()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 20), CutOffAxis.Horizontal);
cutoff.Regenerate(plate, settings);
var codes = cutoff.Drawing.Program.Codes;
Assert.Equal(2, codes.Count);
}
[Fact]
public void CutOff_VerticalCut_TrimsAroundPart()
{
// Create a 10x10 part at the origin, then move it to (20,20)
// so the bounding box is Box(20,20,10,10) and doesn't span the origin.
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
var drawing = new Drawing("sq", pgm);
var plate = new Plate(50, 50);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(20, 20);
plate.Parts.Add(part);
// Vertical cut at X=25 runs along Y from 0 to 50.
// Part BB at (20,20,10,10) with clearance 1 → exclusion X=[19,31], Y=[19,31].
// X=25 is within [19,31] so exclusion applies: skip Y=[19,31].
// Segments: (0, 19) and (31, 50) → 2 segments → 4 codes.
var settings = new CutOffSettings { PartClearance = 1.0 };
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
var codes = cutoff.Drawing.Program.Codes;
Assert.Equal(4, codes.Count);
}
[Fact]
public void CutOff_ShortSegment_FilteredByMinLength()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(20, 0.02)));
pgm.Codes.Add(new LinearMove(new Vector(30, 0.02)));
pgm.Codes.Add(new LinearMove(new Vector(30, 10)));
pgm.Codes.Add(new LinearMove(new Vector(20, 10)));
pgm.Codes.Add(new LinearMove(new Vector(20, 0.02)));
var drawing = new Drawing("sq", pgm);
var plate = new Plate(50, 50);
plate.Parts.Add(new Part(drawing));
var settings = new CutOffSettings { PartClearance = 0.0, MinSegmentLength = 0.05 };
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
var rapidCount = cutoff.Drawing.Program.Codes.Count(c => c is RapidMove);
var lineCount = cutoff.Drawing.Program.Codes.Count(c => c is LinearMove);
Assert.Equal(rapidCount, lineCount);
}
[Fact]
public void CutOff_Overtravel_ExtendsFarEnd()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings { Overtravel = 2.0 };
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
// Plate(100, 50) = Width=100, Length=50. Vertical cut runs along Y (Width axis).
// BoundingBox Y extent = Size.Width = 100. With 2" overtravel = 102.
// Default TowardOrigin: RapidMove to far end (102), LinearMove to near end (0).
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
Assert.Equal(102.0, rapidMoves[0].EndPoint.Y, 5);
}
[Fact]
public void CutOff_StartLimit_TruncatesNearEnd()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical)
{
StartLimit = 20.0
};
cutoff.Regenerate(plate, settings);
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
Assert.Single(linearMoves);
Assert.Equal(20.0, linearMoves[0].EndPoint.Y, 5);
}
[Fact]
public void CutOff_EndLimit_TruncatesFarEnd()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical)
{
EndLimit = 80.0
};
cutoff.Regenerate(plate, settings);
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
Assert.Equal(80.0, rapidMoves[0].EndPoint.Y, 5);
}
[Fact]
public void CutOff_BothLimits_LShapedCornerCut()
{
var plate = new Plate(60, 120);
var settings = new CutOffSettings { PartClearance = 0 };
var hCut = new CutOff(new Vector(85, 30), CutOffAxis.Horizontal)
{
EndLimit = 85.0
};
hCut.Regenerate(plate, settings);
var vCut = new CutOff(new Vector(85, 30), CutOffAxis.Vertical)
{
StartLimit = 30.0
};
vCut.Regenerate(plate, settings);
Assert.True(hCut.Drawing.Program.Codes.Count > 0);
Assert.True(vCut.Drawing.Program.Codes.Count > 0);
}
[Fact]
public void Plate_RegenerateCutOffs_MaterializesParts()
{
var plate = new Plate(100, 50);
var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings());
Assert.Single(plate.Parts);
Assert.True(plate.Parts[0].BaseDrawing.IsCutOff);
}
[Fact]
public void Plate_RegenerateCutOffs_ReplacesOldParts()
{
var plate = new Plate(100, 50);
var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
var settings = new CutOffSettings();
plate.RegenerateCutOffs(settings);
plate.RegenerateCutOffs(settings);
Assert.Single(plate.Parts);
}
[Fact]
public void Plate_RegenerateCutOffs_DoesNotAffectRegularParts()
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Geometry.Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Geometry.Vector(5, 5)));
var drawing = new Drawing("real", pgm);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings());
Assert.Equal(2, plate.Parts.Count);
Assert.False(plate.Parts[0].BaseDrawing.IsCutOff);
Assert.True(plate.Parts[1].BaseDrawing.IsCutOff);
}
}
+1
View File
@@ -25,6 +25,7 @@
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
</ItemGroup>
</Project>
@@ -11,7 +11,7 @@ public class EdgeStartSequencerTests
{
var plate = new Plate(60, 120);
var edgePart = MakePartAt(1, 1);
var centerPart = MakePartAt(25, 55);
var centerPart = MakePartAt(25, 25);
var midPart = MakePartAt(10, 10);
plate.Parts.Add(edgePart);
plate.Parts.Add(centerPart);
+4 -4
View File
@@ -30,7 +30,7 @@ public class ShrinkFillerTests
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height);
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
@@ -73,7 +73,7 @@ public class ShrinkFillerTests
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
ShrinkAxis.Height, token: cts.Token);
ShrinkAxis.Length, token: cts.Token);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
@@ -97,7 +97,7 @@ public class ShrinkFillerTests
}
[Fact]
public void TrimToCount_Height_KeepsPartsNearestToOrigin()
public void TrimToCount_Length_KeepsPartsNearestToOrigin()
{
var parts = new List<Part>
{
@@ -107,7 +107,7 @@ public class ShrinkFillerTests
TestHelpers.MakePartAt(0, 30, 5), // Top = 35
};
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Height);
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Length);
Assert.Equal(2, trimmed.Count);
Assert.True(trimmed.All(p => p.BoundingBox.Top <= 15));
+11 -8
View File
@@ -7,11 +7,12 @@ internal static class TestHelpers
{
public static Part MakePartAt(double x, double y, double size = 1)
{
// CW winding matches CNC convention (OffsetSide.Left = outward)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
var drawing = new Drawing("test", pgm);
return new Part(drawing, new Vector(x, y));
@@ -27,24 +28,26 @@ internal static class TestHelpers
public static Drawing MakeSquareDrawing(double size = 10)
{
// CW winding matches CNC convention (OffsetSide.Left = outward)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
return new Drawing("square", pgm);
}
public static Drawing MakeLShapeDrawing()
{
// CW winding matches CNC convention (OffsetSide.Left = outward)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 5)));
pgm.Codes.Add(new LinearMove(new Vector(5, 5)));
pgm.Codes.Add(new LinearMove(new Vector(5, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Vector(5, 10)));
pgm.Codes.Add(new LinearMove(new Vector(5, 5)));
pgm.Codes.Add(new LinearMove(new Vector(10, 5)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
return new Drawing("lshape", pgm);
}
+27 -2
View File
@@ -1,9 +1,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.0.0
# Visual Studio Version 18
VisualStudioVersion = 18.4.11612.150
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest", "OpenNest\OpenNest.csproj", "{1F1E40E0-5C53-474F-A258-69C9C3FAC15A}"
ProjectSection(ProjectDependencies) = postProject
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532} = {FB1B2EB2-9D80-4499-BA93-B4E2F295A532}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Core", "OpenNest.Core\OpenNest.Core.csproj", "{5A5FDE8D-F8DB-440E-866C-C4807E1686CF}"
EndProject
@@ -23,6 +26,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Tests", "OpenNest.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Api", "OpenNest.Api\OpenNest.Api.csproj", "{44D2810A-16EF-46A4-859C-B897147D8D3C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostProcessors", "PostProcessors", "{4052CFAC-1F12-48BE-872D-F503C3B65D7E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -153,8 +160,26 @@ Global
{44D2810A-16EF-46A4-859C-B897147D8D3C}.Release|x64.Build.0 = Release|Any CPU
{44D2810A-16EF-46A4-859C-B897147D8D3C}.Release|x86.ActiveCfg = Release|Any CPU
{44D2810A-16EF-46A4-859C-B897147D8D3C}.Release|x86.Build.0 = Release|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x64.ActiveCfg = Debug|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x64.Build.0 = Debug|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x86.ActiveCfg = Debug|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Debug|x86.Build.0 = Debug|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|Any CPU.Build.0 = Release|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.ActiveCfg = Release|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.Build.0 = Release|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.ActiveCfg = Release|Any CPU
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FB1B2EB2-9D80-4499-BA93-B4E2F295A532} = {4052CFAC-1F12-48BE-872D-F503C3B65D7E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {86FE17B3-F764-40AE-BCAA-F26B470CA05C}
EndGlobalSection
EndGlobal
+3
View File
@@ -167,6 +167,9 @@ namespace OpenNest.Actions
}
parts.ForEach(p => plateView.Plate.Parts.Add(p.BasePart.Clone() as Part));
if (plateView.Plate.CutOffs.Count > 0)
plateView.Plate.RegenerateCutOffs(plateView.CutOffSettings);
}
private void Fill()
+139
View File
@@ -0,0 +1,139 @@
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Geometry;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace OpenNest.Actions
{
[DisplayName("Sheet Cut-Off")]
public class ActionCutOff : Action
{
private CutOff previewCutOff;
private CutOffSettings settings;
private CutOffAxis lockedAxis = CutOffAxis.Vertical;
private Dictionary<Part, Entity> perimeterCache;
private readonly Timer debounceTimer;
private bool regeneratePending;
public ActionCutOff(PlateView plateView)
: base(plateView)
{
settings = plateView.CutOffSettings;
debounceTimer = new Timer { Interval = 16 };
debounceTimer.Tick += OnDebounce;
ConnectEvents();
}
public override void ConnectEvents()
{
perimeterCache = Plate.BuildPerimeterCache(plateView.Plate);
plateView.MouseMove += OnMouseMove;
plateView.MouseDown += OnMouseDown;
plateView.KeyDown += OnKeyDown;
plateView.Paint += OnPaint;
}
public override void DisconnectEvents()
{
debounceTimer.Stop();
debounceTimer.Dispose();
plateView.MouseMove -= OnMouseMove;
plateView.MouseDown -= OnMouseDown;
plateView.KeyDown -= OnKeyDown;
plateView.Paint -= OnPaint;
previewCutOff = null;
perimeterCache = null;
plateView.Invalidate();
}
public override void CancelAction() { }
public override bool IsBusy() => false;
private void OnMouseMove(object sender, MouseEventArgs e)
{
regeneratePending = true;
debounceTimer.Start();
}
private void OnDebounce(object sender, System.EventArgs e)
{
debounceTimer.Stop();
if (!regeneratePending)
return;
regeneratePending = false;
var pt = plateView.CurrentPoint;
previewCutOff = new CutOff(pt, lockedAxis);
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
plateView.Invalidate();
}
private void OnMouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
return;
var pt = plateView.CurrentPoint;
var cutoff = new CutOff(pt, lockedAxis);
plateView.Plate.CutOffs.Add(cutoff);
plateView.Plate.RegenerateCutOffs(settings);
plateView.Invalidate();
}
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
lockedAxis = lockedAxis == CutOffAxis.Vertical
? CutOffAxis.Horizontal
: CutOffAxis.Vertical;
if (previewCutOff != null)
{
previewCutOff = new CutOff(plateView.CurrentPoint, lockedAxis);
previewCutOff.Regenerate(plateView.Plate, settings, perimeterCache);
plateView.Invalidate();
}
}
else if (e.KeyCode == Keys.Escape)
{
plateView.SetAction(typeof(ActionSelect));
}
}
private void OnPaint(object sender, PaintEventArgs e)
{
if (previewCutOff?.Drawing?.Program == null)
return;
var program = previewCutOff.Drawing.Program;
if (program.Codes.Count == 0)
return;
using var pen = new Pen(Color.FromArgb(128, 64, 64, 64), 1.5f / plateView.ViewScale)
{
DashStyle = DashStyle.Dash
};
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
var pt1 = plateView.PointWorldToGraph(rapid.EndPoint);
var pt2 = plateView.PointWorldToGraph(linear.EndPoint);
e.Graphics.DrawLine(pen, pt1, pt2);
}
}
}
}
}
+6
View File
@@ -133,6 +133,12 @@ namespace OpenNest.Actions
plateView.Invalidate();
status = Status.SetFirstPoint;
}
else if (plateView.SelectedParts.Count > 0)
{
// Part drag completed — regenerate cut-off programs
if (plateView.Plate.CutOffs.Count > 0)
plateView.Plate.RegenerateCutOffs(plateView.CutOffSettings);
}
}
private void plateView_Paint(object sender, PaintEventArgs e)
+32 -10
View File
@@ -28,6 +28,8 @@ namespace OpenNest.Controls
public bool HideDepletedParts { get; set; }
public bool HideQuantity { get; set; }
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (e.Index >= Items.Count || e.Index <= -1)
@@ -38,8 +40,15 @@ namespace OpenNest.Controls
if (dwg == null)
return;
var isComplete = dwg.Quantity.Nested > 0 && dwg.Quantity.Remaining == 0;
var bgBrush = isComplete ? SystemBrushes.Info : Brushes.White;
var isSelected = (e.State & DrawItemState.Selected) != 0;
Brush bgBrush;
if (isSelected)
bgBrush = SystemBrushes.Highlight;
else if (!HideQuantity && dwg.Quantity.Nested > 0 && dwg.Quantity.Remaining == 0)
bgBrush = SystemBrushes.Info;
else
bgBrush = Brushes.White;
e.Graphics.FillRectangle(bgBrush, e.Bounds);
@@ -57,19 +66,32 @@ namespace OpenNest.Controls
pt.X += imageSize.Width + 10;
e.Graphics.DrawString(dwg.Name, nameFont, Brushes.Black, pt);
var textBrush = isSelected ? SystemBrushes.HighlightText : Brushes.Black;
var detailBrush = isSelected ? SystemBrushes.HighlightText : Brushes.Gray;
e.Graphics.DrawString(dwg.Name, nameFont, textBrush, pt);
var bounds = dwg.Program.BoundingBox();
var text1 = string.Format("{0} of {1} nested", dwg.Quantity.Nested, dwg.Quantity.Required);
var text2 = bounds.Size.ToString(4);
var text3 = string.Format("{0} sq/{1}", System.Math.Round(dwg.Area, 4), UnitsHelper.GetShortString(Units));
pt.Y += 22;
e.Graphics.DrawString(text1, Font, Brushes.Gray, pt);
pt.Y += 18;
e.Graphics.DrawString(text2, Font, Brushes.Gray, pt);
pt.Y += 18;
e.Graphics.DrawString(text3, Font, Brushes.Gray, pt);
if (HideQuantity)
{
pt.Y += 22;
e.Graphics.DrawString(text2, Font, detailBrush, pt);
pt.Y += 18;
e.Graphics.DrawString(text3, Font, detailBrush, pt);
}
else
{
var text1 = string.Format("{0} of {1} nested", dwg.Quantity.Nested, dwg.Quantity.Required);
pt.Y += 22;
e.Graphics.DrawString(text1, Font, detailBrush, pt);
pt.Y += 18;
e.Graphics.DrawString(text2, Font, detailBrush, pt);
pt.Y += 18;
e.Graphics.DrawString(text3, Font, detailBrush, pt);
}
}
protected override void OnMouseMove(MouseEventArgs e)
+134 -1
View File
@@ -30,6 +30,10 @@ namespace OpenNest.Controls
private Plate plate;
private Action currentAction;
private Action previousAction;
private CutOffSettings cutOffSettings = new CutOffSettings();
private CutOff selectedCutOff;
private bool draggingCutOff;
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
protected List<LayoutPart> parts;
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
private List<LayoutPart> activeParts = new List<LayoutPart>();
@@ -134,6 +138,27 @@ namespace OpenNest.Controls
public bool FillParts { get; set; }
public CutOffSettings CutOffSettings
{
get => cutOffSettings;
set
{
cutOffSettings = value;
Plate?.RegenerateCutOffs(value);
Invalidate();
}
}
public CutOff SelectedCutOff
{
get => selectedCutOff;
set
{
selectedCutOff = value;
Invalidate();
}
}
public double RotateIncrementAngle { get; set; }
public double OffsetIncrementDistance { get; set; }
@@ -211,6 +236,22 @@ namespace OpenNest.Controls
if (e.Button == MouseButtons.Middle)
middleMouseDownPoint = e.Location;
if (e.Button == MouseButtons.Left && currentAction is ActionSelect)
{
var hitCutOff = GetCutOffAtPoint(CurrentPoint, 5.0 / ViewScale);
if (hitCutOff != null)
{
SelectedCutOff = hitCutOff;
draggingCutOff = true;
dragPerimeterCache = Plate.BuildPerimeterCache(Plate);
return;
}
else
{
SelectedCutOff = null;
}
}
base.OnMouseDown(e);
}
@@ -228,6 +269,15 @@ namespace OpenNest.Controls
}
}
if (draggingCutOff && selectedCutOff != null)
{
draggingCutOff = false;
dragPerimeterCache = null;
Plate.RegenerateCutOffs(cutOffSettings);
Invalidate();
return;
}
base.OnMouseUp(e);
}
@@ -284,6 +334,18 @@ namespace OpenNest.Controls
lastPoint = e.Location;
if (draggingCutOff && selectedCutOff != null)
{
if (selectedCutOff.Axis == CutOffAxis.Vertical)
selectedCutOff.Position = new Vector(CurrentPoint.X, selectedCutOff.Position.Y);
else
selectedCutOff.Position = new Vector(selectedCutOff.Position.X, CurrentPoint.Y);
selectedCutOff.Regenerate(Plate, cutOffSettings, dragPerimeterCache);
Invalidate();
return;
}
base.OnMouseMove(e);
}
@@ -300,7 +362,17 @@ namespace OpenNest.Controls
switch (e.KeyCode)
{
case Keys.Delete:
RemoveSelectedParts();
if (selectedCutOff != null)
{
Plate.CutOffs.Remove(selectedCutOff);
selectedCutOff = null;
Plate.RegenerateCutOffs(cutOffSettings);
Invalidate();
}
else
{
RemoveSelectedParts();
}
break;
case Keys.F:
@@ -390,6 +462,7 @@ namespace OpenNest.Controls
DrawPlate(e.Graphics);
DrawParts(e.Graphics);
DrawCutOffs(e.Graphics);
DrawActiveWorkArea(e.Graphics);
DrawDebugRemnants(e.Graphics);
@@ -541,6 +614,59 @@ namespace OpenNest.Controls
DrawRapids(g);
}
private void DrawCutOffs(Graphics g)
{
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
return;
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);
foreach (var cutoff in Plate.CutOffs)
{
var program = cutoff.Drawing?.Program;
if (program == null || program.Codes.Count == 0)
continue;
var activePen = cutoff == selectedCutOff ? selectedPen : pen;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
}
}
}
}
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
{
if (Plate?.CutOffs == null)
return null;
foreach (var cutoff in Plate.CutOffs)
{
var program = cutoff.Drawing?.Program;
if (program == null)
continue;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
var line = new Geometry.Line(rapid.EndPoint, linear.EndPoint);
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
return cutoff;
}
}
}
return null;
}
private void DrawOffsetGeometry(Graphics g)
{
using (var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100)))
@@ -966,6 +1092,10 @@ namespace OpenNest.Controls
if (parts.Count > 0 && (!cts.IsCancellationRequested || progressForm.Accepted))
{
AcceptPreviewParts(parts);
if (Plate.CutOffs.Count > 0)
Plate.RegenerateCutOffs(cutOffSettings);
sw.Stop();
Status = $"Fill: {parts.Count} parts in {sw.ElapsedMilliseconds} ms";
}
@@ -1109,6 +1239,9 @@ namespace OpenNest.Controls
for (var i = 0; i < SelectedParts.Count; ++i)
SelectedParts[i].Offset(diff);
if (Plate.CutOffs.Count > 0)
Plate.RegenerateCutOffs(cutOffSettings);
}
protected override void UpdateMatrix()
+141 -118
View File
@@ -13,134 +13,157 @@ namespace OpenNest.Forms
private void InitializeComponent()
{
this.gridPanel = new System.Windows.Forms.TableLayoutPanel();
this.toolbarPanel = new System.Windows.Forms.Panel();
this.lblDrawing = new System.Windows.Forms.Label();
this.cboDrawing = new System.Windows.Forms.ComboBox();
this.navPanel = new System.Windows.Forms.Panel();
this.btnPrev = new System.Windows.Forms.Button();
this.btnNext = new System.Windows.Forms.Button();
this.txtPage = new System.Windows.Forms.TextBox();
this.lblPageCount = new System.Windows.Forms.Label();
this.toolbarPanel.SuspendLayout();
this.navPanel.SuspendLayout();
this.SuspendLayout();
//
splitContainer = new System.Windows.Forms.SplitContainer();
drawingListBox = new OpenNest.Controls.DrawingListBox();
gridPanel = new System.Windows.Forms.TableLayoutPanel();
navPanel = new System.Windows.Forms.Panel();
btnPrev = new System.Windows.Forms.Button();
txtPage = new System.Windows.Forms.TextBox();
lblPageCount = new System.Windows.Forms.Label();
btnNext = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
splitContainer.Panel1.SuspendLayout();
splitContainer.Panel2.SuspendLayout();
splitContainer.SuspendLayout();
navPanel.SuspendLayout();
SuspendLayout();
//
// splitContainer
//
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
splitContainer.Location = new System.Drawing.Point(0, 0);
splitContainer.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
splitContainer.Panel1.Controls.Add(drawingListBox);
splitContainer.Panel1MinSize = 180;
//
// splitContainer.Panel2
//
splitContainer.Panel2.Controls.Add(gridPanel);
splitContainer.Panel2.Controls.Add(navPanel);
splitContainer.Size = new System.Drawing.Size(792, 486);
splitContainer.SplitterDistance = 280;
splitContainer.SplitterWidth = 6;
splitContainer.TabIndex = 0;
//
// drawingListBox
//
drawingListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
drawingListBox.Dock = System.Windows.Forms.DockStyle.Fill;
drawingListBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
drawingListBox.FormattingEnabled = true;
drawingListBox.HideDepletedParts = false;
drawingListBox.ItemHeight = 85;
drawingListBox.Location = new System.Drawing.Point(0, 0);
drawingListBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
drawingListBox.Name = "drawingListBox";
drawingListBox.Size = new System.Drawing.Size(280, 486);
drawingListBox.TabIndex = 0;
drawingListBox.Units = Units.Inches;
//
// gridPanel
//
this.gridPanel.ColumnCount = 5;
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
this.gridPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this.gridPanel.Location = new System.Drawing.Point(0, 32);
this.gridPanel.Name = "gridPanel";
this.gridPanel.RowCount = 3;
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.34F));
this.gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
this.gridPanel.Size = new System.Drawing.Size(1200, 732);
this.gridPanel.TabIndex = 0;
//
// toolbarPanel
//
this.toolbarPanel.Controls.Add(this.lblDrawing);
this.toolbarPanel.Controls.Add(this.cboDrawing);
this.toolbarPanel.Dock = System.Windows.Forms.DockStyle.Top;
this.toolbarPanel.Location = new System.Drawing.Point(0, 0);
this.toolbarPanel.Name = "toolbarPanel";
this.toolbarPanel.Size = new System.Drawing.Size(1200, 32);
this.toolbarPanel.TabIndex = 2;
//
// lblDrawing
//
this.lblDrawing.Location = new System.Drawing.Point(6, 0);
this.lblDrawing.Name = "lblDrawing";
this.lblDrawing.Size = new System.Drawing.Size(55, 32);
this.lblDrawing.TabIndex = 0;
this.lblDrawing.Text = "Drawing:";
this.lblDrawing.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// cboDrawing
//
this.cboDrawing.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cboDrawing.Location = new System.Drawing.Point(64, 5);
this.cboDrawing.Name = "cboDrawing";
this.cboDrawing.Size = new System.Drawing.Size(250, 21);
this.cboDrawing.TabIndex = 1;
//
//
gridPanel.ColumnCount = 5;
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
gridPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 20F));
gridPanel.Dock = System.Windows.Forms.DockStyle.Fill;
gridPanel.Location = new System.Drawing.Point(0, 0);
gridPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
gridPanel.Name = "gridPanel";
gridPanel.RowCount = 3;
gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.34F));
gridPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33F));
gridPanel.Size = new System.Drawing.Size(506, 444);
gridPanel.TabIndex = 0;
//
// navPanel
//
this.navPanel.Controls.Add(this.btnPrev);
this.navPanel.Controls.Add(this.txtPage);
this.navPanel.Controls.Add(this.lblPageCount);
this.navPanel.Controls.Add(this.btnNext);
this.navPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.navPanel.Location = new System.Drawing.Point(0, 764);
this.navPanel.Name = "navPanel";
this.navPanel.Size = new System.Drawing.Size(1200, 36);
this.navPanel.TabIndex = 1;
//
//
navPanel.Controls.Add(btnPrev);
navPanel.Controls.Add(txtPage);
navPanel.Controls.Add(lblPageCount);
navPanel.Controls.Add(btnNext);
navPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
navPanel.Location = new System.Drawing.Point(0, 444);
navPanel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
navPanel.Name = "navPanel";
navPanel.Size = new System.Drawing.Size(506, 42);
navPanel.TabIndex = 1;
//
// btnPrev
//
this.btnPrev.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnPrev.Name = "btnPrev";
this.btnPrev.Size = new System.Drawing.Size(80, 28);
this.btnPrev.TabIndex = 0;
this.btnPrev.Text = "< Prev";
this.btnPrev.Click += new System.EventHandler(this.btnPrev_Click);
//
//
btnPrev.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
btnPrev.Location = new System.Drawing.Point(0, 0);
btnPrev.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnPrev.Name = "btnPrev";
btnPrev.Size = new System.Drawing.Size(93, 32);
btnPrev.TabIndex = 0;
btnPrev.Text = "< Prev";
btnPrev.Click += btnPrev_Click;
//
// txtPage
//
this.txtPage.Name = "txtPage";
this.txtPage.Size = new System.Drawing.Size(40, 20);
this.txtPage.TabIndex = 1;
this.txtPage.Text = "1";
this.txtPage.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.txtPage.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txtPage_KeyDown);
//
//
txtPage.Location = new System.Drawing.Point(0, 0);
txtPage.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
txtPage.Name = "txtPage";
txtPage.Size = new System.Drawing.Size(46, 23);
txtPage.TabIndex = 1;
txtPage.Text = "1";
txtPage.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
txtPage.KeyDown += txtPage_KeyDown;
//
// lblPageCount
//
this.lblPageCount.Name = "lblPageCount";
this.lblPageCount.Size = new System.Drawing.Size(50, 28);
this.lblPageCount.TabIndex = 2;
this.lblPageCount.Text = "/ 1";
this.lblPageCount.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
//
lblPageCount.Location = new System.Drawing.Point(0, 0);
lblPageCount.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
lblPageCount.Name = "lblPageCount";
lblPageCount.Size = new System.Drawing.Size(58, 32);
lblPageCount.TabIndex = 2;
lblPageCount.Text = "/ 1";
lblPageCount.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// btnNext
//
this.btnNext.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnNext.Name = "btnNext";
this.btnNext.Size = new System.Drawing.Size(80, 28);
this.btnNext.TabIndex = 3;
this.btnNext.Text = "Next >";
this.btnNext.Click += new System.EventHandler(this.btnNext_Click);
//
//
btnNext.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
btnNext.Location = new System.Drawing.Point(0, 0);
btnNext.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnNext.Name = "btnNext";
btnNext.Size = new System.Drawing.Size(93, 32);
btnNext.TabIndex = 3;
btnNext.Text = "Next >";
btnNext.Click += btnNext_Click;
//
// BestFitViewerForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1200, 800);
this.Controls.Add(this.gridPanel);
this.Controls.Add(this.toolbarPanel);
this.Controls.Add(this.navPanel);
this.KeyPreview = true;
this.Name = "BestFitViewerForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Best-Fit Viewer";
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
this.toolbarPanel.ResumeLayout(false);
this.navPanel.ResumeLayout(false);
this.navPanel.PerformLayout();
this.ResumeLayout(false);
//
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(792, 486);
Controls.Add(splitContainer);
KeyPreview = true;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
Name = "BestFitViewerForm";
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Best-Fit Viewer";
WindowState = System.Windows.Forms.FormWindowState.Maximized;
splitContainer.Panel1.ResumeLayout(false);
splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
splitContainer.ResumeLayout(false);
navPanel.ResumeLayout(false);
navPanel.PerformLayout();
ResumeLayout(false);
}
private System.Windows.Forms.SplitContainer splitContainer;
private Controls.DrawingListBox drawingListBox;
private System.Windows.Forms.TableLayoutPanel gridPanel;
private System.Windows.Forms.Panel toolbarPanel;
private System.Windows.Forms.Label lblDrawing;
private System.Windows.Forms.ComboBox cboDrawing;
private System.Windows.Forms.Panel navPanel;
private System.Windows.Forms.Button btnPrev;
private System.Windows.Forms.Button btnNext;
+37 -9
View File
@@ -41,11 +41,12 @@ namespace OpenNest.Forms
private int currentPage;
private int pageCount;
private CancellationTokenSource computeCts;
private Label lblLoading;
public BestFitResult SelectedResult { get; private set; }
public Drawing SelectedDrawing => activeDrawing;
public BestFitViewerForm(DrawingCollection drawings, Plate plate)
public BestFitViewerForm(DrawingCollection drawings, Plate plate, Units units = Units.Inches)
{
this.drawings = drawings.ToList();
this.plate = plate;
@@ -53,10 +54,12 @@ namespace OpenNest.Forms
DoubleBuffered = true;
InitializeComponent();
drawingListBox.Units = units;
drawingListBox.HideQuantity = true;
foreach (var d in drawings)
cboDrawing.Items.Add(d.Name);
cboDrawing.SelectedIndex = 0;
cboDrawing.SelectedIndexChanged += cboDrawing_SelectedIndexChanged;
drawingListBox.Items.Add(d);
drawingListBox.SelectedIndex = 0;
drawingListBox.SelectedIndexChanged += drawingListBox_SelectedIndexChanged;
navPanel.SizeChanged += (s, ev) => CenterNavControls();
Shown += BestFitViewerForm_Shown;
@@ -93,13 +96,13 @@ namespace OpenNest.Forms
return base.ProcessCmdKey(ref msg, keyData);
}
private void cboDrawing_SelectedIndexChanged(object sender, EventArgs e)
private void drawingListBox_SelectedIndexChanged(object sender, EventArgs e)
{
var index = cboDrawing.SelectedIndex;
if (index < 0 || index >= drawings.Count)
var drawing = drawingListBox.SelectedItem as Drawing;
if (drawing == null)
return;
activeDrawing = drawings[index];
activeDrawing = drawing;
LoadResultsAsync();
}
@@ -145,7 +148,6 @@ namespace OpenNest.Forms
private void SetLoading(bool loading)
{
Cursor = loading ? Cursors.WaitCursor : Cursors.Default;
cboDrawing.Enabled = !loading;
btnPrev.Enabled = !loading;
btnNext.Enabled = !loading;
txtPage.Enabled = !loading;
@@ -155,8 +157,34 @@ namespace OpenNest.Forms
Text = "Best-Fit Viewer — Computing...";
gridPanel.SuspendLayout();
gridPanel.Controls.Clear();
lblLoading = null;
EnsureLoadingLabel();
lblLoading.Text = string.Format("Computing best fits for {0}...", activeDrawing.Name);
gridPanel.ResumeLayout(true);
}
else
{
if (lblLoading != null)
lblLoading.Visible = false;
}
}
private void EnsureLoadingLabel()
{
if (lblLoading != null)
return;
lblLoading = new Label
{
AutoSize = false,
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray,
Font = new Font(Font.FontFamily, 14f),
Dock = DockStyle.Fill
};
gridPanel.Controls.Add(lblLoading, 0, 0);
gridPanel.SetColumnSpan(lblLoading, Columns);
gridPanel.SetRowSpan(lblLoading, Rows);
}
private static ComputeResult ComputeResults(Drawing drawing, double length, double width, double spacing)
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+11 -2
View File
@@ -117,6 +117,7 @@
manualSequenceToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
calculateCutTimeToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
centerPartsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
mnuPlateCutOff = new System.Windows.Forms.ToolStripMenuItem();
mnuWindow = new System.Windows.Forms.ToolStripMenuItem();
mnuWindowCascade = new System.Windows.Forms.ToolStripMenuItem();
mnuWindowTileVertical = new System.Windows.Forms.ToolStripMenuItem();
@@ -627,7 +628,7 @@
//
// mnuPlate
//
mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateFill, toolStripMenuItem9, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateViewInCad, toolStripMenuItem20, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, centerPartsToolStripMenuItem });
mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateFill, toolStripMenuItem9, mnuPlateCutOff, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateViewInCad, toolStripMenuItem20, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, centerPartsToolStripMenuItem });
mnuPlate.Name = "mnuPlate";
mnuPlate.Size = new System.Drawing.Size(45, 20);
mnuPlate.Text = "&Plate";
@@ -685,7 +686,14 @@
toolStripMenuItem9.Size = new System.Drawing.Size(177, 22);
toolStripMenuItem9.Text = "Fill Area";
toolStripMenuItem9.Click += FillArea_Click;
//
//
// mnuPlateCutOff
//
mnuPlateCutOff.Name = "mnuPlateCutOff";
mnuPlateCutOff.Size = new System.Drawing.Size(177, 22);
mnuPlateCutOff.Text = "Sheet Cut-Off";
mnuPlateCutOff.Click += CutOff_Click;
//
// mnuPlateRotate
//
mnuPlateRotate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateRotateCw, mnuPlateRotateCcw, toolStripSeparator2, mnuPlateRotate180 });
@@ -1205,5 +1213,6 @@
private System.Windows.Forms.ToolStripComboBox engineComboBox;
private System.Windows.Forms.ToolStripButton btnAutoNest;
private System.Windows.Forms.ToolStripButton btnShowRemnants;
private System.Windows.Forms.ToolStripMenuItem mnuPlateCutOff;
}
}
+9 -1
View File
@@ -626,7 +626,7 @@ namespace OpenNest.Forms
return;
}
using (var form = new BestFitViewerForm(drawings, plate))
using (var form = new BestFitViewerForm(drawings, plate, activeForm.Nest.Units))
{
if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null)
{
@@ -1179,6 +1179,14 @@ namespace OpenNest.Forms
activeForm.PlateView.SetAction(typeof(ActionSetSequence));
}
private void CutOff_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
activeForm.PlateView.SetAction(typeof(ActionCutOff));
}
#endregion Plate Menu Events
#region Window Menu Events
+1
View File
@@ -16,6 +16,7 @@
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" ReferenceOutputAssembly="false" />
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
</ItemGroup>
<ItemGroup>