Table of Contents

Tool Offsets

Tool Offsets is the Controller branch's brand-neutral offset ledger — one row per integer offset number, each carrying a height and a radius split into an ideal (geometry) component and a wear component. It lives on the General Setup page at /general-setup under the Control-Tree id equipment/controller/program-data/tool-offsets, and that one id is the whole of what this page owns. The leaf is ungated: every brand preset proxies the table behind it, so the node is grown wherever a runner resolves — Brand Matrix.

Important

Anatomy makes no claim that any id named here is stable across versions. The migration hop that keeps an older ?tree= value resolving is described in Tree Ids and Routes.

The panel follows the branch's shared rules — one read on mount, per-control commit, the capture-assign-await-restore write, the two empty layers, and a dense markup table with no sort and no pagination. Those rules, and the places this panel departs from them, are Editing Contract. What is this leaf's own is everything below.

One Node, Two Labels

The node id never changes, and the tree label does. The branch builder pushes this leaf unconditionally, then chooses its label from the snapshot's Siemens tool-offset flag:

Condition Label the tree row reads
No SiemensToolOffsetTable resolves Tool Offsets
One does Tool Offsets (ISO G43 H)

Among the shipped presets only Siemens carries that table, so only Siemens sees the second label — and it sees it beside a second offset leaf, Tool Offsets ($TC_DP), grown from the same flag. The qualifier exists so that the two ledgers do not read as one row of the tree; the id under it is equipment/controller/program-data/tool-offsets either way, so an existing deep link is unaffected by which label is showing. Which flag grows which leaf, and what that flag probes, is Brand Matrix.

A second, independent Siemens signal appears inside the panel. While the runner's brand marker reads Siemens, an orange caption sits above the toolbar:

"On Siemens this table serves the ISO G43 H path only — it is not the Sinumerik tool-offset ledger. D tool calls read the Tool Offsets ($TC_DP) table; when a (T, D) row is missing there, playback falls back to this table's height for the tool number and reports a warning."

The label is chosen from the table flag and the caption from the brand string. Those are computed from different things and can disagree — one of the departures catalogued in Editing Contract.

What a Row Holds

The table is the Fanuc Memory C layout: the H number and the D number address the same row, so one row carries both the length and the radius of one offset. Fanuc's older memory types are narrower — Memory A keeps a single value per offset number (no geometry/wear split, and H and D read the same number), Memory B splits geometry from wear but still lets H and D share one value — so a program written for those controls calls the radius by a different number than the length (G43 H3 with G41 D33, say). A program that pairs H3 with D3 therefore presupposes Memory C, which is what this table models; on the older types the same words would compensate the radius by the tool length.

Column Stores Read by
Tool # the offset number, the row's key the H word, the D word, or the Heidenhain tool number
Ideal Height (mm) the tool's length before wear the height side
Axial Wear (mm) accumulated length wear the height side
Ideal Radius (mm) the cutter's radius before wear the radius side
Radial Wear (mm) accumulated radius wear the radius side

There is no unit column and no unit conversion: ToolOffsetRow stores four plain millimetre doubles, and the panel writes and reads them unchanged. No cell is bounded except the key, which takes a minimum of

  1. The panel renders no description caption above its table: the Siemens caveat is the only line it shows there, and the only other prose it carries is the closing caption the tool-house dependence turns on below the table.

The key column is the branch's one renameable key, and the rename is guarded on both sides of the wire: the panel refuses a value already present in its own rows with the toast "Tool id {id} already exists." and re-reads the table, and the endpoint refuses the same case with a message naming the existing id. Below 1 never reaches either — the numeric widget rejects the bound itself, marks the box and emits nothing. A non-integer that the widget does emit is dropped by the handler with no request and no message at all.

The Sign Convention

On this table an effective value is the ideal minus the wear. FullHeight_mm is the ideal height less the axial wear and FullRadius_mm the ideal radius less the radial wear, and those two are exactly what IToolOffsetConfig hands every consumer. A positive wear therefore shortens the compensation and a negative wear lengthens it. Nothing in the panel states this: the four column headers name the components and no footnote names the operation, which is why the rule belongs here. An offset number the table holds no row for is not an error either: both getters answer 0 for it and the program plays on with a zero compensation — each side reports the miss once, from the block that selects the row (Comp-ToolHeight--RowMissing on the H word, RadiusComp--RowMissing on the D word), and nothing else marks it.

