Compare commits

...

4 Commits

Author SHA1 Message Date
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
12 changed files with 386 additions and 104 deletions
-3
View File
@@ -211,8 +211,5 @@ FakesAssemblies/
.superpowers/
docs/superpowers/
# Documentation (manuals, templates, etc.)
docs/
# Launch settings
**/Properties/launchSettings.json
+15 -8
View File
@@ -288,6 +288,10 @@ namespace OpenNest.CNC
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 minY = 0.0;
double maxX = 0.0;
@@ -303,7 +307,7 @@ namespace OpenNest.CNC
{
var line = (LinearMove)code;
var pt = Mode == Mode.Absolute ?
line.EndPoint :
frameOrigin + line.EndPoint :
line.EndPoint + pos;
if (pt.X > maxX)
@@ -325,7 +329,7 @@ namespace OpenNest.CNC
{
var line = (RapidMove)code;
var pt = Mode == Mode.Absolute
? line.EndPoint
? frameOrigin + line.EndPoint
: line.EndPoint + pos;
if (pt.X > maxX)
@@ -358,8 +362,8 @@ namespace OpenNest.CNC
}
else
{
endpt = arc.EndPoint;
centerpt = arc.CenterPoint;
endpt = frameOrigin + arc.EndPoint;
centerpt = frameOrigin + arc.CenterPoint;
}
double minX1;
@@ -433,10 +437,13 @@ namespace OpenNest.CNC
case CodeType.SubProgramCall:
{
var subpgm = (SubProgramCall)code;
var subPos = subpgm.Offset.X != 0 || subpgm.Offset.Y != 0
? new Vector(subpgm.Offset.X, subpgm.Offset.Y)
: pos;
var box = subpgm.Program.BoundingBox(ref subPos);
if (subpgm.Program == null)
break;
// Sub-program frame origin in this program's frame
// is frameOrigin + Offset, regardless of current pos.
pos = frameOrigin + subpgm.Offset;
var box = subpgm.Program.BoundingBox(ref pos);
if (box.Left < minX)
minX = box.Left;
+8 -3
View File
@@ -1,4 +1,5 @@
using OpenNest.Geometry;
using System.Text;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.CNC
@@ -90,9 +91,13 @@ namespace OpenNest.CNC
public override string ToString()
{
var sb = new StringBuilder();
sb.Append($"G65 P{Id}");
if (Offset.X != 0 || Offset.Y != 0)
return string.Format("G65 P{0} X{1} Y{2}", Id, Offset.X, Offset.Y);
return string.Format("G65 P{0} R{1}", Id, Rotation);
sb.Append($" X{Offset.X} Y{Offset.Y}");
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;
namespace OpenNest.Converters
@@ -9,7 +9,6 @@ namespace OpenNest.Converters
/// Converts the program to absolute coordinates.
/// Does NOT check program mode before converting.
/// </summary>
/// <param name="pgm"></param>
public static void ToAbsolute(Program pgm)
{
var pos = new Vector(0, 0);
@@ -17,21 +16,27 @@ namespace OpenNest.Converters
for (int i = 0; i < pgm.Codes.Count; ++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;
}
}
}
/// <summary>
/// Converts the program to intermental coordinates.
/// Converts the program to incremental coordinates.
/// Does NOT check program mode before converting.
/// </summary>
/// <param name="pgm"></param>
public static void ToIncremental(Program pgm)
{
var pos = new Vector(0, 0);
@@ -39,9 +44,16 @@ namespace OpenNest.Converters
for (int i = 0; i < pgm.Codes.Count; ++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;
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)
{
// 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;
for (int i = 0; i < program.Length; ++i)
@@ -43,20 +46,13 @@ namespace OpenNest.Converters
case CodeType.SubProgramCall:
var subpgm = (SubProgramCall)code;
var savedMode = mode;
var savedPos = curpos;
// Position the sub-program at savedPos + Offset.
// savedPos is the base position ((0,0) here, Part.Location in rendering).
// Offset is the hole center in drawing-local coordinates.
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
// The sub-program's frame origin in this program's frame is
// frameOrigin + Offset — independent of current tool position.
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
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;
}
}
@@ -226,12 +226,23 @@ public sealed class CincinnatiSheetWriter
? _config.FeatureLineNumberStart
: 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();
if (_config.UseLineNumbers)
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($"M98 P{postSubNum}");
// Cancel the local shift (manual §1.52).
w.WriteLine("G52 X0 Y0");
if (!isLastFeature)
w.WriteLine("M47");
}
@@ -44,6 +44,25 @@ public class HoleSubProgramTests
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]
public void Program_SubPrograms_EmptyByDefault()
{
+11 -6
View File
@@ -9,6 +9,12 @@ namespace OpenNest.Controls
{
public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos,
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)
{
@@ -19,10 +25,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
{
var savedPos = pos;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
DrawProgram(g, view, subpgm.Program, ref pos, pen, spacing, arrowSize);
pos = savedPos;
var holeBase = basePos + subpgm.Offset;
pos = holeBase;
DrawProgram(g, view, subpgm.Program, holeBase, ref pos, pen, spacing, arrowSize);
}
continue;
}
@@ -31,7 +36,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
: motion.EndPoint + basePos;
if (code.Type == CodeType.LinearMove)
{
@@ -46,7 +51,7 @@ namespace OpenNest.Controls
{
var center = pgm.Mode == Mode.Incremental
? arc.CenterPoint + pos
: arc.CenterPoint;
: arc.CenterPoint + basePos;
DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize);
}
}
+37 -53
View File
@@ -395,8 +395,8 @@ namespace OpenNest.Controls
var piercePoint = GetFirstPiercePoint(pgm, part.Location);
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
pos = part.Location;
DrawRapids(g, pgm, ref pos, skipFirstRapid: true);
pos = piercePoint;
DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
}
}
@@ -409,15 +409,13 @@ namespace OpenNest.Controls
if (pgm[i] is Motion motion)
{
if (pgm.Mode == Mode.Incremental)
return motion.EndPoint + partLocation;
return motion.EndPoint;
}
}
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;
@@ -425,60 +423,47 @@ namespace OpenNest.Controls
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
if (code is SubProgramCall { Program: { } program } call)
{
var subpgm = (SubProgramCall)code;
var program = subpgm.Program;
// A SubProgramCall is a coordinate-frame shift, not a physical
// 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)
{
var holePos = new Vector(pos.X + subpgm.Offset.X, pos.Y + subpgm.Offset.Y);
if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
DrawLine(g, pos, firstPierce, view.ColorScheme.RapidPen);
// Draw rapid from current position to hole center
if (!(skipFirstRapid && !firstRapidSkipped))
DrawLine(g, pos, holePos, view.ColorScheme.RapidPen);
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.
var subPos = holeBase;
DrawRapids(g, program, holeBase, ref subPos, skipFirstRapid: true);
pos = subPos;
}
}
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 (pgm.Mode == Mode.Incremental)
{
var endpt = motion.EndPoint + pos;
if (code.Type == CodeType.RapidMove)
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
}
pos = endpt;
}
else
{
if (code.Type == CodeType.RapidMove)
}
}
private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped)
{
if (skipFirstRapid && !firstRapidSkipped)
{
firstRapidSkipped = true;
else
DrawLine(g, pos, motion.EndPoint, view.ColorScheme.RapidPen);
}
pos = motion.EndPoint;
}
}
}
return false;
}
return true;
}
private void DrawAllPiercePoints(Graphics g)
@@ -491,11 +476,11 @@ namespace OpenNest.Controls
var part = view.Plate.Parts[i];
var pgm = part.Program;
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)
{
@@ -506,10 +491,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
{
var savedPos = pos;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen);
pos = savedPos;
var holeBase = basePos + subpgm.Offset;
pos = holeBase;
DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
}
}
else
@@ -519,7 +503,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
: motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove)
{
+8 -6
View File
@@ -98,6 +98,9 @@ namespace OpenNest
private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath,
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;
for (var i = 0; i < pgm.Length; ++i)
@@ -147,10 +150,8 @@ namespace OpenNest
{
cutPath.StartFigure();
leadPath.StartFigure();
var savedPos = curpos;
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos);
curpos = savedPos;
}
mode = tmpmode;
break;
@@ -240,6 +241,9 @@ namespace OpenNest
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;
GraphicsPath currentFigure = null;
@@ -308,10 +312,8 @@ namespace OpenNest
if (subpgm.Program != null)
{
var savedPos = curpos;
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
AddProgram(path, subpgm.Program, mode, ref curpos);
curpos = savedPos;
}
mode = tmpmode;
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`.