Namespace Hi.NcParsers.LogicSyntaxs
Classes
- BackBoringSyntax
G87 back boring cycle. Supports modal repetition. Cuts upward from Z to R — used to bore the back side of a workpiece.
Cycle sequence:
- Oriented spindle stop (OSS) at current position
- Rapid (shifted) to init position, then down to bottom Z — tool enters pre-drilled hole without contacting bore wall
- Shift back to hole center at bottom
- Spindle start (CW)
- Feed upward from Z to R-point (back boring cut)
- Oriented spindle stop at R
- Tool shift, rapid retract (shifted) to final Z
- Shift back to center, spindle restart
Q specifies the lateral shift distance (mm). Shift direction defaults to +X (OSS angle 0°).
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax before this syntax runs.
- BoringCycleSyntax
G85/G86/G89 boring cycles.
Cycle sequence:
- Rapid to init position (target XY, previous Z)
- Rapid from init to R-point
- Feed from R-point to bottom Z
- [G86 only] Spindle stop at bottom
- [G89 only] Dwell P seconds at bottom
- Retract: G85/G89 → feed retract, G86 → rapid retract
- [G86 only] Spindle restart (CW) after retract
G85: feed to Z, feed retract — smooth bore finish. G86: feed to Z, spindle stop (implicit), rapid retract. G89: feed to Z, dwell P, feed retract — like G85 with bottom dwell.
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax (modal repetition, G91 conversion, missing-axis fallback) before this syntax runs. Must be placed after CannedCycleResolveSyntax and before IncrementalResolveSyntax in the syntax chain.
- CannedCycleResolveSyntax
Resolves the canned-cycle Group-09 state for the current block and writes the result to the CannedCycle section.
- Active cycle (direct G81..G89 or modal repeat): merges
Parsingoverrides with previous-cycle stored params, applies G91 incremental-to-absolute conversion and missing-axis fallback, writes CannedCycle with Term, ReturnMode, and Params. The resolved cycle sub-section is left inParsingunder the cycle code for downstream cycle syntaxes (DrillingCycleSyntax, etc.) to read. - Explicit cancel (G80 flag present on a non-cycle block):
consumes the G80 flag and writes
CannedCycle =
{ Term: "G80" }, acting as a hard sentinel for Hi.NcParsers.LogicSyntaxs.CannedCycleSyntaxUtil modal lookback. - No Group-09 activity: leaves the block untouched.
A rotary-only block (
C40.) reaches this syntax without aParsingnode — McAbcSyntax consumed the word intoMachineCoordinateStateand dropped the emptied node — but its one-shot RotaryWords record marks it as a block that spoke an axis word, so under an active cycle it is a modal repeat like any X/Y block (Fanuc's any-axis-word rule; HardNc indexes the table and drills again). The cycle's pre-positioning item then carries the block's ABC (WriteCompoundMotion(JsonObject, string, JsonArray, Vec3d)).Must be placed after PositioningSyntax and before the individual cycle syntaxes in the chain.
- Active cycle (direct G81..G89 or modal repeat): merges
- CircularMotionSyntax
Writes McArc motion for circular commands (ISO G02/G03). Detects motion mode from Flags, reads I/J/K center offsets or R radius from Parsing, computes arc center in program coordinates, and writes a one-shot MotionEvent (form + arc params) plus a modal MotionState (Term).
G02/G03 mode is modal (Group 01) — persists across blocks via Term. Arc parameters (I/J/K/R) are per-block and must be present in every arc block.
Must be placed before LinearMotionSyntax in the syntax chain. Both share the Group 01 motion slot; whichever writes a MotionEvent first claims it.
IsIjkAbsolute switches the I/J/K reading to absolute center coordinates (Heidenhain DIN/ISO — the ISO twin of the Klartext CC pole; the Heidenhain list sets it, every other brand keeps the offset default). In that mode the center is modal: letters not written in a block inherit the previous absolute pole (AbsoluteIjkPoleKey, carried by the brand's ModalCarrySyntax), a letter never written falls back to the arc start's component, a letters-free block that commands an endpoint continues the modal arc off the pole (a flags-only block stays motionless), a G91 block reads offsets again and breaks the pole chain, and the plane-normal letter is a center coordinate — never the per-turn helix pitch. Behavior mirrors
HardNcLine.BuildArcNcArg+ArcNcArg.GetCenterOrCenterOnBeginPlane(HiUniNc 3.1.152.2) bit for bit.
- CodedPositionUtil
Shared coded-position resolution for the write-stage consumers (McAbcSyntax for rotary words, IncrementalResolveSyntax for linear words): turns a per-word PositioningOverride entry of the coded family (CodedAbsolute / CodedIncremental / CodedShortest / CodedPositiveOnly / CodedNegativeOnly — stamped by SiemensAcIcSyntax for
CAC()/CIC()/CDC()/CACP()/CACN()) plus the evaluated position number into an axis coordinate via IIndexingPositionConfig, and names the plain override value the caller rewrites the entry to — so the McAbcCyclicPathSyntax tail-pass and every other downstream reader only ever see the established non-coded vocabulary.Failure semantics mirror the Siemens alarms as far as a simulator can: an invalid position number (alarm 17510) or a missing table reports an error diagnostic and resolves to "hold" — the caller writes the anchor so the axis does not move.
CIC(0)also resolves to hold, by specification ("the indexing axis is not traversed") and silently. ACICfrom between two indexing positions advances to the n-th next position in the programmed direction. On a cyclic indexing axis the incremental sign becomes a directional (PositiveOnly / NegativeOnly) approach; increments spanning more than one revolution reach the correct position but collapse the extra full turns (the tail-pass windows cover one revolution).
- CoolantSyntax
Consumes M07 (mist ON), M08 (flood ON), and M09 (coolant OFF) from Flags and writes the ICoolantDef section with both IsOn (convenience flag) and Mode (abstract mode name: Flood / Mist / Off). Modal — persists via backward lookback.
- CoordinateOffsetUtil
Shared utilities for all coordinate offset syntaxes (ISO, Siemens, Heidenhain). Handles section IO, backward lookback, and ProgramToMcTransform composition.
- DrillingCycleSyntax
G81/G82 drilling cycle (rapid retract). Supports modal repetition. G82 covers G81 — the only difference is an optional dwell (P) at the bottom.
Cycle sequence:
- Rapid to init position (target XY, previous Z)
- Rapid from init to R-point
- Feed from R-point to bottom Z
- [G82 only] Dwell P seconds at bottom
- Rapid from bottom to final (G98 → init Z, G99 → R)
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax (modal repetition, G91 conversion, missing-axis fallback) before this syntax runs. Must be placed after CannedCycleResolveSyntax and before IncrementalResolveSyntax in the syntax chain.
- DwellSyntax
Consumes the non-modal G4/G04 dwell sub-section captured by G4Syntax (
Parsing.G4/Parsing.G04) and emits a CompoundMotion with a single Dwell item, which Hi.NcParsers.Semantics.CompoundMotionSemanticUtil resolves into an ActDelay of the dwell duration.Argument dialects are configured per brand preset:
- Fanuc-family default — SecondsPrefixes =
X/U(seconds), MillisecondsPrefixes =P(milliseconds), SpindleRevPrefixes =S(spindle revolutions). - Siemens —
G4 F<seconds>/G4 S<revolutions>: the preset sets SecondsPrefixes =F, clears the milliseconds list, keepsSrevolutions.
Because the capture layer owns the whole argument (the F/X/P word lands inside the dwell sub-section, never in
Parsing.F/Parsing.X), a dwell block cannot poison the modal Feedrate and its X-word cannot mint a ghost motion — structural fixes for theG04 F60000feed-poison andG04 X0.5ghost-motion hazards.Spindle-revolution dwell needs the modal spindle speed: this syntax must be placed after SpindleSpeedSyntax in the Logic bundle so the block's own modal SpindleSpeed section is already written. When no positive rpm is known the dwell is consumed and recorded via an Unsupported Message (
Dwell--SpindleRevUnresolved) instead of being time-simulated — no act is emitted.When several recognized argument prefixes appear on one block the resolution priority is seconds → milliseconds → revolutions; every recognized key is consumed either way. Unrecognized keys inside the sub-section are left in place so they surface through UnconsumedCheckSyntax.
- Fanuc-family default — SecondsPrefixes =
- FanucPathSmoothingSyntax
Consumes Fanuc G05.1 (high-precision contour / AICC II / Nano Smoothing) and records the modal state in the PathSmoothing JSON section using the FanucPathSmoothing schema. Q1 enables, Q0 disables; the optional R{n} precision-level is preserved as Level. The simulation does not alter the tool path — this is a controller-internal interpolation black box; the captured state exists for bidirectional NC-text reconstruction.
Modal carry to subsequent blocks is handled by ModalCarrySyntax, which already tracks the
PathSmoothingsection key and deep-clones it forward.Also consumes the bare
G05 P{n}HPCC family (captured by G05Syntax) into the block-local FanucHpcc section — recognized, intentionally not simulated (the SiemensStopreSyntax pattern). Ignoring P10000/P0 is safe offline; an ignored high-speed cycle machining call (P10001–P10999) means the simulation misses that machining, surfaced as a Warning. See IFanucHpccDef for the P function-selection semantics. The section is deliberately separate from the modalPathSmoothingsection and is not modal-carried.
- FeedrateSyntax
Consumes F (feedrate) from Parsing and G94/G95 mode from Flags. Both are modal — persist across blocks via backward node lookback. Writes resolved state to a IFeedrateDef section.
- FineBoringSyntax
G76 fine boring cycle. Supports modal repetition.
Cycle sequence:
- Rapid to init position (target XY, previous Z)
- Rapid from init to R-point
- Feed from R-point to bottom Z
- Oriented spindle stop (OSS)
- Tool shift by Q in +X direction (clear bore wall)
- Rapid retract (shifted) to final Z
- Tool shift back to center
- Spindle restart (CW)
Q specifies the lateral shift distance (mm) to avoid dragging the tool across the finished bore surface during retract. Shift direction defaults to +X (OSS angle 0°).
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax before this syntax runs.
- G43p4RtcpSyntax
Handles G43.4 RTCP (Rotary Tool Center Point) activation. Writes the IToolHeightCompensationDef section and the ToolHeightCompensationSource entry in ProgramToMcTransform — a tool-normal · offset_mm translation at the block endpoint ABC. The chain entry is tagged KindDynamic when RTCP is active and ABC changes across the block, and KindStatic otherwise.
The RTCP kinematic rotary part (Pn→MC rigid transform) is orthogonal to this syntax and is written by PivotTransformationSyntax on every block, because rotary state remains in effect beyond the RTCP modal (e.g. a non-RTCP
G01afterG49still inherits the last ABC from the program).The "rotary dynamic" distinction lives on the chain entry's KindKey alone and is read via HasDynamicEntry(JsonObject) by LinearMotionSyntax to pick ClLinear vs McLinear.
G43.4 is used by Fanuc, Mazak, Syntec, and Okuma. Siemens (TRAORI) and Heidenhain (M128) are handled by separate syntaxes. Must be placed after ToolHeightOffsetSyntax (to override the ToolHeightCompensation entry when RTCP is active) and before PivotTransformationSyntax (which runs last in the chain).
- G53p1RotaryPositionSyntax
G53.1 — non-modal, one-shot rotary axis positioning. Positions the rotary axes (A/B/C) to align the physical tool axis with the active tilted work plane defined by G68.2. XYZ position is unchanged; only rotary axes move via rapid traverse.
Requires IsoG68p2TiltSyntax (or equivalent) to have written the tilt transform. Uses IMachineKinematics to solve for the target A/B/C via inverse kinematics.
Must be placed after IsoG68p2TiltSyntax (needs tilt data) and before ProgramXyzSyntax in the syntax chain. Writes A/B/C into MachineCoordinateState. Motion is handled by LinearMotionSyntax via modal G00/G01.
- HighSpeedPeckCycleSyntax
G73 high-speed peck drilling cycle (chip breaking). Supports modal repetition. Drills in increments of depth Q, partially retracting by PeckRetractionDistance_mm between strokes (instead of fully back to R like PeckDrillingCycleSyntax).
Cycle sequence:
- Rapid to init position (target XY, previous Z)
- Rapid from init to R-point
- For each stroke: feed Q deeper, rapid retract by d
- If remainder exists: feed to bottom Z, rapid retract by d
- Rapid to final (G98 → init Z, G99 → R)
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax (modal repetition, G91 conversion, missing-axis fallback) before this syntax runs. Must be placed after CannedCycleResolveSyntax and before IncrementalResolveSyntax in the syntax chain.
- IncrementalResolveSyntax
Resolves G91 incremental axis values to absolute in-place within Parsing and its sub-sections. Reads Term written by PositioningSyntax.
Per-word override: a block-root PositioningOverride section (written by SiemensAcIcSyntax for the Siemens per-word coordinate functions, and by the Heidenhain L / C / CC / CYCL CALL POS parsers for the klartext I-prefixed words — see HeidenhainIncrementalAxisWordUtil; the CYCL CALL POS words never reach this syntax, HeidenhainCannedCycleSyntax resolves them on the spot ahead of it) beats the modal term for the listed axes on this block only: an Incremental entry converts that word even under G90, an Absolute entry skips it even under G91 — as does, deliberately, every other non-Incremental value (the rotary-family Shortest / PositiveOnly / NegativeOnly entries are absolute targets; their swing resolution lives in McAbcCyclicPathSyntax, not here). A coded-position entry (CodedAbsolute / CodedIncremental — Siemens
CAC()/CIC()on a linear indexing axis) carries an indexing position number instead of a coordinate: the number is resolved through IIndexingPositionConfig via TryResolveCodedTarget(IIndexingPositionConfig, string, string, double, double, ISentenceCarrier, NcDiagnosticProgress, out double, out string), the word is rewritten to the resolved absolute coordinate, and the entry to Absolute; a failed resolve reports an error and holds the last program position. Axes without an entry follow the modal term unchanged, so brands that never write the section (Fanuc/...) keep the exact legacy behavior.WorkingPathList specifies which JSON paths contain axis values that need incremental-to-absolute conversion. Default:
[["Parsing"], ["Parsing", "G28"]]; the Heidenhain bundle instead walks["Parsing", "CC"]for the klartext circle-center record (CcAwareIncrementalResolveSyntax). All matching paths are converted against the same last program position — a nested record's words are distances from where the tool stands, exactly like the root's.Canned cycle paths (Parsing.G81, G82, G83, …) are intentionally excluded — their Z/R incremental semantics differ from normal axes (R is relative to init level, Z is relative to R-point). Resolution is handled by ResolveCycleCoordinates(JsonObject, Vec3d, double?, double?, double, double, HashSet<string>) inside each cycle syntax class, which runs before this syntax.
Uses AxisNames to determine which tags are motion axes. Traces backward nodes for last known ProgramXyz to resolve incremental values. After this syntax, all axis values in the working paths are absolute — ProgramXyzSyntax can consume them without incremental logic.
Rotary words (A/B/C) at the
Parsingroot never reach this syntax — McAbcSyntax consumes them upstream and follows the modal G91 itself. Inside a nested record they do (G91 G28 C10.— theParsing.G28intermediate), and a rotary axis has no program→MC transform, so its anchor is the previous modal angle (FindPreviousMcAxisDeg(LazyLinkedListNode<SyntaxPiece>, string)) rather than a component of the program XYZ: the intermediate lands at current + 10°, the way HardNc'sParseG28reads it against the previous MC. A never-set rotary axis anchors at 0.The rewrite is in place, and the modal Positioning section keeps saying
G91(the next block's lookback needs it), so a converted word is indistinguishable from a programmed absolute. Every word this syntax converts is therefore also recorded, as programmed, in the one-shot block-root IncrementalWords section, shaped like theParsingtree it came from ({ "X": 10, "G28": { "C": 10 } }). The record doubles as the idempotency guard: a word already listed at its path is left alone, so a second pass over the same block — a project file whose serialized list carries two instances, a re-run over a frozen piece — cannot add the anchor twice. Coded-position words are not recorded (they are not increments of a coordinate; their trace is the rewritten override entry).
- IsoCoordinateOffsetSyntax
ISO/Fanuc/Mazak/Okuma/Syntec: resolves the G54–G59.9 work coordinate offset and the Fanuc-family additional work coordinate systems (
G54.1 Pn, also spelledG54 Pn). Reads G54/G55/.../G59.9 from Flags and the capturedParsing.G54.1 = {P: n}object (written by G54p1Syntax for both spellings), which resolves to the coordinate id"G54.1P{n}"that the brand parameter tables map to#7001+(IsoCoordinateAddressMap). Looks the offset Vec3d up via the IIsoCoordinateConfig dependencies (brand parameter table or IsoCoordinateTable) and composes it into ProgramToMcTransform. Modal — the active coordinate persists via backward lookback. Default coordinate ID is set by StaticInitializer.A block that selects a work coordinate system nobody has configured is reported on that block (never on the modal re-query of the following blocks):
Coord-WorkOffset--AdditionalZerowhen an additional system (G54.1 Pn) resolves to no entry or to (0, 0, 0) — the brand tables seed every P row with zero, hardware-faithfully, so a zero there is the "never entered" state, whereas a zero row of the standard series (G54–G59 and the G59.1–G59.9 extension alike) is a legitimate authoring convention and stays silent;Coord-WorkOffset--NoTableEntrywhen no provider resolves the id at all (e.g. aG59.xon a runner carrying no brand-neutral table beside its brand table); andCoord-WorkOffset--IndexUnresolvedwhen the P word is not a positive integer (vacant variable, non-integer), in which case the active system is kept. A bareG54.1without P is not this syntax's business: the parameterized capture consumes nothing without a parameter, the dotted number lands inParsing.Flagswhere it is not a G54-series member, so the active system is kept and the unconsumed check reports the flag.
- IsoG68RotationSyntax
ISO/Fanuc: resolves G68 (2D coordinate rotation) and G69 (cancel). Computes a rotation Mat4d around the active plane normal and composes it into ProgramToMcTransform.
No IMachineKinematics dependency needed — G68 is pure geometric rotation.
Managed commands: G68, G69 (idempotent with IsoG68p2TiltSyntax).
- IsoG68p2TiltSyntax
ISO/Fanuc: resolves G68.2 (tilted work plane) and G69 (cancel). Computes a tilt Mat4d from I/J/K euler angles (Fanuc ZXZ convention) and composes it into ProgramToMcTransform.
Managed commands: G68.2, G69 (idempotent with IsoG68RotationSyntax). Siemens equivalent: CYCLE800 (separate syntax). Heidenhain equivalent: PLANE SPATIAL (separate syntax).
- IsoLocalCoordinateOffsetSyntax
ISO G52: Local coordinate system offset (additive to G54-series).
ReadsG52 X10 Y20 Z5→ sets local offset.G52 X0 Y0 Z0→ cancels (resets to zero). M30 (program end) → also cancels.Parsing.G52(from G52Syntax), writes IsoLocalCoordinateOffset section, and adds an"IsoLocalCoordinateOffset"entry to the transformation chain. Modal — persists via backward lookback until changed or cancelled.
- LinearMotionSyntax
Writes McLinear motion for linear commands (ISO G00/G01, Heidenhain L/LN). Detects motion mode from Flags, writes a one-shot MotionEvent section (form + isRapid) plus a modal MotionState section (Term) when MachineCoordinateState exists on the block.
McLinearMotionSemantic discriminates between XYZ-only and XYZABC motion by checking whether rotary axis values are present in MachineCoordinateState.
Must be placed after McAbcSyntax in the syntax chain.
- MCodeExpansionSyntax
Expands machine-declared M-codes (IMCodeDeclarationConfig on the controller parameter table) into the canonical ISO flags the regular consumers already understand: tool change →
M06, spindle direction →M03/M04/M05, coolant →M07/M08/M09. Must run ahead of SpindleSpeedSyntax, CoolantSyntax, and ToolChangeSyntax — expanding early is what lets one composite OEM code (e.g.M13= spindle CW + flood coolant) feed several downstream consumers without any of them fighting over who removes the original flag. Same rewrite-into-shared-vocabulary pattern as HeidenhainRadiusCompSyntax (RL/RR/R0 → G41/G42/G40).Two deliberate boundaries keep the rewrite faithful. Declarations whose sole content is a spindle direction (IsSpindleDirectionOnly) are NOT expanded — SpindleSpeedSyntax resolves them in place via TryResolveDirection(string, out SpindleDirection), which keeps legacy
<SpindleMCode>configs bit-identical and avoids the expansion product being re-translated by that same custom-first map (e.g. a mirrored M03↔M04 remap would otherwise flip direction). Expansion codes are inserted at the declared flag's own position, and a code whose raw twin also appears un-declared elsewhere in the block is not emitted — the block's textual order keeps deciding last-wins conflicts exactly as it did before.Declared-but-unmodeled behavior stays loud: a declaration carrying an UnmodeledNote emits one
DeclaredMCode--UnmodeledEffectsinformational diagnostic per occurrence — a declaration replaces the rawParsing--Unconsumedwarning with an explanation, never with silence. A declaration with no effects and no note consumes its code silently by explicit intent. Undeclared codes are untouched and keep falling through to UnconsumedCheckSyntax.
- MachineCoordSelectSyntax
Handles machine coordinate selection — non-modal, one-shot. The axis values (X/Y/Z) in the block are interpreted as machine coordinates, bypassing all work offsets, local coordinates, tool height compensation, and coordinate rotations. If G91 (incremental) is active, the code is ignored per ISO standard. A per-word incremental stamp on the block (block-root PositioningOverride entry Incremental — Siemens
SUPA Y=IC(-10), klartextL IY-10 M91) is a distance in the machine frame: the word is added to the previous machine position of that axis.Defaults to ISO
G53. Brands with additional one-shot machine-coordinate codes widen SupportedCodes — the Siemens preset addsG153andSUPA(both suppress every active frame for one block; in this pipeline all of those reduce to "bypass the composed ProgramToMcTransform", which the ProgramXyz back-derivation below already models). The matched code is stamped verbatim into Term for bidirectional source recovery. Rotary words on the same block (e.g.SUPA G0 B0,G53 A0 C0) are consumed by McAbcSyntax ahead of this syntax — machine and program rotary coincide while no rotary offsets are modeled — and the block is still a machine-coordinate positioning: the linear axes hold their machine position when no X/Y/Z word is given, and the motion is always McLinear. A machine-coordinate block never takes the RTCP tool-center-point linkage: on a real controller G53 applies no compensation, so a rotary swing commanded through it turns the axis in place instead of dragging X/Y/Z to pin the tool tip (the tip's post-swing program coordinate is what the back-derivation reports).Must be placed before IncrementalResolveSyntax and ProgramXyzSyntax in the syntax chain. When a supported code is active, this syntax consumes X/Y/Z from Parsing and writes MachineCoordinateState directly, preventing ProgramXyzSyntax from processing them as program coordinates — and, ahead of the resolve, reading a per-word incremental word raw instead of re-based into the program frame.
- McAbcCyclicPathSyntax
Resolve modular rotary axes to the shortest cyclic path relative to the previous node. Uses IsModularRotary(string) to determine which axes within MachineCoordinateState need cyclic resolution. Falls back to hardcoded A/B/C if no IMachineAxisConfig is available. Must be placed after ProgramXyzSyntax in NcSyntaxList.
Two stages, mirroring McXyzSyntax:
- Root
MachineCoordinateState— anchored at the previous block's modal rotary state. - CompoundMotion.ItemsKey[*]
— sequential walk through items, anchoring item 0 at the previous
block's modal state and item
i > 0at itemi-1's post-cycle value (per-axis chain). Items without a rotaryMachineCoordinateStateare skipped.
Per-word directional override: a block-root PositioningOverride entry (stamped by SiemensAcIcSyntax) valued PositiveOnly (Siemens
ACP()) or NegativeOnly (ACN()) swaps that axis's window for this block only: [anchor, anchor+360°) / (anchor-360°, anchor] instead of the default ±180° — the approach direction is forced even when it is the longer way around. A target congruent with the anchor (within an ULP-scale epsilon) keeps the anchor value verbatim — no move, never a spurious full turn, and no deg→rad→deg drift. Shortest (DC()) is the default window and needs no special path here. The override is read from the current block only (it is one-shot, never carried — deliberately unlike the modalRotaryWrapgate's one-step previous fallback) and applies to the root MC stage, not to CompoundMotion items in general (G28/G74/G75 expansions capture their words in sub-objects the stamping syntax never sees, so an override can only ever describe a root word) — with one exception: an item whose value for an axis equals the root's pre-pass value verbatim carries that same root word (the canned-cycle pre-positioning item that WriteCompoundMotion(JsonObject, string, JsonArray, Vec3d) stamps from the root MC) and inherits the root's directive for that axis, so the default window cannot fold the forced swing the root resolved into the short way. Directional/shortest entries keyed by an axis outside the modular set are reported asCoord-McAbc--003— the promise cannot be honored there and silence would mis-read the program's intent; an entry with no anchor to resolve against (first rotary value in the stream) is reported asCoord-McAbc--004and adopted unwrapped, matching the default path.Incremental words: a rotary word programmed as an increment — per-word (Siemens
IC(), klartextIC+270) or a bare word under modalG91— is a signed traverse by definition. McAbcSyntax already wrote anchor + delta into the MC and listed the axis in the one-shot IncrementalWords record; this pass reads that record — and only that record — and keeps the listed axes verbatim instead of folding them into the ±180° window (a +270° chain dimension must not become a -90° swing). A listed non-modular axis needs nothing: the literal value is what the axis would do anyway. A rotary value the block carries without a word (lookback fill, or an absolute target another writer put there —G53.1, the HeidenhainPLANEsyntax) is not listed and keeps the default window even under G91.- Root
- McAbcSyntax
Writes rotary axis values (A/B/C) into MachineCoordinateState from Parsing and modal lookback.
Only active when IMachineAxisConfig declares rotary axes. Works for both 3+2-axis (no IMachineKinematics) and simultaneous 5-axis configurations.
This syntax is intentionally ABC-only. When the block is rotary-only (no
ProgramXyz, e.g.G00 A30.) the section is created with ABC but without X/Y/Z. McAbcXyzFallbackSyntax — placed after McXyzSyntax — copies X/Y/Z from the previous block's MachineCoordinateState to finish the section. Splitting the XYZ fill out lets this syntax run before McXyzSyntax (and before G43p4RtcpSyntax) without accidentally filling X/Y/Z from prev and thereby short-circuiting DeriveMcXyz(JsonObject, Mat4d).Missing rotary axes are filled from previous MachineCoordinateState lookback, unless the current section already has the value (e.g., from HomeMcInitializer). Values are stored in degrees (matching McAbcCyclicPathSyntax).
Per-word override: a block-root PositioningOverride section (written by SiemensAcIcSyntax for the Siemens
AC()/IC()coordinate functions, and by the Heidenhain L / C parsers for the klartextIA+/IB+/IC+words) marks a rotary word Incremental: the parsed value is then added to the previous modal value of that axis (previousMachineCoordinateStatelookback, falling back to a value already present in the current section, then 0) instead of being written as an absolute angle. The accumulated raw degrees stay monotonic across iterations: the McAbcCyclicPathSyntax tail-pass keeps an Incremental-stamped axis literal (a chain dimension is a signed traverse, never re-shortened), so even a +270° step survives as net rotation. An Absolute entry (fromAC()) matches the default write and needs no special path — and so, deliberately, do the rotary-family entries Shortest (DC()) / PositiveOnly (ACP()) / NegativeOnly (ACN()): this syntax writes the raw absolute target and the shortest/directional swing is resolved by the McAbcCyclicPathSyntax tail-pass, which owns the wrap math. Brands that never write the section keep the exact legacy behavior.Modal G91: a rotary word with no per-word entry follows the block's Positioning state (written ahead of this syntax by PositioningSyntax on every brand list) the way the linear words follow it in IncrementalResolveSyntax — under
G91it takes the same anchor + delta path as the Incremental override, soG91 … C20.repeated turns the table 20° further each block (Fanuc / Siemens / Heidenhain DIN-ISO all read an incremental rotary word as a signed traverse; the McAbcCyclicPathSyntax tail-pass keeps it literal for the axes the IncrementalWords record lists). A per-word entry of any value beats the modal state for its word (G91 C=AC(90)is absolute). Rotary values another writer put on the block without a word — G53p1RotaryPositionSyntax, the HeidenhainPLANEsyntax — are absolute targets and are not touched. HardNc differs here by design of its own:HardNcLine.BuildOrdinaryMcAbcwrites the word as an absolute angle under G91 and warns ("G91 is not effect for commands ABC"), so the two engines diverge on G91 rotary words — the parity fixtures assert the SoftNc leg only.Coded-position overrides (Siemens
CAC()/CIC()/CDC()/CACP()/CACN()) carry an indexing position number instead of an angle: the number is resolved through IIndexingPositionConfig via TryResolveCodedTarget(IIndexingPositionConfig, string, string, double, double, ISentenceCarrier, NcDiagnosticProgress, out double, out string) and the override entry is rewritten to the plain vocabulary (Absolute / Shortest / PositiveOnly / NegativeOnly — a cyclicCICkeeps its programmed direction through the directional values) before the tail-pass runs, so the tail-pass never sees a coded value. A failed resolve (invalid number, missing table) reports an error and holds the axis at its previous value.Every rotary word the block actually carried is also recorded, as programmed, in the one-shot block-root RotaryWords section (
{ "C": 40 }) — the resolved angle goes intoMachineCoordinateState, which after the modal carry can no longer tell "commanded" from "carried". Downstream consumers that need that distinction (CannedCycleResolveSyntax repeating an active canned cycle on a rotary-only block, WriteCompoundMotion(JsonObject, string, JsonArray, Vec3d) riding the block's ABC on the cycle's pre-positioning item) read the record; the section is never modal-carried. Its sibling IncrementalWords records, with the programmed delta, the rotary words this syntax accumulated as increments (per-wordIC()/IC+or a bare word under modal G91): the MC value is an absolute angle from then on and the modal Positioning still says G91, so without the record an accumulated word would read like a programmed absolute. IncrementalResolveSyntax adds the block's linear increments to the same record later in the list, and McAbcCyclicPathSyntax reads it to pass the listed axes through the shortest-path wrap literally.Must be placed before McXyzSyntax so syntaxes that need the current-block ABC to compute transforms (e.g. G43p4RtcpSyntax) can see it; and before McAbcCyclicPathSyntax and LinearMotionSyntax.
- McAbcXyzFallbackSyntax
Fills missing
X/Y/Zon an ABC-only MachineCoordinateState section. Behaviour depends on whether the block is under RTCP with rotary motion, as indicated by HasDynamicEntry(JsonObject):-
Non-dynamic (no RTCP or RTCP with ABC stable) — the
programmed tool tip stays put in MC while rotary axes (if any) are
unchanged, so we simply copy X/Y/Z from the previous block's
MachineCoordinateState. This matches
NC modal XYZ carry-forward for rotary-only blocks such as
G00 A30.(non-RTCP pivoting). -
Dynamic (RTCP active + ABC changing) — the programmed tool tip
must stay fixed in program coordinates while MC XYZ shifts to
compensate the new rotary state. Looks up the last
ProgramXyz and re-derives
MC = inheritedProgramXyz × composedTransform, where the composed transform is the block's endpoint chain (now including PivotTransformSource as a full rotation+translation Mat4d, so the chain already encodes the kinematic IK). The carriedProgramXyzis also stamped onto the current block so downstream consumers see a consistent ProgramXyz + MC pair.
Pair with McAbcSyntax, which runs early to write ABC but deliberately leaves X/Y/Z empty so McXyzSyntax can still derive MC XYZ from
ProgramXyzvia the transform chain when the block carries linear motion. If McXyzSyntax has nothing to derive (noProgramXyz), this syntax completes the MC section as described above.Does nothing when the section already carries all three of X/Y/Z (normal linear-motion blocks), or when there is no section at all (pure parse-only block that introduces no MC). Must be placed after McXyzSyntax and before McAbcCyclicPathSyntax / LinearMotionSyntax.
-
Non-dynamic (no RTCP or RTCP with ABC stable) — the
programmed tool tip stays put in MC while rotary axes (if any) are
unchanged, so we simply copy X/Y/Z from the previous block's
MachineCoordinateState. This matches
NC modal XYZ carry-forward for rotary-only blocks such as
- McXyzSyntax
Derives MachineCoordinateState from ProgramXyz by applying the composed ProgramToMcTransform.
Processes two stages:
- Root
ProgramXyz→ rootMachineCoordinate - CompoundMotion.ItemsKey[*]
— derives
MachineCoordinatefromProgramXyzfor items that haveProgramXyzbut noMachineCoordinate
ProgramXyz(e.g., ReferenceReturnSyntax) and before syntaxes that readMachineCoordinate(e.g., LinearMotionSyntax).- Root
- OrientationVectorResolveSyntax
Shared vector-orientation resolve — the brand-independent half of tool-axis-vector 5-axis programming (Heidenhain LN
TX/TY/TZ; the Fanuc/Syntec/MazakG43.5 I/J/Kand SiemensA3=/B3=/C3=slots when their adapters land). Consumes the ToolOrientationKey section a brand adapter wrote —{ “Vector”: {X,Y,Z}, “Term”: “<brand word>” }, unit vector in program coordinates — and resolves it into rotary-axis degrees on MachineCoordinateState via OrientationToMcAbc(Vec3d, out Vec3d) (axial-only: rotation about the tool axis is free). Everything downstream is the existing RTCP pipeline untouched: the brand RTCP syntax sees the endpoint ABC, marks the tool-height entry KindDynamic on a rotary change, and LinearMotionSyntax routes the block to ClLinear per-step IK.Branch continuity is seeded explicitly: before solving, the chain is set to the previous block's rotary state (McAbcToMat(Vec3d) on the per-axis MC lookback) so the solver follows the current solution branch deterministically — the implicit chain state cannot be trusted under lazy or out-of-order rebuilds — and the solved angles are unwrapped to the nearest ±360° window of that anchor.
Must run after McAbcSyntax (explicit rotary words and lookback land first; a vector on the same block overrides them) and before the brand RTCP syntax and McXyzSyntax (the section is created rotary-only, so the XYZ derivation still runs — the McAbcSyntax rotary-only discipline). Registered per brand list by the brand that has a vector adapter.
Degradation is diagnosed, never silent: no IMachineKinematics →
Orientation-Vector--NoKinematics; no rotary axes →Orientation-Vector--NoRotaryAxes; solver failure →Orientation-Vector--IkFailed. In every case the XYZ motion proceeds and the posture holds at its previous value (deliberately unlike the CLSF path, which drops the motion on IK failure — divergence recorded on the plan card). The section itself stays on the block as the semantic record.
- PeckDrillingCycleSyntax
G83 peck drilling cycle. Supports modal repetition. Drills in increments of depth Q, fully retracting to R between strokes.
Cycle sequence (per stroke):
- Rapid to init position (target XY, previous Z)
- Rapid from init to R-point
- For each stroke: rapid to clearance above previous depth, feed Q deeper, rapid back to R
- If remainder exists: feed to bottom Z, rapid to R
- Rapid from R/bottom to final (G98 → init Z, G99 → R)
Retraction distance is read from ICannedCycleConfig (Fanuc #4002 / Syntec Pr4002, or FallbackConfig fallback).
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax (modal repetition, G91 conversion, missing-axis fallback) before this syntax runs. Must be placed after CannedCycleResolveSyntax and before IncrementalResolveSyntax in the syntax chain.
- PivotTransformUtil
Shared engine for the brand pivot-gate syntaxes (PivotTransformationSyntax — ISO/Fanuc family, SiemensPivotTransformationSyntax — Siemens). Each brand syntax owns only its gate (which modal terms mean “commanded XYZ needs the Pn→MC kinematic rigid transform”); the endpoint-ABC resolution and the
PivotTransformchain entry composition live here so every brand writes the identical JSON vocabulary.
- PivotTransformationSyntax
ISO/Fanuc-family pivot gate: writes the PivotTransformSource entry into ProgramToMcTransform on blocks where the controller is interpreting commanded XYZ in a frame that needs the Pn→MC kinematic rigid transform — namely active RTCP (G43.4) or active tilted plane (G68/G68.2). On plain-mode blocks (no RTCP, no tilted plane), the controller treats commanded XYZ as machine-frame directly, so the indexed rotary angle is a positioning value only and must not fold into the linear axes; this syntax skips those blocks and leaves the chain at identity (or whatever non-kinematic offsets earlier syntaxes contributed). Brand variants with their own modal vocabulary gate the same shared engine (PivotTransformUtil): Siemens TRAORI/CYCLE800 → SiemensPivotTransformationSyntax; Heidenhain M128/PLANE SPATIAL would follow the same pattern.
Mirrors real Fanuc semantics: plain G43 offsets along the active tilted-plane normal (or machine Z when no tilt is active), and plain XYZ moves map directly to machine axis registers regardless of indexed table/head rotary position. Only G43.4 follows the live tool vector and only G68.2 redefines the work-plane orientation — both of which this guard detects via the existing chain markers.
Chain position: must run after all Pn-frame writers (IsoG68p2TiltSyntax, ToolHeightOffsetSyntax, G43p4RtcpSyntax, IsoCoordinateOffsetSyntax, brand-specific coord offset syntaxes) so the guard sees the finalised mode markers and the
PivotTransformentry — when emitted — naturally lands as the last chain element. Must run before McXyzSyntax / ProgramXyzSyntax so they see the completed chain.Silently no-ops when IMachineKinematics is absent (3-axis configurations without rotary kinematics).
- PlaneSelectSyntax
Consumes G17/G18/G19 plane selection from Flags and writes IPlaneSelectDef section using conventional axis-pair names (XY/ZX/YZ). Modal — persists via backward lookback. Default is XY (G17).
Downstream consumers (CircularMotionSyntax, IsoG68RotationSyntax) call GetPlaneNormalDir(JsonObject) to read the resolved plane.
- PolarGCodeCheckSyntax
Warns on G-codes that Fanuc disallows during Polar Coordinate Interpolation (G12.1), per the manual whitelist mirrored from HardNc
IsGCodePolarModeCompatible(IncompatibleDiagId).Placed FIRST in the Logic bundle so the scan sees Parsing Flags before the mode/plane/offset syntaxes consume their codes — at the old in-place check position, G17/G18/G19, G20/G21, G49, G53.1 and G68/G69 cancels had already been eaten and passed silently. Polar-active detection needs no valve of its own: the PREVIOUS block's PolarInterpolationState is already final (the whole Logic bundle ran for it) unless this block exits with G13.1; a block entering with G12.1 is checked too.
Residual blind spot (documented): codes captured as Parsing sub-objects by
ParameterizedFlagSyntaxin the Parsing bundle (G28, G43/G44, G43.4, G05.1, G52, G54.1, G68/G68.2, canned cycles) never reach Flags and stay outside the scan. They cannot corrupt the polar trajectory — the plane normal is fixed by the polar pair — so the gap is diagnostic-only, same as HardNc's own per-code parse-time check.
- PolarInterpolationSyntax
Maintains the modal Polar Coordinate Interpolation valve section (PolarInterpolationState) for Fanuc G12.1/G13.1.
On a G12.1 block: consumes the flag, reads the block's own X/C words as the anchor (InitRxcz; the X word is a diameter and is halved), converts the previous program position (program X/Z + machine C angle) onto the polar hypothetical plane via GetProgramPolarRxczByOrdinaryProgramXcz(Vec3d), and writes both the state section and the entry ProgramPolarRxcz position. Mirrors HardNc
HardNcLinecase12_100.On a G13.1 block: consumes the flag and stops carrying the state — the block itself is already Cartesian, matching HardNc case
13_100.On other blocks: re-materializes the previous block's state section (single-step lookback carry, the PositioningSyntax pattern), and warns
Must be placed before McAbcSyntax so the downstream ProgramRxczSyntax can consume the hypothetical C word before it is interpreted as a rotary machine axis.FanucPolar--IncompatibleGCodefor G-codes outside the Fanuc polar-mode whitelist (mirrors HardNcIsGCodePolarModeCompatible).
- PositioningSyntax
Detects G90/G91 positioning mode from Flags (or by modal lookback) and writes a Positioning section (Term, Mode) to the block JSON.
Fanuc/ISO: reads G90/G91 from Flags (global modal). Heidenhain: klartext has no modal word — the modal state stays at the G90 default and the I-prefixed words (
IX+20) ride on the same per-word override as Siemens, stamped by the L / C / CC / CYCL CALL POS parsers (see HeidenhainIncrementalAxisWordUtil); the DIN/ISO dialect on that brand uses G90/G91 like Fanuc. Siemens: the AC()/IC() per-word override rides on top of this modal state — SiemensAcIcSyntax writes a PositioningOverride section the downstream consumers (IncrementalResolveSyntax, McAbcSyntax) honor per axis.Does NOT convert incremental values — that is handled by IncrementalResolveSyntax which can be placed later in the syntax chain, after canned cycle syntaxes have consumed their parameters with cycle-specific G91 semantics.
- ProgramEndCleanSyntax
Clears the per-block
Vars.Volatiledictionary on blocks that triggered program end (M02 / M30, identified by the ProgramEnd section written by ProgramEndSyntax).Real Fanuc clears non-retained common variables (#100-#499) on program end + reset; this syntax models that behaviour at the simulator level. The clear happens on the same block that carried M02/M30 — the next block's VolatileVariableReadingSyntax carry then sees an empty dictionary on the predecessor and starts fresh.
Pipeline placement: must run after both ProgramEndSyntax (which writes the ProgramEnd section this syntax checks) and VolatileVariableReadingSyntax (so the carry has already happened on this block; this syntax overwrites the result).
Retained common variables (
#500-#999, owned by RetainedCommonVariableTable) are untouched — they survive program end on real hardware (NV-RAM). Local variables (#1-#33, scope: macro call frame) are also untouched here; their lifecycle belongs to G65/G66/M99 push/pop, not program end.Also clears any active FanucModalMacro on the same edge: a G66 modal that was still active when M02/M30 hit is implicitly cancelled, matching real Fanuc reset behaviour. The section is overwritten with a
G67-shaped cancel marker so the carry mechanism in FanucModalMacroSyntax sees the boundary and does not propagate the modal past the program-end edge.
- ProgramEndSyntax
Consumes M02/M30 (program end) from Flags and writes IProgramEndDef section.
Downstream syntaxes that need to reset modal state on program end (e.g. IsoLocalCoordinateOffsetSyntax for G52 reset) should read the ProgramEnd section rather than scanning for M30 in Flags directly.
The program-end edge. On a real controller M02/M30 ends the program and enters the reset state: the modal G codes return to their power-on defaults — tool length compensation is cancelled (G49, which also ends tool-center-point control: Fanuc TCP is cancelled by G49 or reset), the tilted work plane and coordinate rotation are cancelled (G69), cutter radius compensation is cancelled (G40), the canned cycle is cancelled (G80). A simulator that plays a file with several programs chained by M02 must keep playing, so the reset is modelled as an edge between the program-end block and its successor: the program-end block itself keeps the modal state it executed under (its own motion —
Must be placed before syntaxes that depend on the ProgramEnd section.G0 Z100. M30— still sees the compensation), and the successor starts from the reset defaults. Each modal owner tests the edge with IsResetEdge(LazyLinkedListNode<SyntaxPiece>) in its single-stepnode.Previouslookback and writes its cancel state on the successor instead of carrying: ToolHeightOffsetSyntax (G43/G44 → G49), G43p4RtcpSyntax (G43.4 → G49), SiemensTraoriSyntax (TRAORI → TRAFOOF, the D compensation itself stays — Siemens retains the active tool on reset), HeidenhainRtcpSyntax (M128 / TCPM → off, TOOL CALL compensation stays), TiltTransformUtil (every tilt / rotation / frame term → G69), RadiusCompensationSyntax (G41/G42 → G40, the modal D is kept) and CannedCycleResolveSyntax (→ G80). Deliberately not reset: G00/G01, G90/G91, G17–G19, G94/G95, the work offset (G54–G59) and the path-smoothing mode — their reset defaults are controller-parameter dependent and they do not enter the program→machine transform chain; G20/G21 is retained by the controller itself. G52 keeps its existing behaviour of clearing on the program-end block (HardNc parity). A block right after the edge that has no words at all (a comment line) is still the edge — every owner handles it before any "no Parsing" early return, or the modal carry would clone the active section across it.
- ProgramRxczSyntax
Polar-mode sibling of ProgramXyzSyntax: while the PolarInterpolationState valve section is present, consumes the block's X/C/Z words as polar hypothetical-plane coordinates (X = diameter, halved; C = hypothetical axis in mm) and writes:
- ProgramPolarRxcz — the
anchor-relative polar position (G90/G91 resolved against the previous
block's position, mirroring HardNc
NcGroup03.GetNcFromSyntax); - ProgramXyz — the derived ordinary program position (radius, previous program Y, Z), so the downstream McXyzSyntax derives machine XYZ through the normal transform chain — ProgramXyzSyntax itself naturally no-ops because the axis words are already consumed;
- the machine C angle (degrees) into MachineCoordinateState — placed before McAbcSyntax, which then preserves the value instead of treating C as a directly-commanded rotary word;
- on motion-programmed blocks, MotionState and a MotionEvent with McPolarLinear (G00/G01) or McPolarArc (G02/G03 with R or I/J/K resolved on the hypothetical plane).
- ProgramPolarRxcz — the
anchor-relative polar position (G90/G91 resolved against the previous
block's position, mirroring HardNc
- ProgramStopSyntax
Consumes the program-stop words in SupportedCodes (default M00 unconditional / M01 optional) from Flags and writes a IProgramStopDef section on the block that carried the flag. Non-modal: the section is written only on the exact block where the stop code appears.
SupportedCodes is ordered by priority: when several listed words share a block the first listed one wins and stamps
Termwith its literal; every listed word is removed from the block either way. A brand preset widens the list for its own vocabulary (the HeidenhainSTOPword — the SupportedCodes precedent).Siblings with ProgramEndSyntax (M02/M30) which handles end-of-program, not in-program stops.
The parsing layer only records NC intent. Whether M01 actually pauses the run is a runtime/semantic decision gated by the operator's "Optional Stop" switch (analogous to IBlockSkipConfig for block skip).
- ProgramXyzSyntax
Resolves ProgramXyz (leaf coordinate) from syntax XYZ tags. Writes ProgramXyz sub-object to SyntaxPiece.JsonObject. Must be placed after BundleSyntax since it uses cross-node lookback for last position.
McXyzSyntax (placed after this in the chain) reads ProgramXyz and writes MachineCoordinateState.
- ProgramXyzUtil
Shared utilities for ProgramXyz and MachineCoordinateState lookback and resolution. Used by ProgramXyzSyntax, ReferenceReturnSyntax, and semantic resolvers that need position lookback.
Two strategies for "what's the program coordinate at a block's endpoint?" — both invert an MC value through an ProgramToMcTransform chain, but they pick the chain from different nodes:
-
By current-state transform
(ComputeProgramXyzByCurrentTransform(LazyLinkedListNode<SyntaxPiece>, Vec3d)) — modal anchor is
MachineCoordinateState. Re-expresses an
MC value (typically a predecessor's modal MC) into the current
block's program frame using the current block's chain. Suitable for
chain-change blocks where the spindle physically stays put while the
chain (G54 swap, G68.2 activation, G43.4 toggle, tool-height change,
...) re-anchors the program frame; mirrors legacy
HardNcLine.RebuildProgramXyzByMc. -
By corresponding-state transform
(ComputeProgramXyzByCorrespondingTransform(LazyLinkedListNode<SyntaxPiece>)) — modal
anchor is ProgramXyz. Recovers the
program coordinate that
nodeCarryingMcwas originally commanded at, by inverting that same node's own transform on its own MC. Suitable for RTCP rotary-dynamic inheritance, where the modal invariant is "tool tip in workpiece frame stays put while rotary axes turn" — the recovered Vec3d carries forward as the next rotary block's modal ProgramXyz unchanged, regardless of how itsPivotTransformdiffers.
Both strategies yield the same Vec3d when prev and current share the same chain modal state; they only diverge across chain boundaries (RTCP toggle, coord-system swap, tilt activation) and at rotary motion (PivotTransform difference). Pick the wrong one and the result lands in a stale frame:
-
Non-RTCP using "corresponding" — leaves the pre-chain-change values,
so a block emitted right after
G43.4 H03would inherit ProgramXyz still in the G49 frame and the next motion's MC.Z drifts by the introduced tool-height offset. (This was the 2026-04-25 SoftNc / HardNc divergence found on a five-axis sample program.) -
RTCP using "current" — double-counts the rotary
PivotTransformdifference, so the inherited workpiece anchor rotates by the C delta on every rotary block.
Direct callers of the two strategy helpers are rare — typically you call the dispatcher ResolveBlockProgramXyz(LazyLinkedListNode<SyntaxPiece>, Vec3d) (block's own MC vs predecessor lookback, picks strategy from HasDynamicEntry(JsonObject)) or GetLastProgramXyz(LazyLinkedListNode<SyntaxPiece>) (pure predecessor lookback).
-
By current-state transform
(ComputeProgramXyzByCurrentTransform(LazyLinkedListNode<SyntaxPiece>, Vec3d)) — modal anchor is
MachineCoordinateState. Re-expresses an
MC value (typically a predecessor's modal MC) into the current
block's program frame using the current block's chain. Suitable for
chain-change blocks where the spindle physically stays put while the
chain (G54 swap, G68.2 activation, G43.4 toggle, tool-height change,
...) re-anchors the program frame; mirrors legacy
- ReferenceReturnSyntax
Writes ICompoundMotionDef section for G28 reference point return. Reads intermediate XYZ from
Parsing.G28(written by G28Syntax) and converts to machine coordinates via ResolveProgramXyz(JsonNode, LazyLinkedListNode<SyntaxPiece>, ISentenceCarrier, NcDiagnosticProgress).Must be placed after LinearMotionSyntax in the syntax chain. Removes the IMotionEventDef section written by LinearMotionSyntax (G28 handles its own motion). Overwrites root MachineCoordinateState and ProgramXyz with reference position for subsequent block lookback.
- RotaryAxisUtil
Shared utilities for rotary axis (A/B/C) resolution. Used by G53p1RotaryPositionSyntax, McAbcSyntax, IsoG68p2TiltSyntax, IncrementalResolveSyntax and other syntaxes that read or write rotary axis values.
- SpindleSpeedSyntax
Consumes S (spindle speed) and spindle direction M-codes from Parsing. Both are modal — persist across blocks via backward node lookback. Writes resolved state to a ISpindleSpeedDef section. Direction is converted from M-codes to the conventional SpindleDirection enum at this layer.
Direction M-codes: the ISO defaults M03 (CW) / M04 (CCW) / M05 (STOP) always apply; a machine that starts/stops its spindle with custom M-codes (e.g., ultrasonic
M203/M205) declares them on an ISpindleControlConfig dependency (ControllerParameterTableBase), which this syntax consults first — mapped flags are consumed like the ISO ones.Fallback: an S > 0 with no direction ever issued is contradictory (physics would silently never run). The build assumes CW and emits a one-shot
SpindleDirection--AssumedCwvalidation warning — once is structural, not stateful: the stamped CW propagates modally, so later blocks no longer lack a direction. An explicit M05 (STOP) is a real direction and never triggers the fallback.
- TappingCycleSyntax
G84 (right-hand) / G74 (left-hand) tapping cycles. Supports modal repetition.
Cycle sequence:
- Rapid to init position (target XY, previous Z)
- Rapid from init to R-point
- Feed from R-point to bottom Z
- Spindle reverse at bottom
- Feed retract to final Z (G98 → init Z, G99 → R)
- Spindle restore to forward direction
G84: forward = CW (M03), reverse = CCW (M04). G74: forward = CCW (M04), reverse = CW (M03).
Reads absolute coordinates from the cycle section, which is resolved by CannedCycleResolveSyntax (modal repetition, G91 conversion, missing-axis fallback) before this syntax runs. Must be placed after CannedCycleResolveSyntax and before IncrementalResolveSyntax in the syntax chain.
- TiltTransformUtil
Shared utilities for all tilt transform syntaxes (ISO, Siemens, Heidenhain). Handles section IO, backward lookback, and ProgramToMcTransform composition.
- ToolChangeMotionSyntax
Synthesizes the machine motion of a tool change: on a block whose SectionName section carries IsChangeKey = true AND whose tool number actually differs from the previously equipped tool, overlays IToolingMcConfig's per-axis tooling position onto the current machine pose (NaN / missing axis = stays) and emits a one-item rapid ICompoundMotionDef to that target — the axis travel a real machine's M06 macro performs before the changer cycle runs. Root ProgramXyz (and the moved rotary axes in root MachineCoordinateState) are overwritten for subsequent-block modal lookback, mirroring
HardNcLine's M06 handling (McXyz/McAbc_radoverlay +RebuildProgramXyzByMc).A same-number tool call (M06 without an actual change) emits no motion — the parity twin of HardNc's
preT != Toverlay gate. A block with its own motion words folds them into the single rapid: the overlay applies on top of the block's commanded position and the stamped CompoundMotion makes LinearMotionSyntax skip the block, so one contour covers both — the HardNc M06 branch shape. Placement: the ReferenceReturnSyntax (G28) slot — after the offset/frame syntaxes (the ProgramXyz back-derivation needs the composed transform), before McXyzSyntax / McAbcCyclicPathSyntax (root MC XYZ backfill; rotary targets wrapped shortest-path by the cyclic tail-pass).Programs that retract on their own (G75/G28/SUPA before M06 — every healthy post) reach the tooling position before the M06 block, so the synthesized move is zero-length and CompoundMotionSemantic emits nothing. Only a program that leaves the tool elsewhere (typically hand-edited) gets an actual synthesized travel — and the machining steps along it surface any material contact, plus the runtime's
ToolChange--UnsafePosediagnostic.
- ToolChangeSyntax
Consumes T (tool number) and M06 (tool change) from Parsing. T is modal — persists across blocks. M06 triggers the change. Writes resolved state to a
ToolChangesection:{ “ToolId”: 1, “IsChange”: true, “Term”: “M06” }. TermKey records the trigger command and is only written when IsChangeKey is true (i.e. the block actually carried the tool-change M code); modal-only blocks omit it.Two more keys mirror HardNc's
T/PreparationTsplit. PreparedToolIdKey holds the second T of a dual tool word (T10 T2 M06: 10 is loaded, 2 is pre-selected) and is carried until the next change loads it. EquippedToolIdKey is written on non-change blocks and names the tool in the spindle — the ToolId of the last change — becauseToolIdon such a block may already be a pre-selection (T2alone). Consumers read it through ReadEquippedToolId(JsonObject).ToolIdis an int for numeric calls (T5) and a string for Siemens string tool calls (T="D8R1", captured by SiemensToolCallSyntax); both shapes carry modally. String names are resolved to tool numbers at the semantic layer (ToolChangeSemantic) — this syntax records the call verbatim.The trigger is machine-configurable. A custom tool-change M-code (Siemens MD22560
$MC_TOOL_CHANGE_M_CODE) is declared on the controller parameter table (IsToolChange) and reaches this syntax already expanded to M06 by MCodeExpansionSyntax. Turret/lathe machines where theTword itself performs the change (Siemens MD22550$MC_TOOL_CHANGE_MODE= 0) set ToolWordTriggersChange; the block then triggers with ToolWordTerm recorded as TermKey. Without that config a bareTblock stays pre-selection only — magazine rotation is the PLC's business and moves no feed axis.
- ToolHeightOffsetSyntax
Resolves ISO tool height offset (G43/G44/G49) to the effective offset value (mm) and composes the offset as a translation into the accumulated ProgramToMcTransform matrix.
RTCP modes (G43.4, TRAORI, M128) are handled by separate brand-specific syntaxes (e.g., G43p4RtcpSyntax).
- UnitModeSyntax
Detects the unit-system code (ISO Group 06: G20 inch / G21 metric) from Flags and writes a Unit section (Term, System). Modal — absence of an explicit flag inherits the previous block's unit, defaulting to Metric at program start.
The code vocabulary is configurable per brand: InchCodes / MetricCodes default to ISO G20 / G21; the Siemens preset uses G70+G700 / G710+G71 instead (G70/G71 switch geometry-word interpretation only, G700/G710 also switch feedrate interpretation — a distinction preserved via the verbatim Term but irrelevant to this record-only syntax; both inch variants warn identically). The first MetricCodes entry doubles as the program-start default
Term. RS-274-D, Syntec and Fanuc turning G-code system C all spell the units G70 inch / G71 metric, and the Fanuc milling dialects (RS-274-D descendants) read them the same way, so the Fanuc/Syntec presets carry all four codes. The finishing/roughing-cycle collision exists only on Fanuc turning G-code systems A/B (system C moved those cycles to G72/G73) — a future turning preset for those systems must not inherit this milling preset's vocabulary.The HiNC pipeline works exclusively in millimetres. When an inch code is detected this syntax emits an
Unit--InchNotSupportedUnsupported Error so upstream callers are forced to pre-convert the NC program to metric — while still recording what the program said. Metric codes are accepted as no-op confirmations of the default.
Enums
- BareG28Behavior
Configurable handling for a G28 block with no axis specifiers (“bare G28”) — value of BareG28. Real Fanuc-class controllers vary: older 0i-M alarms (PS010), some 30i variants send every configured axis to home. Default to Alarm so silent NC bugs surface; opt into AllAxesHome per syntax instance.