Warning

The neighbouring Siemens ledger uses the opposite convention. On SiemensToolOffsetTable an effective value is the geometry plus the wear, and that panel prints the addition in a footnote under its own table. On a Siemens project both leaves are present at once, so one wear number entered in the two ledgers moves the tool in opposite directions. See Siemens Tool Offsets.

How the height reaches the machine

The height side is consumed as a translation, not as a property of the tool. On a G43 block the ISO tool-height syntax reads the effective height for the H number and composes it into the program-to-machine transform chain as a translation of that many millimetres along the current tool orientation; G44 negates the same number and G49 writes zero. The G43.4 RTCP path reads the same number for the same H word and re-aims it through the machine kinematics. On Heidenhain there is no cancel word: a TOOL CALL block reads the effective height for the tool number and adds the block's DL delta to it, and the result composes into the same chain entry.

Nothing on that path reads the tool the project actually carries. The modelled tool assembly's own spindle-buckle-to-tool-tip length is what decides where its tip sits once the machine has moved by the compensation, so the stored ideal height and the modelled tool's length are two independent numbers that have to agree for the programmed contour to land on the workpiece. Making them agree is the entire purpose of the tool-house dependence below, which writes SpindleBuckleToToolTipLength straight into the ideal column.

How the radius reaches the path

The radius side is read by the G41/G42 radius-compensation syntax against the D number, and it is signed rather than absolute. A negative effective radius — a radial wear larger than the ideal radius — is accepted, and it offsets the path to the opposite of the programmed side. On the first block of a compensation move the parser raises a validation warning for that case on Heidenhain only, mirroring the look-ahead check a TNC control performs; the other brands take it silently.

An effective radius larger than the inner radius of the contour it compensates is a different case. The compensated path then runs against the programmed direction — an inner arc smaller than the offset, or an inner step shorter than it — and a control stops there (Fanuc alarm PS0041, interference in cutter compensation). Both engines report it as the validation warning RadiusComp--Interference on the offending block and keep playing the swapped-side path, so the run stays inspectable. The usual cause is this table rather than the program: a CAM post that emits the tool-centre path and leaves G41 D.. in place expects the D row to hold the wear only, so the ideal radius of such a row is 0 — and a tool-house refresh left on its cutter-radius basis writes the cutter's real radius into every row and silently turns that program into an overcut. The radius basis below is what tells the refresh which strategy the program follows; the tell-tale signs of a tool-centre program are lead-in and lead-out arcs smaller than the tool radius and a finishing contour that lies one tool radius inside the wall.

The macro-variable window

On the Fanuc, Mazak and Syntec syntax lists a variable lookup maps #2001 through #2200 onto the effective height of offsets 1 through 200, so a macro program can read the same subtraction the G43 path applies. It is a read: the lookup exposes no writer, and neither the Siemens nor the Heidenhain syntax list registers it.

Where a blank cell comes from

A cleared numeric cell is not a stored value — the panel drops a null or a non-finite edit before sending anything, so clearing a cell leaves the number on the server and the box repopulates on the next remount. A blank cell that arrives from the server is a different thing: it is NaN, which the numeric widget renders as empty text. The tool-house refresh is what writes one, for a tool whose tip length does not resolve. Only the Siemens $TC_DP fallback tests for it — a NaN height there is reported as a configuration warning and degrades to zero — while the ISO G43 path composes whatever the table returned.

Tool-House Dependence

Above the table sits the toggle Set ideal offset dependent on tool house. It is the one setting this leaf edits that is not a dependency at all: it writes IsIdealOffsetDependentOnToolHouse, a project-level element serialized beside the runner suit, so it is untouched by a runner install and by the sweep that follows a brand switch. Its stored default is on; the reader answers off when no project is loaded, which is a fallback rather than the model's own default.

With the dependence on, the panel hands three things over:

  • The two ideal columns become read-only, and a closing caption says so: "Ideal columns mirror the Tool House; only the wear columns are editable." Read-only is a screen guarantee only — the write is row-scoped, so the ideal values ride along on every wear edit, and the endpoint writes all four components unconditionally. That the two ideal cells still commit on Enter is one of the panel's catalogued departures from Editing Contract.
  • The Tool # cell is replaced by bold plain text, so the key cannot be renamed.
  • Row CRUD disappears: the Add button is not rendered and each row's delete button becomes a grey em dash. A Refresh from Tool House button appears next to the toggle in their place.

