Table of Contents

Control Tree

The Control Tree is the engine shared by the three tree-shaped pages of the web client: the Execution page at /execution, the Equipment page at /equipment and the Tool House page at /tool-house. Each page creates exactly one host — useControlTreeHost('execution'), useControlTreeHost('equipment') or useControlTreeHost('toolhouse') — provides it to its panes under one injection key, and the host owns everything after that: the node graph, the root builders, the selection and the URL that carries it, the selected node whose editor PrimarySlavePanel resolves, the mission enable ticks, and every structural operation on the Mission branch. All three pages render the same two panes, ControlTreePanel and PrimarySlavePanel; the Execution and Equipment pages carry the selection in a ?tree= query, and the Tool House page spells it as its path.

Important

Node ids are role paths, not identities that survive a version change. Anatomy makes no claim that a node id — in a ?tree= query or a Tool House path — is stable across releases; see Tree Ids and Routes for the migration hop that keeps an older link resolving.

The Node

Every branch of every tree is built from one structure, ControlNode, declared in itemTypes.ts:

  • id — the node's role path, slash-separated, whose first segment is the owning page's scope (execution/…, equipment/…, toolhouse/…). This is the addressing surface: the URL (the ?tree= query, or the Tool House path, which spells the id after its toolhouse/tool-<id> prefix), the persisted expansion list and the persisted last selection all hold ids. No page's tree has a row carrying the bare scope (the Tool House list column hands its whole-house panel a static toolhouse node, but that node is not in the tree): the Equipment page's roots are Mechanism, Spindle Capability, Environment and Controller, equipment/mechanism first; the Execution page's four roots are execution/mission, execution/program, execution/step-variables and execution/step-colors; and the Tool House page's roots are the picked tool's faces, General first, under a toolhouse/tool-<id> pair that names no row either — the first segment is a scope key only.
  • label, labelKey, labelParams — the display text; see Display Labels.
  • itemType — the registry key that decides the node's editor and its child builder.
  • key — the bound object. In the geometry and equipment branches it is an IndexService key, re-minted on every re-index; in the Mission branch it is the Mission API command path (0, 1, the dotted 0.2 of a nested list entry); in the Tool House tree it is the tool id as a string, except on the freeform holder's geometry and transformer slots, which carry session-scoped IndexService keys (see the Tool House scope). Waves whose panels read a module-level state singleton rather than an indexed object leave it empty — the SoftNc controller leaves, the spindle sections, the Environment root's Ambient Temperature and Coolant leaves and the Program branch all do. In the Step Variables branch it is the variable's key, empty on the root; in the Step Colors branch it is the tree's key as the list holds it, on the tree's row and on every node row under it, empty on the root.
  • ctx — the parent-provided SlotCtx: the afterChange commit chain, a slot's onCreate create-and-rebind hook, and a slot picker's allowedKinds / allowNone constraints.
  • children — grown by the builders, not declared by the tree column.
  • selectable, info / infoKey, and the mission / program / stepVariable / stepColor bookkeeping records the Mission, Program, Step Variables and Step Colors waves stamp on their own nodes.

The split between id and key is the load-bearing one. A key is re-minted every time the host re-indexes the model, so nothing durable may hang off it; an id names a role in the model and therefore survives a rebuild, which is why expansion state, the URL and the landing selection are all keyed on ids. Some id families are positional rather than role-based: Mission entries and Program files are ${parentId}/${index}, and a GeomCombination's items and a free fluting's flutes are numbered the same way (item-<index>, flute-<i>), so moving or deleting one renumbers its siblings and a link to one names a position in the list rather than that entry. For the Mission branch the host keeps three id sets of its own that must follow the rows rather than the numbers — the persisted expansion list, the Add Command focus and the selection — and translates all three through every mission row operation it runs; see the row operations.

The Item-Type Registry

ITEM_TYPES maps an itemType string onto a TreeItemDef carrying up to two members:

  • panel — the editor mounted for a node of this type: the dock's editor row on the Execution page, the full-height editor column on the Equipment and Tool House pages.
  • buildChildren — an async builder that returns this node's children.

Two flavours of type share the map. Slot types (Geometry, Transformer) have a kind picker as their panel and grow at most one child, the concrete kind, at the kind-independent id ${slot.id}/type — so switching a slot's kind keeps both the selection and the expansion. A slot holding nothing grows no child at all, which for the four geometry slots whose picker offers None is an ordinary state rather than a failure. Kind types (Box3d, StaticTranslation, …) register SoleEditorPanel, which looks the concrete editor up in the geometry and transformer editor maps and binds it to the node's key. The composite geometry kind does not embed nested editors: GeomCombination grows one slot per item, so the tree owns the structure and every panel stays shallow. A transform on a geometry is not a kind of its own: a slot holding a TransformationGeom grows its inner kind's node as usual, labelled Transformed {kind} and headed by the Transform picker, and hangs the transformer's kind node under it at ${slot.id}/type/transformer — the …/type/inner-geom and …/type/inner-transformer slots that kind used to grow are gone. The slot panels — GeometrySlotPanel and TransformerSlotPanel — serve the workpiece's Raw Geometry and Target Geometry, a combination's item slots and the Tool House freeform holder's geometry and two transformer slots; the three mechanism roots grow no slots, because a mechanism's anchors, branches, transformers and solids are edited in the Equipment page's Topology column, outside the tree (see Equipment Topology).

The map is one flat namespace assembled in itemTypes.ts: the Group stem type and the geometry, transformer, workpiece-leaf, thermal-condition and spindle entries are written inline, and seven per-wave registries are spread in beside them. The Execution page's four roots are registry types — MissionRoot, ProgramRoot, StepVariableRoot and StepColorsRoot, from their waves — and no wrapper type stands over them. The three mechanism roots the panel host renders itself — Machine Tool, Fixture and Workpiece, the Mechanism group's children — have no entry here at all.

