Compare commits

24 Commits

Author SHA1 Message Date
aj 3c53d6fecd fix(engine): default FillContext.Policy to avoid null-deref in ReportProgress
FillContext.ReportProgress dereferences Policy.Comparer, so any caller
that forgot to set Policy hit a NullReferenceException. Default to
FillPolicy(DefaultFillComparer) so tests and ad-hoc callers work without
boilerplate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:28:58 -04:00
aj e239967a7b feat(cincinnati): emit SubProgramCall features as M98 hole calls
When a feature is a single SubProgramCall, wrap the call with a G52
offset shift, emit M98 P<num>, reset G52, and add M47 between features.
Accepts an optional hole subprogram id map so the post can remap
drawing-local subprogram ids to machine subprogram numbers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:17:31 -04:00
aj 9d57d3875a fix(cnc): offset SubProgramCall positions in Program.Offset
Program.Offset only adjusted Motion codes, so subprogram calls kept
their original offsets after a part was translated. Apply the offset
to SubProgramCall.Offset too so hole subprograms follow the part.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:17:26 -04:00
aj 0e299d7f6f feat(cincinnati): seed material library defaults and add selector dropdown
Adds the full Cincinnati material/etch library list as the committed
default config (seeded into Posts/ on build only when no runtime config
exists), plus a Selected Library override in the PropertyGrid backed by
a TypeConverter that populates from MaterialLibraries. MainForm calls
the new IPostProcessorNestAware hook before showing the config so the
dropdown opens preselected to the best match by nest material and
nearest thickness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:16:29 -04:00
aj c6f544c5d7 feat(ui): populate material combobox from post processors
Replaces the material textbox on EditNestInfoForm with a combobox whose
items are aggregated from every loaded post processor that implements the
new IMaterialProvidingPostProcessor interface. CincinnatiPostProcessor
exposes its configured MaterialLibraries entries. Free-text entry still
works so custom materials remain usable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:12:54 -04:00
aj 9563094c2b fix(ui): show Drawings tab before Plates in EditNestForm
Users need to import a drawing first, so Drawings tab should be the
default landing tab to reduce steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:10:58 -04:00
aj 091e750e1b chore(cad-importer): remove dead code and cover named detector branch
- Drop CadImportResult.Document: no caller reads it after the
  migrations (BendDetectorRegistry runs inside CadImporter.Import
  itself, and downstream callers only consume the entity/bend data).
- Drop dead CadConverterForm.GetNextColor() helper: zero callers
  since GetDrawings stopped needing it.
- Drop stale 'using OpenNest.Properties;' and unused 'newItems'
  local in OnSplitClicked.
- Add Import_WhenNamedDetectorDoesNotExist_ReturnsEmptyBends to
  cover the previously untested named-detector branch in
  CadImporter.Import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:13:44 -04:00
aj 87b965f895 refactor(ui): use CadImporter in BomImportForm
Replaces the hand-rolled DXF->Drawing pipeline (Dxf.Import + bend
detection + normalize + ConvertGeometry + pierce offset extraction)
with a single CadImporter.ImportDrawing call. Brings BomImportForm's
output in line with the rest of the callers: drawings now carry
Source.Offset, SourceEntities, SuppressedEntityIds, and detected bends,
and round-trip cleanly through nest files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:11:49 -04:00
aj 08f60690a7 docs: document CadImporter service in CLAUDE.md 2026-04-10 13:27:46 -04:00
aj a4609c816c refactor(ui): use CadImporter.BuildDrawing in CadConverterForm.GetDrawings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:24:26 -04:00
aj 5a4272696e refactor(ui): use CadImporter.Import in CadConverterForm.AddFile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:18:49 -04:00
aj 2cf03be360 refactor(training): use CadImporter for DXF import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:13:00 -04:00
aj 041e184d93 refactor(api): use CadImporter for DXF import in NestRunner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:08:35 -04:00
aj 26df3174ea refactor(mcp): use CadImporter for DXF import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:05:28 -04:00
aj 0f5aace126 refactor(console): use CadImporter for DXF import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:03:16 -04:00
aj 399f8dda6e feat: add CadImporter.ImportDrawing convenience method
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:59:06 -04:00
aj d921558b9c feat: add CadImporter.BuildDrawing stage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:51:58 -04:00
aj bf3e3e1f42 feat: add CadImporter.Import stage with bend detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:37:12 -04:00
aj e120ece014 feat: add CadImportResult data object for CadImporter 2026-04-10 12:28:17 -04:00
aj 264e8264be feat: add CadImportOptions for CadImporter service 2026-04-10 12:25:04 -04:00
aj 24babe353e fix: show both offset and rotation in SubProgramCall.ToString
The either/or format meant a SubProgramCall with both a non-zero
Offset and non-zero Rotation would only show the Offset, hiding the
rotation metadata. The data model supports both independently, so the
display should too.

Also fixes a zero-field leak where the old fallback emitted
`G65 P_ R0` for calls with no rotation. Now each field is only shown
when non-zero, and `G65 P_` with no arguments is emitted when
neither is set.