The radius basis

Length and radius do not follow the tool house the same way, because shops post their programs under two cutter-compensation strategies and only one of them wants the cutter's radius in the table:

Basis The NC path is The D row holds A refresh writes to Ideal Radius
Cutter radius the part contour; the control offsets it by the tool radius geometry (the cutter radius) plus wear the largest radius on the cutter profile
Zero-based the tool-centre path — the CAM system already offset it, G41 D.. stays in the program the wear only; geometry is 0 0

The basis is a project-level setting — RadiusOffsetBasis (RadiusOffsetBasis, serialized beside the tool-house dependence, default cutter radius so an older project refreshes exactly as before) — chosen from the Radius basis select in the panel's toolbar and written through PUT api/mech/soft-nc-runner/tool-offsets/radius-basis. Every refresh honours it: the button, the play-time refresh while the dependence is on, and both refresh endpoints. The height side is untouched by the choice; the wear columns are never written by any refresh.

Pick the basis from the program, not from habit: a zero-based program shows lead-in and lead-out arcs whose radius is smaller than the tool radius and a finishing contour one tool radius inside the wall it produces (a Ø8 end mill following a circle of radius 2.5 is finishing a Ø13 bore, not a Ø5 one); a cutter-radius program draws the part contour itself. Refreshing a zero-based program on the cutter-radius basis is the overcut the radius side reports as RadiusComp--Interference — see How the radius reaches the path.

The table does not otherwise track the tool library

Offset number and tool id are independent integers, and with the dependence off nothing reconciles them. A row may name a number the project's tool library has never held, and the library may hold tools with no row; neither state is reported anywhere on this panel, and both are stored and played as written. The library itself is edited on Tool House Page.

The refresh is what imposes a one-to-one mapping, and it does so in both directions.

The refresh is destructive, and it is not only a button

One endpoint backs both the button and the toggle, and it runs three steps. For every tool in the project's library it writes the ideal height from that tool's spindle-buckle-to-tool-tip length — NaN where the length does not resolve — and, where the cutter is a milling cutter, the ideal radius by the radius basis: the largest radius on its cutter profile under cutter radius, 0 under zero-based; a row is created for a library tool that had none. It then deletes every row whose number is not in the library. The wear columns are never touched.

The engine carries the same three steps of its own, as UpdateIdealByToolHouse(API), and that copy is what playback calls. The two agree step for step and part company only on a missing library: the engine method returns without doing anything, while the endpoint answers unsuccessfully with “No tool house available”.

Three consequences deserve stating plainly.

  • There is no confirmation. The dialog that guards a single delete button does not guard this, and there is no undo.
  • Turning the toggle on runs it immediately. Enabling the dependence is two server calls in one attempt: the flag write, then the refresh. A project with no tool library fails the second with “No tool house available” after the first has already committed, and the panel's local toggle springs back while the server-side flag stays on — the two-call rollback recorded in Editing Contract. Turning the toggle off writes the flag and stops there — no refresh, and no re-read.
  • Playback runs it too. While the flag is on, starting an NC program refreshes the table from the library before the first block, so a hand-typed ideal value is replaced whether or not the panel was ever opened, and an empty library at that moment empties the table. No panel is told: what is on screen is whatever the last read returned until the selection moves away and back.

Row Life Cycle

The add and the delete are the two flows the branch spells one way here and another way elsewhere.

Add is a fieldless toolbar button, because the server mints the key: the endpoint takes the highest existing number plus one — or 1 on an empty table — inserts an all-zero row and returns the number, and the panel appends that row locally rather than re-reading. All four zeros are the server's actual new-row values, not a screen placeholder.

Delete opens a confirmation dialog first, titled Remove tool offset over "Remove the offset row for tool #{id}?", and filters the row out locally once the request resolves. Deleting a number the table no longer holds answers unsuccessfully with a message naming it.

Rename is optimistic and re-sorts the rows ascending once the write returns; a failure restores the previous number. Every other cell is a per-cell handler that sends the whole row.

When the Table Is Absent