Wave File What it contributes
Mission missionItemTypes.ts MissionRoot, whose panel is informational — the command count and the run-order hint that names the row affordances; MissionCommand, whose panel is the kind's editor, or only a Title field for a List; MissionSection; section children at ${command.id}/${sectionId}; the per-kind bespoke editors, with a generic field editor as the fallback
Program programItemTypes.ts ProgramRoot, ProgramFile, ProgramConversionFile, ProgramGeomDiff; writeback conversions at ${root.id}/dst-${index}, the run's geometry-difference result at ${root.id}/geom-diff
Step Variables stepVariableItemTypes.ts StepVariableRoot, whose builder reads the whole branch in one GET api/execution/step-variables and keeps a failed read's branch; StepVariable, one per variable of the project's list at execution/step-variables/<key>, with /<occurrence> on the second and later under a repeated key
Step Colors stepColorItemTypes.ts StepColorsRoot, whose builder reads the whole branch in one GET api/execution/step-color-trees, hands every listed tree's editor its item before the rows grow, and keeps a failed read's branch; StepColorTree, one per tree of the project's list at execution/step-colors/<key>, with /<occurrence> on the second and later under a repeated key, whose builder grows the tree's rows, nested, from its draft or its item with no request; StepColorGroup and StepColorLeaf, one per node of the tree, their ids flat under the tree's (see Step Colors Branch)
Tool House toolHouseItemTypes.ts ToolHouseRoot, the whole-house panel the tool list column mounts, with no builder — the tools are list rows, not tree rows; ToolNode, the tool's virtual container, with a builder and no panel; ToolGeneral, the General root; the cutter, holder, clamping and intelligent-holder branches under toolhouse/tool-<id>/…
SoftNc controller softNcItemTypes.ts the controller root and its leaves on two planes — …/machine/<seg> and …/program-data/<seg> — a core every runner grows plus the brand-driven ones
Runner suits runnerSuitItemTypes.ts CsvRunnerRoot and ClRunnerRoot, flat single-panel leaves with no builder

The Program branch is fetched whole: its root builder takes one file-tree response, one conversions response and one workpiece diff-settings response, stashes each file's raw subtree on the node so the file nodes' builder only maps what is already in hand, and mints the geometry-difference result node only when the third reports an attached overlay. The SoftNc branch is the opposite — its builder reads the runner snapshot once, and with no runner installed grows nothing at all. Where one resolves, the two plane stems appear over a fixed core that stands for every brand: six machine leaves, and Work Coordinates and Tool Offsets on the program-data plane. The leaves beside that core are the brand-driven ones — each appears only where the snapshot reports its backing table, so a brand switch changes which nodes exist. One flag does double duty: the Siemens $TC_DP flag adds its own two leaves and renames the core Tool Offsets leaf to its ISO G43 H reading, so the two offset ledgers do not read as one.

The two runner-suit nodes are conditional, and the host — not the registry — decides. With a project open, a CSV or CL controller node is built when its Preference checkbox is on, or when the current ?tree= value starts with that node's id. The checkbox is device-local and off by default; the route is the only other reveal, deliberately not the persisted last selection, so a node switched off while it is selected does not resurrect itself. Stepping off comes first: when the selection sits on a node the user has just unchecked, the host moves the selection — and, while the Equipment page is showing, the URL — to the Controller root before rebuilding.

Building a Branch

buildSubtree(node, services, depth) is the whole builder. It looks the node's type up, returns without touching children when the type declares no buildChildren or when depth has reached MAX_DEPTH (12, a guard against a self-referencing model), and otherwise awaits the builder, recurses into each returned child, and assigns the result. Building is eager: a branch is materialised in full at build time rather than lazily on expansion.

Builders receive one service, registerKey, which registers a freshly minted IndexService key with the page's cleanup hub — see Webapi with Hub-Cleanup Assistance.

rebuildBranch(node) is buildSubtree plus one thing: it adds the node's id to the expansion list so the fresh children are visible, and reports a failure as a toast named after the node's display label.

The structure-changed event

A panel that has changed the shape of the model emits structure-changed (or type-changed, wired to the same handler). The optional payload is a StructureChangeRequest:

  • scopeId — the branch to rebuild. Without it the scope is the selected node itself, which is correct only while the change stays inside the emitting panel's own branch.
  • selectId — the selection to adopt afterwards. It is assigned directly, bypassing the dirty-switch gate, because the emitting panel is being replaced on purpose. A null clears the selection. Two cases do not assign it: a change scoped to toolhouse, described below, whose tool is reached through the URL, and a request with yieldsToUser when the user selected something meanwhile.
  • yieldsToUser — selectId yields to the user: it is dropped when a selection the user made (requestSelect counts them) landed between the emit and the end of the rebuild, including a click a dirty gate held until the panel's request answered.
  • inserted — entries a panel inserted into a mission list itself: the list node's id, the child index the run starts at and its count. The host then treats the rebuild as a mission operation — it holds the busy gate and slides its index-based ids past the new siblings — rather than as a plain branch refresh.
  • readAtGeneration — a write's own re-read of the Step Colors root: the step-property registry's generation the write answered with. A read of the branch running since the client knew of that generation serves it, and a read that landed while the write was on the wire is followed by one more.

One command editor emits the event: the Program File editor, whose multi-pick turns every extra file into a new Program File command inserted right after it, in pick order; the command panel hosting it re-scopes the event to the parent list, keeps that command selected and passes the insert range along. One panel emits it with no payload and so takes the default scope, its own node: the GeomCombination item panel after an item add, an item remove or a clear, since its item slots must be regrown. The controller brand switch and the runner's Object-Management install re-scope to the controller root. The Tool House type selectors — cutter type, the cutter's integral mode, holder type and fluting type — and a flute add re-scope to their own node, the add selecting the new flute; a flute delete re-scopes to its fluting parent. The Step Variables panels re-scope to the branch's root: New, an applied rename and Delete name the node to select, with yieldsToUser; Refresh, an Apply that keeps its key and a refusal that the variable is gone re-read the branch alone. After each, a selection the read withdrew, or a selectId it did not grow (the read failed), goes to the branch's root. The Step Colors panels re-scope to the branch's root for Add, Apply and Delete, each with its readAtGeneration, and for Refresh, and to their tree for a node added, moved or deleted and a Revert, which re-grow the tree's rows from the draft without a read; a scope the tree does not have yet, a tree an Apply has just re-keyed, reads the whole branch. A node such a change selects is unfolded to.

