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 Equipment page at /equipment 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 only prose above it is the Siemens caveat and the hint for the selected radius basis, and the only prose below it is the closing caption the tool-house dependence turns on.

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. Which row a block selects is the H word's business, not the table's: an omitted H re-selects the modal H (H0 until one is written, and H0 is 0 by definition — no row to miss), and the H / T agreement check reports an H that is not the tool number. See ISO core.

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-mount-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 SpindleMountToToolTipLength 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 milling cutter's 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, into Ideal Height, for a tool whose tip length does not resolve. Such a row refuses every edit of its other cells: the write is the whole row, the browser serializes the NaN it carries as null, and the endpoint rejects the request, so the panel restores the cell and shows its save-error toast. While the dependence is on, that row's wear columns therefore cannot be edited until the tool's length resolves; with the dependence off, a number typed into the blank cell itself is the edit that goes through. 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 one of the two settings this leaf edits that are not a dependency at all (the other is the radius basis beside it). The toggle 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. A read-only cell sends no request of its own: Enter still re-parses what it shows, but the numeric widget does not emit a value it was given back unchanged, and the empty text of a NaN cell parses to null, which the handler drops.
  • 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 the cutter radius as its ideal radius, with the radial wear beside it 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, which is also what a project file without the element reads as) — chosen from the Radius basis select in the panel's toolbar and written through PUT api/mech/soft-nc-runner/tool-offsets/radius-basis. The select is shown whether or not the dependence is on, and a grey hint under the toolbar states what a refresh writes under the selected basis. Every refresh of this table honours it, because the refresh endpoint and the play-time path both hand the project's basis to the same engine method: the button, the refresh that enabling the dependence runs, and the play-time refresh while the dependence is on. The height side is untouched by the choice; the wear columns are never written by any refresh.

Changing the basis while the dependence is on is itself a refresh: the panel writes the basis, runs the refresh at once and re-reads the table, so the ideal-radius column follows the choice before anything plays. With the dependence off the choice is only stored, for the next refresh to use. A failure of either call re-reads the table, so the select then shows whichever basis the server holds — the write may have committed before the refresh failed. The endpoint refuses a value that parses as neither a basis name nor a number (the select only ever sends one of the two names), and answers the coded no-project payload when no project is loaded; the reader answers cutter radius in that case.

The program decides the basis, not 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 the button, the toggle and a basis change made while the dependence is on, and it hands the project's library and basis to UpdateIdealByToolHouse(API), which runs three steps. For every tool in the library it writes the ideal height from that tool's spindle-mount-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.

Playback calls the same method, so a refresh from the panel and a refresh at play time write the same values. The two part company only on a missing library: the method returns without doing anything, while the endpoint refuses before calling it, answering unsuccessfully with “No tool house available”.

Four 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.
  • Choosing a radius basis runs it too. While the dependence is on, switching the Radius basis select re-runs the refresh at once, row removal included.
  • 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 and the radius-basis select 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 and the radius-basis write reach neither string, because what they write is a project-level element rather than the table; they are the two routes on this leaf that answer a coded payload instead, one that 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

  • Equipment Control Tree — the tree column of /equipment
    • 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 Column — beside the tree column, at the page's full height: 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 two-sentence hints chosen by the selected basis, each saying what a refresh writes and which programs the basis suits: "A tool-house refresh writes the cutter's largest profile radius into the ideal-radius column. …" or "A tool-house refresh writes 0 into the ideal-radius column and leaves the radial wear alone. …"
      • 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 radius-basis select with its hint line and the refresh it runs while the dependence is on, 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 and the radius basis read as cutter radius when the server omits it, the dependence and basis writes, 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 guard that emits nothing for an unchanged value, 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 brand string the Siemens caption is gated on; the tree's label switch reads its own fetch of the same snapshot endpoint in softNcItemTypes.ts.
  • 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 radius-basis label, its two options and two hints, 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 and its radius basis, the whole-row write, the add that mints the next number, the delete and the rename with their refusals, the project-level dependence and basis writes, and the refresh that refuses a missing library and hands the project's basis to the engine method.
  • Common/ApiError.cs — the coded no-project payload the dependence and basis writes answer 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, its radius written by the basis, and its row removal.
  • HiMech/NcParsers/Dependencys/Generic/RadiusOffsetBasis.cs — the two radius bases and what each makes the refresh write into the ideal radius.
  • 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 and the radius basis, their stored defaults and the elements the project serializes them 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-mount-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, with the project's radius basis.
  • 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
  • Cutter Geometry — what the ideal height the refresh writes is made of: the holder's gauge length plus the stick-out