The leaf carries both of the branch's empty layers. The shared one renders "No NC runner — load a project first." while the snapshot reports no runner. The panel's own read then gates on whether a ToolOffsetTable resolved, and renders "No tool-offset table on the active runner." when it did not — which hides the dependence toggle along with the table, since the whole body sits behind that guard. A failed write answers inside a success envelope. The row write, the delete, the rename and the refresh all go through the branch's shared dependency helper and carry No ToolOffsetTable on the active runner, which the panel shows as its own context followed by that sentence. Add does not use that helper: it tests the table itself and answers No tool-offset table on the active runner — the panel's own absent-table line, less its full stop. The dependence write reaches neither string, because what it writes is a project-level element rather than the table; it is the one route on this leaf that answers a coded payload instead, and it reports no project loaded.

That second layer is hard to reach. ToolOffsetTableProxy is a seedless get-or-create placeholder: on wiring it installs a bare table into the project's per-case list whenever the project holds none, and the suit wires its proxies when it is deserialized and again on every runner assignment.

Layout

  • General Setup Control Tree — the left dock of /general-setup
    • Controller Node Row
      • Program Data Group Row
        • Tool Offsets Node Row — equipment/controller/program-data/tool-offsets; reads Tool Offsets (ISO G43 H) while a Siemens $TC_DP table resolves
  • Editor Row — the panel of whichever node is selected
    • Tool Offsets Panel
      • No-Runner Body — "No NC runner — load a project first."; shown instead of everything below
      • Absent-Table Body — "No tool-offset table on the active runner."; shown instead of the body
      • Siemens Caveat Line — orange caption, shown while the brand marker reads Siemens, with G43 H and D set in code style
      • Toolbar
        • Set ideal offset dependent on tool house Toggle
        • Refresh from Tool House Button — flat, refresh icon; only while the dependence is on
        • Radius basis Select — dense, outlined; Cutter radius (D row = cutter radius; NC is the part contour) or Zero-based (D row holds wear only; NC is the tool-centre path)
        • Add Button — primary, add icon, right-aligned; only while the dependence is off
      • Radius-Basis Hint Line — grey caption under the toolbar, one of two sentences chosen by the selected basis
      • Offset Table — dense, flat, bordered
        • Header Row — Tool #, Ideal Height (mm), Axial Wear (mm), Ideal Radius (mm), Radial Wear (mm), then an unlabelled action column
        • Offset Row, one per offset number, ascending
          • Tool Number Cell — a numeric field with a minimum of 1, or bold plain text while the dependence is on
          • Ideal Height, Axial Wear, Ideal Radius and Radial Wear Numeric Fields — the two ideal ones read-only while the dependence is on
          • Delete Button — or a grey em dash while the dependence is on
      • Dependent Footnote — "Ideal columns mirror the Tool House; only the wear columns are editable."; only while the dependence is on
    • Remove-Row Confirmation Dialog — Remove tool offset, "Remove the offset row for tool #{id}?", with a cancel button

Source Code Path

See HiNC App Anatomy for git repository links.

Web Application

HiNC-2025-webservice (Quasar CLI SPA):

  • wwwroot-src/src/components/controlTree/SoftNcToolOffsetsPanel.vue — the panel: the dependence toggle and its two-call enable, the refresh button, the read-only ideal columns, the renameable key with its duplicate guard, the confirmed delete, the locally appended add, and the Siemens caption gated on the brand string.
  • wwwroot-src/src/components/controlTree/softNcItemTypes.ts — the branch builder: this leaf's id, its two labels and label keys, and the snapshot flag that switches between them.
  • wwwroot-src/src/api/softNcRunner.ts — the typed wrappers: the row shape with its presence and dependence flags, the whole-row write, the add that returns a minted number, the rename, the delete, and the refresh call.
  • wwwroot-src/src/components/widgets/NumericInput.vue — the numeric cell: commit on blur or Enter, the minimum rejected inside the widget, the empty-text-to-null parse the panel drops, and the NaN that renders as an empty box.
  • wwwroot-src/src/components/controlTree/SoftNcEmptyState.vue — the shared no-runner line the panel opens with.
  • wwwroot-src/src/composables/useSoftNcRunner.ts — the shared snapshot behind that first guard and behind the tree's choice of label.
  • wwwroot-src/src/components/controlTree/SoftNcSiemensToolOffsetsPanel.vue — the neighbouring ledger on Siemens, and the footnote in which it states the opposite sign rule.
  • wwwroot-src/src/i18n/en/softNc.ts — the two node labels, the Siemens caveat, the four column headers, the dependent footnote, the duplicate-key toast, the absent-table line and the panel's error contexts.
  • wwwroot-src/src/i18n/en/common.ts — the shared Tool #, Add, toggle and refresh labels.
  • wwwroot-src/src/i18n/en/dialog.ts — the remove-row dialog's title and message.
  • Mech/SoftNcRunnerController.cs — the REST surface: the reader with its presence and dependence flags, the whole-row write, the add that mints the next number, the delete and the rename with their refusals, the project-level dependence write, and the refresh with its drop of rows the library no longer holds.
  • Common/ApiError.cs — the coded no-project payload the dependence write answers with, the one answer on this leaf that can be re-rendered in the application locale.

