mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
Three things a plugin needs before it can actually use panes.
## mirrorGlobal — the camera-director problem
The 3D highways read their free camera from a plain global,
`window.__h3dCamCtl` (highway_3d/FREECAM_BRIDGE.md), once per frame in
_resolveFreeCam(). A camera panel in the main window just writes that
object and the camera moves. A panel in a POP-OUT window cannot:
window.__h3dCamCtl there is a different object in a different realm, and
writing it moves nothing.
So a pane declares one field — `mirrorGlobal: '__h3dCamCtl'` — and
pane-mirror.js (main realm, where the renderers live) copies that pane's
state onto the global whenever it changes. highway_3d, keys_highway_3d
and drum_highway_3d are NOT modified and do not know panes exist.
The rule that makes it work: MUTATE THE OBJECT, NEVER REPLACE IT. A
renderer may be holding the reference, and swapping in a new object would
leave it reading an orphan. Keys the pane doesn't set are left alone
rather than deleted — the global may carry a renderer's own bookkeeping.
Closing the pane deliberately leaves the global as-is: closing the camera
panel should not snap the camera back to a default, which is exactly what
happens today (nobody clears __h3dCamCtl).
## Manifest-declared panes
"panes": [{ "id": "camera_director", "title": "Camera Director",
"script": "panes/camera.js", "mirrorGlobal": "__h3dCamCtl" }]
Declaring a pane beats calling panes.register() from screen.js because it
becomes openable FROM THE RAIL OR THE TRAY WITHOUT THE PLUGIN'S SCREEN
EVER HAVING BEEN VISITED — core registers a stub from the manifest and
fetches the script only when the user opens it. A pane you can only reach
by first navigating to the screen it was meant to replace is not much of a
pane.
The script sets `window.feedBackPane_<id> = { mount, unmount }`, mirroring
the existing window.feedBackViz_<id> convention, and the SAME file is what
a pop-out window loads in its own realm.
`script` is validated as a relpath under the plugin's src/ and served
through the sandboxed /api/plugins/<id>/src/ route — the containment rule
`styles` already has for assets/. Traversal, absolute paths, drive letters,
backslashes and non-.js are rejected; a bad entry is dropped with a warning
rather than failing the whole plugin, because one malformed pane should not
cost the user a working plugin.
Note the projection is written TWICE — _nav_entry() and the /api/plugins
route re-project independently — so panes had to be added to both, plus the
pending branch (a pane can be opened while its plugin is still installing
deps; the script is fetched on open, not at discovery).
## docs/plugin-panes.md
The contract, and the one rule it all hangs on: mount(root, ctx) runs in a
realm that may not have the app in it. Everything comes through ctx, or the
pane works docked and silently dies popped out.
Verified: manifest validation rejects ../.., C:\, non-.js, dupes and
missing fields while passing a good entry; /api/plugins projects panes[] for
all 20 plugins. mirrorGlobal mutates the global IN PLACE — a reference held
the way _resolveFreeCam holds it sees the change, and a renderer's own field
on that object survives — both for a local write and for a write arriving
over the channel from a pop-out realm.
pytest: 2401 passed, 8 failed — all 8 reproduce on a clean main (including
the one in tests/test_plugins.py) and are unrelated.
Signed-off-by: topkoa <topkoa@gmail.com>
81 lines
3.4 KiB
JavaScript
81 lines
3.4 KiB
JavaScript
/*
|
|
* fee[dB]ack — global mirroring for panes.
|
|
*
|
|
* The camera-director problem, solved without touching a single renderer.
|
|
*
|
|
* The 3D highways read their free-camera state from a plain global —
|
|
* `window.__h3dCamCtl = { enabled, heightMul, distMul, yaw, pitch, panX, panY }`
|
|
* (plugins/highway_3d/FREECAM_BRIDGE.md) — once per frame, in _resolveFreeCam().
|
|
* A camera panel in the main window just writes that object and the camera moves.
|
|
*
|
|
* A panel in a POP-OUT window cannot: `window.__h3dCamCtl` there is a different
|
|
* object in a different realm, and writing it moves nothing.
|
|
*
|
|
* So a pane declares one manifest field:
|
|
*
|
|
* panes.register({ id: 'camera_director', mirrorGlobal: '__h3dCamCtl', ... })
|
|
*
|
|
* and this file — which runs in the MAIN realm, where the renderers live — copies
|
|
* that pane's state onto the global whenever it changes. The pane writes
|
|
* ctx.state, the state store is authoritative here, and the renderers keep reading
|
|
* the plain global they always read. highway_3d, keys_highway_3d and
|
|
* drum_highway_3d are not modified, and do not know panes exist.
|
|
*
|
|
* The one rule that makes it work: MUTATE THE OBJECT, NEVER REPLACE IT. A renderer
|
|
* may be holding the reference (_resolveFreeCam caches it), and swapping in a new
|
|
* object would leave it writing to — and reading from — an orphan.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
const panes = window.feedBack && window.feedBack.panes;
|
|
const bus = window.feedBack;
|
|
if (!panes || !bus) return;
|
|
|
|
// paneId -> unsubscribe
|
|
const mirrors = new Map();
|
|
|
|
function _target(name) {
|
|
// Create the global if the plugin that owns it hasn't yet (a pane can be
|
|
// restored at boot before its renderer ever runs). Reuse it if it exists —
|
|
// see the rule above.
|
|
if (!window[name] || typeof window[name] !== 'object') window[name] = {};
|
|
return window[name];
|
|
}
|
|
|
|
function _apply(name, state) {
|
|
const target = _target(name);
|
|
// Copy the pane's whole state root onto the global. Keys the pane doesn't
|
|
// set are left alone rather than deleted — the global may carry fields the
|
|
// pane knows nothing about (a renderer's own bookkeeping), and clearing
|
|
// them would be a silent, spooky breakage.
|
|
Object.keys(state).forEach((k) => { target[k] = state[k]; });
|
|
}
|
|
|
|
bus.on('panes:opened', (e) => {
|
|
const id = e.detail && e.detail.id;
|
|
const spec = panes.get(id);
|
|
if (!spec || !spec.mirrorGlobal || mirrors.has(id)) return;
|
|
const entry = panes._entry(id);
|
|
if (!entry) return;
|
|
|
|
// Sync once on open: a pane's persisted state is the user's last camera,
|
|
// and it should take effect the moment the pane exists — not on their next
|
|
// nudge of a slider.
|
|
_apply(spec.mirrorGlobal, entry.state.all());
|
|
|
|
mirrors.set(id, entry.state.subscribe((all) => _apply(spec.mirrorGlobal, all)));
|
|
});
|
|
|
|
bus.on('panes:closed', (e) => {
|
|
const id = e.detail && e.detail.id;
|
|
const unsub = mirrors.get(id);
|
|
if (!unsub) return;
|
|
unsub();
|
|
mirrors.delete(id);
|
|
// The global is deliberately LEFT AS IT IS. Closing the camera panel should
|
|
// not snap the camera back to a default — that is exactly what happens
|
|
// today (nobody clears __h3dCamCtl), and it is the behaviour users expect.
|
|
});
|
|
})();
|