smapoles's picture
Stand up chart factory static site
1f92077 verified
Raw History Blame Contribute Delete
5 kB
// renderers/_shared/mount.js — the {update, destroy} lifecycle skeleton
// every renderer builds on (CONTRACT 3). Tracks disposables (event
// listeners, observers, animation loops, WebGL renderers) so destroy()
// reliably stops loops, disposes GL, and removes nodes — the lifecycle
// IDIO_GRAPH_V2 lacked.
//
// Typical renderer usage:
// import { createLifecycle } from '../_shared/mount.js';
// export function mount(el, payload) {
// const lc = createLifecycle(el);
// const { card, body } = createCard(el, payload.meta); // cell.js
// ...build scene...
// lc.trackThree(renderer); // WebGL cleanup
// lc.raf((tMs) => { ...per-frame... }); // auto-cancelled loop
// lc.observeResize(body, (w, h) => { ... }); // container-driven sizing
// return lc.handle((fresh) => { ...re-render from fresh payload... });
// }
/**
* Create a lifecycle context bound to a mount host.
* Everything registered through it is torn down (in reverse order) by
* destroy(), which finally clears the host's children.
*/
export function createLifecycle(el) {
const disposables = [];
let destroyed = false;
function runDisposable(d) {
try {
if (typeof d === 'function') d();
else if (d && typeof d.destroy === 'function') d.destroy();
else if (d && typeof d.dispose === 'function') d.dispose();
} catch (err) {
console.error('[mount] disposable failed during destroy:', err);
}
}
const lc = {
el,
get isDestroyed() { return destroyed; },
/**
* Register anything needing teardown: a function, or an object with
* .destroy() or .dispose() (three.js geometries/materials/textures,
* observers, child mount handles). Returns the argument for chaining.
*/
track(d) {
if (d != null) disposables.push(d);
return d;
},
/** addEventListener with automatic removal on destroy. */
listen(target, type, fn, opts) {
target.addEventListener(type, fn, opts);
lc.track(() => target.removeEventListener(type, fn, opts));
return fn;
},
/**
* Watch an element's content-box size (ResizeObserver), auto-
* disconnected on destroy. fn(width, height, entry). No-ops (returns
* null) where ResizeObserver is unavailable (jsdom smoke tests).
*/
observeResize(target, fn) {
if (typeof ResizeObserver === 'undefined') return null;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const box = entry.contentBoxSize?.[0];
const w = box ? box.inlineSize : entry.contentRect.width;
const h = box ? box.blockSize : entry.contentRect.height;
fn(w, h, entry);
}
});
ro.observe(target);
lc.track(() => ro.disconnect());
return ro;
},
/**
* Start a requestAnimationFrame loop: fn(timestampMs) every frame
* until stopped. Returns stop(); also auto-stopped on destroy.
* (For three.js prefer renderer.setAnimationLoop + trackThree —
* this is for canvas/SVG/DOM animation.)
*/
raf(fn) {
let id = 0;
let running = true;
const loop = (t) => {
if (!running) return;
fn(t);
id = requestAnimationFrame(loop);
};
id = requestAnimationFrame(loop);
const stop = () => {
running = false;
cancelAnimationFrame(id);
};
lc.track(stop);
return stop;
},
/** setInterval with automatic clearInterval on destroy. Returns stop(). */
interval(fn, ms) {
const id = setInterval(fn, ms);
const stop = () => clearInterval(id);
lc.track(stop);
return stop;
},
/**
* Take ownership of a THREE.WebGLRenderer: on destroy the animation
* loop is stopped, the GL context disposed and force-lost, and the
* canvas removed from the DOM.
*/
trackThree(renderer) {
lc.track(() => {
try {
renderer.setAnimationLoop?.(null);
renderer.dispose();
renderer.forceContextLoss?.();
renderer.domElement?.remove();
} catch (err) {
console.error('[mount] three renderer teardown failed:', err);
}
});
return renderer;
},
/** Tear everything down (idempotent) and empty the mount host. */
destroy() {
if (destroyed) return;
destroyed = true;
for (let i = disposables.length - 1; i >= 0; i -= 1) {
runDisposable(disposables[i]);
}
disposables.length = 0;
el.replaceChildren();
},
/**
* Produce the CONTRACT-3 mount handle. `update` is the renderer's
* re-render-from-fresh-payload function; it is guarded so calls after
* destroy() are ignored.
*/
handle(update) {
return {
update(payload) {
if (!destroyed && typeof update === 'function') update(payload);
},
destroy() { lc.destroy(); },
};
},
};
return lc;
}