HiAPI Engine

  • HiMech/NcParsers/Dependencys/Generic/ToolOffsetTable.cs — the ledger itself: the four stored components, the subtraction that makes an effective value ideal minus wear, the integer-keyed row dictionary, and the tool-house update with its row creation and its row removal.
  • HiMech/NcParsers/Dependencys/Generic/ToolOffsetTableProxy.cs — the seedless get-or-create placeholder every brand preset carries in place of the table, and the bare instance it installs into a project holding none.
  • HiMech/NcParsers/Dependencys/IToolOffsetConfig.cs — the single-integer offset contract the consumers read through, and the pointer to the Siemens tool-and-edge contract beside it.
  • HiMech/NcParsers/Dependencys/Siemens/SiemensToolOffsetTable.cs — the neighbouring ledger, and the addition that makes its effective values geometry plus wear.
  • HiMech/NcParsers/SoftNcRunner.cs — the five brand presets, each of which carries the tool-offset proxy, and the proxy resolution every read of the table goes through.
  • HiMech/NcParsers/LogicSyntaxs/ToolHeightOffsetSyntax.cs — the ISO height path: the H word, the G43 / G44 / G49 arms, and the translation along the tool orientation both brand siblings compose through.
  • HiMech/NcParsers/LogicSyntaxs/G43p4RtcpSyntax.cs — the RTCP path that reads the same height for the same H word and re-aims it through the kinematics.
  • HiMech/NcParsers/LogicSyntaxs/Heidenhain/HeidenhainToolOffsetSyntax.cs — the TOOL CALL path: the tool number as the offset id, the DL delta added to the table height, and the absence of a cancel word.
  • HiMech/NcParsers/LogicSyntaxs/Siemens/SiemensToolOffsetSyntax.cs — the D path's fallback onto this table when the $TC_DP map has no row, its warning, and the NaN case it degrades to zero.
  • HiMech/NcParsers/PostLogicSyntaxs/RadiusCompensationSyntax.cs — the radius side: the D word, the signed offset that flips the compensated side when it goes negative, and the Heidenhain-only warning for that case.
  • HiMech/NcParsers/Dependencys/Fanuc/FanucToolOffsetVariableLookup.cs — the read-only macro window onto the effective heights, and the address range it answers for.
  • HiMech/NcParsers/Syntaxs/FanucSyntaxUtil.cs, HiMech/NcParsers/Syntaxs/MazakSyntaxUtil.cs and HiMech/NcParsers/Syntaxs/SyntecSyntaxUtil.cs — the three syntax lists that register that window.
  • HiMech/MachiningProcs/MachiningActRunnerConfig.cs — the tool-house dependence flag, its stored default and the element the project serializes it as.
  • HiMech/MachiningProcs/MachiningSession.cs — the play loop's refresh of the table from the library before the first block, run whenever the flag is on.
  • HiMech/Machining/MachiningToolHouse.cs — the tool-id-keyed library the refresh walks.
  • HiMech/Machining/IMachiningTool.cs — the tool contract the library's entries answer to, and where the spindle-buckle-to-tool-tip length the refresh reads is declared.
  • HiMech/Milling/MillingTools/MillingTool.cs — the assembly walk behind that length, and the NaN it answers when the walk does not resolve.
  • HiNc/MachiningProcs/LocalProjectService.cs — the facade that owns the library and the runner suit, and the entry point the play-time refresh reaches this table through.
  • HiNc/MachiningProcs/MachiningProject.cs — where the library, the runner configuration and the suit hang off the project, and the load path that leaves the library unset when the project file names none.

See Also

  • Program Data Plane — the plane this leaf sits on, where its table is stored, and what a brand switch does to the tables beside it
  • Siemens Tool Offsets — the $TC_DP ledger that appears next to this one on Siemens, and whose wear convention is the opposite of the one stated above