Note: SubProgramCall.ToString is purely a debug/display aid. The
Cincinnati post emits sub-calls via the G52 + M98 bracket, not via
G65, so this format doesn't correspond to real machine output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:37:46 -04:00
aj e63be93051 fix: emit G52 bracket for hole sub-program calls
CincinnatiSheetWriter.WriteHoleSubprogramCall emitted
`M98 P<num> X<x> Y<y>`, but per manual §3.98 ("M98 SUB-PROGRAM CALL
WITH NO ARGUMENTS") M98 takes only P and L — the X/Y had no defined
meaning to the control. The intent was to position the sub-program at
the hole center, which is what G52 is for per §1.52 ("local work
coordinate system") and which explicitly does not move the nozzle.

Emit the documented G52 bracket instead:
  G52 X<hole.x> Y<hole.y>
  M98 P<holeSubNum>
  G52 X0 Y0

The hole sub-program is authored in hole-local coordinates, so its
first rapid (the lead-in to the pierce point) resolves to the absolute
pierce under the G52 shift and moves the tool directly there from the
previous feature's end — no phantom rapid to the hole center.

Also add docs/cincinnati-post-output.md as the reference for the full
post output format, with every emitted G/M code cross-referenced to
the Cincinnati programming manual. Un-ignore docs/ (docs/superpowers/
stays ignored) and track the PDF manual alongside the reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:21:15 -04:00
aj ba3c3cbea3 fix: draw sub-program rapid directly to lead-in pierce
The SubProgramCall branch in DrawRapids used to draw a rapid from the
previous feature's end to the hole center, then rely on the sub-program's
own first rapid to draw from center to the lead-in pierce. That rendered
a phantom center-hop segment that doesn't exist physically — a
SubProgramCall is a coordinate-frame shift (emitted as a G52 bracket on
Cincinnati), not a move to the hole center.

Look ahead through the sub-program for its first pierce point in
absolute coordinates and draw a single direct rapid from pos to that
pierce. Recurse into the sub with skipFirstRapid: true so the sub's
first rapid isn't drawn again on top.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:17:35 -04:00
aj 572fa06a21 fix: track tool position through sub-programs in ConvertMode
ConvertMode.ToIncremental skipped SubProgramCall codes entirely when
computing deltas, so parent motions after a sub-call were encoded as if
the tool never moved. Several traversal sites (ConvertProgram,
GraphicsHelper, PlateRenderer, CutDirectionArrows, Program.BoundingBox)
worked around this with save/restore hacks that treated sub-calls as
transparent — but DrawRapids legitimately tracks actual tool position,
so after the last hole the first perimeter rapid was applied to the
wrong base, drifting the rendered perimeter past the plate edge by
roughly the distance to the last hole.

Fix the root cause: ToIncremental and ToAbsolute now walk sub-programs
to compute where they leave the tool, and advance pos accordingly. The
other traversals capture a frameOrigin at entry and compute sub-call
placement as frameOrigin + Offset, letting pos advance naturally
through the sub recursion. All the save/restore workarounds are
removed.

Program.BoundingBox also picks up the same frame-origin treatment,
which corrects a latent bug where absolute-mode endpoints and nested
sub-calls dropped the parent's frame origin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:51:51 -04:00
38 changed files with 1191 additions and 266 deletions
-3
View File
@@ -211,8 +211,5 @@ FakesAssemblies/
.superpowers/ .superpowers/
docs/superpowers/ docs/superpowers/
# Documentation (manuals, templates, etc.)
docs/
# Launch settings # Launch settings
**/Properties/launchSettings.json **/Properties/launchSettings.json
+3
View File
@@ -57,6 +57,8 @@ File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
- `NestReader`/`NestWriter` — custom ZIP-based nest format (JSON metadata + G-code programs, v2 format). - `NestReader`/`NestWriter` — custom ZIP-based nest format (JSON metadata + G-code programs, v2 format).
- `ProgramReader` — G-code text parser. - `ProgramReader` — G-code text parser.
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types. - `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
### OpenNest.Console (console app, depends on Core + Engine + IO) ### OpenNest.Console (console app, depends on Core + Engine + IO)
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`). Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`).
@@ -117,3 +119,4 @@ Always keep `README.md` and `CLAUDE.md` up to date when making changes that affe
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies. - `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load). - **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity. - **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). Console, MCP, API, and Training projects use `ImportDrawing` for headless conversion. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
+13 -9
View File
@@ -5,8 +5,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
namespace OpenNest.Api; namespace OpenNest.Api;
@@ -30,15 +28,21 @@ public static class NestRunner
if (!File.Exists(part.DxfPath)) if (!File.Exists(part.DxfPath))
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath); throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
var geometry = Dxf.GetGeometry(part.DxfPath); Drawing drawing;
if (geometry.Count == 0) try
{
drawing = CadImporter.ImportDrawing(part.DxfPath,
new CadImportOptions { Quantity = part.Quantity });
}
catch (System.Exception ex)
{
throw new InvalidOperationException(
$"Failed to import DXF: {part.DxfPath}", ex);
}
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}"); throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
var normalized = ShapeProfile.NormalizeEntities(geometry);
var pgm = ConvertGeometry.ToProgram(normalized);
var name = Path.GetFileNameWithoutExtension(part.DxfPath);
var drawing = new Drawing(name);
drawing.Program = pgm;
drawings.Add(drawing); drawings.Add(drawing);
} }
+6 -17
View File
@@ -1,5 +1,4 @@
using OpenNest; using OpenNest;
using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using System; using System;
@@ -241,25 +240,15 @@ static class NestConsole
static Drawing ImportDxf(string path) static Drawing ImportDxf(string path)
{ {
var geometry = Dxf.GetGeometry(path); try
if (geometry.Count == 0)
{ {
Console.Error.WriteLine($"Error: failed to read DXF file or no geometry found: {path}"); return CadImporter.ImportDrawing(path);
}
catch (System.Exception ex)
{
Console.Error.WriteLine($"Error: failed to import DXF '{path}': {ex.Message}");
return null; return null;
} }
var normalized = ShapeProfile.NormalizeEntities(geometry);
var pgm = ConvertGeometry.ToProgram(normalized);
if (pgm == null)
{
Console.Error.WriteLine($"Error: failed to convert geometry: {path}");
return null;
}
var name = Path.GetFileNameWithoutExtension(path);
return new Drawing(name, pgm);
} }
static void ApplyTemplate(Plate plate, Options options) static void ApplyTemplate(Plate plate, Options options)
+27 -8
View File
@@ -128,6 +128,12 @@ namespace OpenNest.CNC
{ {
var code = Codes[i]; var code = Codes[i];
if (code is SubProgramCall subpgm)
{
subpgm.Offset = new Geometry.Vector(
subpgm.Offset.X + x, subpgm.Offset.Y + y);
}
if (code is Motion == false) if (code is Motion == false)
continue; continue;
@@ -150,6 +156,12 @@ namespace OpenNest.CNC
{ {
var code = Codes[i]; var code = Codes[i];
if (code is SubProgramCall subpgm)
{
subpgm.Offset = new Geometry.Vector(
subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y);
}
if (code is Motion == false) if (code is Motion == false)
continue; continue;
@@ -288,6 +300,10 @@ namespace OpenNest.CNC
private Box BoundingBox(ref Vector pos) private Box BoundingBox(ref Vector pos)
{ {
// Capture the frame origin at entry. Sub-program Offsets and
// absolute-mode endpoints are relative to this fixed origin.
var frameOrigin = pos;
double minX = 0.0; double minX = 0.0;
double minY = 0.0; double minY = 0.0;
double maxX = 0.0; double maxX = 0.0;
@@ -303,7 +319,7 @@ namespace OpenNest.CNC
{ {
var line = (LinearMove)code; var line = (LinearMove)code;
var pt = Mode == Mode.Absolute ? var pt = Mode == Mode.Absolute ?
line.EndPoint : frameOrigin + line.EndPoint :
line.EndPoint + pos; line.EndPoint + pos;
if (pt.X > maxX) if (pt.X > maxX)
@@ -325,7 +341,7 @@ namespace OpenNest.CNC
{ {
var line = (RapidMove)code; var line = (RapidMove)code;
var pt = Mode == Mode.Absolute var pt = Mode == Mode.Absolute
? line.EndPoint ? frameOrigin + line.EndPoint
: line.EndPoint + pos; : line.EndPoint + pos;
if (pt.X > maxX) if (pt.X > maxX)
@@ -358,8 +374,8 @@ namespace OpenNest.CNC
} }
else else
{ {
endpt = arc.EndPoint; endpt = frameOrigin + arc.EndPoint;
centerpt = arc.CenterPoint; centerpt = frameOrigin + arc.CenterPoint;
} }
double minX1; double minX1;
@@ -433,10 +449,13 @@ namespace OpenNest.CNC
case CodeType.SubProgramCall: case CodeType.SubProgramCall:
{ {
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
var subPos = subpgm.Offset.X != 0 || subpgm.Offset.Y != 0 if (subpgm.Program == null)
? new Vector(subpgm.Offset.X, subpgm.Offset.Y) break;
: pos;
var box = subpgm.Program.BoundingBox(ref subPos); // Sub-program frame origin in this program's frame
// is frameOrigin + Offset, regardless of current pos.
pos = frameOrigin + subpgm.Offset;
var box = subpgm.Program.BoundingBox(ref pos);
if (box.Left < minX) if (box.Left < minX)
minX = box.Left; minX = box.Left;
+8 -3
View File
@@ -1,4 +1,5 @@
using OpenNest.Geometry; using System.Text;
using OpenNest.Geometry;
using OpenNest.Math; using OpenNest.Math;
namespace OpenNest.CNC namespace OpenNest.CNC
@@ -90,9 +91,13 @@ namespace OpenNest.CNC
public override string ToString() public override string ToString()
{ {
var sb = new StringBuilder();
sb.Append($"G65 P{Id}");
if (Offset.X != 0 || Offset.Y != 0) if (Offset.X != 0 || Offset.Y != 0)
return string.Format("G65 P{0} X{1} Y{2}", Id, Offset.X, Offset.Y); sb.Append($" X{Offset.X} Y{Offset.Y}");
return string.Format("G65 P{0} R{1}", Id, Rotation); if (Rotation != 0)
sb.Append($" R{Rotation}");
return sb.ToString();
} }
} }
} }
+53 -9
View File
@@ -1,4 +1,4 @@
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Geometry; using OpenNest.Geometry;
namespace OpenNest.Converters namespace OpenNest.Converters
@@ -9,7 +9,6 @@ namespace OpenNest.Converters
/// Converts the program to absolute coordinates. /// Converts the program to absolute coordinates.
/// Does NOT check program mode before converting. /// Does NOT check program mode before converting.
/// </summary> /// </summary>
/// <param name="pgm"></param>
public static void ToAbsolute(Program pgm) public static void ToAbsolute(Program pgm)
{ {
var pos = new Vector(0, 0); var pos = new Vector(0, 0);
@@ -17,21 +16,27 @@ namespace OpenNest.Converters
for (int i = 0; i < pgm.Codes.Count; ++i) for (int i = 0; i < pgm.Codes.Count; ++i)
{ {
var code = pgm.Codes[i]; var code = pgm.Codes[i];
var motion = code as Motion;
if (motion != null) if (code is SubProgramCall subCall && subCall.Program != null)
{ {
motion.Offset(pos); // Sub-program is placed at Offset in this program's frame.
// After it runs, the tool is at Offset + (sub's end in its own frame).
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
continue;
}
if (code is Motion motion)
{
motion.Offset(pos.X, pos.Y);
pos = motion.EndPoint; pos = motion.EndPoint;
} }
} }
} }
/// <summary> /// <summary>
/// Converts the program to intermental coordinates. /// Converts the program to incremental coordinates.
/// Does NOT check program mode before converting. /// Does NOT check program mode before converting.
/// </summary> /// </summary>
/// <param name="pgm"></param>
public static void ToIncremental(Program pgm) public static void ToIncremental(Program pgm)
{ {
var pos = new Vector(0, 0); var pos = new Vector(0, 0);
@@ -39,9 +44,16 @@ namespace OpenNest.Converters
for (int i = 0; i < pgm.Codes.Count; ++i) for (int i = 0; i < pgm.Codes.Count; ++i)
{ {
var code = pgm.Codes[i]; var code = pgm.Codes[i];
var motion = code as Motion;
if (motion != null) if (code is SubProgramCall subCall && subCall.Program != null)
{
// Sub-program is placed at Offset in this program's frame,
// regardless of where the tool was before the call.
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
continue;
}
if (code is Motion motion)
{ {
var pos2 = motion.EndPoint; var pos2 = motion.EndPoint;
motion.Offset(-pos.X, -pos.Y); motion.Offset(-pos.X, -pos.Y);
@@ -49,5 +61,37 @@ namespace OpenNest.Converters
} }
} }
} }
/// <summary>
/// Computes the tool position after executing <paramref name="pgm"/>,
/// given that the program's frame origin is at <paramref name="startPos"/>
/// in the caller's frame. Walks nested sub-program calls recursively.
/// </summary>
private static Vector ComputeEndPosition(Program pgm, Vector startPos)
{
var pos = startPos;
for (int i = 0; i < pgm.Codes.Count; ++i)
{
var code = pgm.Codes[i];
if (code is SubProgramCall subCall && subCall.Program != null)
{
// Nested sub's frame origin in the caller's frame is startPos + Offset.
pos = ComputeEndPosition(subCall.Program, startPos + subCall.Offset);
continue;
}
if (code is Motion motion)
{
if (pgm.Mode == Mode.Incremental)
pos = pos + motion.EndPoint;
else
pos = startPos + motion.EndPoint;
}
}
return pos;
}
} }
} }
+6 -10
View File
@@ -20,6 +20,9 @@ namespace OpenNest.Converters
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry) private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
{ {
// Capture the frame origin at entry. Sub-program Offsets are relative
// to this fixed origin, not to the current tool position.
var frameOrigin = curpos;
mode = program.Mode; mode = program.Mode;
for (int i = 0; i < program.Length; ++i) for (int i = 0; i < program.Length; ++i)
@@ -43,20 +46,13 @@ namespace OpenNest.Converters
case CodeType.SubProgramCall: case CodeType.SubProgramCall:
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
var savedMode = mode; var savedMode = mode;
var savedPos = curpos;
// Position the sub-program at savedPos + Offset. // The sub-program's frame origin in this program's frame is
// savedPos is the base position ((0,0) here, Part.Location in rendering). // frameOrigin + Offset — independent of current tool position.
// Offset is the hole center in drawing-local coordinates. curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry); AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
mode = savedMode; mode = savedMode;
// Restore curpos: ConvertMode.ToIncremental skips SubProgramCalls
// when computing deltas, so subsequent incremental codes expect
// curpos to be where it was before the call.
curpos = savedPos;
break; break;
} }
} }
@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace OpenNest
{
public interface IMaterialProvidingPostProcessor
{
IEnumerable<string> GetMaterialNames();
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace OpenNest
{
public interface IPostProcessorNestAware
{
void PrepareForNest(Nest nest);
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ namespace OpenNest.Engine.Strategies
public int PlateNumber { get; init; } public int PlateNumber { get; init; }
public CancellationToken Token { get; init; } public CancellationToken Token { get; init; }
public IProgress<NestProgress> Progress { get; init; } public IProgress<NestProgress> Progress { get; init; }
public FillPolicy Policy { get; init; } public FillPolicy Policy { get; init; } = new FillPolicy(new DefaultFillComparer());
public int MaxQuantity { get; init; } public int MaxQuantity { get; init; }
public PartType PartType { get; set; } public PartType PartType { get; set; }
+39
View File
@@ -0,0 +1,39 @@
namespace OpenNest.IO
{
/// <summary>
/// Options controlling how <see cref="CadImporter"/> loads a CAD file
/// and builds a <see cref="Drawing"/>.
/// </summary>
public class CadImportOptions
{
/// <summary>
/// Detector name to use for bend detection. Null = auto-detect.
/// </summary>
public string BendDetectorName { get; set; }
/// <summary>
/// When false, skips bend detection entirely. Default true.
/// </summary>
public bool DetectBends { get; set; } = true;
/// <summary>
/// Override the drawing name. Null = filename without extension.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Required quantity on the produced drawing. Default 1.
/// </summary>
public int Quantity { get; set; } = 1;
/// <summary>
/// Customer name on the produced drawing. Default null.
/// </summary>
public string Customer { get; set; }
/// <summary>
/// Returns a default options instance.
/// </summary>
public static CadImportOptions Default => new CadImportOptions();
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using OpenNest.Bending;
using OpenNest.Geometry;
namespace OpenNest.IO
{
/// <summary>
/// Intermediate result of <see cref="CadImporter.Import"/>. Holds raw loaded
/// geometry and detected bends. Callers may mutate <see cref="Entities"/> and
/// <see cref="Bends"/> before passing to <see cref="CadImporter.BuildDrawing"/>.
/// </summary>
public class CadImportResult
{
/// <summary>
/// All entities loaded from the source file, including promoted bend
/// source entities. Mutable.
/// </summary>
public List<Entity> Entities { get; set; } = new List<Entity>();
/// <summary>
/// Bends detected during import. Mutable — callers may add, remove,
/// or replace entries before building the drawing.
/// </summary>
public List<Bend> Bends { get; set; } = new List<Bend>();
/// <summary>
/// Bounding box of <see cref="Entities"/> at import time. May be stale
/// if callers mutate <see cref="Entities"/>; recompute if needed.
/// </summary>
public Box Bounds { get; set; }
/// <summary>
/// Absolute path to the source file.
/// </summary>
public string SourcePath { get; set; }
/// <summary>
/// Default drawing name (filename without extension, unless overridden).
/// </summary>
public string Name { get; set; }
}
}
+140
View File
@@ -0,0 +1,140 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Bending;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO.Bending;
namespace OpenNest.IO
{
/// <summary>
/// Shared service that converts a CAD source file into a fully-populated
/// <see cref="Drawing"/>. Used by the UI, console, MCP, API, and training
/// tools so all code paths produce identical drawings.
/// </summary>
public static class CadImporter
{
/// <summary>
/// Load a DXF file, run bend detection, and return a mutable result
/// ready for interactive editing or direct conversion to a Drawing.
/// </summary>
public static CadImportResult Import(string path, CadImportOptions options = null)
{
options ??= CadImportOptions.Default;
var dxf = Dxf.Import(path);
var bends = new List<Bend>();
if (options.DetectBends && dxf.Document != null)
{
bends = options.BendDetectorName == null
? BendDetectorRegistry.AutoDetect(dxf.Document)
: BendDetectorRegistry.GetByName(options.BendDetectorName)
?.DetectBends(dxf.Document)
?? new List<Bend>();
}
Bend.UpdateEtchEntities(dxf.Entities, bends);
return new CadImportResult
{
Entities = dxf.Entities,
Bends = bends,
Bounds = dxf.Entities.GetBoundingBox(),
SourcePath = path,
Name = options.Name ?? Path.GetFileNameWithoutExtension(path),
};
}
/// <summary>
/// Convenience for headless callers: Import a file and build a Drawing
/// in a single call, using all loaded entities and detected bends.
/// </summary>
public static Drawing ImportDrawing(string path, CadImportOptions options = null)
{
options ??= CadImportOptions.Default;
var result = Import(path, options);
return BuildDrawing(
result,
result.Entities,
result.Bends,
options.Quantity,
options.Customer,
editedProgram: null);
}
/// <summary>
/// Build a fully-populated <see cref="Drawing"/> from an import result plus
/// the caller's current entity and bend state. UI callers pass the currently
/// visible subset; headless callers pass the full lists.
///
/// The produced drawing has:
/// - Program generated from the visible entities, with its first rapid moved
/// to the origin and the pierce location stored in Source.Offset
/// - SourceEntities containing all non-bend-source entities from the result
/// - SuppressedEntityIds containing entities whose layer or IsVisible is false
/// - Bends copied from the provided list
/// - Customer, Quantity, Source.Path from options / result
/// </summary>
/// <param name="result">Import result from <see cref="Import"/>.</param>
/// <param name="entities">
/// Entities to build the program from. Typically the currently visible subset.
/// </param>
/// <param name="bends">Bends to attach to the drawing.</param>
/// <param name="quantity">Required quantity.</param>
/// <param name="customer">Customer name, or null.</param>
/// <param name="editedProgram">
/// When non-null, replaces the generated program (used by the UI to honor
/// in-place G-code edits). Source.Offset is still populated from the
/// generated program so round-trips stay consistent.
/// </param>
public static Drawing BuildDrawing(
CadImportResult result,
IEnumerable<Entity> entities,
IEnumerable<Bend> bends,
int quantity,
string customer,
OpenNest.CNC.Program editedProgram)
{
var visible = entities as IList<Entity> ?? new List<Entity>(entities);
var bendList = bends as IList<Bend> ?? new List<Bend>(bends);
var normalized = ShapeProfile.NormalizeEntities(visible);
var pgm = ConvertGeometry.ToProgram(normalized);
var offset = Vector.Zero;
if (pgm != null && pgm.Codes.Count > 0 && pgm[0].Type == OpenNest.CNC.CodeType.RapidMove)
{
var rapid = (OpenNest.CNC.RapidMove)pgm[0];
offset = rapid.EndPoint;
pgm.Offset(-offset);
}
var drawing = new Drawing(result.Name)
{
Color = Drawing.GetNextColor(),
Customer = customer,
};
drawing.Source.Path = result.SourcePath;
drawing.Source.Offset = offset;
drawing.Quantity.Required = quantity;
drawing.Bends.AddRange(bendList);
drawing.Program = editedProgram ?? pgm;
var bendSources = new HashSet<Entity>(
bendList.Where(b => b.SourceEntity != null).Select(b => b.SourceEntity));
drawing.SourceEntities = result.Entities
.Where(e => !bendSources.Contains(e))
.ToList();
drawing.SuppressedEntityIds = new HashSet<System.Guid>(
drawing.SourceEntities
.Where(e => !(e.Layer != null && e.Layer.IsVisible && e.IsVisible))
.Select(e => e.Id));
return drawing;
}
}
}
+11 -19
View File
@@ -1,6 +1,4 @@
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using OpenNest.Shapes; using OpenNest.Shapes;
using System.ComponentModel; using System.ComponentModel;
@@ -96,24 +94,18 @@ namespace OpenNest.Mcp.Tools
if (!File.Exists(path)) if (!File.Exists(path))
return $"Error: file not found: {path}"; return $"Error: file not found: {path}";
var geometry = Dxf.GetGeometry(path); try
{
var drawing = CadImporter.ImportDrawing(path, new CadImportOptions { Name = name });
_session.Drawings.Add(drawing);
if (geometry.Count == 0) var bbox = drawing.Program.BoundingBox();
return "Error: failed to read DXF file or no geometry found"; return $"Imported drawing '{drawing.Name}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
}
var normalized = ShapeProfile.NormalizeEntities(geometry); catch (System.Exception ex)
var pgm = ConvertGeometry.ToProgram(normalized); {
return $"Error: failed to import '{path}': {ex.Message}";
if (pgm == null) }
return "Error: failed to convert geometry to program";
var drawingName = name ?? Path.GetFileNameWithoutExtension(path);
var drawing = new Drawing(drawingName, pgm);
drawing.Color = Drawing.GetNextColor();
_session.Drawings.Add(drawing);
var bbox = pgm.BoundingBox();
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
} }
[McpServerTool(Name = "create_drawing")] [McpServerTool(Name = "create_drawing")]
@@ -16,11 +16,16 @@ public sealed class CincinnatiPartSubprogramWriter
{ {
private readonly CincinnatiPostConfig _config; private readonly CincinnatiPostConfig _config;
private readonly CincinnatiFeatureWriter _featureWriter; private readonly CincinnatiFeatureWriter _featureWriter;
private readonly CoordinateFormatter _fmt;
private readonly Dictionary<int, int> _holeSubprograms;
public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config) public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config,
Dictionary<int, int> holeSubprograms = null)
{ {
_config = config; _config = config;
_featureWriter = new CincinnatiFeatureWriter(config); _featureWriter = new CincinnatiFeatureWriter(config);
_fmt = new CoordinateFormatter(config.PostedAccuracy);
_holeSubprograms = holeSubprograms;
} }
/// <summary> /// <summary>
@@ -44,6 +49,15 @@ public sealed class CincinnatiPartSubprogramWriter
for (var i = 0; i < ordered.Count; i++) for (var i = 0; i < ordered.Count; i++)
{ {
var (codes, isEtch) = ordered[i]; var (codes, isEtch) = ordered[i];
var isLastFeature = i == ordered.Count - 1;
// SubProgramCall features are emitted as M98 hole calls
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
{
WriteHoleSubprogramCall(w, holeCall, i, isLastFeature);
continue;
}
var featureNumber = i == 0 var featureNumber = i == 0
? _config.FeatureLineNumberStart ? _config.FeatureLineNumberStart
: 1000 + i + 1; : 1000 + i + 1;
@@ -55,7 +69,7 @@ public sealed class CincinnatiPartSubprogramWriter
FeatureNumber = featureNumber, FeatureNumber = featureNumber,
PartName = drawingName, PartName = drawingName,
IsFirstFeatureOfPart = false, IsFirstFeatureOfPart = false,
IsLastFeatureOnSheet = i == ordered.Count - 1, IsLastFeatureOnSheet = isLastFeature,
IsSafetyHeadraise = false, IsSafetyHeadraise = false,
IsExteriorFeature = false, IsExteriorFeature = false,
IsEtch = isEtch, IsEtch = isEtch,
@@ -70,6 +84,30 @@ public sealed class CincinnatiPartSubprogramWriter
w.WriteLine($"M99 (END OF {drawingName})"); w.WriteLine($"M99 (END OF {drawingName})");
} }
private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call,
int featureIndex, bool isLastFeature)
{
var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num)
? num : call.Id;
var featureNumber = featureIndex == 0
? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1;
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber} ");
sb.Append($"G52 X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}");
w.WriteLine(sb.ToString());
w.WriteLine($"M98 P{postSubNum}");
w.WriteLine("G52 X0 Y0");
if (!isLastFeature)
w.WriteLine("M47");
}
/// <summary> /// <summary>
/// If the program has no leading rapid, inserts a synthetic rapid at the /// If the program has no leading rapid, inserts a synthetic rapid at the
/// last motion endpoint (the contour return point). This ensures the feature /// last motion endpoint (the contour return point). This ensures the feature
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
namespace OpenNest.Posts.Cincinnati namespace OpenNest.Posts.Cincinnati
{ {
@@ -277,6 +279,24 @@ namespace OpenNest.Posts.Cincinnati
[DisplayName("Etch Libraries")] [DisplayName("Etch Libraries")]
[Description("Gas-to-library mapping for etch operations.")] [Description("Gas-to-library mapping for etch operations.")]
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new(); public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
[Category("B. Libraries")]
[DisplayName("Selected Library")]
[Description("Overrides Material/Thickness/Gas auto-resolution. Pick an existing entry from Material Libraries, or leave blank to auto-resolve.")]
[TypeConverter(typeof(MaterialLibraryNameConverter))]
public string SelectedLibrary { get; set; } = "";
public string FindBestLibrary(string materialName, double thickness)
{
if (MaterialLibraries == null || string.IsNullOrEmpty(materialName))
return "";
return MaterialLibraries
.Where(e => string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase))
.OrderBy(e => System.Math.Abs(e.Thickness - thickness))
.Select(e => e.Library)
.FirstOrDefault() ?? "";
}
} }
public class MaterialLibraryEntry public class MaterialLibraryEntry
@@ -9,7 +9,7 @@ using OpenNest.CNC;
namespace OpenNest.Posts.Cincinnati namespace OpenNest.Posts.Cincinnati
{ {
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor, IPostProcessorNestAware, IMaterialProvidingPostProcessor
{ {
private static readonly JsonSerializerOptions JsonOptions = new() private static readonly JsonSerializerOptions JsonOptions = new()
{ {
@@ -25,6 +25,23 @@ namespace OpenNest.Posts.Cincinnati
object IConfigurablePostProcessor.Config => Config; object IConfigurablePostProcessor.Config => Config;
public IEnumerable<string> GetMaterialNames()
{
if (Config?.MaterialLibraries == null)
return System.Array.Empty<string>();
return Config.MaterialLibraries
.Select(e => e.Material)
.Where(s => !string.IsNullOrWhiteSpace(s));
}
public void PrepareForNest(Nest nest)
{
var materialName = nest?.Material?.Name ?? "";
var thickness = nest?.Thickness ?? 0.0;
Config.SelectedLibrary = Config.FindBestLibrary(materialName, thickness);
}
public CincinnatiPostProcessor() public CincinnatiPostProcessor()
{ {
var configPath = GetConfigPath(); var configPath = GetConfigPath();
@@ -128,7 +145,8 @@ namespace OpenNest.Posts.Cincinnati
// Part sub-programs (if enabled) // Part sub-programs (if enabled)
if (subprogramEntries != null) if (subprogramEntries != null)
{ {
var partSubWriter = new CincinnatiPartSubprogramWriter(Config); var partSubWriter = new CincinnatiPartSubprogramWriter(Config,
holeMapping.Count > 0 ? holeMapping : null);
var sheetDiagonal = firstPlate != null var sheetDiagonal = firstPlate != null
? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width ? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width
+ firstPlate.Size.Length * firstPlate.Size.Length) + firstPlate.Size.Length * firstPlate.Size.Length)
@@ -226,12 +226,23 @@ public sealed class CincinnatiSheetWriter
? _config.FeatureLineNumberStart ? _config.FeatureLineNumberStart
: 1000 + featureIndex + 1; : 1000 + featureIndex + 1;
// Shift the local origin to the hole center via G52 (manual §1.52).
// G52 does not move the nozzle, so the sub-program's first rapid
// (the lead-in to the pierce point) takes the tool straight from the
// previous feature's end to pierce. The hole sub-program is authored
// in hole-local coordinates and resolves to `hole + local` under the
// shift. See docs/cincinnati-post-output.md for the full bracket.
var sb = new StringBuilder(); var sb = new StringBuilder();
if (_config.UseLineNumbers) if (_config.UseLineNumbers)
sb.Append($"N{featureNumber} "); sb.Append($"N{featureNumber} ");
sb.Append($"M98 P{postSubNum} X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}"); sb.Append($"G52 X{_fmt.FormatCoord(call.Offset.X)} Y{_fmt.FormatCoord(call.Offset.Y)}");
w.WriteLine(sb.ToString()); w.WriteLine(sb.ToString());
w.WriteLine($"M98 P{postSubNum}");
// Cancel the local shift (manual §1.52).
w.WriteLine("G52 X0 Y0");
if (!isLastFeature) if (!isLastFeature)
w.WriteLine("M47"); w.WriteLine("M47");
} }
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace OpenNest.Posts.Cincinnati
{
public sealed class MaterialLibraryNameConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) => false;
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
var config = context?.Instance as CincinnatiPostConfig;
var names = new List<string> { "" };
if (config?.MaterialLibraries != null)
{
names.AddRange(config.MaterialLibraries
.Select(e => e.Library)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase));
}
return new StandardValuesCollection(names);
}
}
}
@@ -10,15 +10,20 @@ public sealed class MaterialLibraryResolver
private readonly List<MaterialLibraryEntry> _materialLibraries; private readonly List<MaterialLibraryEntry> _materialLibraries;
private readonly List<EtchLibraryEntry> _etchLibraries; private readonly List<EtchLibraryEntry> _etchLibraries;
private readonly string _selectedLibrary;
public MaterialLibraryResolver(CincinnatiPostConfig config) public MaterialLibraryResolver(CincinnatiPostConfig config)
{ {
_materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>(); _materialLibraries = config.MaterialLibraries ?? new List<MaterialLibraryEntry>();
_etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>(); _etchLibraries = config.EtchLibraries ?? new List<EtchLibraryEntry>();
_selectedLibrary = config.SelectedLibrary ?? "";
} }
public string ResolveCutLibrary(string materialName, double thickness, string gas) public string ResolveCutLibrary(string materialName, double thickness, string gas)
{ {
if (!string.IsNullOrEmpty(_selectedLibrary))
return EnsureLibExtension(_selectedLibrary);
var entry = _materialLibraries.FirstOrDefault(e => var entry = _materialLibraries.FirstOrDefault(e =>
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) && string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) &&
System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance && System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance &&
@@ -6,11 +6,19 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" /> <ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="OpenNest.Posts.Cincinnati.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="CopyToPostsDir" AfterTargets="Build"> <Target Name="CopyToPostsDir" AfterTargets="Build">
<PropertyGroup> <PropertyGroup>
<PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir> <PostsDir>..\OpenNest\bin\$(Configuration)\$(TargetFramework)\Posts\</PostsDir>
<ConfigJson>$(MSBuildProjectDirectory)\OpenNest.Posts.Cincinnati.json</ConfigJson>
<DeployedConfigJson>$(PostsDir)OpenNest.Posts.Cincinnati.json</DeployedConfigJson>
</PropertyGroup> </PropertyGroup>
<MakeDir Directories="$(PostsDir)" /> <MakeDir Directories="$(PostsDir)" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" /> <Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" />
<Copy SourceFiles="$(ConfigJson)" DestinationFolder="$(PostsDir)" SkipUnchangedFiles="true" ContinueOnError="true" Condition="!Exists('$(DeployedConfigJson)')" />
</Target> </Target>
</Project> </Project>
@@ -0,0 +1,163 @@
{
"ConfigurationName": "CL940",
"PostedUnits": "Inches",
"PostedAccuracy": 4,
"UseLineNumbers": true,
"FeatureLineNumberStart": 1,
"UseSheetSubprograms": true,
"SheetSubprogramStart": 101,
"UsePartSubprograms": false,
"PartSubprogramStart": 200,
"VariableDeclarationSubprogram": 100,
"CoordModeBetweenParts": "G92",
"ProcessParameterMode": "LibraryFile",
"DefaultAssistGas": "O2",
"DefaultEtchGas": "N2",
"UseExactStopMode": false,
"UseSpeedGas": false,
"UseAntiDive": true,
"UseSmartRapids": false,
"KerfCompensation": "ControllerSide",
"DefaultKerfSide": "Left",
"InteriorM47": "Always",
"ExteriorM47": "Always",
"M47OverrideDistanceThreshold": null,
"SafetyHeadraiseDistance": 2000,
"PalletExchange": "EndOfSheet",
"LeadInFeedratePercent": 0.5,
"LeadInArcLine2FeedratePercent": 0.5,
"LeadOutFeedratePercent": 0.5,
"CircleFeedrateMultiplier": 0.8,
"ArcFeedrate": "None",
"ArcFeedrateRanges": [
{ "MaxRadius": 0.125, "FeedratePercent": 0.25, "VariableNumber": 123 },
{ "MaxRadius": 0.75, "FeedratePercent": 0.5, "VariableNumber": 124 },
{ "MaxRadius": 4.5, "FeedratePercent": 0.8, "VariableNumber": 125 }
],
"UserVariableStart": 200,
"SheetWidthVariable": 110,
"SheetLengthVariable": 111,
"MaterialLibraries": [
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "AIR", "Library": "AL032AIR" },
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "N2", "Library": "AL032N2" },
{ "Material": "Aluminum", "Thickness": 0.032, "Gas": "O2", "Library": "AL032O2" },
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "AIR", "Library": "AL050AIR" },
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "N2", "Library": "AL050N2" },
{ "Material": "Aluminum", "Thickness": 0.050, "Gas": "O2", "Library": "AL050O2" },
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "AIR", "Library": "AL063AIR" },
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "N2", "Library": "AL063N2" },
{ "Material": "Aluminum", "Thickness": 0.063, "Gas": "O2", "Library": "AL063O2" },
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "AIR", "Library": "AL080AIR" },
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "N2", "Library": "AL080N2" },
{ "Material": "Aluminum", "Thickness": 0.080, "Gas": "O2", "Library": "AL080O2" },
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "AIR", "Library": "AL090AIR" },
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "N2", "Library": "AL090N2" },
{ "Material": "Aluminum", "Thickness": 0.090, "Gas": "O2", "Library": "AL090O2" },
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "AIR", "Library": "AL100AIR" },
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "N2", "Library": "AL100N2" },
{ "Material": "Aluminum", "Thickness": 0.100, "Gas": "O2", "Library": "AL100O2" },
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "AIR", "Library": "AL125AIR" },
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "N2", "Library": "AL125N2" },
{ "Material": "Aluminum", "Thickness": 0.125, "Gas": "O2", "Library": "AL125O2" },
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "AIR", "Library": "AL190AIR" },
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "N2", "Library": "AL190N2" },
{ "Material": "Aluminum", "Thickness": 0.190, "Gas": "O2", "Library": "AL190O2" },
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "AIR", "Library": "AL250AIR" },
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "N2", "Library": "AL250N2" },
{ "Material": "Aluminum", "Thickness": 0.250, "Gas": "O2", "Library": "AL250O2" },
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "AIR", "Library": "AL375AIR" },
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "N2", "Library": "AL375N2" },
{ "Material": "Aluminum", "Thickness": 0.375, "Gas": "O2", "Library": "AL375O2" },
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "AIR", "Library": "AL500AIR" },
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "N2", "Library": "AL500N2" },
{ "Material": "Aluminum", "Thickness": 0.500, "Gas": "O2", "Library": "AL500O2" },
{ "Material": "Aluminum", "Thickness": 0.625, "Gas": "N2", "Library": "AL625N2" },
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "AIR", "Library": "AL750AIR" },
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "N2", "Library": "AL750N2" },
{ "Material": "Aluminum", "Thickness": 0.750, "Gas": "O2", "Library": "AL750O2" },
{ "Material": "Aluminum", "Thickness": 1.000, "Gas": "AIR", "Library": "AL1000AIR" },
{ "Material": "Aluminum", "Thickness": 1.000, "Gas": "N2", "Library": "AL1000N2" },
{ "Material": "Galvanized Steel", "Thickness": 0.135, "Gas": "N2", "Library": "GALV135N2" },
{ "Material": "Galvanized Steel", "Thickness": 0.188, "Gas": "N2", "Library": "GALV188N2" },
{ "Material": "Carbon Steel", "Thickness": 0.036, "Gas": "AIR", "Library": "MS036AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.036, "Gas": "N2", "Library": "MS036N2" },
{ "Material": "Carbon Steel", "Thickness": 0.048, "Gas": "AIR", "Library": "MS048AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.048, "Gas": "N2", "Library": "MS048N2" },
{ "Material": "Carbon Steel", "Thickness": 0.060, "Gas": "AIR", "Library": "MS060AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.060, "Gas": "N2", "Library": "MS060N2" },
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "AIR", "Library": "MS075AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "N2", "Library": "MS075N2" },
{ "Material": "Carbon Steel", "Thickness": 0.075, "Gas": "N2", "Library": "MS075N2FE" },
{ "Material": "Carbon Steel", "Thickness": 0.090, "Gas": "N2", "Library": "MS090N2" },
{ "Material": "Carbon Steel", "Thickness": 0.105, "Gas": "AIR", "Library": "MS105AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.105, "Gas": "N2", "Library": "MS105N2" },
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "AIR", "Library": "MS120AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "N2", "Library": "MS120N2" },
{ "Material": "Carbon Steel", "Thickness": 0.120, "Gas": "N2", "Library": "MS120N2FE" },
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "AIR", "Library": "MS135AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2" },
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2FE" },
{ "Material": "Carbon Steel", "Thickness": 0.135, "Gas": "N2", "Library": "MS135N2Panel" },
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "AIR", "Library": "MS188AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "N2", "Library": "MS188N2" },
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "N2", "Library": "MS188N2FLOORPLATE" },
{ "Material": "Carbon Steel", "Thickness": 0.188, "Gas": "O2", "Library": "MS188O2" },
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "AIR", "Library": "MS250AIR" },
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "N2", "Library": "MS250N2" },
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "N2", "Library": "MS250N2FLOORPLATE" },
{ "Material": "Carbon Steel", "Thickness": 0.250, "Gas": "O2", "Library": "MS250O2" },
{ "Material": "Carbon Steel", "Thickness": 0.313, "Gas": "O2", "Library": "MS313O2" },
{ "Material": "Carbon Steel", "Thickness": 0.375, "Gas": "O2", "Library": "MS375O2" },
{ "Material": "Carbon Steel", "Thickness": 0.500, "Gas": "N2", "Library": "MS500N2" },
{ "Material": "Carbon Steel", "Thickness": 0.500, "Gas": "O2", "Library": "MS500O2" },
{ "Material": "Carbon Steel", "Thickness": 0.625, "Gas": "O2", "Library": "MS625O2" },
{ "Material": "Carbon Steel", "Thickness": 0.750, "Gas": "O2", "Library": "MS750O2" },
{ "Material": "Carbon Steel", "Thickness": 1.000, "Gas": "O2", "Library": "MS1000O2" },
{ "Material": "Stainless Steel", "Thickness": 0.036, "Gas": "AIR", "Library": "SS036AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.036, "Gas": "N2", "Library": "SS036N2" },
{ "Material": "Stainless Steel", "Thickness": 0.048, "Gas": "AIR", "Library": "SS048AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.048, "Gas": "N2", "Library": "SS048N2" },
{ "Material": "Stainless Steel", "Thickness": 0.060, "Gas": "AIR", "Library": "SS060AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.060, "Gas": "N2", "Library": "SS060N2" },
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "AIR", "Library": "SS075AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "N2", "Library": "SS075N2" },
{ "Material": "Stainless Steel", "Thickness": 0.075, "Gas": "N2", "Library": "SS075N2FE" },
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "AIR", "Library": "SS105AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "N2", "Library": "SS105N2" },
{ "Material": "Stainless Steel", "Thickness": 0.105, "Gas": "N2", "Library": "SS105N2FE" },
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "AIR", "Library": "SS120AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "N2", "Library": "SS120N2" },
{ "Material": "Stainless Steel", "Thickness": 0.120, "Gas": "N2", "Library": "SS120N2FE" },
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "AIR", "Library": "SS135AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "N2", "Library": "SS135N2" },
{ "Material": "Stainless Steel", "Thickness": 0.135, "Gas": "N2", "Library": "SS135N2FE" },
{ "Material": "Stainless Steel", "Thickness": 0.188, "Gas": "AIR", "Library": "SS188AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.188, "Gas": "N2", "Library": "SS188N2" },
{ "Material": "Stainless Steel", "Thickness": 0.250, "Gas": "AIR", "Library": "SS250AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.250, "Gas": "N2", "Library": "SS250N2" },
{ "Material": "Stainless Steel", "Thickness": 0.313, "Gas": "N2", "Library": "SS313N2" },
{ "Material": "Stainless Steel", "Thickness": 0.375, "Gas": "AIR", "Library": "SS375AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.375, "Gas": "N2", "Library": "SS375N2" },
{ "Material": "Stainless Steel", "Thickness": 0.500, "Gas": "AIR", "Library": "SS500AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.500, "Gas": "N2", "Library": "SS500N2" },
{ "Material": "Stainless Steel", "Thickness": 0.625, "Gas": "N2", "Library": "SS625N2" },
{ "Material": "Stainless Steel", "Thickness": 0.750, "Gas": "AIR", "Library": "SS750AIR" },
{ "Material": "Stainless Steel", "Thickness": 0.750, "Gas": "N2", "Library": "SS750N2" },
{ "Material": "Stainless Steel", "Thickness": 1.000, "Gas": "AIR", "Library": "SS1000AIR" },
{ "Material": "Stainless Steel", "Thickness": 1.000, "Gas": "N2", "Library": "SS1000N2" },
{ "Material": "Phenolic", "Thickness": 0.0, "Gas": "", "Library": "Phenolic" },
{ "Material": "Gasket", "Thickness": 0.250, "Gas": "N2", "Library": "GASKET250N2" }
],
"EtchLibraries": [
{ "Gas": "AIR", "Library": "EtchAIR" },
{ "Gas": "N2", "Library": "EtchN2" },
{ "Gas": "N2", "Library": "EtchN2_fast" },
{ "Gas": "N2", "Library": "Etchn2_no_mark_pvc" },
{ "Gas": "O2", "Library": "EtchO2" },
{ "Gas": "O2", "Library": "ETCHO2FINE" }
]
}
@@ -44,6 +44,25 @@ public class HoleSubProgramTests
Assert.Contains("Y2.5", str); Assert.Contains("Y2.5", str);
} }
[Fact]
public void SubProgramCall_ToString_IncludesOffsetAndRotation()
{
var call = new SubProgramCall { Id = 1000, Offset = new Vector(1.5, 2.5), Rotation = 30 };
var str = call.ToString();
Assert.Contains("P1000", str);
Assert.Contains("X1.5", str);
Assert.Contains("Y2.5", str);
Assert.Contains("R30", str);
}
[Fact]
public void SubProgramCall_ToString_OmitsZeroFields()
{
var call = new SubProgramCall { Id = 1000 };
var str = call.ToString();
Assert.Equal("G65 P1000", str);
}
[Fact] [Fact]
public void Program_SubPrograms_EmptyByDefault() public void Program_SubPrograms_EmptyByDefault()
{ {
+138
View File
@@ -0,0 +1,138 @@
using System.IO;
using System.Linq;
using OpenNest.IO;
using Xunit;
namespace OpenNest.Tests.IO
{
public class CadImporterTests
{
private static string TestDxf =>
Path.Combine("Bending", "TestData", "4526 A14 PT11.dxf");
[Fact]
public void Import_LoadsEntitiesAndDetectsBends()
{
var result = CadImporter.Import(TestDxf);
Assert.NotNull(result);
Assert.NotEmpty(result.Entities);
Assert.NotNull(result.Bends);
Assert.NotNull(result.Bounds);
Assert.Equal(TestDxf, result.SourcePath);
Assert.Equal("4526 A14 PT11", result.Name);
}
[Fact]
public void Import_WhenDetectBendsFalse_ReturnsEmptyBends()
{
var result = CadImporter.Import(TestDxf, new CadImportOptions { DetectBends = false });
Assert.Empty(result.Bends);
}
[Fact]
public void Import_WhenNameOverrideProvided_UsesOverride()
{
var result = CadImporter.Import(TestDxf, new CadImportOptions { Name = "custom" });
Assert.Equal("custom", result.Name);
}
[Fact]
public void Import_WhenNamedDetectorDoesNotExist_ReturnsEmptyBends()
{
// Exercises the named-detector branch: when BendDetectorName doesn't
// match any registered detector, bends should be an empty list
// (not a crash, and no fall-through to auto-detect).
var result = CadImporter.Import(TestDxf,
new CadImportOptions { BendDetectorName = "__nonexistent__" });
Assert.Empty(result.Bends);
}
[Fact]
public void BuildDrawing_ProducesDrawingWithProgramAndMetadata()
{
var result = CadImporter.Import(TestDxf);
var drawing = CadImporter.BuildDrawing(
result,
result.Entities,
result.Bends,
quantity: 5,
customer: "ACME",
editedProgram: null);
Assert.NotNull(drawing);
Assert.Equal("4526 A14 PT11", drawing.Name);
Assert.Equal("ACME", drawing.Customer);
Assert.Equal(5, drawing.Quantity.Required);
Assert.Equal(TestDxf, drawing.Source.Path);
Assert.NotNull(drawing.Program);
Assert.NotEmpty(drawing.Program.Codes);
Assert.NotNull(drawing.SourceEntities);
Assert.NotEmpty(drawing.SourceEntities);
}
[Fact]
public void BuildDrawing_ExtractsFirstRapidAsSourceOffset()
{
var result = CadImporter.Import(TestDxf);
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
quantity: 1, customer: null, editedProgram: null);
Assert.NotNull(drawing.Source.Offset);
// After offset extraction, the program's first rapid must start at origin.
var firstRapid = (OpenNest.CNC.RapidMove)drawing.Program.Codes[0];
Assert.Equal(0, firstRapid.EndPoint.X, 6);
Assert.Equal(0, firstRapid.EndPoint.Y, 6);
}
[Fact]
public void BuildDrawing_WhenEntityHidden_TracksSuppressedId()
{
var result = CadImporter.Import(TestDxf);
// Suppress the first non-bend-source entity
var bendSources = result.Bends
.Where(b => b.SourceEntity != null)
.Select(b => b.SourceEntity)
.ToHashSet();
var hidden = result.Entities.First(e => !bendSources.Contains(e));
hidden.IsVisible = false;
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
quantity: 1, customer: null, editedProgram: null);
Assert.Contains(hidden.Id, drawing.SuppressedEntityIds);
}
[Fact]
public void BuildDrawing_WhenEditedProgramProvided_UsesEditedProgram()
{
var result = CadImporter.Import(TestDxf);
var edited = new OpenNest.CNC.Program();
edited.MoveTo(new OpenNest.Geometry.Vector(0, 0));
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
quantity: 1, customer: null, editedProgram: edited);
Assert.Same(edited, drawing.Program);
}
[Fact]
public void ImportDrawing_ComposesImportAndBuild()
{
var drawing = CadImporter.ImportDrawing(TestDxf,
new CadImportOptions { Quantity = 3, Customer = "ACME" });
Assert.NotNull(drawing);
Assert.Equal("4526 A14 PT11", drawing.Name);
Assert.Equal(3, drawing.Quantity.Required);
Assert.Equal("ACME", drawing.Customer);
Assert.NotNull(drawing.Program);
Assert.NotNull(drawing.SourceEntities);
}
}
}
+15 -6
View File
@@ -1,8 +1,8 @@
using OpenNest; using OpenNest;
using OpenNest.Engine.BestFit; using OpenNest.Engine.BestFit;
using OpenNest.Engine.ML; using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Gpu; using OpenNest.Gpu;
using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using OpenNest.Training; using OpenNest.Training;
using System; using System;
@@ -128,17 +128,26 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
continue; continue;
} }
var entities = Dxf.GetGeometry(file); Drawing drawing;
if (entities.Count == 0) try
{
drawing = CadImporter.ImportDrawing(file,
new CadImportOptions { DetectBends = false, Name = Path.GetFileName(file) });
}
catch (System.Exception ex)
{
Console.WriteLine($" - SKIP ({ex.Message})");
skippedGeometry++;
continue;
}
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
{ {
Console.WriteLine(" - SKIP (no geometry)"); Console.WriteLine(" - SKIP (no geometry)");
skippedGeometry++; skippedGeometry++;
continue; continue;
} }
var drawing = new Drawing(Path.GetFileName(file));
var normalized = ShapeProfile.NormalizeEntities(entities);
drawing.Program = OpenNest.Converters.ConvertGeometry.ToProgram(normalized);
drawing.UpdateArea(); drawing.UpdateArea();
drawing.Color = PartColors[colorIndex % PartColors.Length]; drawing.Color = PartColors[colorIndex % PartColors.Length];
colorIndex++; colorIndex++;
+11 -6
View File
@@ -9,6 +9,12 @@ namespace OpenNest.Controls
{ {
public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos, public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos,
Pen pen, double spacing, float arrowSize) Pen pen, double spacing, float arrowSize)
{
DrawProgram(g, view, pgm, pos, ref pos, pen, spacing, arrowSize);
}
private static void DrawProgram(Graphics g, DrawControl view, Program pgm, Vector basePos, ref Vector pos,
Pen pen, double spacing, float arrowSize)
{ {
for (var i = 0; i < pgm.Length; ++i) for (var i = 0; i < pgm.Length; ++i)
{ {
@@ -19,10 +25,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
if (subpgm.Program != null) if (subpgm.Program != null)
{ {
var savedPos = pos; var holeBase = basePos + subpgm.Offset;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y); pos = holeBase;
DrawProgram(g, view, subpgm.Program, ref pos, pen, spacing, arrowSize); DrawProgram(g, view, subpgm.Program, holeBase, ref pos, pen, spacing, arrowSize);
pos = savedPos;
} }
continue; continue;
} }
@@ -31,7 +36,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos ? motion.EndPoint + pos
: motion.EndPoint; : motion.EndPoint + basePos;
if (code.Type == CodeType.LinearMove) if (code.Type == CodeType.LinearMove)
{ {
@@ -46,7 +51,7 @@ namespace OpenNest.Controls
{ {
var center = pgm.Mode == Mode.Incremental var center = pgm.Mode == Mode.Incremental
? arc.CenterPoint + pos ? arc.CenterPoint + pos
: arc.CenterPoint; : arc.CenterPoint + basePos;
DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize); DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize);
} }
} }
+42 -58
View File
@@ -395,8 +395,8 @@ namespace OpenNest.Controls
var piercePoint = GetFirstPiercePoint(pgm, part.Location); var piercePoint = GetFirstPiercePoint(pgm, part.Location);
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen); DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
pos = part.Location; pos = piercePoint;
DrawRapids(g, pgm, ref pos, skipFirstRapid: true); DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
} }
} }
@@ -409,15 +409,13 @@ namespace OpenNest.Controls
if (pgm[i] is Motion motion) if (pgm[i] is Motion motion)
{ {
if (pgm.Mode == Mode.Incremental) return motion.EndPoint + partLocation;
return motion.EndPoint + partLocation;
return motion.EndPoint;
} }
} }
return partLocation; return partLocation;
} }
private void DrawRapids(Graphics g, Program pgm, ref Vector pos, bool skipFirstRapid = false) private void DrawRapids(Graphics g, Program pgm, Vector basePos, ref Vector pos, bool skipFirstRapid = false)
{ {
var firstRapidSkipped = false; var firstRapidSkipped = false;
@@ -425,62 +423,49 @@ namespace OpenNest.Controls
{ {
var code = pgm[i]; var code = pgm[i];
if (code.Type == CodeType.SubProgramCall) if (code is SubProgramCall { Program: { } program } call)
{ {
var subpgm = (SubProgramCall)code; // A SubProgramCall is a coordinate-frame shift, not a physical
var program = subpgm.Program; // rapid to the hole center. The Cincinnati post emits it as a
// G52 bracket, so the physical rapid is the sub-program's first
// motion, which goes straight from here to the lead-in pierce.
// Look ahead for that pierce point and draw the direct rapid,
// then recurse with skipFirstRapid so the sub doesn't also draw
// its first rapid on top. See docs/cincinnati-post-output.md.
var holeBase = basePos + call.Offset;
var firstPierce = GetFirstPiercePoint(program, holeBase);
if (program != null) if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
{ DrawLine(g, pos, firstPierce, view.ColorScheme.RapidPen);
var holePos = new Vector(pos.X + subpgm.Offset.X, pos.Y + subpgm.Offset.Y);
// Draw rapid from current position to hole center var subPos = holeBase;
if (!(skipFirstRapid && !firstRapidSkipped)) DrawRapids(g, program, holeBase, ref subPos, skipFirstRapid: true);
DrawLine(g, pos, holePos, view.ColorScheme.RapidPen); pos = subPos;
else
firstRapidSkipped = true;
pos = holePos;
DrawRapids(g, program, ref pos);
// Don't restore pos — let it advance so the next hole's
// rapid starts from where this one ended.
}
} }
else else if (code is Motion motion)
{ {
var motion = code as Motion; var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
if (motion != null) if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
{ DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
if (pgm.Mode == Mode.Incremental)
{
var endpt = motion.EndPoint + pos;
if (code.Type == CodeType.RapidMove) pos = endpt;
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
}
pos = endpt;
}
else
{
if (code.Type == CodeType.RapidMove)
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
DrawLine(g, pos, motion.EndPoint, view.ColorScheme.RapidPen);
}
pos = motion.EndPoint;
}
}
} }
} }
} }
private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped)
{
if (skipFirstRapid && !firstRapidSkipped)
{
firstRapidSkipped = true;
return false;
}
return true;
}
private void DrawAllPiercePoints(Graphics g) private void DrawAllPiercePoints(Graphics g)
{ {
using var brush = new SolidBrush(Color.Red); using var brush = new SolidBrush(Color.Red);
@@ -491,11 +476,11 @@ namespace OpenNest.Controls
var part = view.Plate.Parts[i]; var part = view.Plate.Parts[i];
var pgm = part.Program; var pgm = part.Program;
var pos = part.Location; var pos = part.Location;
DrawProgramPiercePoints(g, pgm, ref pos, brush, pen); DrawProgramPiercePoints(g, pgm, part.Location, ref pos, brush, pen);
} }
} }
private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen) private void DrawProgramPiercePoints(Graphics g, Program pgm, Vector basePos, ref Vector pos, Brush brush, Pen pen)
{ {
for (var i = 0; i < pgm.Length; ++i) for (var i = 0; i < pgm.Length; ++i)
{ {
@@ -506,10 +491,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
if (subpgm.Program != null) if (subpgm.Program != null)
{ {
var savedPos = pos; var holeBase = basePos + subpgm.Offset;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y); pos = holeBase;
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen); DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
pos = savedPos;
} }
} }
else else
@@ -519,7 +503,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos ? motion.EndPoint + pos
: motion.EndPoint; : motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove) if (code.Type == CodeType.RapidMove)
{ {
+2 -30
View File
@@ -1,9 +1,5 @@
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using OpenNest.IO.Bending;
using OpenNest.IO.Bom; using OpenNest.IO.Bom;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -470,33 +466,9 @@ namespace OpenNest.Forms
try try
{ {
var result = Dxf.Import(part.DxfPath); var drawing = CadImporter.ImportDrawing(part.DxfPath,
new CadImportOptions { Quantity = part.Qty ?? 1 });
var bends = new List<Bend>();
if (result.Document != null)
bends = BendDetectorRegistry.AutoDetect(result.Document);
Bend.UpdateEtchEntities(result.Entities, bends);
var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath);
var drawing = new Drawing(drawingName);
drawing.Color = Drawing.GetNextColor();
drawing.Source.Path = part.DxfPath;
drawing.Quantity.Required = part.Qty ?? 1;
drawing.Material = new Material(material); drawing.Material = new Material(material);
if (bends.Count > 0)
drawing.Bends.AddRange(bends);
var normalized = ShapeProfile.NormalizeEntities(result.Entities);
var pgm = ConvertGeometry.ToProgram(normalized);
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
{
var rapid = (RapidMove)pgm[0];
drawing.Source.Offset = rapid.EndPoint;
pgm.Offset(-rapid.EndPoint);
}
drawing.Program = pgm;
nest.Drawings.Add(drawing); nest.Drawings.Add(drawing);
} }
catch (Exception ex) catch (Exception ex)
+36 -72
View File
@@ -5,7 +5,6 @@ using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using OpenNest.IO.Bending; using OpenNest.IO.Bending;
using OpenNest.Properties;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
@@ -74,36 +73,24 @@ namespace OpenNest.Forms
{ {
try try
{ {
var result = Dxf.Import(file); var options = new CadImportOptions
{
BendDetectorName = detectorIndex == 0 ? null : detectorName,
};
var result = CadImporter.Import(file, options);
if (result.Entities.Count == 0) if (result.Entities.Count == 0)
return; return;
// Compute bounds
var bounds = result.Entities.GetBoundingBox();
// Detect bends (detectorIndex/Name captured on UI thread)
var bends = new List<Bend>();
if (result.Document != null)
{
bends = detectorIndex == 0
? BendDetectorRegistry.AutoDetect(result.Document)
: BendDetectorRegistry.GetByName(detectorName)
?.DetectBends(result.Document)
?? new List<Bend>();
}
Bend.UpdateEtchEntities(result.Entities, bends);
var item = new FileListItem var item = new FileListItem
{ {
Name = Path.GetFileNameWithoutExtension(file), Name = result.Name,
Entities = result.Entities, Entities = result.Entities,
Path = file, Path = result.SourcePath,
Quantity = 1, Quantity = 1,
Customer = string.Empty, Customer = string.Empty,
Bends = bends, Bends = result.Bends,
Bounds = bounds, Bounds = result.Bounds,
EntityCount = result.Entities.Count EntityCount = result.Entities.Count
}; };
@@ -368,7 +355,6 @@ namespace OpenNest.Forms
: Path.GetTempPath(); : Path.GetTempPath();
var index = fileList.SelectedIndex; var index = fileList.SelectedIndex;
var newItems = new List<string>();
var splitWriter = new SplitDxfWriter(); var splitWriter = new SplitDxfWriter();
var splitItems = new List<FileListItem>(); var splitItems = new List<FileListItem>();
@@ -381,7 +367,6 @@ namespace OpenNest.Forms
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName)); var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
splitWriter.Write(splitPath, splitDrawing); splitWriter.Write(splitPath, splitDrawing);
newItems.Add(splitPath);
// Re-import geometry but keep bends from the split drawing // Re-import geometry but keep bends from the split drawing
var result = Dxf.Import(splitPath); var result = Dxf.Import(splitPath);
@@ -669,53 +654,35 @@ namespace OpenNest.Forms
foreach (var item in fileList.Items) foreach (var item in fileList.Items)
{ {
var entities = item.Entities.Where(e => e.Layer.IsVisible && e.IsVisible).ToList(); var visible = item.Entities
.Where(e => e.Layer.IsVisible && e.IsVisible)
if (entities.Count == 0)
continue;
var drawing = new Drawing(item.Name);
drawing.Color = Drawing.GetNextColor();
drawing.Customer = item.Customer;
drawing.Source.Path = item.Path;
drawing.Quantity.Required = item.Quantity;
// Copy bends
if (item.Bends != null)
drawing.Bends.AddRange(item.Bends);
var normalized = ShapeProfile.NormalizeEntities(entities);
var pgm = ConvertGeometry.ToProgram(normalized);
var firstCode = pgm[0];
if (firstCode.Type == CodeType.RapidMove)
{
var rapid = (RapidMove)firstCode;
drawing.Source.Offset = rapid.EndPoint;
pgm.Offset(-rapid.EndPoint);
// Keep the rapid (now at origin) — it marks the contour
// start and is needed by the post for correct pierce placement.
}
if (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
drawing.Program = programEditor.Program;
else
drawing.Program = pgm;
// Store all entities with stable GUIDs; track suppressed by ID
var bendSources = new HashSet<Entity>(
(item.Bends ?? new List<Bend>())
.Where(b => b.SourceEntity != null)
.Select(b => b.SourceEntity));
drawing.SourceEntities = item.Entities
.Where(e => !bendSources.Contains(e))
.ToList(); .ToList();
drawing.SuppressedEntityIds = new HashSet<Guid>( if (visible.Count == 0)
drawing.SourceEntities continue;
.Where(e => !(e.Layer.IsVisible && e.IsVisible))
.Select(e => e.Id)); // Rebuild a CadImportResult from the FileListItem's current state so
// BuildDrawing sees the user's edits (filters, suppressions, new bends).
var result = new CadImportResult
{
Entities = item.Entities,
Bends = item.Bends ?? new List<Bend>(),
Bounds = item.Bounds,
SourcePath = item.Path,
Name = item.Name,
};
var editedProgram = (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null)
? programEditor.Program
: null;
var drawing = CadImporter.BuildDrawing(
result,
visible,
result.Bends,
item.Quantity,
item.Customer,
editedProgram);
drawings.Add(drawing); drawings.Add(drawing);
@@ -780,9 +747,6 @@ namespace OpenNest.Forms
item.SuppressedEntityIds = null; item.SuppressedEntityIds = null;
} }
private static Color GetNextColor() => Drawing.GetNextColor();
private static bool IsDirectoryWritable(string path) private static bool IsDirectoryWritable(string path)
{ {
try try
+1 -1
View File
@@ -81,8 +81,8 @@
// //
// tabControl1 // tabControl1
// //
tabControl1.Controls.Add(tabPage1);
tabControl1.Controls.Add(tabPage2); tabControl1.Controls.Add(tabPage2);
tabControl1.Controls.Add(tabPage1);
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill; tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
tabControl1.ItemSize = new System.Drawing.Size(100, 22); tabControl1.ItemSize = new System.Drawing.Size(100, 22);
tabControl1.Location = new System.Drawing.Point(0, 0); tabControl1.Location = new System.Drawing.Point(0, 0);
+4 -3
View File
@@ -63,7 +63,7 @@
this.textBox2 = new System.Windows.Forms.TextBox(); this.textBox2 = new System.Windows.Forms.TextBox();
this.label5 = new System.Windows.Forms.Label(); this.label5 = new System.Windows.Forms.Label();
this.labelMaterial = new System.Windows.Forms.Label(); this.labelMaterial = new System.Windows.Forms.Label();
this.materialBox = new System.Windows.Forms.TextBox(); this.materialBox = new System.Windows.Forms.ComboBox();
this.tabPage2 = new System.Windows.Forms.TabPage(); this.tabPage2 = new System.Windows.Forms.TabPage();
this.tabPage3 = new System.Windows.Forms.TabPage(); this.tabPage3 = new System.Windows.Forms.TabPage();
this.notesBox = new System.Windows.Forms.TextBox(); this.notesBox = new System.Windows.Forms.TextBox();
@@ -516,9 +516,10 @@
// materialBox // materialBox
// //
this.materialBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); this.materialBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
this.materialBox.FormattingEnabled = true;
this.materialBox.Location = new System.Drawing.Point(135, 159); this.materialBox.Location = new System.Drawing.Point(135, 159);
this.materialBox.Name = "materialBox"; this.materialBox.Name = "materialBox";
this.materialBox.Size = new System.Drawing.Size(224, 22); this.materialBox.Size = new System.Drawing.Size(224, 24);
this.materialBox.TabIndex = 11; this.materialBox.TabIndex = 11;
// //
// label3 // label3
@@ -729,6 +730,6 @@
private System.Windows.Forms.RadioButton radioButton2; private System.Windows.Forms.RadioButton radioButton2;
private System.Windows.Forms.Label label5; private System.Windows.Forms.Label label5;
private System.Windows.Forms.Label labelMaterial; private System.Windows.Forms.Label labelMaterial;
private System.Windows.Forms.TextBox materialBox; private System.Windows.Forms.ComboBox materialBox;
} }
} }
+3
View File
@@ -15,6 +15,9 @@ namespace OpenNest.Forms
{ {
InitializeComponent(); InitializeComponent();
foreach (var name in PostProcessorMaterials.Names)
materialBox.Items.Add(name);
timer = new Timer timer = new Timer
{ {
SynchronizingObject = this, SynchronizingObject = this,
+6
View File
@@ -351,6 +351,9 @@ namespace OpenNest.Forms
postProcessorMenuItem.Tag = postProcessor; postProcessorMenuItem.Tag = postProcessor;
postProcessorMenuItem.Click += PostProcessor_Click; postProcessorMenuItem.Click += PostProcessor_Click;
mnuNestPost.DropDownItems.Add(postProcessorMenuItem); mnuNestPost.DropDownItems.Add(postProcessorMenuItem);
if (postProcessor is IMaterialProvidingPostProcessor materialProvider)
PostProcessorMaterials.AddFrom(materialProvider);
} }
} }
} }
@@ -1157,6 +1160,9 @@ namespace OpenNest.Forms
if (postProcessor == null) if (postProcessor == null)
return; return;
if (postProcessor is IPostProcessorNestAware nestAware)
nestAware.PrepareForNest(activeForm.Nest);
if (postProcessor is IConfigurablePostProcessor configurable) if (postProcessor is IConfigurablePostProcessor configurable)
{ {
using var configForm = new PostProcessorConfigForm(configurable); using var configForm = new PostProcessorConfigForm(configurable);
+8 -6
View File
@@ -98,6 +98,9 @@ namespace OpenNest
private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath, private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath,
Program pgm, Mode mode, ref Vector curpos) Program pgm, Mode mode, ref Vector curpos)
{ {
// Capture the frame origin at entry. Sub-program Offsets are relative
// to this fixed origin, not to the current tool position.
var frameOrigin = curpos;
mode = pgm.Mode; mode = pgm.Mode;
for (var i = 0; i < pgm.Length; ++i) for (var i = 0; i < pgm.Length; ++i)
@@ -147,10 +150,8 @@ namespace OpenNest
{ {
cutPath.StartFigure(); cutPath.StartFigure();
leadPath.StartFigure(); leadPath.StartFigure();
var savedPos = curpos; curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos); AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos);
curpos = savedPos;
} }
mode = tmpmode; mode = tmpmode;
break; break;
@@ -240,6 +241,9 @@ namespace OpenNest
private static void AddProgram(GraphicsPath path, Program pgm, Mode mode, ref Vector curpos) private static void AddProgram(GraphicsPath path, Program pgm, Mode mode, ref Vector curpos)
{ {
// Capture the frame origin at entry. Sub-program Offsets are relative
// to this fixed origin, not to the current tool position.
var frameOrigin = curpos;
mode = pgm.Mode; mode = pgm.Mode;
GraphicsPath currentFigure = null; GraphicsPath currentFigure = null;
@@ -308,10 +312,8 @@ namespace OpenNest
if (subpgm.Program != null) if (subpgm.Program != null)
{ {
var savedPos = curpos; curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
AddProgram(path, subpgm.Program, mode, ref curpos); AddProgram(path, subpgm.Program, mode, ref curpos);
curpos = savedPos;
} }
mode = tmpmode; mode = tmpmode;
+30
View File
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest
{
public static class PostProcessorMaterials
{
private static readonly List<string> materials = new();
public static IReadOnlyList<string> Names => materials;
public static void AddFrom(IMaterialProvidingPostProcessor provider)
{
if (provider == null)
return;
foreach (var name in provider.GetMaterialNames())
{
if (!string.IsNullOrWhiteSpace(name)
&& !materials.Contains(name, StringComparer.OrdinalIgnoreCase))
{
materials.Add(name);
}
}
materials.Sort(StringComparer.OrdinalIgnoreCase);
}
}
}
Binary file not shown.
+212
View File
@@ -0,0 +1,212 @@
# Cincinnati Post Output Reference
Reference for the G-code structure emitted by `OpenNest.Posts.Cincinnati`.
Every code listed here maps to a section in the Cincinnati Laser Programming
Manual (`docs/CINCINNATI LASER PROGRAMMING MANUAL.pdf`, EM-423 R-02/11).
Section numbers in parentheses (e.g. `§1.52`) refer to the manual.
If you add a new emission in the post, either cite the manual section it maps
to, or flag it here as a known custom extension. "Custom code" in this project
means something that is not documented in the manual but that the Cincinnati
control is known to accept — none exist today and we should not introduce any
without confirming the control behavior.
## Overall file structure
A generated file contains, in order:
1. **Main program** (`CincinnatiPreambleWriter.WriteMainProgram`)
Preamble, unit/mode setup, initial library, variable-declaration call, one
`M98 P<sheetSubNum>` call per plate quantity, and `M30` to end.
2. **Variable declaration sub-program** (`CincinnatiPreambleWriter.WriteVariableDeclaration`)
Machine variables (`#number = value`) used across the nest, terminated
with `M99`.
3. **Sheet sub-programs** (`CincinnatiSheetWriter.Write`), one per unique plate
layout. A sheet sub-program contains the cutting sequence for a whole
plate, either with features inlined or with `M98` calls into part
sub-programs.
4. **Part sub-programs** (`CincinnatiPartSubprogramWriter.Write`), one per
unique `(drawing, rotation)` pair, only emitted when
`Config.UsePartSubprograms` is enabled.
5. **Hole sub-programs** (`CincinnatiPartSubprogramWriter.Write` reused with a
`"HOLE"` label), one per unique hole geometry keyed by radius and lead-in
normal angle.
Sub-program bodies start with a `:<subNum>` label and end with `M99`.
## Feature blocks
A "feature" is a single contour: lead-in → cut moves → lead-out. Each feature
block in a sheet or sub-program output follows this order
(`CincinnatiFeatureWriter.Write`):
1. `G0 X_ Y_` — rapid to the pierce point (§1.00).
2. Optional part-name comment, only on the first feature of each part.
3. `G89 P<library>` — load process parameters (§2.89). `P` is a library file
name; the `(...)` trailing comment carries speed-class info.
4. `G84` (cut) or `G85` (etch / no-pierce) — pierce and start cut, or start
cut without pierce (§2.84 / §2.85).
5. `M130 (ANTI DIVE OFF)` — disable anti-dive, only if configured (§3.130).
6. Contour moves:
- `G41` (left) or `G42` (right) kerf compensation on the first cut move
(§1.41 / §1.42), suppressed for etch features.
- `G1 X_ Y_ [F<feedvar>]` — linear cut move (§1.01). Feedrate references a
machine variable such as `#148` and is emitted only when it changes.
- `G2 X_ Y_ I_ J_ [F<feedvar>]` (CW) or `G3` (CCW) — arc (§1.02 / §1.03).
`I`/`J` are incremental offsets from the current position to the center.
7. `G40` — cancel kerf compensation (§1.40), only if it was applied.
8. `M35` (or `M135` if SpeedGas is enabled) — beam off (§3.35 / §3.135).
9. `M131 (ANTI DIVE ON)` — re-enable anti-dive (§3.131).
10. `M47` or `M47 P<distance>` — raise Z-axis, unless this is the last feature
on the sheet (§3.47). A leading `/` (block delete, §5.6) is prepended when
the configured override distance exceeds the default.
Sheet sub-program and sheet-level feature calls add `G92 X#5021 Y#5022`
(§1.92) at the top so the local origin is anchored to the machine's current
absolute position (`#5021`/`#5022` are the machine X/Y system variables).
## Sub-program call patterns
There are two distinct call-site patterns, depending on whether the call
targets a whole-part sub-program or a hole sub-program.
### Part sub-program call (`WriteSubprogramCall`)
Used when `Config.UsePartSubprograms` is enabled. The tool physically rapids
to the part corner, then G92 sets the current position as the local origin,
the sub-program executes in its own local coordinate frame, and G92 restores
the original absolute position after return.
```
G0 X<left> Y<bottom> ; rapid to part bounding box corner (§1.00)
(PART: <name>)
G92 X0 Y0 ; set local origin at current position (§1.92)
M98 P<partSubNum> (<name>) ; call the part sub-program (§3.98)
G92 X<left> Y<bottom> ; restore the sheet coordinate system (§1.92)
M47 ; head raise unless this is the last part (§3.47)
```
This pattern uses G92 because the tool is physically positioned at the part
corner first. The sub-program's coordinates are part-local, so they are
interpreted against the new origin until G92 restores the sheet frame.
### Hole sub-program call (`WriteHoleSubprogramCall`)
Used for the `SubProgramCall` codes that a `ContourCuttingStrategy` emits for
each circular hole. Unlike parts, we do **not** want a physical rapid to the
hole center before calling — the sub-program's first rapid is the lead-in to
the pierce point, and the machine should travel directly from the previous
feature's end to that pierce.
```
G52 X<hole.x> Y<hole.y> ; shift local origin to hole center (§1.52)
M98 P<holeSubNum> ; call the shared hole sub-program (§3.98)
G52 X0 Y0 ; restore the original coordinate system (§1.52)
M47 ; head raise unless this is the last feature (§3.47)
```
G52 specifies the new origin in the current work coordinate system and — per
§1.52 — "does not move the cutting nozzle". The hole sub-program is written
in hole-local coordinates (origin at the hole center, produced by
`ContourCuttingStrategy`), so its first `G0 X_ Y_` resolves to `hole + local`
in absolute terms. That is the first physical motion, and it takes the tool
straight from wherever it was to the lead-in pierce point. G52 X0 Y0 cancels
the shift after `M99` returns control.
## G-code reference
These are every G/M code the post emits, grouped by category. Anything here is
documented in the programming manual. Anything not here should be audited the
next time the post is edited.
### Motion modes and contouring
| Code | Description | Manual |
| --- | --- | --- |
| `G0 X_ Y_` | Rapid traverse | §1.00 |
| `G1 X_ Y_ F_` | Linear feedrate move | §1.01 |
| `G2 X_ Y_ I_ J_ F_` | Clockwise arc | §1.02 |
| `G3 X_ Y_ I_ J_ F_` | Counter-clockwise arc | §1.03 |
### Units and coordinate mode
| Code | Description | Manual |
| --- | --- | --- |
| `G20` | Inch mode | §1.20 |
| `G21` | Metric mode | §1.21 |
| `G90` | Absolute mode | §1.90 |
### Kerf compensation
| Code | Description | Manual |
| --- | --- | --- |
| `G40` | Cancel kerf compensation | §1.40 |
| `G41` | Kerf compensation, left side | §1.41 |
| `G42` | Kerf compensation, right side | §1.42 |
### Work coordinate systems
| Code | Description | Manual |
| --- | --- | --- |
| `G52 X_ Y_` | Temporary local work coordinate offset. Does not move the tool. `G52 X0 Y0` cancels. | §1.52 |
| `G92 X_ Y_` | Sets the current tool position to `(X, Y)` in the work coordinate system, implicitly redefining the WCS origin. | §1.92 |
### Exact stop
| Code | Description | Manual |
| --- | --- | --- |
| `G61` | Exact stop mode | §1.61 |
### Cutting operations (custom Cincinnati G-codes)
| Code | Description | Manual |
| --- | --- | --- |
| `G84` | Pierce and start cut | §2.84 |
| `G85` | Start cut without pierce (used for etch) | §2.85 |
| `G89 P<file>` | Load process parameters from a library file | §2.89 |
| `G121` | Enable non-stop cutting (Smart Rapids) | §2.121 |
### Program flow
| Code | Description | Manual |
| --- | --- | --- |
| `M30` | End of main program with rewind | §3.30 |
| `M98 P_` | Sub-program call. **Takes only `P` and `L` — not `X`/`Y`.** | §3.98 |
| `M99` | Return from sub-program | §3.99 |
### Machine state
| Code | Description | Manual |
| --- | --- | --- |
| `M35` | Beam off | §3.35 |
| `M42` | Retract Z-axis | §3.42 |
| `M47 [P<dist>]` | Raise Z-axis, optionally by a distance | §3.47 |
| `M50` | Switch pallets | §3.50 |
| `M130` | Anti-dive off | §3.130 |
| `M131` | Anti-dive on | §3.131 |
| `M135` | Discharge current off (keeps assist gas on) | §3.135 |
### Comments, labels, and block delete
| Syntax | Description | Manual |
| --- | --- | --- |
| `(text)` | Inline comment | §5.4 |
| `:<number>` | Sub-program label | §3.98 |
| `/<block>` | Block delete — operator can toggle the line off | §5.6 |
| `N<number>` | Line number, used by M99 P / GOTO targets | §5.5 |
## System variables referenced
| Variable | Description | Manual |
| --- | --- | --- |
| `#148` | Default cut feedrate variable (used in `F#148`) | §2.89 |
| `#5021` | Current machine X position | §6 (table of system variables) |
| `#5022` | Current machine Y position | §6 (table of system variables) |
Project-defined variables start at `Config.SheetWidthVariable` /
`Config.SheetLengthVariable` and at `Config.UserVariableStart`. Those ranges
are documented in `CincinnatiPostConfig.cs`.