Everything that rewrites the tool collection — New Tool, a duplicate, a delete, an id rename, an Object-Management install of the whole house — names the scope toolhouse, which is no row of the tree, and onNodeStructureChanged gives it a branch of its own. When the selectId names another tool (a new tool, a duplicate, a rename), the host rewrites the URL's tool number, keeping the node path, and the route change re-roots the tree; otherwise (a delete, an install) it re-seats the tree in place, because the same tool number can be a different tool after an install.

Every other structural change to the Mission branch is an operation of the host itself, not an event, because the tree rows are the only surface that offers it: Add Command, a drag between rows, Move up / Move down, Duplicate and Delete. A drag is HTML5 drag-and-drop between mission rows; every command row drags, the Mission root row only receives, and where the pointer sits in the target row decides the operation. A plain command row has no edge band: it splits at its midline, the upper half putting the dragged command before that row and the lower half after it — a reorder when both share a list, a relocation across lists otherwise. A List row keeps that before / after reading only in its top and bottom quarters; its middle half, and the whole of the Mission root row, take the command into that list, at the tail. A drop onto the dragged row's own subtree is refused, and a drop aborts when a rebuild mid-drag re-stamped the dragged node, since its id may then name another command. The operations work in entry indices, the ids' own, and a list's rows need not be all its entries — with the physics preference off the NC Optimization Config command is an entry with no row, which the Add Command dialog does not offer then either — so each does what it does with the preference on, Move up / Move down as many presses as it takes to change the rows shown, and one that would change nothing the tree shows sends nothing; an append counts every entry (a List that shows no row keeps its entry count from its build). All five run through one helper that holds a busy gate (one operation at a time), runs the open panel's dirty-switch gate when that panel is Mission's own — the rebuild re-stamps no other branch, and another branch's editor, a step variable's or a color tree's, would offer to switch away when nothing switches —, calls the Mission API, translates the expansion list and the Add Command focus through the operation, rebuilds the branch of the list or lists it rewrote — the one list, or the nearest list above both ends of a cross-list drag — and only then translates the selection and names any post-operation selection, since the ids it names exist only after the rebuild. A refused gate drops the operation and says so in a notification ("Cancelled — the open editor keeps its unsaved edits."); the delete's own confirm dialog runs before the gate, so a cancelled delete forces no save prompt on the open editor.

The selection moves only when it rides on the acted-on row — that row itself, or a section or nested entry under it. Then it follows a stepped or dragged command, lands on the clone after a duplicate, and after a delete lands on the nearest remaining row of that list, or on the list node itself once it shows none. Otherwise the editor the user is in stays on its own, re-indexed row. Add Command never moves the selection: it reveals the new row and makes it the focus, so a run of adds lands in order. The focus is the command most recently selected — a section row counts for its owning command — or the last one added, and an Add Command from a list row's + inserts right after that focus when the focus is a direct entry of that list, and at the list's tail otherwise; the Mission root row's + therefore never chases a focus inside a nested list.

Whole-tree rebuilds, and the refreshes that are not rebuilds

rebuildTree() replaces the root array outright, and runs after an Object-Management swap of the machine tool or the fixture, after a blank machining chain is created, after either runner-suit preference flips, and when Show Physics Options flips on the equipment host. Each of the mechanism swaps ends by re-pulling the equipment topology graph and roles. An edit made in the Topology column, or a role bound on a mechanism root, rebuilds nothing: no tree node carries a key such an edit changes, so the host only re-pulls the graph (reinitializeMechanism), which keeps an open workpiece geometry editor mounted. On the execution host, an execution-status transition re-runs buildSubtree on the Program root directly rather than through rebuildBranch, so a run does not force that branch open on every transition. The Program row's Refresh button is no event either: it calls the host's refreshProgramTree, which rebuilds the branch through rebuildBranch — queued behind any re-read already running — so the branch opens under it.

A whole-tree build never installs over a newer one: the newer read the newer flags and keys — the flag's hydration, which MainLayout runs after a page began its first build, is one. An older build that answers first does install, so whatever waited on it — the first build's deep-link adoption, a flip's reseat — acts on a tree that stands, and the newer installs over it when it answers; when the newer fails, the older's tree stays. On the execution host the flag gates the Mission branch alone, so a flip rebuilds that branch in place, as a mission operation (rebuildMissionForFlag): it waits for an operation that holds the busy gate and holds the gate while it runs, and it first runs the open Mission panel's dirty-switch gate, since the rebuild re-stamps that panel. A refused gate leaves the branch as it is until the selection next moves; a flip made while the page is not shown waits for the page when a Mission editor is open on it, so the gate's prompt never comes up over another page; a flip that comes while the page's first tree is being built is taken once that tree is in, and the deep link is adopted after the Mission operations settle. Program, Step Variables and Step Colors keep their nodes and leave every read to their own queues; the Step Colors branch's key options follow the preference as the server holds it, so that branch reads again, through its queue, once the server has taken the flip.

On the tool-house host every whole-tree rebuild is a re-seat — rebuildTree() for the tool the URL names, then a fresh adoption of the URL's node path — and it runs when the URL's tool number changes, when the set of tool ids in the house changes (watched by id, not by list, so a committed field edit, which refreshes the list, rebuilds nothing), on a toolhouse-scoped structure change and when Show Physics Options flips. Re-seats are serialized: a request arriving mid-build runs one more pass, and a build ends in an adoption only while the URL still names the tool it built, so two quick clicks in the tool list settle on the second tool.

