This commit is contained in:
2026-09-09 18:06:21 +08:00
parent 161af68fe3
commit 45b9f9ed5d
79 changed files with 1167 additions and 1532 deletions
@@ -94,9 +94,8 @@ slot, or the geometry switchboard embedded inline.</p>
<h2 id="layout">Layout</h2>
<ul>
<li><strong>Edit Mode</strong> — a selector over three modes. Its internal values are <code>MinMax</code>, <code>MinDimension</code> and
<code>CenterDimension</code>; the web labels them &ldquo;Min / Max&rdquo;, &ldquo;Min + Dimension&rdquo; and &ldquo;Center + Dimension&rdquo;,
the desktop client &ldquo;Min and Max&rdquo;, &ldquo;Min and Dimension&rdquo; and &ldquo;Center and Dimension&rdquo;.</li>
<li><strong>Min</strong>, <strong>Max</strong>, <strong>Dimension</strong>, <strong>Center</strong> — four vector rows, one X / Y / Z input each. The web
<code>CenterDimension</code>, labelled &ldquo;Min / Max&rdquo;, &ldquo;Min + Dimension&rdquo; and &ldquo;Center + Dimension&rdquo;.</li>
<li><strong>Min</strong>, <strong>Max</strong>, <strong>Dimension</strong>, <strong>Center</strong> — four vector rows, one X / Y / Z input each. The editor
captions Dimension &ldquo;Dimension (Max − Min)&rdquo; and Center &quot;Center ((Max + Min) / 2)&quot;; the Min and Max
captions come from the shared label set rather than the geometry one.</li>
</ul>
@@ -107,18 +106,14 @@ in anything but Center + Dimension.</p>
<p>Only <a class="xref" href="../../api/Hi.Geom.Box3d.html#Hi_Geom_Box3d_Min">Min</a> and <a class="xref" href="../../api/Hi.Geom.Box3d.html#Hi_Geom_Box3d_Max">Max</a> are settable on the type;
<a class="xref" href="../../api/Hi.Geom.Box3d.html#Hi_Geom_Box3d_Dim">Dim</a> and <a class="xref" href="../../api/Hi.Geom.Box3d.html#Hi_Geom_Box3d_Center">Center</a> are derived and get-only. That is why the
other two modes are arithmetic over the same two stored corners rather than a different storage
shape — and on the web that arithmetic happens in the browser, which then posts the resulting Min
and Max.</p>
shape — and that arithmetic happens in the browser, which then posts the resulting Min and Max.</p>
<h2 id="behavior">Behavior</h2>
<ul>
<li><strong>A row commits on blur or Enter, not per keystroke.</strong> The vector widget emits only from its blur
handlers, suppresses the emit when the parsed value is unchanged, and reverts an unparseable entry
to its previous value.</li>
<li><strong>The web editor has no read-only variant.</strong> The desktop control additionally carries an
<code>IsInfoMode</code> property that a host sets to turn the whole control into a display: every field goes
read-only and the Edit Mode selector is hidden outright. Two hosts use it, the STL file control and
the geometry combination control. The web equivalent is not this editor — the STL file editor draws
its own read-only bounding-box rows.</li>
<li><strong>This editor has no read-only variant.</strong> A host that needs one draws its own rows instead — the
STL file editor's read-only bounding-box rows are not this editor.</li>
<li><strong>Four endpoints ship without a caller.</strong> The controller exposes <code>UpdateByMinDimension</code>,
<code>UpdateByCenterDimension</code>, <code>IndexDimension</code> and <code>IndexCenter</code>; the shipped SPA calls none of them,
because the editor resolves every mode to a plain <code>Update</code>.</li>
@@ -101,16 +101,12 @@ of each row, disabled once only two pairs remain.</li>
<li><strong>The caption</strong> — a one-line statement of the type: a solid of revolution defined by (Z, R) pairs,
minimum two.</li>
</ul>
<p>The desktop control is shaped differently for the same job: a title, an <strong>Add Point</strong> and a <strong>Clear
All</strong> button, and a data grid with Z, R and Actions columns. Clear All has no web counterpart — it
confirms, wipes the list and re-seeds a single pair.</p>
<h2 id="behavior">Behavior</h2>
<ul>
<li><strong>A cell commits on blur or Enter.</strong> An empty or non-finite entry is dropped rather than written,
and a negative radius is clamped to zero — on the client and again on the server.</li>
<li><strong>The floor is two pairs, and both ends enforce it.</strong> The web disables the remove button at two
and the controller refuses the call below two. The desktop client blocks only at one, with a
warning dialog.</li>
<li><strong>The floor is two pairs, and both ends enforce it.</strong> The remove button is disabled at two and
the controller refuses the call below two.</li>
<li><strong>Creating one seeds three pairs.</strong> The create endpoint starts a new cylindroid at (0, 20),
(30, 20) and (60, 10) rather than empty.</li>
<li><strong>Four endpoints ship without a caller.</strong> The controller exposes <code>GetPairCount</code>, <code>GetPairAt</code>,
@@ -119,12 +115,10 @@ warning dialog.</li>
<h2 id="tessellation-resolution-is-not-a-property-of-the-shape">Tessellation Resolution Is Not a Property of the Shape</h2>
<p>There is no longitude count, and no per-instance resolution, on a cylindroid. The STL longitude
number is derived internally at generation time from a resolution the caller supplies; the type
carries only a static default. The desktop client once had a Longitude Number field and its handler
was deleted when the backing property went away.</p>
carries only a static default.</p>
<p>Where resolution <em>is</em> exposed, it belongs to the holder rather than to the shape: the Tool House
cylindroid holder carries a <strong>Resolution</strong> surface beside its Geometry one — a tree node on the web,
a tab in the desktop client — offering a linear resolution in millimetres and an angle resolution in
degrees, described in the app as the tessellation resolution used for display and collision meshing.</p>
cylindroid holder carries a <strong>Resolution</strong> tree node beside its Geometry one, offering a linear
resolution in millimetres and an angle resolution in degrees, described in the app as the tessellation resolution used for display and collision meshing.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
@@ -89,13 +89,12 @@
<p>The Extended Cylinder editor edits one number on an <a class="xref" href="../../api/Hi.Geom.ExtendedCylinder.html">ExtendedCylinder</a>: its full
length, measured from <code>Z=0</code> and <strong>including the span below the start section</strong>. Its only home on
either client is the Tool House cutter's Upper Beam, where the start section is the flute top.</p>
it is the Tool House cutter's Upper Beam, where the start section is the flute top.</p>
<h2 id="layout">Layout</h2>
<ul>
<li>An explanatory block stating what the type is and, in bold, how the length is measured.</li>
<li><strong>Full Length</strong> — a single numeric field in millimetres, with a hint under it.</li>
</ul>
<p>The desktop panel is a title over the same one field.</p>
<h2 id="behavior">Behavior</h2>
<ul>
<li><strong>The hint states a lower bound only when that bound is positive.</strong> The bound is the Z of the
@@ -113,19 +112,15 @@ minimum — its minimum is inclusive — so a value exactly equal to the bound p
caught one layer up by the editor, which raises an error to its host as a banner rather than an
inline field message. The controller then rejects non-finite, non-positive, and at-or-below-bound
values with a 400.</li>
<li><strong>The field commits on blur or Enter.</strong> The desktop control does not: it writes on every
keystroke, and performs no minimum, positivity or finiteness check at all. The range guard is a
web and backend feature.</li>
<li><strong>The field commits on blur or Enter</strong>, and the range guard runs on both the field and the backend.</li>
<li><strong>Creating one through the cutter is not the same as creating one bare.</strong> The Upper Beam creates
through the cutter's own endpoint so the backend seeds a valid full length and can return
geometry-issue warnings — which the Upper Beam panel renders as persistent banners. The bare
create endpoint seeds a length of 100 and a single start pair.</li>
</ul>
<h2 id="reach">Reach</h2>
<p>Both clients restrict this kind to the cutter's upper beam, by different mechanisms. In the desktop
client the switchboard's Extended Cylinder entry ships collapsed and is revealed by a property that
exactly one host sets — the milling cutter panel. On the web <strong>no Geometry slot offers the kind at
all</strong>: it is absent from the container kinds, so it can be neither a transformation geometry's inner
<p>This kind is restricted to the cutter's upper beam, and the mechanism is absence rather than a flag:
<strong>no Geometry slot offers it at all</strong>. it is absent from the container kinds, so it can be neither a transformation geometry's inner
geometry nor a combination child, and it was taken out of the workpiece's raw geometry once a saved
project was found to reload one degenerate. The single surface that offers it is the cutter's Upper
Beam, which mounts the switchboard itself over its own six-kind list rather than being a Geometry
@@ -87,10 +87,10 @@
<article data-uid="Anatomy-GeomCombinationControl">
<h1 id="geometry-combination-control">Geometry Combination Control</h1>
<p>A <a class="xref" href="../../api/Hi.Geom.GeomCombination.html">GeomCombination</a> is several geometries treated as one, and it has <strong>two faces on the
web</strong>: an inline editor that renders every child's own editor at once, and a Control-Tree panel that
manages the list only and puts each child on its own node. The desktop client has the inline shape
alone. There is no selection model on any of them — every child carries its own remove button.</p>
<p>A <a class="xref" href="../../api/Hi.Geom.GeomCombination.html">GeomCombination</a> is several geometries treated as one, and it has <strong>two faces</strong>: an
inline editor that renders every child's own editor at once, and a Control-Tree panel that manages
the list only and puts each child on its own node. There is no selection model on either — every
child carries its own remove button.</p>
<h2 id="layout">Layout</h2>
<h3 id="the-inline-editor">The inline editor</h3>
<ul>
@@ -103,33 +103,23 @@ editable at once.</li>
<p>Deliberately shallow: a list of read-only rows with Add, a per-row remove and Clear all, and a
standing hint that an <strong>Item</strong> child node is where a child is actually edited. No editor is embedded
here; each child is its own <code>Item</code> node — <code>.../item-{index}</code> — under this one.</p>
<h3 id="the-desktop-control">The desktop control</h3>
<p>A title, then an <strong>Add</strong> and a <strong>Clear All</strong> button over a scrolling list of bordered cards, each
holding that child's own geometry management panel and a remove button — no index badge and no type
caption. Below the list sits a collapsed <strong>Combination Information</strong> expander reporting the aggregate
triangle count and bounding box, the latter as a read-only box control.</p>
<h2 id="adding-a-child">Adding a Child</h2>
<p>The two clients differ here, and the web is the one that gained something:</p>
<ul>
<li><strong>Web</strong> — pick the kind first, then Add; the request carries the kind.</li>
<li><strong>Desktop</strong> — Add takes no kind and always appends a 100 mm cube. The kind is switched afterwards,
on that child's own Geometry Type combo.</li>
</ul>
<p>Pick the kind first, then Add; the request carries the kind.</p>
<p>Five kinds may be children — Box3d, Cylindroid, StlFile, TransformationGeom and a <strong>nested</strong>
GeomCombination, so combinations nest arbitrarily deep. <code>CubeTreeFile</code> and <code>ExtendedCylinder</code> are
accepted by neither client's switch.</p>
accepted by neither switch.</p>
<h2 id="behavior">Behavior</h2>
<ul>
<li><strong>A type switch rewires the slot, not just the alias.</strong> Changing a child's kind goes through
<code>SetItemAt</code>, which replaces the entry in <a class="xref" href="../../api/Hi.Geom.GeomCombination.html#Hi_Geom_GeomCombination_StlSources">StlSources</a> itself.</li>
<li><strong>The aggregate STL is cached, and a child's own controller does not invalidate it.</strong> Editing a
child through its own endpoints leaves the combination returning the mesh it had already built, so
both web faces call <code>CleanCache</code> before bubbling the change, and compose that call into the
ancestor chain. This is the behavioural fact the rest of the page depends on.</li>
both faces call <code>CleanCache</code> before bubbling the change, and compose that call into the ancestor
chain. This is the behavioural fact the rest of the page depends on.</li>
<li><strong>Removing a child does not renumber the ones after it.</strong> Item keys are minted as
<code>{key}-item-{index}</code> and the remove endpoint leaves later indices pointing at the old aliases,
which is why every structural change rebuilds the branch and re-mints them.</li>
<li><strong>Clear all confirms only on the desktop.</strong> The web posts it directly.</li>
<li><strong>Clear all is posted directly, with no confirmation.</strong></li>
</ul>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
@@ -175,7 +165,7 @@ cache.</li>
<h2 id="see-also">See Also</h2>
<ul>
<li><a href="geom-manage-control.html">Geometry Management Panel</a> — the switchboard that offers this kind, and
the panel each desktop child card embeds</li>
the panel each child card embeds</li>
</ul>
</article>
@@ -88,21 +88,17 @@
<h1 id="geometry-management-panel">Geometry Management Panel</h1>
<p>The geometry switchboard is one control: a kind picker over the kinds its host allows, and — unless
the host asked for the picker alone — the picked kind's editor beneath it. On the web it binds a
the host asked for the picker alone — the picked kind's editor beneath it. It binds a
<strong>key into the shared object store</strong>, not an object, and every Control-Tree Geometry slot is this
same control in selector-only mode.</p>
<h2 id="layout">Layout</h2>
<p>Web: a geometry-type dropdown; a separator, drawn only while a kind is active; then that kind's
editor. Picking a kind commits immediately — there is no Apply button. In selector-only mode
everything below the dropdown is dropped, which is the form each Geometry slot node shows.</p>
<p>Desktop: a combo box on the left of a head line with a <strong>Convert</strong> menu on its right, over a bordered
content presenter that hosts the kind's own control.</p>
<h2 id="what-it-binds">What It Binds</h2>
<p>The web panel has no target-geometry property. It takes a <code>modelKey</code> naming the object it edits and
mutates what sits behind that key; a host that also owns the <em>field</em> holding the geometry passes an
<code>onCreate</code> hook, so switching the kind rebinds the host's field rather than just the key. The
desktop panel is the one that exchanges an <a class="xref" href="../../api/Hi.Geom.IStlSource.html">IStlSource</a> directly, through a getter and
setter pair its host supplies.</p>
<code>onCreate</code> hook, so switching the kind rebinds the host's field rather than just the key.</p>
<p>Each host's field type decides which kinds it <em>may</em> offer; whether the kind survives a save decides
which of those it <em>does</em>. A fixture's geometry and a transformation geometry's inner geometry are
<a class="xref" href="../../api/Hi.Geom.IGetStl.html">IGetStl</a>; a workpiece's initial geometry is an <code>IMakeXmlSource</code>, which is why the
@@ -117,7 +113,7 @@ host that wires that source, the cutter's upper beam.</p>
<a class="xref" href="../../api/Hi.Geom.Cylindroid.html">Cylindroid</a>, <a class="xref" href="../../api/Hi.Geom.StlFile.html">StlFile</a>, <a class="xref" href="../../api/Hi.Geom.TransformationGeom.html">TransformationGeom</a>,
<a class="xref" href="../../api/Hi.Geom.GeomCombination.html">GeomCombination</a> and <a class="xref" href="../../api/Hi.Cbtr.CubeTreeFile.html">CubeTreeFile</a>. <a class="xref" href="../../api/Hi.Geom.ExtendedCylinder.html">ExtendedCylinder</a> is
in the kind map but out of that default: a host that can wire its start section names it explicitly.
Nothing is hidden by a flag on the web.</p>
Nothing is hidden by a flag.</p>
<p><strong>The default is a guard rail, not a shipped list.</strong> Every host passes its own whitelist — the five
Control-Tree Geometry slots from the node context, the four inline hosts as a constant — so no
shipped surface renders the default at all, and the whitelist is what really decides reach.</p>
@@ -151,17 +147,13 @@ shipped surface renders the default at all, and the whitelist is what really dec
only when the host asks for it: the three General Setup slots and a transformation geometry's inner
geometry do, while the mechanism anchor, the cutter upper beam and combination children do not, so
those pickers cannot clear the slot. Clearing goes through the host's create hook with the literal
kind <code>None</code>, which the container-aware endpoints map to null. The desktop combo always lists a
<strong>None</strong> item.</p>
<h2 id="converting-is-desktop-only">Converting Is Desktop-Only</h2>
<p>Wrapping an existing geometry into a <a class="xref" href="../../api/Hi.Geom.TransformationGeom.html">TransformationGeom</a> — and extracting it back out
— exists only in the desktop client, as items in the <strong>Convert</strong> menu rather than as buttons. The
<em>Convert to Transformation</em> item re-titles itself <em>Extract from Transformation</em> when the current
geometry already is one; the combination item behaves the same way, and its extract is offered only
when the combination holds exactly one child. Three code-behind properties can hide the menu and each
of its two items, but no shipped host sets any of them.</p>
<p><strong>The web has no equivalent, and the difference is not cosmetic.</strong> Picking <code>TransformationGeom</code> in
the picker creates a new, empty one and discards the geometry that was there — it does not wrap it.</p>
kind <code>None</code>, which the container-aware endpoints map to null.</p>
<h2 id="there-is-no-way-to-wrap-an-existing-geometry">There Is No Way to Wrap an Existing Geometry</h2>
<p>Wrapping a geometry that is already in a slot into a <a class="xref" href="../../api/Hi.Geom.TransformationGeom.html">TransformationGeom</a> — and
extracting it back out — is not offered anywhere in the application, and the difference from what
the picker looks like it does is not cosmetic. <strong>Picking <code>TransformationGeom</code> creates a new, empty
one and discards the geometry that was there.</strong> The same holds for a combination: there is no
operation that takes a geometry and puts a container around it in place.</p>
<h2 id="in-the-control-tree">In the Control Tree</h2>
<p><code>Geometry</code> is a slot item type. Its child builder probes the slot's key for the type behind it and,
when that type is a known kind, grows exactly <strong>one</strong> child node of that kind bound to the same key
@@ -219,8 +211,8 @@ editor</li>
<li><a href="extended-cylinder-panel.html">Extended Cylinder Panel</a> — the same, and the kind exactly one host offers</li>
<li><a href="stlfile-control.html">STL File Control</a> — the same, for an STL reference</li>
<li><a href="meshed-geom-panel.html">Meshed Geometry Panel</a> — the same, for a voxel cube tree</li>
<li><a href="transformation-geom-control.html">Transformation Geometry Control</a> — the container kind this panel
can wrap into on the desktop client</li>
<li><a href="transformation-geom-control.html">Transformation Geometry Control</a> — one of the container kinds
this panel offers, and why picking it replaces rather than wraps</li>
<li><a href="geom-combination-control.html">Geometry Combination Control</a> — the other container kind, whose
every child embeds this panel again</li>
</ul>
@@ -105,18 +105,14 @@ when nothing is chosen.</li>
<li>A standing description: the geometry is a pre-computed voxel cube tree, and loading it is deferred
until it is needed.</li>
</ul>
<p>The desktop panel is a read-only path field with <strong>Browse</strong> and <strong>Reload</strong> buttons. The web has no
Reload: the controller has no such action.</p>
<h2 id="reach">Reach</h2>
<p><strong>One slot offers this kind: the workpiece's Raw Geometry.</strong> The fixture's geometry and the
workpiece's target geometry both exclude it, and it is not among the container kinds, so it can be
neither a transformation geometry's inner geometry nor a combination child. The Control-Tree node
carries the chosen file in its label, as <code>MeshedGeomFile [&lt;path&gt;]</code>.</p>
<p>The desktop client reaches it differently. There, the workpiece page carries a <strong>Geometry Source</strong>
combo — Common Geometry or Meshed Geometry — that decides whether this panel or the geometry
switchboard is visible. The web has no such toggle: picking the <code>MeshedGeomFile</code> kind <em>is</em> choosing
the meshed source. The classification survives on the server, which still reports a raw geometry of
this type as a meshed one.</p>
<p>There is no separate geometry-source toggle: picking the <code>MeshedGeomFile</code> kind <em>is</em> choosing the
meshed source. The classification survives on the server, which still reports a raw geometry of this
type as a meshed one.</p>
<h2 id="key-model">Key Model</h2>
<p><a class="xref" href="../../api/Hi.Cbtr.CubeTreeFile.html">CubeTreeFile</a> is the file reference; <a class="xref" href="../../api/Hi.Cbtr.CubeTree.html">CubeTree</a> is the voxel tree itself.
The reader resolves the stored relative path and opens it with no extension test of its own — <code>.wct</code>
@@ -89,19 +89,17 @@
<p>The STL file editor points a geometry at an <code>.stl</code> file rather than describing a shape: the model
holds a file reference, and the editor is a picker over it plus a read-only summary of what was
loaded. On the web the file lives on the <strong>server</strong>, not on the machine running the browser, and
loaded. The file lives on the <strong>server</strong>, not on the machine running the browser, and
that difference shapes everything else on the page.</p>
<h2 id="layout">Layout</h2>
<ol>
<li><strong>File reference</strong> — a read-only field showing the path currently referenced, with an empty-state
hint when there is none.</li>
<li><strong>The picker</strong> — on the web a <strong>Select</strong> dropdown whose menu holds <em>Browse…</em>, <em>Browse Resource…</em>
and, only while a path is set, <em>Clear</em>. Either Browse entry opens the in-app File Explorer dialog
filtered to <code>.stl</code>. In the desktop client this is a <strong>Browse</strong> button opening the operating
system's file dialog, beside a <strong>Reload</strong> button and a status line; the web has no Reload.</li>
<li><strong>STL info</strong> — behind an info icon on the web, opening a dialog; a collapsed <em>Information</em>
expander in the desktop client. Both show the triangle count and the bounding box as read-only
vector rows, and the web dialog states an empty case when nothing is loaded.</li>
<li><strong>The picker</strong> — a <strong>Select</strong> dropdown whose menu holds <em>Browse…</em>, <em>Browse Resource…</em> and, only
while a path is set, <em>Clear</em>. Either Browse entry opens the in-app File Explorer dialog filtered
to <code>.stl</code>. There is no reload action.</li>
<li><strong>STL info</strong> — behind an info icon, opening a dialog that shows the triangle count and the
bounding box as read-only vector rows, and states an empty case when nothing is loaded.</li>
</ol>
<h2 id="behavior">Behavior</h2>
<ul>
@@ -115,8 +113,8 @@ re-homed path, not the one that was chosen.</li>
walking the triangles rather than kept resident. Only Min and Max come back from the server;
Dimension and Center are derived in the browser from them.</li>
<li><strong>The picker previews.</strong> Double-clicking an <code>.stl</code> row in the File Explorer dialog swaps its slave
panel to a 3D preview of that file before the pick is confirmed — a capability the desktop file
dialog has no equivalent of. The dialog opens with that panel hidden, which is why the double-click
panel to a 3D preview of that file before the pick is confirmed. The dialog opens with that panel
hidden, which is why the double-click
is the gesture that reaches a preview there; a single click does so only once the toolbar's pencil
has revealed the panel. The preview belongs to the File Explorer rather than to this editor — see
<a class="xref" href="../util/stl-preview-pane.html">STL Preview Pane</a>.</li>
@@ -88,16 +88,16 @@
<h1 id="transformation-geometry-control">Transformation Geometry Control</h1>
<p>A <a class="xref" href="../../api/Hi.Geom.TransformationGeom.html">TransformationGeom</a> is an inner geometry plus the transformer that places it, and the
editor is those two things side by side. It has two faces on the web: an embedded two-card editor,
editor is those two things side by side. It has two faces: an embedded two-card editor,
and a Control-Tree panel that shows only the status of each half and puts the editing on child
nodes.</p>
<div class="IMPORTANT">
<h5>Important</h5>
<p><strong>Choosing a kind here replaces, it does not wrap.</strong> Picking an inner geometry calls the
container-aware create endpoint, which constructs a fresh instance and discards whatever was
there. Wrapping an existing geometry into a transformation geometry, and extracting it back out,
exists only in the desktop client's
<a href="geom-manage-control.html">Geometry Management Panel</a> Convert menu.</p>
there. There is no operation anywhere that wraps an existing geometry into a transformation
geometry, or extracts it back out — see
<a href="geom-manage-control.html">Geometry Management Panel</a>.</p>
</div>
<h2 id="layout">Layout</h2>
<h3 id="the-embedded-editor">The embedded editor</h3>
@@ -117,22 +117,14 @@ its own default of <strong>all seven</strong> transformer kinds applies.</li>
<strong>Inner Geometry</strong> slot and an <strong>Inner Transformer</strong> slot as children, and each of those grows its
own kind child. The registry deliberately overrides the kind → editor map here, so the two-card
editor never appears inside a tree.</p>
<h3 id="the-desktop-control">The desktop control</h3>
<p>A geometry-type combo over the inner geometry's own control, and a transformer group box. Its lists
are narrower on both sides: three inner geometry kinds — Box3d, Cylindroid and StlFile, with no
unset entry — and a transformer list restricted to No Transform, Static Translation, Static Freeform
and General Transform. <strong>Static Rotation is unreachable there for a second reason</strong>: the desktop
transformer picker never builds it into its base list, so it is absent whatever the restriction says.</p>
<h2 id="behavior">Behavior</h2>
<ul>
<li><strong>The identity transformer is the floor on the web.</strong> The inner-transformer picker offers no null
entry, and the server installs a <code>NoTransform</code> whenever the field is null, so the slot is never
unset. The desktop panel instead carries a <strong>Not Set</strong> entry, shown by default and never removed by
its type filter, which leaves the slot holding a null transformer.</li>
<li><strong>The identity transformer is the floor.</strong> The inner-transformer picker offers no null entry, and
the server installs a <code>NoTransform</code> whenever the field is null, so the slot is never unset.</li>
<li><strong>Creation is container-aware on both halves.</strong> Both go through this type's own create endpoints
rather than the generic per-kind ones, so the owning field is rebound and not merely the store
entry.</li>
<li><strong>Neither client previews.</strong> There is no viewport on this control. It reports edits upward — one
<li><strong>The control does not preview.</strong> There is no viewport on it. It reports edits upward — one
event when a half is mutated, another when a half is replaced — and the hosting page's shared 3D
canvas re-renders from there.</li>
<li><strong>A null inner geometry has a deterministic key anyway.</strong> When the inner geometry is unset the
@@ -88,10 +88,8 @@
<h1 id="transformer-select-panel">Transformer Select Panel</h1>
<p>The Transformer Select Panel is the switchboard that picks which <a class="xref" href="../../api/Hi.Mech.Topo.ITransformer.html">ITransformer</a>
occupies a transformer-valued slot and edits the one that is there. Both clients carry it under the
same name: <code>wwwroot-src/src/components/topo/TransformerSelectPanel.vue</code> on the web,
<code>Mech/Topo/TransformerSelectPanel.xaml</code> on WPF. For the model behind the picker — what each
transformer does to a frame — see
occupies a transformer-valued slot and edits the one that is there. For the model behind the picker
— what each transformer does to a frame — see
<a class="xref" href="../../technique/mechanism/transformers.html">Handle Transform Matrix by ITransformer</a>.</p>
<h2 id="surface">Surface</h2>
<p>The panel's principal home is a <strong>Transformer slot</strong> of the General Setup page's Control Tree
@@ -158,13 +156,10 @@ hook, in which case that hook runs instead so the owning domain object is reboun
IndexService entry.</p>
<p>Which kinds it offers is the <code>allowedKinds</code> prop, defaulting to all seven. A Control-Tree slot feeds
it from the node's <code>ctx.allowedKinds</code>; none of the four General Setup slots narrows it, so all seven
appear there. The WPF panel restricts through four properties on its code-behind instead:
<code>AllowedTransformerTypes</code>, <code>SelectionFilter</code>, <code>ShowNoTransform</code> and <code>ShowNotSet</code>.</p>
appear there.</p>
<p>When the probed kind falls outside <code>allowedKinds</code>, both the active and the selected kind reset to
empty: the dropdown blanks, and the embedded form prints <em>No transformer attached.</em> in place of an
editor. The out-of-list transformer is not rendered. The WPF client resolves that case the other
way — <code>UpdateUI()</code> leaves the combo unselected when no item matches, then still calls
<code>UpdateTransformerPanel()</code> and builds the current transformer's content panel.</p>
editor. The out-of-list transformer is not rendered.</p>
<h2 id="transformer-kinds">Transformer Kinds</h2>
<p>The web client offers seven kinds, one editor each.</p>
<table>
@@ -213,11 +208,7 @@ way — <code>UpdateUI()</code> leaves the combo unselected when no item matches
</tr>
</tbody>
</table>
<p>The WPF client offers six of them — <code>NoTransform</code>, <code>StaticTranslation</code>, <code>StaticFreeform</code>,
<code>GeneralTransform</code>, <code>DynamicTranslation</code>, <code>DynamicRotation</code> — plus a <em>Not Set</em> entry that leaves the
slot holding a null transformer. It has no <code>StaticRotation</code> panel, and selecting <code>NoTransform</code> there
simply empties its content area. The web picker offers no null entry of either sort: identity is
expressed by choosing <code>NoTransform</code>.</p>
<p>The picker offers no null entry: identity is expressed by choosing <code>NoTransform</code>.</p>
<div class="NOTE">
<h5>Note</h5>
<p>The <a class="xref" href="../../api/Hi.Mech.Topo.GeneralTransform.html">GeneralTransform</a> editor is a composition, not a flat form: a scale field,
@@ -226,9 +217,7 @@ embedded in their own bordered cards, titled <em>Rotation sub-transformer</em> a
sub-transformer</em>. It keys the two sub-editors off <code>POST /api/GeneralTransform/IndexRotation</code> and
<code>/IndexTranslation</code>, edits the scale itself, and owns no vector widget — every
<a class="xref" href="../../api/Hi.Geom.Vec3d.html">Vec3d</a> field on it comes from the nested editors. The effective transform is
<code>T × R × scale × I</code>. Its WPF counterpart <code>Mech/Topo/GeneralTransformPanel.xaml</code> takes the other
approach: three Vec3dControls of its own (translation, rotation axis, rotation pivot) plus scale
and angle text boxes, embedding no sub-panel.</p>
<code>T × R × scale × I</code>.</p>
</div>
<h2 id="key-model">Key Model</h2>
<ul>