feat(panes): panes.state(id) — let a plugin apply its own pane's values

mirrorGlobal covers the case where a pane drives a plain global that some
renderer reads each frame. It does not cover the far more common one: a
plugin whose MAIN-realm code is the authority — it clamps, it persists, it
emits events, it owns the audio graph or the camera rig — and which must
therefore APPLY the pane's values itself rather than have core splat them
somewhere.

Camera Director is the case that forced this. Its brain is the sole writer
of the camera store, the sole broadcaster on splitscreen's channel, and the
only thing that clamps an axis to its legal range. A pane cannot write
window.__h3dCamCtl behind its back without desynchronising its presets, its
persistence, and the panel's own sliders — and running the brain inside the
pane realm would make it a SECOND store writer and a second broadcaster,
racing the real one.

So: `panes.state(id)` hands the main realm the open pane's store
(get/set/all/subscribe). A plugin seeds it on `panes:opened`, subscribes,
and applies what comes back through its own API. The pane stays
realm-agnostic — it only ever touches ctx.state — and the plugin stays the
single source of truth.

For that to work, the hub now broadcasts EVERY change to the store, not just
the ones a pane asked for: it subscribes to the store on connect rather than
echoing pane-originated writes by hand. A value the plugin clamps or corrects
therefore reaches the pane window immediately, and there is exactly one path
by which state arrives in a pane — so it cannot drift.

Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-12 17:58:51 -04:00
parent 254e26bb3a
commit 330995588c
3 changed files with 79 additions and 5 deletions
+36
View File
@@ -150,6 +150,42 @@ one rule above.
---
## Driving a pane from the main realm — `panes.state(id)`
Most plugins with a pane are the **authority** over what the pane controls: they
clamp values, persist presets, emit events, and own the audio graph or the camera
rig. Such a plugin should not have core splat the pane's values somewhere — it
should *apply them itself*.
`panes.state(id)` is the main realm's handle on an open pane's store:
```js
feedBack.on('panes:opened', (e) => {
if (e.detail.id !== 'camera_director') return;
const state = feedBack.panes.state('camera_director');
// Seed it, so the pane opens showing the live camera, not defaults.
AXES.forEach((k) => state.set(k, myApi.getAxis(k)));
// …and apply whatever the pane sends back, through your own API — which
// clamps, persists, and tells the rest of your plugin.
state.subscribe((all, change) => {
if (!change) return;
myApi.setAxis(change.path, change.value);
});
});
```
The pane stays realm-agnostic (it only ever touches `ctx.state`), and your plugin
remains the single source of truth. **Every write to the store is broadcast to the
pane window**, whichever realm made it — so a value your code clamps or corrects
shows up in the pane immediately, and there is exactly one way state reaches a pane.
Guard against a write you just made coming straight back (compare against your
current value before applying), or a clamp will ping-pong.
Returns `null` when the pane is closed.
## `mirrorGlobal` — for panes that drive a plain global
The 3D highways read their free camera from `window.__h3dCamCtl` once per frame.