Two refreshes deliberately mutate existing nodes instead of rebuilding:

  • A mission command's title is re-read from the entry list and written onto the node's label.
  • A file-backed geometry leaf's label is re-read when its source file changes.

The mission one exists because a rebuild of the Mission branch re-mints the per-build stamp that forms part of the editor's remount key, and remounting an open editor mid-edit would cost the user their cursor and any staged autosave. The geometry one is the same principle without the stamp: a title change and a source-file change alter no structure, so the label is all that has to move, and replacing the node under an open file editor would buy nothing.

The commit chain

The tree has no component bubbling, so the full post-edit commit chain lives on each node as ctx.afterChange, composed level by level as the branch is built. A GeomCombination item's chain cleans that combination's aggregated cache before running its parent's chain; the two workpiece geometry slots re-commit the swap-in before clearing their own cache, and skip that re-commit when the slot has been set to none. Tool-house nodes share one refresh, of the tool list and the tool canvas; the freeform holder's slots run one step before it — the geometry slot resyncs the holder, and each transformer slot re-attaches its transformer. Every other family carries a no-op: the Mission, Program, spindle, SoftNc and thermal-condition nodes and the workpiece's Mesh and Material leaves have panels that write straight to the model, and the pages' canvases draw that model; the group stems and the three mechanism roots have no field editor to commit for at all — a mechanism's transformers and solids are edited in the Topology column, whose edits rewire the equipment and redraw the canvas without passing through the tree.

Selection and the Dirty-Switch Gate

Every selection change funnels through requestSelect(id). It returns immediately when the id is already selected; it refuses outright while a mission row operation, a tick's own writes, or the Show Physics Options rebuild of the Mission branch (its dirty prompt included) are in flight — the operation ends by naming the post-operation selection, which would silently undo a change landed during its rebuild; otherwise it awaits the mounted panel's optional tryConsumeBeforeSwitch(targetId), handed the node the selection goes to, and abandons the switch when that returns false. The requests are numbered: one that a newer request overtook while it waited yields, so two clicks made while a gate waits end on the second. A gate whose panel unmounted while it asked gave no decision (lastAnswerUndecided); the host then asks the panel mounted in its place when that one holds the same draft (draftOwner), and keeps the answer otherwise.

Three entrances use it: a click in the tree, a change to the URL's selection — the ?tree= value, or the Tool House path — and a select-node event from the mounted panel — the wire the panel host puts on every registry panel, so a group stem's child list and the Program branch's conversion jumps both arrive through that one. Several paths assign the selection directly and skip the gate: the selectId of a structure change; the post-operation selection of a mission row operation, which has already run the gate itself; and the host's own moves — the Program branch's live follow, the step back to the Program root when a re-read drops the selected file, the step onto the Controller root when the CSV or CL controller node the selection stands on is switched off, and, when a Show Physics Options flip on the equipment or execution host, a re-read of the Step Variables or the Step Colors branch, or a structure change of either whose read did not grow the node its selectId names leaves the selection on a node the tree no longer has, the step onto its nearest ancestor the tree still has, or onto the landing selection when its whole root went (reseatWithdrawnSelection). Two branches take their own way. A withdrawn step variable goes to the Step Variables root, never onto another variable under the same key. A withdrawn color tree's node goes to the same node under the tree an applied rename moved it to, else to that tree, else to its own tree, else to the Step Colors root, never by the id's /-prefixes; a ?tree= id and the remembered landing id land by the same rule (see Step Colors Branch).

There is no “nothing selected” state while a tree has rows. Re-clicking the selected row makes the tree yield null, and that is ignored; an empty ?tree= lands on the persisted last selection for that page when the built tree still contains it, and otherwise on the page's first root — Mechanism on the equipment host, and Mission on the execution host. A bare /tool-house lands the same way on remembered state: the tool already shown, or the first tool, and the node path last asked for, clamped to the deepest node that tool grows, or General. Only a tool house with no tool leaves the selection empty. The editor is therefore always alive, and on the Execution page reclaiming its height is the editor row's own collapse toggle rather than a deselection.

The gate itself is registered by the panel host: the mounted registry panel is handed to the host through a template ref, and the host asks that instance for tryConsumeBeforeSwitch. Panels that write one field per request need no gate and register none. Five panels expose it. StepVariablePanel first lets an Apply on the wire land, then, with changes not applied, asks Cancel, Discard or Apply & Switch, and keeps the edit as a draft when it is unmounted without being asked (see Step Variables Branch). The tree's, a group's and a leaf's panels of a color tree share one draft per tree and one gate over it: it first lets an Apply on the wire land, lets a switch to the tree or to another node of it through unasked, and asks Cancel, Discard or Apply & Switch for a switch out of the tree with changes not applied (see Step Colors Branch). MissionCommandSlavePanel:

  • on a list entry it waits for a title save already on the wire, then flushes the debounced title save while the command path is still live, and allows the switch only when that save landed;
  • on any other entry it forwards its inner editor's gate. Three editors have one. The Script editor first lets a save already on the wire land, then with unsaved text asks save, discard or cancel: cancel returns false, discard reverts to the last confirmed text and puts the autosave to sleep until the next keystroke, and save proceeds only when the flush landed — a conflict answered with Cancel leaves the edit staged and the switch refused. The NC Code editor waits for a save on the wire, drains its debounced field save, and returns false when that save failed. The generic field editor — the fallback for a kind with no editor of its own — sends the text fields not yet committed, waits for every save on the wire, and returns false when any of them failed.

