This commit is contained in:
2026-09-15 01:12:50 +08:00
parent 2db889ad89
commit 1a75503105
99 changed files with 4168 additions and 726 deletions
@@ -92,8 +92,9 @@ page at <code>/execution</code> and the General Setup page at <code>/general-set
host — <code>useControlTreeHost('execution')</code> or <code>useControlTreeHost('equipment')</code> — provides it to its
panes under one injection key, and the host owns everything after that: the node graph, the branch
builders, the selection and the <code>?tree=</code> query that carries it, the editor panel a selection resolves
to, and the mission enable ticks. The Tool House page at <code>/tool-house</code> reuses the same node shape and
the same item-type registry but renders a branch as nested tabs and creates no host.</p>
to, the mission enable ticks, and every structural operation on the Mission branch. The Tool House
page at <code>/tool-house</code> reuses the same node shape and the same item-type registry but renders a branch
as nested tabs and creates no host.</p>
<div class="IMPORTANT">
<h5>Important</h5>
<p>Node ids are role paths, not identities that survive a version change. Anatomy makes no claim that
@@ -105,7 +106,10 @@ for the migration hop that keeps an older link resolving.</p>
<ul>
<li><code>id</code> — the node's role path, slash-separated, whose first segment is the owning page's scope
(<code>execution/…</code>, <code>equipment/…</code>, <code>toolhouse/…</code>). This is the addressing surface: the <code>?tree=</code> query,
the persisted expansion list and the persisted last selection all hold ids.</li>
the persisted expansion list and the persisted last selection all hold ids. The General Setup
page's root carries its scope as its own id, <code>equipment</code>; the Execution page has no node with the
bare <code>execution</code> id — its two roots are <code>execution/mission</code> and <code>execution/program</code>, and the
segment is a scope key only.</li>
<li><code>label</code>, <code>labelKey</code>, <code>labelParams</code> — the display text; see <a href="#display-labels">Display Labels</a>.</li>
<li><code>itemType</code> — the registry key that decides the node's editor and its child builder.</li>
<li><code>key</code> — the bound object. In the geometry and equipment branches it is an IndexService key,
@@ -126,7 +130,9 @@ therefore survives a rebuild, which is why expansion state, the URL and the land
keyed on ids. Two id families are positional rather than role-based: Mission entries are
<code>${parentId}/${index}</code> and Program files are <code>${parentId}/${index}</code>, so moving or deleting a mission
command renumbers its siblings and a link to one names a position in the list rather than that
command.</p>
command. 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 <a href="#the-structure-changed-event">the row operations</a>.</p>
<h2 id="the-item-type-registry">The Item-Type Registry</h2>
<p><code>ITEM_TYPES</code> maps an <code>itemType</code> string onto a <code>TreeItemDef</code> carrying up to three members:</p>
<ul>
@@ -144,10 +150,11 @@ binds it to the node's key. The two composite geometry kinds do not embed nested
<code>TransformationGeom</code> grows an inner-geometry and an
inner-transformer slot, and <code>GeomCombination</code> grows one slot per item, so the tree owns the structure
and every panel stays shallow.</p>
<p>The map is one flat namespace assembled in <code>itemTypes.ts</code>: the <code>Group</code> stem type, the geometry,
transformer, workpiece-leaf, thermal-condition and spindle entries, and the Execution root's own type
are written inline, and five per-wave registries are spread in beside them. The three roots the panel
host renders itself — Machine Tool, Fixture and Workpiece — have no entry here at all.</p>
<p>The map is one flat namespace assembled in <code>itemTypes.ts</code>: the <code>Group</code> stem type and the geometry,
transformer, workpiece-leaf, thermal-condition and spindle entries are written inline, and five
per-wave registries are spread in beside them. The Execution page's two roots are registry types —
<code>MissionRoot</code> and <code>ProgramRoot</code>, from their waves — and no wrapper type stands over them. The three
roots the panel host renders itself — Machine Tool, Fixture and Workpiece — have no entry here at all.</p>
<table>
<thead>
<tr>
@@ -160,7 +167,7 @@ host renders itself — Machine Tool, Fixture and Workpiece — have no entry he
<tr>
<td>Mission</td>
<td><code>missionItemTypes.ts</code></td>
<td><code>MissionRoot</code>, <code>MissionCommand</code>, <code>MissionSection</code>; section children at <code>${command.id}/${sectionId}</code>; the per-kind bespoke editors, with a generic field editor as the fallback</td>
<td><code>MissionRoot</code>, whose panel is informational — the command count and the run-order hint that names the row affordances; <code>MissionCommand</code>, whose panel is the kind's editor, or only a Title field for a List; <code>MissionSection</code>; section children at <code>${command.id}/${sectionId}</code>; the per-kind bespoke editors, with a generic field editor as the fallback</td>
</tr>
<tr>
<td>Program</td>
@@ -219,15 +226,50 @@ correct only while the change stays inside the emitting panel's own branch.</li>
<li><code>selectId</code> — the selection to adopt afterwards. It is assigned directly, <strong>bypassing the
dirty-switch gate</strong>, because the emitting panel is being replaced on purpose. A <code>null</code> clears the
selection.</li>
<li><code>inserted</code> — 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.</li>
</ul>
<p>A mission command's move, duplicate and delete re-scope to the parent list, because those operations
rewrite the parent's children, and they name the post-operation selection: the clone's slot after a
duplicate, the neighbouring index after a delete, the parent list when the delete emptied it. Moving
an entry out of a nested list re-scopes to the grandparent. The controller brand switch and the
runner's Object-Management install re-scope to the controller root. The Tool House type selectors and
a flute add re-scope to their own node; everything that rewrites the tool collection — a new tool, a
duplicate, a delete, an id rename, an Object-Management install — re-scopes to the tool-house root,
and a flute delete re-scopes to its fluting parent.</p>
<p>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. Two panels emit it with no payload and so take the default scope, their own node: the Program
root panel's refresh button, which re-reads the file tree from the current session and rebuilds the
Program branch through <code>rebuildBranch</code>, so the branch opens under it; and the <code>GeomCombination</code>
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 and a flute add re-scope to their own node; everything that
rewrites the tool collection — a new tool, a duplicate, a delete, an id rename, an
Object-Management install — re-scopes to the tool-house root, and a flute delete re-scopes to its
fluting parent.</p>
<p>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. All five run through one helper that holds a busy gate
(one operation at a time), runs the open panel's dirty-switch gate, 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
(&quot;Cancelled — the open editor keeps its unsaved edits.&quot;); the delete's own confirm dialog runs
before the gate, so a cancelled delete forces no save prompt on the open editor.</p>
<p>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 entry of that list, or on the list
node itself once the list is empty. 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 <code>+</code>
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 <code>+</code> therefore never chases a focus inside a nested list.</p>
<h3 id="whole-tree-rebuilds-and-the-refreshes-that-are-not-rebuilds">Whole-tree rebuilds, and the refreshes that are not rebuilds</h3>
<p><code>rebuildTree()</code> 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, and after either runner-suit
@@ -258,30 +300,38 @@ to the model, and the pages' canvases draw that model; the group stems and the M
Workpiece roots have no field editor to commit for at all.</p>
<h2 id="selection-and-the-dirty-switch-gate">Selection and the Dirty-Switch Gate</h2>
<p>Every selection change funnels through <code>requestSelect(id)</code>. It returns immediately when the id is
already selected; otherwise it awaits the mounted panel's optional <code>tryConsumeBeforeSwitch()</code> and
abandons the switch when that returns false.</p>
already selected; it refuses outright while a mission row operation, or a tick's own writes, 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
<code>tryConsumeBeforeSwitch()</code> and abandons the switch when that returns false.</p>
<p>Three entrances use it: a click in the tree, a change to the URL's <code>?tree=</code> value, and a <code>select-node</code>
event from the mounted panel — the wire the panel host puts on every registry panel, so a group stem's
child list, a mission list's entry row and the Program branch's conversion jumps all arrive through
that one. A fourth path — the <code>selectId</code> of a structure change — assigns the selection directly and
skips the gate.</p>
child list and the Program branch's conversion jumps both arrive through that one. Two paths assign
the selection directly and skip the gate: the <code>selectId</code> of a structure change, and the
post-operation selection of a mission row operation, which has already run the gate itself.</p>
<p>There is no &ldquo;nothing selected&rdquo; state. Re-clicking the selected row makes the tree yield <code>null</code>, and
that is ignored; an empty <code>?tree=</code> lands on the persisted last selection for that page when the built
tree still contains it, and on the page's root otherwise. The editor row is therefore always alive,
tree still contains it, and otherwise on the page's root — General Setup on the equipment host, and
Mission, the first of its two roots, on the execution host. The editor row is therefore always alive,
and reclaiming its height is the row's own collapse toggle rather than a deselection.</p>
<p>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 <code>tryConsumeBeforeSwitch</code>. Panels that
write one field per request need no gate and register none. One panel exposes it —
<code>MissionCommandSlavePanel</code>:</p>
<ul>
<li>on a <code>list</code> entry it flushes the debounced title save while the command path is still live, then
allows the switch;</li>
<li>on any other entry it forwards its inner editor's gate. Today only the script editor has one: with
unsaved text it asks save, discard or cancel, returning false on cancel and on a flush that ends in
an error.</li>
<li>on a <code>list</code> 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;</li>
<li>on any other entry it forwards its inner editor's gate. Two 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.</li>
</ul>
<p>The same gate is run before duplicating a command, so the clone is made from what has just been typed
rather than from the last saved state.</p>
<p>The same 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.
For the same reason both the NC Code editor and the list title cancel, never flush, on unmount.</p>
<h2 id="display-labels">Display Labels</h2>
<p><code>nodeDisplayLabel(n)</code> returns the translation of <code>labelKey</code> — with <code>labelParams</code> interpolated — when
one is set, and the verbatim <code>label</code> otherwise. <code>labelKey</code> is therefore the form every role label the
@@ -316,10 +366,14 @@ the selection's type registers no panel.</li>
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.</p>
<p>On the Execution page the editor row grows a header: the transport bar is pinned above the scroll area
whenever the Execution root or any of its descendants is selected, so the run controls never scroll
away; that one instance also owns the transport's function-key shortcuts, which are attached for as
long as it is mounted.</p>
<p>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 because that header is the same component on General Setup, which
renders neither piece. This is the page's sole <code>ExecutionToolBar</code>, 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.</p>
<p>A pure <code>Group</code> stem is not a node without an editor. <code>Group</code> registers <code>GroupInfoPanel</code>, whose body is
the stem's intro line followed by a clickable list of its children; a row click emits <code>select-node</code>
and the host moves the selection there. The General Setup root, the two Anchor stems, the workpiece
@@ -341,7 +395,10 @@ a mission section that carries its own enable flag. Writing the ticked set walks
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.</p>
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.</p>
<p>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
@@ -350,17 +407,43 @@ command stays fully editable.</p>
<ul>
<li>Left Dock — both tree pages, one nav-bar button toggling the whole column
<ul>
<li><strong>Control Tree</strong> Expansion Row — collapses in place, keeping its header; stays mounted while
collapsed, so the tree keeps its scroll position
<li><strong>Control Tree</strong> Expansion Row — collapses in place, keeping its header, which is the same
page-neutral header on both pages; stays mounted while collapsed, so the tree keeps its scroll
position and the run header keeps its shortcuts
<ul>
<li>Tree — one root row per page: <strong>Execution</strong> or <strong>General Setup</strong>
<li>Run Header — Execution page only, pinned above the tree scroller and separated from it by a
rule
<ul>
<li>Execution Transport Bar — the six transport buttons; the strip scrolls horizontally when the
column is narrower than it</li>
<li>Execution Status Badge — the status label in its status colour; never shrinks</li>
</ul>
</li>
<li>Tree — the Execution page has two root rows, <strong>Mission</strong> above <strong>Program</strong>; the General Setup
page has one, <strong>General Setup</strong>
<ul>
<li>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</li>
<li>Node Row — the label is a real link to this node's <code>?tree=</code> URL on the 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 icons are rendered.</li>
current selection where it is. No node icons are rendered.</li>
<li>Node CheckBox — mission command rows, and mission section rows carrying an enable flag</li>
<li>Execution Status Badge — on the <strong>Execution</strong> root row only</li>
<li>Row Operations — on every mission command row, right-aligned: <strong>Move up</strong>, <strong>Move down</strong>,
<strong>Duplicate</strong> and <strong>Delete</strong>. 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</li>
<li>Add Command — the <code>+</code> 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</li>
</ul>
</li>
<li>Spinner and &ldquo;Loading project…&rdquo; — shown instead of the tree until the first build lands</li>
@@ -372,7 +455,6 @@ down to a 120 px editor floor and a 100 px tree floor</li>
<li><strong>Editor</strong> Expansion Row — its header carries the selection breadcrumb, the node path joined with
slashes, and falls back to <strong>Editor</strong>; stays mounted while collapsed
<ul>
<li>Execution Transport Bar — Execution page, while an Execution-scope node is selected</li>
<li>The selected node's editor panel, or &ldquo;Select an item in the Control Tree to edit it here.&rdquo;</li>
</ul>
</li>
@@ -405,17 +487,28 @@ when the project changes, and a host is therefore created once per project per p
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the host: the two scopes' root
builders, the selection gate, the <code>?tree=</code> sync, the mission ticks, the rebuild entry points, the
re-index and Object-Management handlers, and the reactive surface the panes consume.</li>
builders, the selection gate, the <code>?tree=</code> sync, the mission ticks, the five mission row
operations with their shared busy gate and id translation, the rebuild entry points, the re-index
and Object-Management handlers, and the reactive surface the panes consume.</li>
<li><code>wwwroot-src/src/components/controlTree/useMissionTreeDrag.ts</code> — 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.</li>
<li><code>wwwroot-src/src/components/controlTree/itemTypes.ts</code> — the <code>ControlNode</code> and <code>TreeItemDef</code> shapes,
the <code>SlotCtx</code> hooks, the structure-change payload, <code>nodeDisplayLabel</code> / <code>nodeDisplayInfo</code>, the
geometry and transformer builders, the <code>ITEM_TYPES</code> map and <code>buildSubtree</code>.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreePanel.vue</code> — the tree column: the node anchors,
the click handling that keeps a plain click in-app, and the execution-status badge.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreePanel.vue</code> — 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 <code>+</code> and the dialog it opens.</li>
<li><code>wwwroot-src/src/components/execution/ExecutionToolBar.vue</code> — the six transport buttons, and the
F5–F8 shortcuts the sole instance registers.</li>
<li><code>wwwroot-src/src/utils/executionStatusPresent.ts</code> — the badge's status label and colour.</li>
<li><code>wwwroot-src/src/components/controlTree/AddCommandDialog.vue</code> — the search-first command picker
over the server-served catalog.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreeDock.vue</code> — the two-row dock and the height
divider that sets the editor row's stored pixel height.</li>
<li><code>wwwroot-src/src/components/controlTree/PrimarySlavePanel.vue</code> — the panel host: the three inline
root types, the registry lookup, the remount key, the gate registration and the transport header.</li>
root types, the registry lookup, the remount key and the gate registration.</li>
<li><code>wwwroot-src/src/components/controlTree/ContentSlavePanel.vue</code> — the General Setup content column
and its unkeyed content panel.</li>
<li><code>wwwroot-src/src/components/controlTree/GroupInfoPanel.vue</code> — a group stem's intro and its
@@ -433,10 +526,22 @@ the core leaves every runner grows, and the snapshot flags that decide which bra
<li><code>wwwroot-src/src/components/controlTree/toolHouseItemTypes.ts</code> — the Tool House wave and its
<code>tool-&lt;id&gt;</code> role paths.</li>
<li><code>wwwroot-src/src/components/controlTree/runnerSuitItemTypes.ts</code> — the two runner-suit leaves.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionCommandSlavePanel.vue</code> — the command control bar, and
the only panel that exposes a dirty-switch gate.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionRootPanel.vue</code> — the Mission root's informational
panel: the command count and the run-order hint.</li>
<li><code>wwwroot-src/src/components/controlTree/ProgramRootPanel.vue</code> — the Program root's summary panel
and the refresh button that emits a payload-less <code>structure-changed</code>.</li>
<li><code>wwwroot-src/src/components/controlTree/GeomCombinationTreePanel.vue</code> — the combination's item list,
whose add, remove and clear emit the same payload-less event.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionCommandSlavePanel.vue</code> — a command's editor host: a
List's Title field with its debounced save, and the only panel that exposes a dirty-switch gate.</li>
<li><code>wwwroot-src/src/components/mission/ScriptCommandPanel.vue</code> — the autosaving editor whose gate is
forwarded, with its save / discard / cancel prompt.</li>
<li><code>wwwroot-src/src/composables/useAutoSave.ts</code> — 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.</li>
<li><code>wwwroot-src/src/components/mission/NcCodeCommandPanel.vue</code> — the second forwarded gate: the
debounced field save it drains, and the unmount that cancels instead of flushing.</li>
<li><code>wwwroot-src/src/components/mission/NcFileCommandPanel.vue</code> — the one command editor that emits
<code>structure-changed</code>, with the <code>inserted</code> range of its multi-pick.</li>
<li><code>wwwroot-src/src/components/panels/PanelExpansion.vue</code> — the expansion row both dock rows are built
from.</li>
<li><code>wwwroot-src/src/composables/useViewPrefs.ts</code> — the device-local expansion list, last selections,