The Mission editor's gate runs before every mission row operation, not only before a selection change, because the operation's rebuild remounts the open editor and mission paths are index-based: a debounced save that outlived the remount would land on whichever command slid into the path it captured at mount. It runs before the Show Physics Options rebuild of the Mission branch for the same remount. Neither asks another branch's editor, a step variable's or a color tree's: neither rebuild re-stamps another branch. For the same reason the NC Code editor and the list title cancel, never flush, on unmount, and the generic field editor drops a text draft still unsent.

Display Labels

nodeDisplayLabel(n) returns the translation of labelKey — with labelParams interpolated — when one is set, and the verbatim label otherwise. labelKey is therefore the form every role label the client translates itself takes, while label is the storage for text it cannot: server-composed mission titles, file-backed geometry paths and engine type names. The translator function is read inside the call rather than captured, so a language change re-renders the whole tree's role labels.

Two labels are composed rather than looked up. A mission command's label is the title the server composes for it — the command's kind name in the request language, with the command's own text in brackets when there is one, as in Script [Warm-Up]. That title always arrives filled in, so the node's labelKey is always dropped and the tree's own kind-name fallback is a safety net that never fires in practice. Because titles are composed server-side per request, a language change also re-pulls the command entries and rewrites those labels in place. A file-backed geometry leaf carries its source file in brackets, so sibling instances read apart: StlFile [Geom/x.stl]. The voxel kind is renamed for display, appearing as MeshedGeomFile [...] rather than by its type name.

Group stems take the same treatment for their intro text: infoKey wins over info, and a stem with neither falls back to a sentence naming the group.

The Panel Host

PrimarySlavePanel decides what the editor pane shows, in this order:

  1. The three inline root types — MachineToolRoot, FixtureRoot, WorkpieceRoot — are rendered by the panel itself, and are never resolved from the registry. Each is a summary ending with the mechanism's Roles panel (MechanismRolesPanel, titled “Roles”): one row per role — its label, its kind (anchor or branch), “required” where it is, and the transformer type an axis role needs (“needs DynamicTranslation” on X, Y, Z; “needs DynamicRotation” on A, B, C) — with a select over the mechanism's own anchors or branches and a “Pick on the graph” button that arms the role for a click in the Topology column, and the server's checks below the rows, or "All checks pass.". The two with a stand-alone file surface, Machine Tool and Fixture, carry an Object-Management button above the summary; the workpiece has none, since its geometry, mesh and material are authored through its child tree items. What none of the three carries is an editor for the mechanism's anchors, branches, transformers or solids: those are the Topology column's (Equipment Topology), and a click on a part's cluster there selects that part's root here.
  2. Otherwise the registry's panel for the selection's item type, mounted with the node as its only prop, with changed, type-changed, structure-changed, select-node and error wired to the host.
  3. A spinner while the first build is still running, and the select-an-item hint after that when the selection's type registers no panel.

What decides a remount is the mounted component's key, ${id}|${key}|${stamp} — the node id, the node key, and the mission stamp where the node has one. So the editor remounts when the selection moves, when the bound object's key is re-minted, and when the Mission branch was rebuilt beneath it. That third term is exactly why the mission label refresh above mutates the node instead of rebuilding.

The editor row carries nothing but the editor. The run controls sit in the tree pane instead: on the Execution page the Control Tree pane pins a run header above its tree scroller — the transport bar and the live execution-status badge — whatever the selection is. It rides the pane's content rather than the dock's expansion header, which carries only the pane title. This is the page's sole ExecutionToolBar, so it owns the F5–F8 shortcuts, and because the dock keeps the tree pane mounted while its row is collapsed, a collapsed tree row does not drop them. When the tree column narrows, the six-button strip scrolls horizontally and the badge never shrinks, so the run state is the last thing a resize can hide.

A pure Group stem is not a node without an editor. Group registers GroupInfoPanel, whose body is the stem's intro line followed by a clickable list of its children; a row click emits select-node and the host moves the selection there. The Mechanism root, the Environment root, the workpiece Material stem, the controller's two plane stems and its Parameters (Native) stem are all of this kind.

The Equipment and Tool House pages lay the same two panes out as columns rather than dock rows: the tree column and, beside it, the editor column at the page's full height, so an editor that carries a chart or a wide table has room for it. The spindle's Power Contours and Torque Contours items therefore chart their own contours above the point editor instead of handing the charts to a separate view. The Tool House page puts its tool list in front of the two columns, and its editor title leads the breadcrumb with the tool — Tool #1 / Cutter / Flute Profile — because the tool itself has no row.

The Tool House Scope

The toolhouse host is the third scoped host over the registry, and it differs from the other two in what its tree is rooted at: the facets of the one tool picked in the page's tool list, flattened. buildToolHouseRoots points the session at that tool, builds the tool's virtual container — toolContainerNode(toolId), a ToolNode at toolhouse/tool-<id> that is never drawn as a row — and once its subtree is built promotes the container's children to the tree's roots, with toolGeneralNode(toolId) sorted first: General, Cutter, Holder, Clamping and, while Show Physics Options is on, Int. Holder. General is the ToolGeneral leaf whose panel carries what the container would show if it had a row — Tool ID, Note, the auto Abstract Note, Duplicate and Delete. The flattening leaves every id as it was (toolhouse/tool-<id>/cutter/…), and a build that fails still leaves General, so a tool whose cutter read broke can still be renamed, duplicated or deleted.

The tree holds one tool rather than the library, and that is a constraint rather than a choice. The holder's index keys are per session, not per tool — FreeformHolderController.cs mints {sessionKey}-HolderGeom and its two transformer keys, CylindroidHolderController.cs mints {sessionKey}-HolderCylindroid, and each is re-pointed on every read — so two tools' holder branches built in one pass would both address the tool built last. The tools are therefore rows of the list column rather than rows of the tree, which is why ToolHouseRoot, the whole-house panel (Object Management and New Tool) that heads the list, has no buildChildren.

The selection rides the URL path, /tool-house/:toolId?/:node*: the tool number, then the selected node's id with its toolhouse/tool-<id> prefix removed. toolNodePathOf() and toolTreeId() in treeRoutes.ts convert between the two halves, routeForTreeId translates a tool-house id to that route without losing a segment, and the page never reads or writes ?tree=. The URL is the authority: a typed or bookmarked path is adopted as it stands, a path the tool does not grow is clamped to its deepest built node, and the URL is rewritten to the node actually selected; only a bare /tool-house falls back on memory (see the landing rule). A list row is a router link that moves :toolId and carries the node path over, so picking another tool keeps the editor on the same node; while the URL holds a clamped prefix of the path last asked for (desiredNodePath), the link carries that whole path instead, so a node one tool had to clamp comes back on the next tool that grows it. See Tool House Page for the columns around the tree.

The Tick Column

The tree runs a strict tick strategy, and a node hides its checkbox unless it is a mission command or a mission section that carries its own enable flag. Writing the ticked set walks every command node at any depth: a command whose state changed is PUT to the Mission API and, on success, has the new state copied onto its section children; a section whose state changed is dispatched to its kind's writer. A failed write raises a toast and leaves the model untouched, so the getter re-derives the old value and the box snaps back. The ticks share the mission operations' busy gate in both directions: a tick during a row operation is ignored — it would write to the node's pre-operation path — and the box snaps back the same way, while a tick's own writes hold the gate so no row operation can overtake them on the server and land the enable flag on whichever command slid into the ticked path.

A row dims when it is itself disabled — a disabled command, or a section whose own flag is off — or when an ancestor command is disabled, which is how a disabled command greys its whole subtree while each descendant keeps its own state. The switch decides only whether the command runs: a disabled command stays fully editable.

Layout

  • Left Dock — Execution page, one nav-bar button toggling the whole column
    • Control Tree Expansion Row — collapses in place, keeping its header, a plain Control Tree title that carries no run chrome; stays mounted while collapsed, so the tree keeps its scroll position and the run header keeps its shortcuts
      • Run Header — Execution page only, pinned above the tree scroller and separated from it by a rule
        • Execution Transport Bar — the six transport buttons; the strip scrolls horizontally when the column is narrower than it
        • Execution Status Badge — the status label in its status colour; never shrinks
      • Tree — the Execution page has four root rows, Mission, Program, Step Variables and Step Colors; the Equipment page has Mechanism, Spindle Capability, Environment and Controller, the middle two only while Show Physics Options is on, plus a revealed CSV Controller or CL Controller row; the Tool House page has the picked tool's General, Cutter, Holder, Clamping and, while Show Physics Options is on, Int. Holder
        • Drag Handle — on every mission command row, List rows included, immediately before the label. It is the first thing the row's own header renders, but the tree puts the expand arrow, on a row with children, and then the enable checkbox ahead of that header, so the handle sits to the checkbox's right rather than at the row's left edge. The Mission root row has none. While a drag is under way the dragged row dims, a line along the target row's top or bottom edge marks a before / after drop, and a tinted target row marks a drop into that list
        • Node Row — the label is a real link to this node's URL on the page — ?tree= on the Execution and Equipment pages, the path on the Tool House page — so the browser's context menu offers open-in-new-tab and copy-link; a plain left click keeps the in-app selection instead of navigating, while a modified or middle click opens a tab and leaves the current selection where it is. No node icons are rendered.
        • Step Colors Row Details — on the Step Colors rows only: a leaf's 10 px color swatch before the label; after it, the node's condition in one line, in grey, which shortens before the label does; on a tree row, an Inspecting badge while the color index reads the tree and the engine's state while the tree is not registered; see Step Colors Branch
        • Node CheckBox — mission command rows, and mission section rows carrying an enable flag
        • Row Operations — on every mission command row, right-aligned: Move up, Move down, Duplicate and Delete. Absent from the layout at rest, so a resting label keeps the column's full width, and shown on the hovered row and on the selected row; on a device with no hover they are always shown. Move up is disabled on a list's first entry, Move down on its last, and all four while an operation is in flight
        • Add Command — the + at the right edge of the Mission root row and of every List row, always visible, scoped to that row's list; disabled without an open project and while an operation is in flight. It opens the Add Command dialog — a search box over the command catalog, grouped by category, which fetches the catalog on every opening and accepts one pick per opening
        • Program Row Actions — on the Program root row, right-aligned and always visible: the Follow switch, Refresh, and the Recompute step physics button, the last only while Show Physics Options is on; see Program Branch
      • Spinner and “Loading project…” — shown instead of the tree until the first build lands
    • Row Divider — a 5 px bar between the rows, shown only while both are open; dragging it upwards grows the editor row, which keeps a stored pixel height while the tree above absorbs the change, down to a 120 px editor floor and a 100 px tree floor
    • Editor Expansion Row — its header carries the selection breadcrumb, the node path joined with slashes, and falls back to Editor; stays mounted while collapsed
      • The selected node's editor panel, or “Select an item in the Control Tree to edit it here.”
  • Equipment Columns — the same two panes side by side instead of a dock, each under a plain title bar and each with its own button in the menu bar's column row, then the canvas
    • Control Tree Column — the tree above, without a run header
    • Editor Column — its title bar carries the selection breadcrumb and falls back to Editor; the selected node's editor panel at the page's full height
  • Topology Column — Equipment page only, the fourth and rightmost column (tree, editor, canvas, topology) and the fourth button of the menu bar's column row, “Toggle the Topology column”. It is not part of the tree: it draws the whole authored equipment as one graph and edits the picked anchor or branch; see Equipment Topology. Shown by default only on a window at least 1280 px wide; its width is stored beside the tree and editor widths
  • Tool House Columns — four columns in three nested pixel splitters, each with its own button in the menu bar's column row, the three tree-side columns under a plain title bar; see Tool House Page
    • Tool House Column — the whole-house panel over one router-link row per tool; it picks the tool the tree is rooted at and is not a tree pane
    • Control Tree Column — the picked tool's tree, without a run header; No tool selected. in its place while the house has no tool
    • Editor Column — its title bar carries the selection breadcrumb led by the tool and falls back to Editor; the selected node's editor panel at the page's full height
    • Tool Canvas Column — the rightmost column, outside the tree

The dock rows' states and height and every column's visibility and width are device-local browser preferences held by one module. Each page keeps its own visibility record. treeColPx is both the Execution dock's width and the tree column's width on the other two pages, editorColPx is shared by the Equipment and Tool House editor columns so the two pages line up, and toolListColPx and topologyColPx belong to one page each. All three pages share one expansion list because their id spaces do not overlap. Which tool the Tool House tree shows is no preference: it rides the URL.

Lifecycle

The host is created while its page's script runs and provided before its panes mount; the equipment host's creation also drops the previous project's topology graph, roles, selection and armed role, before the Topology column mounts. The page's onMounted then awaits initialize(). That call connects the cleanup hub; re-indexes the machine tool, the workpiece and the fixture in that order on the equipment host, and initializes the tool-house session and re-reads its tool list on the tool-house host; builds the tree; and then adopts whatever the URL names. The host pulls no topology itself: the Topology column, mounted even while hidden, pulls the graph and the roles that it and the three Roles panels share when it mounts and finds none. A loading flag is held for the whole call, which is what the tree and editor panes render their spinners from; later rebuilds mutate an already-populated tree and never re-enter it. A missing project, or a project without a fixture or workpiece, answers 404, and those are treated as empty states rather than errors, so no toast appears. Without an open project the Mission, Program, Step Variables, Step Colors and controller branches are still declared, as childless stems: their builders run only once a project is loaded.

Because the layout keys its keep-alive wrapper on a project epoch, every page is destroyed and rebuilt when the project changes, and a host is therefore created once per project per page.

Source Code Path

See HiNC App Anatomy for git repository links.

Web Application

HiNC-2025-webservice (Quasar CLI SPA):

  • wwwroot-src/src/components/controlTree/useControlTreeHost.ts — the host: the three scopes' root builders (buildToolHouseRoots flattens the picked tool's container into roots), the selection gate, the URL sync (?tree= on two pages, the path on Tool House, with adoptToolhouseSelection clamping a path to its deepest built node and desiredNodePath remembering it across tools), the mission ticks, the five mission row operations with their shared busy gate and id translation, the Show Physics Options rebuild of the Mission branch that joins them (rebuildMissionForFlag), the rebuild entry points (the serialized reseatToolRoot among them, and rebuildTree's rule that no build installs over a newer one), the toolhouse branch of onNodeStructureChanged, the Program row's Refresh and Recompute, the re-index and Object-Management handlers, and the reactive surface the panes consume.
  • wwwroot-src/src/components/controlTree/useMissionTreeDrag.ts — the drag-and-drop bookkeeping between mission rows: which rows drag, the drop mode from the pointer's position in the target row, and the row classes behind the drop indicators.
  • wwwroot-src/src/components/controlTree/itemTypes.ts — the ControlNode and TreeItemDef shapes, the SlotCtx hooks, the structure-change payload, nodeDisplayLabel / nodeDisplayInfo, the geometry and transformer builders, the ITEM_TYPES map and buildSubtree.
  • wwwroot-src/src/components/controlTree/ControlTreePanel.vue — the tree column: the Execution page's run header with the transport bar and the execution-status badge, the node anchors, the click handling that keeps a plain click in-app, the drag handles, the row operations, the Add Command + and the dialog it opens, and the Program row's Follow, Refresh and Recompute controls.
  • wwwroot-src/src/components/execution/ExecutionToolBar.vue — the six transport buttons, and the F5–F8 shortcuts the sole instance registers.
  • wwwroot-src/src/utils/executionStatusPresent.ts — the badge's status label and colour.
  • wwwroot-src/src/components/controlTree/AddCommandDialog.vue — the search-first command picker over the server-served catalog.
  • wwwroot-src/src/components/controlTree/ControlTreeDock.vue — the two-row dock and the height divider that sets the editor row's stored pixel height.
  • wwwroot-src/src/components/controlTree/PrimarySlavePanel.vue — the panel host: the three inline root types, the registry lookup, the remount key and the gate registration.
  • wwwroot-src/src/components/mech/MechanismRolesPanel.vue — the Roles panel each inline root ends with: the per-role select, the arm-and-pick button and the server's checks.
  • wwwroot-src/src/composables/useEquipmentTopology.ts — the topology graph, roles, selection and armed-role state the Roles panels share with the Topology column.
  • wwwroot-src/src/components/controlTree/GroupInfoPanel.vue — a group stem's intro and its navigating child list.
  • wwwroot-src/src/components/controlTree/SoleEditorPanel.vue — a kind node's editor, resolved from the geometry and transformer editor maps.
  • wwwroot-src/src/components/controlTree/missionItemTypes.ts — the Mission wave: node bookkeeping, the section table and its enable-flag readers and writers, the per-kind editors and display names.
  • wwwroot-src/src/components/controlTree/programItemTypes.ts — the Program wave: the file tree from one response, the writeback conversion nodes, and the geometry-difference result node read from the workpiece diff settings.
  • wwwroot-src/src/components/controlTree/stepVariableItemTypes.ts — the Step Variables wave: the root's one-request builder, the variable nodes, a failed read's branch kept; reached only through itemTypes.ts, with the node ids in stepVariableIds.ts.
  • wwwroot-src/src/components/controlTree/stepColorItemTypes.ts — the Step Colors wave: the root's one-request builder, the tree nodes and their nested node rows grown from the tree's draft, a failed read's branch kept; reached only through itemTypes.ts, with the node ids in stepColorIds.ts and the one draft per tree in stepColorTreeEditor.ts.
  • wwwroot-src/src/components/controlTree/softNcItemTypes.ts — the controller wave: the two planes, the core leaves every runner grows, and the snapshot flags that decide which brand leaves join them.
  • wwwroot-src/src/components/controlTree/toolHouseItemTypes.ts — the Tool House wave: toolContainerNode, the virtual tool container; toolGeneralNode, the General root; the cutter, fluting and holder builders; and the tool-<id> role paths.
  • wwwroot-src/src/components/controlTree/toolhouse/ToolHouseRootPanel.vue — the whole-house panel over the tool list, whose New Tool and Object-Management install emit a toolhouse-scoped structure change.
  • wwwroot-src/src/components/controlTree/toolhouse/ToolNodePanel.vue — the General root's panel, whose id rename, Duplicate and Delete emit the same.
  • wwwroot-src/src/composables/useToolHouse.ts — the tool list, the session key the holder branches read, and the shared refresh of the list and the tool canvas.
  • wwwroot-src/src/components/controlTree/runnerSuitItemTypes.ts — the two runner-suit leaves.
  • wwwroot-src/src/components/controlTree/MissionRootPanel.vue — the Mission root's informational panel: the command count and the run-order hint.
  • wwwroot-src/src/components/controlTree/ProgramRootPanel.vue — the Program root's summary panel, which emits nothing: the branch's Follow, Refresh and Recompute ride the tree row.
  • wwwroot-src/src/components/controlTree/StepVariableRootPanel.vue and StepVariablePanel.vue — the Step Variables root's two lists and a variable's editor, which emit root-scoped structure changes; the editor exposes the dirty-switch gate.
  • wwwroot-src/src/components/controlTree/StepColorsRootPanel.vue, StepColorTreePanel.vue, StepColorGroupPanel.vue and StepColorLeafPanel.vue — the Step Colors root's two lists and a tree's, a group's and a leaf's panels, which emit root-scoped structure changes for a write and tree-scoped ones for a change of the draft's shape; the last three expose one dirty-switch gate per tree (useStepColorSwitchGate.ts).
  • wwwroot-src/src/components/controlTree/GeomCombinationTreePanel.vue — the combination's item list, whose add, remove and clear emit a payload-less structure-changed.
  • wwwroot-src/src/components/controlTree/MissionCommandSlavePanel.vue — a command's editor host: a List's Title field with its debounced save, and one of the five registry panels that expose a dirty-switch gate (the others are StepVariablePanel.vue and a color tree's three panels).
  • wwwroot-src/src/components/mission/ScriptCommandPanel.vue — the autosaving editor whose gate is forwarded, with its save / discard / cancel prompt.
  • wwwroot-src/src/composables/useAutoSave.ts — the Script editor's autosave: the flush, the wait for a save on the wire, and the discard that sleeps the instance until the next edit.
  • wwwroot-src/src/components/mission/NcCodeCommandPanel.vue — the second forwarded gate: the debounced field save it drains, and the unmount that cancels instead of flushing.
  • wwwroot-src/src/components/mission/GenericCommandPanel.vue — the third forwarded gate: the generic field editor's uncommitted text fields sent, and every save on the wire awaited.
  • wwwroot-src/src/components/mission/NcFileCommandPanel.vue — the one command editor that emits structure-changed, with the inserted range of its multi-pick.
  • wwwroot-src/src/components/panels/PanelExpansion.vue — the expansion row both dock rows are built from.
  • wwwroot-src/src/composables/useViewPrefs.ts — the device-local expansion list, last selections, per-page column visibility, column widths, dock sizes and the two runner-suit checkboxes.
  • wwwroot-src/src/components/AppMenuBar.vue — the menu bar's column row: one button per column of the current tree page.
  • wwwroot-src/src/composables/useCleanupHub.ts — the key registration the builders call.
  • wwwroot-src/src/router/treeRoutes.ts — the id migration hop, the page a given id belongs to, and toolNodePathOf / toolTreeId, which split a Tool House id into the URL's tool number and node path and join them back.
  • wwwroot-src/src/pages/ExecutionPage.vue — creates and provides the execution host.
  • wwwroot-src/src/pages/EquipmentPage.vue — creates and provides the equipment host, and lays the tree, editor, canvas and Topology columns side by side.
  • wwwroot-src/src/pages/ToolHousePage.vue — creates and provides the toolhouse host, and lays the tool list, tree, editor and tool canvas columns side by side; the list rows are router links built by the host's toolLocation.
  • wwwroot-src/src/layouts/MainLayout.vue — the project epoch that decides when a host is rebuilt.
  • wwwroot-src/src/i18n/en/tree.ts — the dock labels, the group intros and the node role labels.
  • wwwroot-src/src/i18n/en/toolhouse.ts — the Tool House node labels, Tool #{id} among them.
  • Mech/FreeformHolderController.cs — on the server, mints the per-session freeform holder keys the holder branch reads.
  • Mech/CylindroidHolderController.cs — on the server, mints the per-session cylindroid holder key.

See Also

  • Tree Ids and Routes — how a node id becomes a URL, which page owns a given id, and how an older id keeps resolving
  • Session State — the project epoch and the stores a host is rebuilt around
  • Execution Page — the first live host: the run cockpit's Mission, Program, Step Variables and Step Colors branches
  • Step Colors Branch — the branch whose panels share one draft and one switch gate per tree, and whose node ids stay flat under a tree
  • Equipment Page — the second live host: the equipment branches and its four columns
  • Tool House Page — the third live host: one tool's faces as the tree's roots, beside the tool list that picks the tool
  • Stick Tool Panel — the General root and the tool's faces the toolhouse scope builds
  • Controller Branch — the one branch whose node set is not fixed: it is regrown from a snapshot, so which children exist depends on the controller in the project
  • Equipment Topology Column — the Equipment page's fourth column, outside the tree, where the anchors and branches the three mechanism roots' Roles panels bind are edited
  • Transform Picker — the Transform picker in a geometry slot, and the …type / …type/transformer nodes it adds to the tree